From 5d13d36cb387fd9b9fb943c1dd20c0228e94ea59 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 29 Aug 2023 20:20:21 +0800 Subject: [PATCH] Updated on 2026-08-14 --- .../com/tangem/core/ui/components/Shimmers.kt | 23 +- .../com/tangem/core/ui/res/TangemDimens.kt | 1 + .../presentation/common/WalletPreviewData.kt | 4 +- .../common/component/TokenItem.kt | 517 ++---------------- .../common/component/token/LockedRectangle.kt | 18 + .../component/token/TokenCryptoInfoBlock.kt | 116 ++++ .../component/token/TokenFiatInfoBlock.kt | 180 ++++++ .../common/component/token/TokenIcon.kt | 152 +++++ .../common/state/TokenItemState.kt | 105 ++-- .../organizetokens/OrganizeTokensScreen.kt | 4 +- ...ryptoCurrencyStatusToTokenItemConverter.kt | 2 +- 11 files changed, 582 insertions(+), 540 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/LockedRectangle.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoInfoBlock.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatInfoBlock.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenIcon.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt index e793e21eaf..640f986b3e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import com.tangem.core.ui.res.TangemTheme import com.valentinilk.shimmer.shimmer @@ -14,13 +15,13 @@ import com.valentinilk.shimmer.shimmer * Rectangle shimmer item with rounded shape from DS */ @Composable -fun RectangleShimmer(modifier: Modifier = Modifier) { +fun RectangleShimmer(modifier: Modifier = Modifier, radius: Dp = TangemTheme.dimens.radius6) { Box( modifier = modifier .shimmer() .background( color = TangemTheme.colors.button.secondary, - shape = RoundedCornerShape(TangemTheme.dimens.radius6), + shape = RoundedCornerShape(size = radius), ), ) } @@ -31,16 +32,14 @@ fun RectangleShimmer(modifier: Modifier = Modifier) { */ @Composable fun CircleShimmer(modifier: Modifier = Modifier) { - Box(modifier = modifier.shimmer()) { - Box( - modifier = Modifier - .matchParentSize() - .background( - color = TangemTheme.colors.button.secondary, - shape = CircleShape, - ), - ) - } + Box( + modifier = modifier + .shimmer() + .background( + color = TangemTheme.colors.button.secondary, + shape = CircleShape, + ), + ) } // region preview diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 1c60364fa2..b4fd692f7e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -46,6 +46,7 @@ data class TangemDimens internal constructor( val size10: Dp = 10.dp, val size11: Dp = 11.dp, val size12: Dp = 12.dp, + val size14: Dp = 14.dp, val size16: Dp = 16.dp, val size18: Dp = 18.dp, val size20: Dp = 20.dp, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 1e70c05083..3166b376ec 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -98,7 +98,7 @@ internal object WalletPreviewData { hasPending = true, tokenOptions = TokenOptionsState.Visible( fiatAmount = "321 $", - priceChange = PriceChangeConfig( + config = PriceChangeConfig( valueInPercent = "2%", type = PriceChangeConfig.Type.UP, ), @@ -126,7 +126,7 @@ internal object WalletPreviewData { amount = "5,412 MATIC", hasPending = true, tokenOptions = TokenOptionsState.Hidden( - priceChange = PriceChangeConfig( + config = PriceChangeConfig( valueInPercent = "2%", type = PriceChangeConfig.Type.UP, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 84efe5f86a..32f714b566 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -1,320 +1,72 @@ package com.tangem.feature.wallet.presentation.common.component -import androidx.annotation.DrawableRes -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.ColorMatrix -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import androidx.compose.ui.unit.Dp import androidx.constraintlayout.compose.ConstrainedLayoutReference import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayoutScope import androidx.constraintlayout.compose.Dimension -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.marketprice.PriceChangeConfig import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemTypography -import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.common.component.token.TokenCryptoInfoBlock +import com.tangem.feature.wallet.presentation.common.component.token.TokenFiatInfoBlock +import com.tangem.feature.wallet.presentation.common.component.token.TokenIcon import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState import org.burnoutcrew.reorderable.ReorderableLazyListState -import org.burnoutcrew.reorderable.detectReorder - -private const val DOTS = "•••" -private const val GRAY_SCALE_SATURATION = 0f - -val TOKEN_ITEM_HEIGHT: Dp - @Composable - @ReadOnlyComposable - get() = TangemTheme.dimens.size68 +// TODO: Add custom token state: [REDACTED_JIRA] @Composable -internal fun TokenItem(state: TokenItemState, modifier: Modifier = Modifier) { - // TODO: Add custom token state: [REDACTED_JIRA] - when (state) { - is TokenItemState.Content -> ContentTokenItem(state, modifier) - is TokenItemState.Loading -> LoadingTokenItem(modifier) - is TokenItemState.Draggable -> DraggableTokenItem(state, modifier, reorderableTokenListState = null) - is TokenItemState.Unreachable -> UnreachableTokenItem(state, modifier) - is TokenItemState.Locked -> LockedTokenItem(modifier) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun ContentTokenItem(content: TokenItemState.Content, modifier: Modifier = Modifier) { - val hapticFeedback = LocalHapticFeedback.current - InternalTokenItem( - modifier = modifier.combinedClickable( - onClick = content.onItemClick, - onLongClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - content.onItemLongClick() - }, - ), - name = content.name, - tokenIconUrl = content.tokenIconUrl, - tokenIconResId = content.tokenIconResId, - networkBadgeIconResId = content.networkBadgeIconResId, - amount = if (content.tokenOptions is TokenOptionsState.Hidden) DOTS else content.amount, - hasPending = content.hasPending, - options = { ref -> - TokenOptionsBlock( - modifier = Modifier.constrainAsOptionsItem(scope = this, ref), - state = content.tokenOptions, - ) - }, - isTestnet = content.isTestnet, - ) -} - -@Composable -internal fun DraggableTokenItem( - state: TokenItemState.Draggable, +internal fun TokenItem( + state: TokenItemState, modifier: Modifier = Modifier, reorderableTokenListState: ReorderableLazyListState? = null, ) { - InternalTokenItem( - modifier = modifier, - name = state.name, - tokenIconUrl = state.tokenIconUrl, - tokenIconResId = state.tokenIconResId, - networkBadgeIconResId = state.networkBadgeIconResId, - amount = state.fiatAmount, - hasPending = false, - isTestnet = state.isTestnet, - options = { ref -> - Box( - modifier = Modifier - .size(TangemTheme.dimens.size32) - .constrainAsOptionsItem(scope = this, ref) - .let { - if (reorderableTokenListState != null) { - it.detectReorder(reorderableTokenListState) - } else { - it - } - }, - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_drag_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } - }, - ) -} + BaseContainer(modifier = modifier) { + val (iconRef, cryptoInfoRef, fiatInfoRef) = createRefs() -@Composable -internal fun UnreachableTokenItem(state: TokenItemState.Unreachable, modifier: Modifier = Modifier) { - InternalTokenItem( - modifier = modifier, - name = state.name, - tokenIconUrl = state.tokenIconUrl, - tokenIconResId = state.tokenIconResId, - networkBadgeIconResId = state.networkBadgeIconResId, - amount = null, - hasPending = false, - options = { ref -> - Text( - modifier = Modifier.constrainAsOptionsItem(scope = this, ref), - text = stringResource(id = R.string.common_unreachable), - style = TangemTypography.body2, - color = TangemTheme.colors.text.tertiary, - ) - }, - ) -} + TokenIcon( + state = state, + modifier = Modifier.constrainAs(iconRef) { + centerVerticallyTo(parent) + start.linkTo(parent.start) + }, + ) -@Composable -private fun LoadingTokenItem(modifier: Modifier = Modifier) { - BaseSurface(modifier) { - Row( + TokenCryptoInfoBlock( + state = state, modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = TangemTheme.dimens.spacing12, - vertical = TangemTheme.dimens.spacing4, - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), - ) { - CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size72, - height = TangemTheme.dimens.size12, - ), - ) - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size50, - height = TangemTheme.dimens.size12, - ), - ) - } - Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size40, - height = TangemTheme.dimens.size12, - ), - ) - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size40, - height = TangemTheme.dimens.size12, - ), - ) - } - } - } + .padding(horizontal = TangemTheme.dimens.spacing8) + .constrainAs(cryptoInfoRef) { + centerVerticallyTo(parent) + start.linkTo(iconRef.end) + end.linkTo(fiatInfoRef.start) + width = Dimension.fillToConstraints + }, + ) + + TokenFiatInfoBlock( + state = state, + modifier = Modifier.constrainAsOptionsItem(scope = this, ref = fiatInfoRef), + reorderableTokenListState = reorderableTokenListState, + ) } } @Composable -private fun LockedTokenItem(modifier: Modifier = Modifier) { - BaseSurface(modifier) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = TangemTheme.dimens.spacing12, - vertical = TangemTheme.dimens.spacing4, - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), - ) { - Box(modifier = Modifier.size(size = TangemTheme.dimens.size42)) { - Box( - modifier = Modifier - .matchParentSize() - .background( - color = TangemTheme.colors.button.secondary, - shape = CircleShape, - ), - ) - } - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { - LockedContent( - modifier = Modifier.size( - width = TangemTheme.dimens.size72, - height = TangemTheme.dimens.size12, - ), - ) - LockedContent( - modifier = Modifier.size( - width = TangemTheme.dimens.size50, - height = TangemTheme.dimens.size12, - ), - ) - } - Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { - LockedContent( - modifier = Modifier.size( - width = TangemTheme.dimens.size40, - height = TangemTheme.dimens.size12, - ), - ) - LockedContent( - modifier = Modifier.size( - width = TangemTheme.dimens.size40, - height = TangemTheme.dimens.size12, - ), - ) - } - } - } - } -} - -@Composable -private fun LockedContent(modifier: Modifier = Modifier) { - Box( - modifier = modifier.background( - color = TangemTheme.colors.field.primary, - shape = RoundedCornerShape(TangemTheme.dimens.radius6), - ), - ) -} - -/** - * Block for end part of token item - * shows status is reachable, is drag, hidden or show balance - */ -@OptIn(ExperimentalAnimationApi::class) -@Composable -private fun TokenOptionsBlock(state: TokenOptionsState, modifier: Modifier = Modifier) { - AnimatedContent(targetState = state, label = "Update the options", modifier = modifier) { options -> - when (options) { - is TokenOptionsState.Visible -> { - TokenFiatPercentageBlock(fiatAmount = options.fiatAmount, priceChange = options.priceChange) - } - is TokenOptionsState.Hidden -> { - TokenFiatPercentageBlock(fiatAmount = DOTS, priceChange = options.priceChange) - } - } - } -} - -@Suppress("LongParameterList") -@Composable -private fun InternalTokenItem( - name: String, - tokenIconUrl: String?, - @DrawableRes tokenIconResId: Int, - @DrawableRes networkBadgeIconResId: Int?, - amount: String?, - hasPending: Boolean, - options: @Composable ConstraintLayoutScope.(ref: ConstrainedLayoutReference) -> Unit, +inline fun BaseContainer( modifier: Modifier = Modifier, - isTestnet: Boolean = false, + crossinline content: @Composable ConstraintLayoutScope.() -> Unit, ) { Box( modifier = modifier - .defaultMinSize(minHeight = TOKEN_ITEM_HEIGHT) + .defaultMinSize(minHeight = TangemTheme.dimens.size68) .background(color = TangemTheme.colors.background.primary), ) { ConstraintLayout( @@ -322,38 +74,10 @@ private fun InternalTokenItem( .fillMaxSize() .padding( horizontal = TangemTheme.dimens.spacing14, - vertical = TangemTheme.dimens.spacing4, + vertical = TangemTheme.dimens.spacing14, ), - ) { - val (iconItem, tokenNameItem, optionsItem) = createRefs() - - TokenIcon( - modifier = Modifier.constrainAs(iconItem) { - centerVerticallyTo(parent) - start.linkTo(parent.start) - }, - tokenIconUrl = tokenIconUrl, - tokenIconResId = tokenIconResId, - networkBadgeIconRes = networkBadgeIconResId, - isTestnet = isTestnet, - ) - - TokenTitleAmountBlock( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing8) - .constrainAs(tokenNameItem) { - centerVerticallyTo(parent) - start.linkTo(iconItem.end) - end.linkTo(optionsItem.start) - width = Dimension.fillToConstraints - }, - title = name, - amount = amount, - hasPending = hasPending, - ) - - options(optionsItem) - } + content = content, + ) } } @@ -367,173 +91,6 @@ private fun Modifier.constrainAsOptionsItem(scope: ConstraintLayoutScope, ref: C } } -@Composable -private fun BaseSurface( - modifier: Modifier = Modifier, - onClick: (() -> Unit)? = null, - content: @Composable () -> Unit, -) { - Surface( - modifier = modifier.defaultMinSize(minHeight = TOKEN_ITEM_HEIGHT), - color = TangemTheme.colors.background.primary, - onClick = onClick ?: {}, - enabled = onClick != null, - ) { - content() - } -} - -@Composable -private fun TokenTitleAmountBlock(title: String, amount: String?, hasPending: Boolean, modifier: Modifier = Modifier) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), - ) { - Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { - Text( - text = title, - style = TangemTypography.subtitle2, - color = TangemTheme.colors.text.primary1, - ) - - AnimatedVisibility(visible = hasPending, modifier = Modifier.align(Alignment.CenterVertically)) { - Image( - painter = painterResource(id = R.drawable.img_loader_15), - contentDescription = null, - ) - } - } - - AnimatedVisibility(visible = !amount.isNullOrBlank()) { - Text( - text = requireNotNull(amount), - style = TangemTypography.body2, - color = TangemTheme.colors.text.tertiary, - ) - } - } -} - -@OptIn(ExperimentalAnimationApi::class) -@Composable -private fun TokenFiatPercentageBlock( - fiatAmount: String, - priceChange: PriceChangeConfig, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier.requiredWidth(IntrinsicSize.Max)) { - Text( - modifier = Modifier.align(Alignment.End), - text = fiatAmount, - style = TangemTypography.body2, - color = TangemTheme.colors.text.primary1, - ) - SpacerH2() - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, - ) { - AnimatedContent( - targetState = priceChange.type, - label = "Update the price change's arrow", - modifier = Modifier.align(Alignment.CenterVertically), - ) { - Image( - painter = painterResource( - id = when (priceChange.type) { - PriceChangeConfig.Type.UP -> R.drawable.img_arrow_up_8 - PriceChangeConfig.Type.DOWN -> R.drawable.img_arrow_down_8 - }, - ), - contentDescription = null, - ) - } - - SpacerW4() - - AnimatedContent( - targetState = priceChange.type, - label = "Update the price change's arrow", - modifier = Modifier.align(Alignment.CenterVertically), - ) { - Text( - modifier = Modifier.align(Alignment.CenterVertically), - text = priceChange.valueInPercent, - style = TangemTypography.body2, - color = when (priceChange.type) { - PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent - PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning - }, - ) - } - } - } -} - -@Composable -private fun TokenIcon( - tokenIconUrl: String?, - @DrawableRes tokenIconResId: Int, - isTestnet: Boolean, - modifier: Modifier = Modifier, - @DrawableRes networkBadgeIconRes: Int? = null, -) { - Box( - modifier = modifier - .padding(end = TangemTheme.dimens.spacing16) - .size(TangemTheme.dimens.size42), - ) { - val tokenImageModifier = Modifier - .align(Alignment.BottomStart) - .size(TangemTheme.dimens.size36) - - val iconData: Any = remember(tokenIconUrl) { - if (tokenIconUrl.isNullOrEmpty()) tokenIconResId else tokenIconUrl - } - val colorFilter = remember(isTestnet) { - if (isTestnet) { - val colorMatrix = ColorMatrix().also { - it.setToSaturation(GRAY_SCALE_SATURATION) - } - - ColorFilter.colorMatrix(colorMatrix) - } else { - null - } - } - - SubcomposeAsyncImage( - modifier = tokenImageModifier, - model = ImageRequest.Builder(LocalContext.current) - .data(iconData) - .placeholder(tokenIconResId) - .error(tokenIconResId) - .fallback(tokenIconResId) - .crossfade(enable = true) - .build(), - colorFilter = colorFilter, - contentDescription = null, - ) - - AnimatedVisibility( - visible = networkBadgeIconRes != null, - modifier = Modifier - .align(Alignment.TopEnd) - .size(TangemTheme.dimens.size18) - .background(color = Color.White, shape = CircleShape), - ) { - Image( - modifier = Modifier - .padding(all = TangemTheme.dimens.spacing0_5) - .align(Alignment.Center), - painter = painterResource(id = requireNotNull(networkBadgeIconRes)), - colorFilter = colorFilter, - contentDescription = null, - ) - } - } -} - // region preview @Preview diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/LockedRectangle.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/LockedRectangle.kt new file mode 100644 index 0000000000..cf31ecffca --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/LockedRectangle.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.wallet.presentation.common.component.token + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun LockedRectangle(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.background.secondary, + shape = RoundedCornerShape(TangemTheme.dimens.radius4), + ), + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoInfoBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoInfoBlock.kt new file mode 100644 index 0000000000..40740ab8c7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoInfoBlock.kt @@ -0,0 +1,116 @@ +package com.tangem.feature.wallet.presentation.common.component.token + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemTypography +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.common.state.TokenItemState.Companion.DOTS +import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState + +@Composable +internal fun TokenCryptoInfoBlock(state: TokenItemState, modifier: Modifier = Modifier) { + when (state) { + is TokenItemState.ContentState -> ContentBlock(state = state, modifier = modifier) + is TokenItemState.Loading -> LoadingBlock(modifier = modifier) + is TokenItemState.Locked -> LockedBlock(modifier = modifier) + } +} + +@Composable +private fun ContentBlock(state: TokenItemState.ContentState, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + CurrencyNameText( + name = state.name, + hasPending = (state as? TokenItemState.Content)?.hasPending == true, + ) + + AmountText( + amount = when (state) { + is TokenItemState.Content -> if (state.tokenOptions is TokenOptionsState.Hidden) DOTS else state.amount + is TokenItemState.Draggable -> state.fiatAmount + is TokenItemState.Unreachable -> null + }, + ) + } +} + +@Composable +private fun CurrencyNameText(name: String, hasPending: Boolean) { + Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + Text( + text = name, + style = TangemTypography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + + PendingTransactionImage(hasPending = hasPending, modifier = Modifier.align(Alignment.CenterVertically)) + } +} + +@Composable +private fun PendingTransactionImage(hasPending: Boolean, modifier: Modifier = Modifier) { + AnimatedVisibility(visible = hasPending, modifier = modifier) { + Image( + painter = painterResource(id = R.drawable.img_loader_15), + contentDescription = null, + ) + } +} + +@Composable +private fun AmountText(amount: String?) { + AnimatedVisibility(visible = !amount.isNullOrBlank()) { + if (amount == null) return@AnimatedVisibility + Text( + text = amount, + style = TangemTypography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Composable +private fun LoadingBlock(modifier: Modifier = Modifier) { + NonContentContainer(modifier = modifier) { + RectangleShimmer(modifier = Modifier.nameSize(), radius = TangemTheme.dimens.radius4) + RectangleShimmer(modifier = Modifier.amountSize(), radius = TangemTheme.dimens.radius4) + } +} + +@Composable +private fun LockedBlock(modifier: Modifier = Modifier) { + NonContentContainer(modifier = modifier) { + LockedRectangle(modifier = Modifier.nameSize()) + LockedRectangle(modifier = Modifier.amountSize()) + } +} + +@Composable +private fun NonContentContainer(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10), + content = content, + ) +} + +private fun Modifier.nameSize(): Modifier = composed { + return@composed size(width = TangemTheme.dimens.size72, height = TangemTheme.dimens.size12) +} + +private fun Modifier.amountSize(): Modifier = composed { + return@composed size(width = TangemTheme.dimens.size50, height = TangemTheme.dimens.size12) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatInfoBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatInfoBlock.kt new file mode 100644 index 0000000000..ef5d0d5f72 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatInfoBlock.kt @@ -0,0 +1,180 @@ +package com.tangem.feature.wallet.presentation.common.component.token + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerW4 +import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemTypography +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.common.state.TokenItemState.Companion.DOTS +import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState +import org.burnoutcrew.reorderable.ReorderableLazyListState +import org.burnoutcrew.reorderable.detectReorder + +@Composable +internal fun TokenFiatInfoBlock( + state: TokenItemState, + modifier: Modifier = Modifier, + reorderableTokenListState: ReorderableLazyListState? = null, +) { + when (state) { + is TokenItemState.Content -> ContentBlock(state = state.tokenOptions, modifier = modifier) + is TokenItemState.Draggable -> { + DraggableBlock( + modifier = modifier, + reorderableTokenListState = reorderableTokenListState, + ) + } + is TokenItemState.Unreachable -> UnreachableBlock(modifier = modifier) + is TokenItemState.Loading -> LoadingBlock(modifier = modifier) + is TokenItemState.Locked -> LockedBlock(modifier = modifier) + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun ContentBlock(state: TokenOptionsState, modifier: Modifier = Modifier) { + Column( + modifier = modifier.requiredWidth(IntrinsicSize.Max), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + AnimatedContent( + targetState = state, + label = "Update the fiat percentage block", + modifier = Modifier.align(Alignment.End), + ) { + Text( + text = when (it) { + is TokenOptionsState.Visible -> it.fiatAmount + is TokenOptionsState.Hidden -> DOTS + }, + style = TangemTypography.body2, + color = TangemTheme.colors.text.primary1, + ) + } + + PriceChangeBlock(config = state.config) + } +} + +@Composable +private fun PriceChangeBlock(config: PriceChangeConfig) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + PriceChangeIcon(type = config.type, modifier = Modifier.align(Alignment.CenterVertically)) + SpacerW4() + PriceChangeText(config = config, modifier = Modifier.align(Alignment.CenterVertically)) + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun PriceChangeIcon(type: PriceChangeConfig.Type, modifier: Modifier = Modifier) { + AnimatedContent( + targetState = type, + label = "Update the price change's arrow", + modifier = modifier, + ) { + Image( + painter = painterResource( + id = when (it) { + PriceChangeConfig.Type.UP -> R.drawable.img_arrow_up_8 + PriceChangeConfig.Type.DOWN -> R.drawable.img_arrow_down_8 + }, + ), + contentDescription = null, + ) + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun PriceChangeText(config: PriceChangeConfig, modifier: Modifier = Modifier) { + AnimatedContent( + targetState = config.type, + label = "Update the price change's arrow", + modifier = modifier, + ) { + Text( + text = config.valueInPercent, + style = TangemTypography.body2, + color = when (it) { + PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent + PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning + }, + ) + } +} + +@Composable +private fun DraggableBlock(reorderableTokenListState: ReorderableLazyListState?, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size32) + .then( + other = if (reorderableTokenListState != null) { + Modifier.detectReorder(reorderableTokenListState) + } else { + Modifier + }, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_drag_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } +} + +@Composable +private fun UnreachableBlock(modifier: Modifier = Modifier) { + Text( + modifier = modifier, + text = stringResource(id = R.string.common_unreachable), + style = TangemTypography.body2, + color = TangemTheme.colors.text.tertiary, + ) +} + +@Composable +private fun LoadingBlock(modifier: Modifier = Modifier) { + NonContentContainer(modifier = modifier) { + RectangleShimmer(modifier = Modifier.viewSize(), radius = TangemTheme.dimens.radius4) + RectangleShimmer(modifier = Modifier.viewSize(), radius = TangemTheme.dimens.radius4) + } +} + +@Composable +private fun LockedBlock(modifier: Modifier = Modifier) { + NonContentContainer(modifier = modifier) { + LockedRectangle(modifier = Modifier.viewSize()) + LockedRectangle(modifier = Modifier.viewSize()) + } +} + +@Composable +private fun NonContentContainer(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10), + content = content, + ) +} + +private fun Modifier.viewSize(): Modifier = composed { + return@composed size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenIcon.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenIcon.kt new file mode 100644 index 0000000000..6ddfe82790 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenIcon.kt @@ -0,0 +1,152 @@ +package com.tangem.feature.wallet.presentation.common.component.token + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.presentation.common.state.TokenItemState + +private const val GRAY_SCALE_SATURATION = 0f + +@Composable +internal fun TokenIcon(state: TokenItemState, modifier: Modifier = Modifier) { + when (state) { + is TokenItemState.ContentState -> ContentIcon(content = state, modifier = modifier) + is TokenItemState.Loading -> LoadingIcon(modifier = modifier) + is TokenItemState.Locked -> LockedIcon(modifier = modifier) + } +} + +@Composable +private fun ContentIcon(content: TokenItemState.ContentState, modifier: Modifier = Modifier) { + BaseContainer(modifier = modifier) { + val isTestnet = when (content) { + is TokenItemState.Content -> content.isTestnet + is TokenItemState.Draggable -> content.isTestnet + is TokenItemState.Unreachable -> false + } + + val colorFilter = remember(isTestnet) { + if (isTestnet) { + ColorFilter.colorMatrix( + colorMatrix = ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }, + ) + } else { + null + } + } + + Icon( + content = content, + colorFilter = colorFilter, + modifier = Modifier.align(Alignment.BottomStart), + ) + + NetworkBadge( + iconResId = content.networkBadgeIconResId, + colorFilter = colorFilter, + modifier = Modifier.align(Alignment.TopEnd), + ) + } +} + +@Composable +private fun Icon(content: TokenItemState.ContentState, colorFilter: ColorFilter?, modifier: Modifier = Modifier) { + val iconUrl = content.tokenIconUrl + val iconData: Any = remember(iconUrl) { + if (iconUrl.isNullOrEmpty()) content.tokenIconResId else iconUrl + } + + SubcomposeAsyncImage( + modifier = modifier.iconSize(), + model = ImageRequest.Builder(context = LocalContext.current) + .data(data = iconData) + .placeholder(drawableResId = content.tokenIconResId) + .error(drawableResId = content.tokenIconResId) + .fallback(drawableResId = content.tokenIconResId) + .crossfade(enable = true) + .build(), + colorFilter = colorFilter, + contentDescription = null, + ) +} + +@Composable +private fun BoxScope.NetworkBadge( + @DrawableRes iconResId: Int?, + colorFilter: ColorFilter?, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = iconResId != null, + modifier = modifier + .size(TangemTheme.dimens.size18) + .background(color = TangemTheme.colors.background.primary, shape = CircleShape), + ) { + if (iconResId == null) return@AnimatedVisibility + + Image( + modifier = Modifier + .padding(all = TangemTheme.dimens.spacing2) + .align(Alignment.Center), + painter = painterResource(id = iconResId), + colorFilter = colorFilter, + contentDescription = null, + ) + } +} + +@Composable +private fun LoadingIcon(modifier: Modifier = Modifier) { + BaseContainer(modifier) { + CircleShimmer( + modifier = Modifier + .iconSize() + .align(alignment = Alignment.BottomStart), + ) + } +} + +@Composable +private fun LockedIcon(modifier: Modifier = Modifier) { + BaseContainer(modifier) { + Box( + modifier = Modifier + .iconSize() + .align(Alignment.BottomStart), + ) { + Box( + modifier = Modifier + .matchParentSize() + .background(color = TangemTheme.colors.background.secondary, shape = CircleShape), + ) + } + } +} + +@Composable +private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { + Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content) +} + +private fun Modifier.iconSize(): Modifier = composed { + return@composed this.size(size = TangemTheme.dimens.size36) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index d732e148c0..8310f134d5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -14,92 +14,111 @@ internal sealed interface TokenItemState { /** Loading token state */ data class Loading(override val id: String) : TokenItemState + /** Locked token state */ data class Locked(override val id: String) : TokenItemState + /** Content state */ + sealed class ContentState( + override val id: String, + open val tokenIconUrl: String?, + @DrawableRes open val tokenIconResId: Int, + @DrawableRes open val networkBadgeIconResId: Int?, + open val name: String, + ) : TokenItemState + /** * Content token state * - * @property id unique id - * @property tokenIconUrl token icon url - * @property tokenIconResId token icon resource id - * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin - * @property name token name - * @property amount amount of token - * @property hasPending pending tx in blockchain - * @property tokenOptions state for token options - * @property isTestnet indicates whether the token is from test network or not - * @property onItemClick callback which will be called when an item is clicked - * @property onItemLongClick callback which will be called when an item is long clicked + * @property id unique id + * @property tokenIconUrl token icon url + * @property tokenIconResId token icon resource id + * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin + * @property name token name + * @property amount amount of token + * @property hasPending pending tx in blockchain + * @property tokenOptions state for token options + * @property isTestnet indicates whether the token is from test network or not + * @property onItemClick callback which will be called when an item is clicked + * @property onItemLongClick callback which will be called when an item is long clicked */ data class Content( override val id: String, - val tokenIconUrl: String?, - @DrawableRes val tokenIconResId: Int, - @DrawableRes val networkBadgeIconResId: Int?, - val name: String, + override val tokenIconUrl: String?, + @DrawableRes override val tokenIconResId: Int, + @DrawableRes override val networkBadgeIconResId: Int?, + override val name: String, val amount: String, val hasPending: Boolean, val tokenOptions: TokenOptionsState, val isTestnet: Boolean, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, - ) : TokenItemState + ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name) /** * Draggable token state * - * @property id unique id - * @property tokenIconUrl token icon url - * @property tokenIconResId token icon resource id - * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin - * @property name token name - * @property fiatAmount fiat amount of token - * @property isTestnet indicates whether the token is from test network or not + * @property id unique id + * @property tokenIconUrl token icon url + * @property tokenIconResId token icon resource id + * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin + * @property name token name + * @property fiatAmount fiat amount of token + * @property isTestnet indicates whether the token is from test network or not */ data class Draggable( override val id: String, - val tokenIconUrl: String?, - @DrawableRes val tokenIconResId: Int, - @DrawableRes val networkBadgeIconResId: Int?, - val name: String, + override val tokenIconUrl: String?, + @DrawableRes override val tokenIconResId: Int, + @DrawableRes override val networkBadgeIconResId: Int?, + override val name: String, val fiatAmount: String, val isTestnet: Boolean, - ) : TokenItemState + ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name) /** * Unreachable token state * - * @property id token id - * @property tokenIconUrl token icon url - * @property tokenIconResId token icon resource id - * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin - * @property name token name + * @property id token id + * @property tokenIconUrl token icon url + * @property tokenIconResId token icon resource id + * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin + * @property name token name */ data class Unreachable( override val id: String, - val tokenIconUrl: String?, - @DrawableRes val tokenIconResId: Int, - @DrawableRes val networkBadgeIconResId: Int?, - val name: String, - ) : TokenItemState + override val tokenIconUrl: String?, + @DrawableRes override val tokenIconResId: Int, + @DrawableRes override val networkBadgeIconResId: Int?, + override val name: String, + ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name) /** Token options state */ @Immutable sealed interface TokenOptionsState { + val config: PriceChangeConfig + /** * Visible token options state * - * @property fiatAmount fiat amount of token - * @property priceChange value of price changing + * @property fiatAmount fiat amount of token + * @property config value of price changing */ - data class Visible(val fiatAmount: String, val priceChange: PriceChangeConfig) : TokenOptionsState + data class Visible( + override val config: PriceChangeConfig, + val fiatAmount: String, + ) : TokenOptionsState /** * Hidden token options state * - * @property priceChange value of price changing + * @property config value of price changing */ - data class Hidden(val priceChange: PriceChangeConfig) : TokenOptionsState + data class Hidden(override val config: PriceChangeConfig) : TokenOptionsState + } + + companion object { + const val DOTS = "•••" } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index cd55c2f577..238ff49a18 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -33,7 +33,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkGroupItem -import com.tangem.feature.wallet.presentation.common.component.DraggableTokenItem +import com.tangem.feature.wallet.presentation.common.component.TokenItem import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState @@ -151,7 +151,7 @@ private fun LazyItemScope.DraggableItem( networkName = item.networkName, reorderableTokenListState = reorderableState, ) - is DraggableItem.Token -> DraggableTokenItem( + is DraggableItem.Token -> TokenItem( modifier = itemModifier, state = item.tokenItemState, reorderableTokenListState = reorderableState, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index 5c860ab3e5..b19dcf646a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -47,7 +47,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter( } else { TokenItemState.TokenOptionsState.Visible( fiatAmount = getFormattedFiatAmount(), - priceChange = getPriceChangeConfig(), + config = getPriceChangeConfig(), ) }, isTestnet = currency.network.isTestnet,