Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-21 17:50:41 +03:00
commit effe637f9f
9 changed files with 192 additions and 54 deletions

View file

@ -29,7 +29,7 @@ import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.flicker
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
@ -133,10 +133,11 @@ private fun NameAndInfo(
)
} else {
Text(
modifier = Modifier.flicker(isFlickering),
text = balanceValue,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2.applyBladeBrush(
isEnabled = isFlickering,
textColor = TangemTheme.colors.text.tertiary,
),
maxLines = 1,
)
}

View file

@ -27,7 +27,7 @@ import com.tangem.core.ui.res.TangemThemePreview
fun Modifier.flicker(
isFlickering: Boolean,
targetTextAlpha: Float = 0.4f,
animationDurationMillis: Int = 1200,
animationDurationMillis: Int = 1500,
): Modifier = composed {
var alphaChange by remember { mutableStateOf(false) }

View file

@ -0,0 +1,83 @@
package com.tangem.core.ui.components.text
import android.content.res.Configuration
import androidx.compose.animation.core.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.res.LocalBladeAnimation
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlin.math.sqrt
data class BladeAnimation(
val offset: Float,
)
@Composable
fun rememberBladeAnimation(): BladeAnimation {
val infiniteTransition = rememberInfiniteTransition()
val offset by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1500, easing = LinearEasing),
),
)
return BladeAnimation(offset)
}
@Suppress("MagicNumber")
@Composable
fun TextStyle.applyBladeBrush(isEnabled: Boolean, textColor: Color): TextStyle {
val offset = LocalBladeAnimation.current.offset
return if (isEnabled) {
val brush = remember(offset, textColor) {
object : ShaderBrush() {
override fun createShader(size: Size): Shader {
val center = Offset(size.width / 2f, size.height / 2f)
val diagonal = sqrt(size.width * size.width + size.height * size.height)
val direction = Offset(x = 1f, y = 0.5f)
val halfDist = diagonal / 2f
val baseStart = center - direction * halfDist
val baseEnd = center + direction * halfDist
val shift = direction * offset * diagonal
return LinearGradientShader(
colors = listOf(textColor.copy(alpha = 0.2f), textColor),
from = baseStart + shift,
to = baseEnd + shift,
colorStops = listOf(0.0f, 0.25f),
tileMode = TileMode.Mirror,
)
}
}
}
this.copy(brush = brush)
} else {
this.copy(
color = textColor,
)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
Text(
"Blade animation text",
style = TangemTheme.typography.body1.applyBladeBrush(true, Color.Black),
)
}
}

View file

