diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt index 3c240d2049..c4a34ce6ee 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt @@ -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, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Flicker.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Flicker.kt index 357e123c64..8ed1c1cbaf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Flicker.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Flicker.kt @@ -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) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt new file mode 100644 index 0000000000..6cbb851dc3 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt @@ -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), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenCryptoAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenCryptoAmount.kt index 2375e04202..58524bee7e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenCryptoAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenCryptoAmount.kt @@ -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, + ), ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt index 48f73c0b5f..76975fb223 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt @@ -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, + ), ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenPrice.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenPrice.kt index 761ade8992..b2ee3f5568 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenPrice.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenPrice.kt @@ -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 + }, + ), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index 821dcd5bde..de31c28cc4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -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> { error("No MainBottomSheetColor provided") } +val LocalBladeAnimation = staticCompositionLocalOf { + error("No MainBottomSheetColor provided") +} + /** * Determines whether the dark theme should be used based on the given [AppThemeMode]. * diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index 91f638f929..ae100c11a3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -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, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 6c62f4d63e..3d366acb33 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -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(