Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-03 17:39:04 +05:00
parent fbc15cbb8c
commit 7b00754e24
12 changed files with 77 additions and 51 deletions

View file

@ -4,12 +4,7 @@ package com.tangem.core.ui.components.background.northernlights
import android.os.Build
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.StartOffset
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@ -21,7 +16,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.components.background.shaderBackground
import com.tangem.core.ui.res.LocalPowerSavingState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.shader.NorthernLightsMeshGradientShader
/**
@ -29,10 +23,14 @@ import com.tangem.core.ui.shader.NorthernLightsMeshGradientShader
* Uses a RuntimeShader on Android 13+ and falls back to a simpler implementation on older versions and in power saving mode.
*/
@Composable
fun NorthernLightsBackground(modifier: Modifier = Modifier, forceSimpleVersion: Boolean = false) {
fun NorthernLightsBackground(
containerColor: Color,
modifier: Modifier = Modifier,
forceSimpleVersion: Boolean = false,
) {
val isPowerSavingMode by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
if (!forceSimpleVersion && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !isPowerSavingMode) {
NorthernLightsBackgroundWithShader(modifier)
NorthernLightsBackgroundWithShader(containerColor, modifier)
} else {
MovingColorfulBlubsBackground(modifier)
}
@ -40,9 +38,8 @@ fun NorthernLightsBackground(modifier: Modifier = Modifier, forceSimpleVersion:
@Suppress("LongMethod")
@Composable
private fun NorthernLightsBackgroundWithShader(modifier: Modifier = Modifier) {
private fun NorthernLightsBackgroundWithShader(containerColor: Color, modifier: Modifier = Modifier) {
val transition = rememberInfiniteTransition(label = "FluidMeshGradientV2")
val backgroundColor = TangemTheme.colors2.surface.level1
// Each track cycles through 4 states (matching the screenshot frames):
// deep/dark → saturated+bright → light/pastel → vibrant/vivid → back
@ -128,7 +125,7 @@ private fun NorthernLightsBackgroundWithShader(modifier: Modifier = Modifier) {
Color(0xFF1444AA),
Color(0xFF4422BB),
Color(0xFF331199),
backgroundColor,
containerColor,
),
speed = 0.5f,
scale = 4f,
@ -139,12 +136,12 @@ private fun NorthernLightsBackgroundWithShader(modifier: Modifier = Modifier) {
colorsArray[1] = color2
colorsArray[2] = color3
colorsArray[3] = color4
colorsArray[4] = backgroundColor
colorsArray[4] = containerColor
shader.updateColors(colorsArray)
Box(
modifier = modifier
.background(backgroundColor)
.background(containerColor)
.fillMaxSize()
.shaderBackground(shader),
)

View file

@ -32,15 +32,15 @@ internal fun ProvideHaze(content: @Composable () -> Unit) {
*/
@Composable
fun Modifier.hazeEffectTangem(
state: HazeState = LocalHazeState.current,
style: HazeStyle = HazeStyle.Unspecified,
configure: HazeEffectScope.() -> Unit = {},
): Modifier {
val powerSavingEnabled = LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
val hazeState = LocalHazeState.current
val isGlobalBlurEnabled = hazeState.blurEnabled && !powerSavingEnabled.value
val isGlobalBlurEnabled = state.blurEnabled && !powerSavingEnabled.value
val rootBackground by LocalRootBackgroundColor.current
return hazeEffect(hazeState, style) {
return hazeEffect(state, style) {
fallbackTint = HazeTint(rootBackground)
if (isGlobalBlurEnabled) {
configure()
@ -78,5 +78,5 @@ fun Modifier.hazeForegroundEffectTangem(
* Applies a haze source to the [Modifier] using the current global haze state.
*/
@Composable
fun Modifier.hazeSourceTangem(zIndex: Float = 0f, key: Any? = null) =
this.hazeSource(LocalHazeState.current, zIndex, key)
fun Modifier.hazeSourceTangem(state: HazeState = LocalHazeState.current, zIndex: Float = 0f, key: Any? = null) =
this.hazeSource(state, zIndex, key)

View file

@ -106,7 +106,10 @@ enum class TangemMessageEffect(val isAnimatable: Boolean) {
Color(0x1AFFFFFF),
)
} else {
persistentListOf()
persistentListOf(
Color(0xFFE1E1E1),
Color(0xFFE1E1E1),
)
}
}
}
@ -192,7 +195,10 @@ enum class TangemMessageEffect(val isAnimatable: Boolean) {
Color(0x17E44848),
)
None -> if (isInDarkTheme) {
persistentListOf()
persistentListOf(
Color(0x1AFFFFFF),
Color(0x1AFFFFFF),
)
} else {
persistentListOf(
Color(0x0d000000),
@ -238,9 +244,10 @@ internal fun Modifier.messageEffectBackground(
val isInDarkTheme = LocalIsInDarkTheme.current
val borderGradientColors = remember { messageEffect.getBorderGradient(isInDarkTheme) }
val gradientColors = remember { messageEffect.getColorGradient(isInDarkTheme) }
val gradientTint = remember { messageEffect.getGradientTint(isInDarkTheme) }
val angle by rememberAnimationAngle(messageEffect.isAnimatable)
val brush = Brush.sweepGradient(messageEffect.getColorGradient(isInDarkTheme))
val brush = remember { Brush.sweepGradient(messageEffect.getColorGradient(isInDarkTheme)) }
val padding = 1.dp.toPx()
return this
@ -254,14 +261,14 @@ internal fun Modifier.messageEffectBackground(
border(
width = 1.dp,
brush = Brush.sweepGradient(
colors = messageEffect.getBorderGradient(isInDarkTheme),
colors = borderGradientColors,
center = Offset.Infinite,
),
shape = RoundedCornerShape(radius),
)
}
.hazeForegroundEffectTangem(
style = HazeStyle(tints = messageEffect.getGradientTint(isInDarkTheme)),
style = HazeStyle(tints = gradientTint),
isBlurEnabled = true,
) {
fallbackTint = HazeTint(

View file

@ -25,10 +25,7 @@ import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.TokenElementsTestTags
@ -40,12 +37,13 @@ import com.tangem.core.ui.test.TokenElementsTestTags
* @param modifier Modifier for the composable
*/
@Composable
fun TangemHeaderRow(headerRowUM: TangemHeaderRowUM, modifier: Modifier = Modifier) {
fun TangemHeaderRow(headerRowUM: TangemHeaderRowUM, modifier: Modifier = Modifier, isBalanceHidden: Boolean = false) {
TangemHeaderRow(
headTangemIconUM = headerRowUM.startIconUM,
footerTangemIconRes = headerRowUM.endIconRes,
title = headerRowUM.title,
subtitle = headerRowUM.subtitle,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
)
}
@ -63,6 +61,7 @@ fun TangemHeaderRow(headerRowUM: TangemHeaderRowUM, modifier: Modifier = Modifie
@Composable
fun TangemHeaderRow(
modifier: Modifier = Modifier,
isBalanceHidden: Boolean = false,
subtitle: TextReference? = null,
onItemClick: (() -> Unit)? = null,
@DrawableRes footerTangemIconRes: Int? = null,
@ -92,7 +91,7 @@ fun TangemHeaderRow(
) {
val wrappedSubtitle = remember(this) { requireNotNull(subtitle) }
Text(
text = wrappedSubtitle.resolveAnnotatedReference(),
text = wrappedSubtitle.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
@ -131,6 +130,7 @@ fun TangemHeaderRow(
fun TangemHeaderRow(
title: TextReference,
modifier: Modifier = Modifier,
isBalanceHidden: Boolean = false,
subtitle: TextReference? = null,
headTangemIconUM: TangemIconUM? = null,
@DrawableRes footerTangemIconRes: Int? = null,
@ -172,7 +172,7 @@ fun TangemHeaderRow(
) {
val wrappedSubtitle = remember(this) { requireNotNull(subtitle) }
Text(
text = wrappedSubtitle.resolveAnnotatedReference(),
text = wrappedSubtitle.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,

View file

@ -24,6 +24,12 @@ internal fun RowScope.TokenRowPriceChangeContent(
isFlickering: Boolean,
isAvailable: Boolean = true,
) {
val color = when (priceChangeState.type) {
PriceChangeType.UP -> TangemTheme.colors2.graphic.status.accent
PriceChangeType.DOWN -> TangemTheme.colors2.graphic.status.warning
PriceChangeType.NEUTRAL -> TangemTheme.colors2.graphic.neutral.tertiary
}
AnimatedContent(
targetState = priceChangeState.type,
label = "Update the price change's arrow",
@ -39,11 +45,7 @@ internal fun RowScope.TokenRowPriceChangeContent(
},
),
),
tint = when (animatedType) {
PriceChangeType.UP -> TangemTheme.colors2.graphic.status.accent
PriceChangeType.DOWN -> TangemTheme.colors2.graphic.status.warning
PriceChangeType.NEUTRAL -> TangemTheme.colors2.graphic.neutral.secondary
},
tint = color,
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens2.x3),
)
@ -61,7 +63,7 @@ internal fun RowScope.TokenRowPriceChangeContent(
style = TangemTheme.typography2.captionSemibold12.applyBladeBrush(
isEnabled = isFlickering,
textColor = if (isAvailable) {
TangemTheme.colors2.text.neutral.secondary
color
} else {
TangemTheme.colors2.text.status.disabled
},

View file

@ -34,7 +34,7 @@ internal fun NewsSlider(newsSliderConfig: NewsSliderConfig) {
.conditionalCompose(
condition = isRedesignEnabled,
modifier = {
hazeSourceTangem(-1f)
hazeSourceTangem(zIndex = -1f)
},
)
.background(color = background),

View file

@ -1,4 +1,5 @@
@file:Suppress("MagicNumber", "LongMethod")
package com.tangem.feature.tester.presentation.storybook.page.background
import androidx.compose.foundation.background
@ -15,6 +16,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.background.northernlights.NorthernLightsBackground
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory
@Composable
@ -22,6 +24,7 @@ internal fun NorthernLightsStory(state: NorthernLightsStory, modifier: Modifier
Box(modifier = modifier.fillMaxSize()) {
NorthernLightsBackground(
modifier = Modifier.fillMaxSize(),
containerColor = TangemTheme.colors2.surface.level1,
forceSimpleVersion = state.variant == NorthernLightsStory.Variant.Simple,
)

View file

@ -13,6 +13,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonUM
import com.tangem.feature.wallet.presentation.wallet.ui.components.fastForEach
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@ -36,7 +37,7 @@ internal class TokenActionsComponent @AssistedInject constructor(
),
) {
Column {
params.actions.forEach { action ->
params.actions.fastForEach { action ->
if (action.isEnabled) {
val rowColors = if (action.isWarning) {
getWarningRowColors()

View file

@ -54,10 +54,7 @@ import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingTopBar
import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.*
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM
@ -181,9 +178,16 @@ private fun WalletContent2(
Box(
modifier = Modifier
.fillMaxSize()
.hazeSourceTangem(-1f),
.hazeSourceTangem(zIndex = -1f),
) {
NorthernLightsBackground(Modifier.matchParentSize())
NorthernLightsBackground(
containerColor = if (LocalIsInDarkTheme.current) {
TangemTheme.colors2.surface.level1
} else {
TangemTheme.colors2.surface.level2
},
modifier = Modifier.matchParentSize(),
)
WalletPagerIndicator(
pagerState = walletsPagerState,
@ -201,7 +205,7 @@ private fun WalletContent2(
state.wallets2[state.selectedWalletIndex]
}
LaunchedEffect(walletsPagerState.currentPage) {
LaunchedEffect(walletsPagerState.currentPage, currentWallet.walletsBalanceUM) {
if (walletsPagerState.currentPage == currentWalletIndex) {
walletBalance = (currentWallet.walletsBalanceUM as? WalletBalanceUM.Content)?.balanceInAppBar
}

View file

@ -45,6 +45,7 @@ import com.tangem.feature.wallet.presentation.preview.WalletBalancePreview
import com.tangem.feature.wallet.presentation.preview.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM
import com.tangem.feature.wallet.presentation.wallet.ui.components.fastForEach
import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.ImmutableList
private const val MIN_SCALE = 0.75f
@ -135,7 +136,19 @@ private fun Balance(walletBalanceUM: WalletBalanceUM, isBalanceHidden: Boolean,
),
)
}
is WalletBalanceUM.Error,
is WalletBalanceUM.Error -> {
Text(
text = StringsSigns.DASH_SIGN,
style = TangemTheme.typography2.titleRegular44,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
autoSize = TextAutoSize.StepBased(
minFontSize = TangemTheme.typography2.bodySemibold15.fontSize,
maxFontSize = TangemTheme.typography2.titleRegular44.fontSize,
),
)
}
is WalletBalanceUM.Loading,
-> {
TextShimmer(

View file

@ -45,16 +45,14 @@ internal fun WalletListContent(
overscrollEffect = rememberOverscrollEffect(),
) {
notifications(
notifications = currentWallet.notifications.map { it.messageUM }
.toPersistentList(),
notifications = currentWallet.notifications.map { it.messageUM }.toPersistentList(),
contentColor = containerColor,
modifier = movableItemModifier,
)
notificationsCarousel(
containerColor = containerColor,
modifier = movableItemModifier,
notifications = currentWallet.notifications.map { it.messageUM }
.toPersistentList(),
notifications = currentWallet.notificationsCarousel.map { it.messageUM }.toPersistentList(),
)
tangemPay(

View file

@ -204,6 +204,7 @@ private fun LazyListScope.portfolioItem(
)
is TangemHeaderRowUM -> TangemHeaderRow(
headerRowUM = tokenRowUM,
isBalanceHidden = isBalanceHidden,
modifier = itemModifier,
)
}