@ -10,7 +10,7 @@ import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.audits.AuditLabel
import com.tangem.core.ui.components.flicker
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.stringResourceSafe
@ -53,12 +53,14 @@ internal fun TokenCryptoAmount(
@Composable
private fun CryptoAmountText(amount: String, modifier: Modifier = Modifier, isFlickering: Boolean = false) {
Text(
modifier = modifier.flicker(isFlickering),
modifier = modifier,
text = amount,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.caption2.applyBladeBrush(
isEnabled = isFlickering,
textColor = TangemTheme.colors.text.tertiary,
),
)
}

View file

@ -18,7 +18,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.flicker
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState as TokenFiatAmountState
@ -97,12 +97,14 @@ private fun FiatAmountText(
isFlickering: Boolean = false,
) {
Text(
modifier = modifier.flicker(isFlickering),
modifier = modifier,
text = text,
color = if (isAvailable) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.body2,
style = TangemTheme.typography.body2.applyBladeBrush(
isEnabled = isFlickering,
textColor = if (isAvailable) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.tertiary,
),
)
}

View file

@ -22,6 +22,7 @@ import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.components.SpacerW6
import com.tangem.core.ui.components.flicker
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
@ -42,10 +43,15 @@ internal fun TokenPrice(state: TokenPriceState?, modifier: Modifier = Modifier)
)
}
is TokenPriceState.TextContent -> {
PriceText(text = state.value.resolveReference(), modifier = modifier, isAvailable = state.isAvailable)
PriceText(
text = state.value.resolveReference(),
isFlickering = false,
modifier = modifier,
isAvailable = state.isAvailable,
)
}
is TokenPriceState.Unknown -> {
PriceText(text = DASH_SIGN, modifier = modifier)
PriceText(text = DASH_SIGN, isFlickering = false, modifier = modifier)
}
is TokenPriceState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
@ -65,37 +71,62 @@ private fun PriceBlock(
type: PriceChangeType? = null,
priceChangePercent: String? = null,
) {
Row(modifier = modifier.flicker(isFlickering), verticalAlignment = Alignment.CenterVertically) {
PriceText(text = price, modifier = Modifier.weight(weight = 1f, fill = false))
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
PriceText(
modifier = Modifier.weight(weight = 1f, fill = false),
text = price,
isFlickering = isFlickering,
)
SpacerW6()
if (type != null) {
PriceChangeIcon(type = type)
PriceChangeIcon(
modifier = Modifier.flicker(isFlickering),
type = type,
)
SpacerW4()
}
PriceChangeText(type = type, text = priceChangePercent)
}
}
@Composable
private fun PriceText(text: String, modifier: Modifier = Modifier, isAvailable: Boolean = true) {
AnimatedContent(targetState = text, label = "Update the price text", modifier = modifier) { animatedText ->
Text(
text = animatedText,
color = if (isAvailable) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.caption2,
PriceChangeText(
type = type,
isFlickering = isFlickering,
text = priceChangePercent,
)
}
}
@Composable
private fun PriceChangeIcon(type: PriceChangeType) {
AnimatedContent(targetState = type, label = "Update the price change's arrow") { animatedType ->
private fun PriceText(
text: String,
isFlickering: Boolean,
modifier: Modifier = Modifier,
isAvailable: Boolean = true,
) {
AnimatedContent(targetState = text, label = "Update the price text", modifier = modifier) { animatedText ->
Text(
text = animatedText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.caption2.applyBladeBrush(
isEnabled = isFlickering,
textColor = if (isAvailable) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.tertiary,
),
)
}
}
@Composable
private fun PriceChangeIcon(type: PriceChangeType, modifier: Modifier = Modifier) {
AnimatedContent(
targetState = type,
label = "Update the price change's arrow",
) { animatedType ->
Icon(
modifier = modifier,
painter = painterResource(
id = when (animatedType) {
PriceChangeType.UP -> R.drawable.ic_arrow_up_8
@ -114,19 +145,26 @@ private fun PriceChangeIcon(type: PriceChangeType) {
}
@Composable
private fun PriceChangeText(type: PriceChangeType?, text: String?, modifier: Modifier = Modifier) {
private fun PriceChangeText(
type: PriceChangeType?,
text: String?,
isFlickering: Boolean,
modifier: Modifier = Modifier,
) {
AnimatedContent(targetState = text, modifier = modifier, label = "Update the price change's text") { animatedText ->
Text(
text = animatedText ?: DASH_SIGN,
color = when (type) {
PriceChangeType.UP -> TangemTheme.colors.text.accent
PriceChangeType.DOWN -> TangemTheme.colors.text.warning
PriceChangeType.NEUTRAL -> TangemTheme.colors.text.disabled
null -> TangemTheme.colors.text.tertiary
},
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.caption2.applyBladeBrush(
isEnabled = isFlickering,
textColor = when (type) {
PriceChangeType.UP -> TangemTheme.colors.text.accent
PriceChangeType.DOWN -> TangemTheme.colors.text.warning
PriceChangeType.NEUTRAL -> TangemTheme.colors.text.disabled
null -> TangemTheme.colors.text.tertiary
},
),
)
}
}

View file

@ -14,6 +14,8 @@ import androidx.compose.ui.platform.LocalView
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.TangemShimmer
import com.tangem.core.ui.components.text.BladeAnimation
import com.tangem.core.ui.components.text.rememberBladeAnimation
import com.tangem.core.ui.haptic.DefaultHapticManager
import com.tangem.core.ui.haptic.HapticManager
import com.tangem.core.ui.haptic.VibratorHapticManager
@ -98,6 +100,7 @@ fun TangemTheme(
LocalSnackbarHostState provides snackbarHostState,
LocalEventMessageHandler provides eventMessageHandler,
LocalWindowSize provides windowSize,
LocalBladeAnimation provides rememberBladeAnimation(),
) {
CompositionLocalProvider(
LocalTangemShimmer provides TangemShimmer,
@ -305,6 +308,10 @@ val LocalMainBottomSheetColor = staticCompositionLocalOf<MutableState<Color>> {
error("No MainBottomSheetColor provided")
}
val LocalBladeAnimation = staticCompositionLocalOf<BladeAnimation> {
error("No MainBottomSheetColor provided")
}
/**
* Determines whether the dark theme should be used based on the given [AppThemeMode].
*

View file

@ -14,7 +14,7 @@ import androidx.constraintlayout.compose.ConstraintLayout
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.buttons.HorizontalActionChips
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.flicker
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
@ -115,10 +115,12 @@ private fun FiatBalance(
),
)
is TokenDetailsBalanceBlockState.Content -> Text(
modifier = modifier.flicker(state.isBalanceFlickering),
modifier = modifier,
text = state.displayFiatBalance.orMaskWithStars(isBalanceHidden),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h2.applyBladeBrush(
isEnabled = state.isBalanceFlickering,
textColor = TangemTheme.colors.text.primary1,
),
)
is TokenDetailsBalanceBlockState.Error -> Text(
modifier = modifier,
@ -143,10 +145,12 @@ private fun CryptoBalance(
),
)
is TokenDetailsBalanceBlockState.Content -> Text(
modifier = modifier.flicker(state.isBalanceFlickering),
modifier = modifier,
text = state.displayCryptoBalance.orMaskWithStars(isBalanceHidden),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2.applyBladeBrush(
isEnabled = state.isBalanceFlickering,
textColor = TangemTheme.colors.text.tertiary,
),
)
is TokenDetailsBalanceBlockState.Error -> Text(
modifier = modifier,

View file

@ -40,7 +40,7 @@ import androidx.constraintlayout.compose.Dimension
import com.tangem.core.ui.components.FontSizeRange
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.ResizableText
import com.tangem.core.ui.components.flicker
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
@ -277,15 +277,16 @@ private fun Balance(state: WalletCardState, isBalanceHidden: Boolean, modifier:
when (state) {
is WalletCardState.Content -> {
ResizableText(
modifier = Modifier
.defaultMinSize(minHeight = TangemTheme.dimens.size32)
.flicker(isFlickering = state.isBalanceFlickering),
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32),
text = balance,
fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize),
color = TangemTheme.colors.text.primary1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.h2,
style = TangemTheme.typography.h2
.applyBladeBrush(
isEnabled = state.isBalanceFlickering,
textColor = TangemTheme.colors.text.primary1,
),
)
}
is WalletCardState.Error -> NonContentBalanceText(