From 050c7b6106e52edd4885eb1eb99bb3781ab4e6d1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 24 Jan 2026 13:48:16 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../core/ui/components/account/AccountIcon.kt | 104 +++++++-- .../components/currency/icon/ContentIcon.kt | 5 +- .../components/currency/icon/CurrencyIcon.kt | 18 +- .../currency/icon/CurrencyIconState.kt | 4 + .../core/ui/components/token/TokenItem.kt | 61 ++++-- .../components/token/internal/TokenTitle.kt | 2 +- .../ui/components/tokenlist/TokenListItem.kt | 201 ++++++++++++++---- .../organizetokens/OrganizeTokensScreen.kt | 1 + .../presentation/wallet/ui/WalletScreen.kt | 67 +----- .../wallet/ui/components/WalletsList.kt | 3 +- .../ui/components/common/WalletContent.kt | 5 +- .../MultiCurrencyAccountContent.kt | 117 +++++----- .../multicurrency/MultiCurrencyContent.kt | 3 - 13 files changed, 369 insertions(+), 222 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/account/AccountIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/account/AccountIcon.kt index ab7844aaa5..dd5ea8c8a6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/account/AccountIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/account/AccountIcon.kt @@ -2,12 +2,21 @@ package com.tangem.core.ui.components.account import android.content.res.Configuration import androidx.annotation.DrawableRes +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -15,7 +24,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.tangem.core.ui.R import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -36,14 +47,28 @@ enum class AccountIconSize { */ @Composable fun AccountResIcon(@DrawableRes resId: Int, color: Color, size: AccountIconSize, modifier: Modifier = Modifier) { - val boxModifier = modifier.selectBoxModifier(size) - val iconSize = Modifier.selectIconSize(size) + val boxSize by animateDpAsState( + targetValue = size.boxSizeInDp(), + animationSpec = animation(), + ) + val boxShapeCornerSize by animateDpAsState( + targetValue = size.boxShapeSizeInDp(), + animationSpec = animation(), + ) + val iconSize by animateDpAsState( + targetValue = size.iconSizeInDp(), + animationSpec = animation(), + ) + Box( contentAlignment = Alignment.Center, - modifier = boxModifier.background(color), + modifier = modifier + .size(boxSize) + .clip(RoundedCornerShape(boxShapeCornerSize)) + .background(color), ) { Icon( - modifier = iconSize, + modifier = Modifier.size(iconSize), tint = TangemTheme.colors.text.constantWhite, imageVector = ImageVector.vectorResource(id = resId), contentDescription = null, @@ -64,7 +89,14 @@ fun AccountResIcon(@DrawableRes resId: Int, color: Color, size: AccountIconSize, */ @Composable fun AccountCharIcon(char: Char, color: Color, size: AccountIconSize, modifier: Modifier = Modifier) { - val boxModifier = modifier.selectBoxModifier(size) + val boxSize by animateDpAsState( + size.boxSizeInDp(), + animationSpec = animation(), + ) + val boxShapeCornerSize by animateDpAsState( + size.boxShapeSizeInDp(), + animationSpec = animation(), + ) val textStyle = when (size) { AccountIconSize.Default -> TangemTheme.typography.h3 AccountIconSize.Large -> TangemTheme.typography.h1 @@ -72,32 +104,52 @@ fun AccountCharIcon(char: Char, color: Color, size: AccountIconSize, modifier: M AccountIconSize.Small -> TangemTheme.typography.subtitle2 AccountIconSize.ExtraSmall -> TangemTheme.typography.caption1 } + + val textSize by animateFloatAsState( + textStyle.fontSize.value, + animationSpec = animation(), + ) + Box( contentAlignment = Alignment.Center, - modifier = boxModifier.background(color), + modifier = modifier + .size(boxSize) + .clip(RoundedCornerShape(boxShapeCornerSize)) + .background(color), ) { Text( + modifier = Modifier.animateContentSize(animationSpec = animation()), text = char.uppercase(), - style = textStyle, + style = textStyle.copy(fontSize = textSize.sp), color = TangemTheme.colors.text.constantWhite, ) } } -private fun Modifier.selectIconSize(size: AccountIconSize): Modifier = when (size) { - AccountIconSize.Default -> this.size(20.dp) - AccountIconSize.Large -> this.size(40.dp) - AccountIconSize.Medium -> this.size(16.dp) - AccountIconSize.Small -> this.size(12.dp) - AccountIconSize.ExtraSmall -> this.size(8.dp) +private fun animation() = tween(durationMillis = 350) + +private fun AccountIconSize.iconSizeInDp(): Dp = when (this) { + AccountIconSize.Default -> 20.dp + AccountIconSize.Large -> 40.dp + AccountIconSize.Medium -> 16.dp + AccountIconSize.Small -> 12.dp + AccountIconSize.ExtraSmall -> 8.dp } -private fun Modifier.selectBoxModifier(size: AccountIconSize): Modifier = when (size) { - AccountIconSize.Default -> size(36.dp).clip(RoundedCornerShape(10.dp)) - AccountIconSize.Large -> size(88.dp).clip(RoundedCornerShape(24.dp)) - AccountIconSize.Medium -> size(28.dp).clip(RoundedCornerShape(8.dp)) - AccountIconSize.Small -> size(20.dp).clip(RoundedCornerShape(6.dp)) - AccountIconSize.ExtraSmall -> size(14.dp).clip(RoundedCornerShape(4.dp)) +private fun AccountIconSize.boxSizeInDp(): Dp = when (this) { + AccountIconSize.Default -> 36.dp + AccountIconSize.Large -> 88.dp + AccountIconSize.Medium -> 28.dp + AccountIconSize.Small -> 20.dp + AccountIconSize.ExtraSmall -> 14.dp +} + +private fun AccountIconSize.boxShapeSizeInDp(): Dp = when (this) { + AccountIconSize.Default -> 10.dp + AccountIconSize.Large -> 24.dp + AccountIconSize.Medium -> 8.dp + AccountIconSize.Small -> 6.dp + AccountIconSize.ExtraSmall -> 4.dp } @Preview(showBackground = true) @@ -115,7 +167,20 @@ private fun Preview() { @Composable private fun Sample() { + var sizeState by remember { mutableStateOf(AccountIconSize.ExtraSmall) } + Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + AccountResIcon(resId = R.drawable.ic_shirt_24, color = Color.Red, size = sizeState) + Button(onClick = { + sizeState = when (sizeState) { + AccountIconSize.Default -> AccountIconSize.Large + AccountIconSize.Large -> AccountIconSize.Medium + AccountIconSize.Medium -> AccountIconSize.Small + AccountIconSize.Small -> AccountIconSize.ExtraSmall + AccountIconSize.ExtraSmall -> AccountIconSize.Default + } + }) { Text("Change") } + AccountResIcon(resId = R.drawable.ic_shirt_24, color = Color.Red, size = AccountIconSize.Default) AccountResIcon(resId = R.drawable.ic_rounded_star_24, color = Color.Blue, size = AccountIconSize.Large) AccountResIcon(resId = R.drawable.ic_user_24, color = Color.Magenta, size = AccountIconSize.Medium) @@ -123,6 +188,7 @@ private fun Sample() { AccountResIcon(resId = R.drawable.ic_wallet_24, color = Color.Green, size = AccountIconSize.ExtraSmall) } Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + AccountCharIcon(char = 'D', color = Color.Red, size = sizeState) AccountCharIcon(char = 'D', color = Color.Red, size = AccountIconSize.Default) AccountCharIcon(char = 'L', color = Color.Blue, size = AccountIconSize.Large) AccountCharIcon(char = 'M', color = Color.Magenta, size = AccountIconSize.Medium) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt index e8fa3055bf..27dceeec39 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt @@ -14,7 +14,6 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import com.tangem.core.ui.R import com.tangem.core.ui.components.account.AccountCharIcon -import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.account.AccountResIcon import com.tangem.core.ui.components.currency.DefaultCurrencyIcon import com.tangem.core.ui.extensions.resolveReference @@ -66,13 +65,13 @@ internal fun ContentIcon( modifier = modifier, resId = icon.resId, color = icon.color, - size = AccountIconSize.Default, + size = icon.size, ) is CurrencyIconState.CryptoPortfolio.Letter -> AccountCharIcon( modifier = modifier, char = icon.char.resolveReference().first(), color = icon.color, - size = AccountIconSize.Default, + size = icon.size, ) CurrencyIconState.Loading, CurrencyIconState.Locked, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt index bb17dc63b6..21e71e7535 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.getGreyScaleColorFilter @@ -33,12 +34,20 @@ fun CurrencyIcon( state: CurrencyIconState, modifier: Modifier = Modifier, shouldDisplayNetwork: Boolean = true, + withFixedSize: Boolean = true, iconSize: Dp = 36.dp, ) { - BaseContainer(modifier = modifier) { + Box( + modifier = modifier + .conditional(withFixedSize) { + size(size = 40.dp) + }, + ) { val iconModifier = Modifier .align(Alignment.Center) - .size(iconSize) + .conditional(withFixedSize) { + size(iconSize) + } when (state) { is CurrencyIconState.Loading -> LoadingIcon(modifier = iconModifier) @@ -132,9 +141,4 @@ private fun BoxScope.ContentIconContainer( modifier = Modifier.align(Alignment.BottomEnd), ) } -} - -@Composable -private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { - Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt index 420e0a839f..5bdd5f41f4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt @@ -4,6 +4,7 @@ import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color import com.tangem.core.ui.R +import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.extensions.TextReference /** @@ -92,17 +93,20 @@ sealed class CurrencyIconState { override val shouldShowCustomBadge: Boolean = false override val topBadgeIconResId: Int? = null abstract val color: Color + abstract val size: AccountIconSize data class Icon( @DrawableRes val resId: Int, override val color: Color, override val isGrayscale: Boolean, + override val size: AccountIconSize = AccountIconSize.Default, ) : CryptoPortfolio() data class Letter( val char: TextReference, override val color: Color, override val isGrayscale: Boolean, + override val size: AccountIconSize = AccountIconSize.Default, ) : CryptoPortfolio() } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt index fc56519c01..631d330388 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.graphics.Color @@ -31,6 +32,7 @@ import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState import com.tangem.core.ui.components.token.state.TokenItemState.Subtitle2State import com.tangem.core.ui.components.token.state.TokenItemState.PromoBannerState +import com.tangem.core.ui.components.token.state.TokenItemState.TitleState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.extensions.stringReference @@ -65,6 +67,7 @@ fun TokenItem( state: TokenItemState, isBalanceHidden: Boolean, modifier: Modifier = Modifier, + composables: TokenItemComposables? = null, itemPaddingValues: PaddingValues = PaddingValues(horizontal = TangemTheme.dimens.spacing12), ) { TokenItem( @@ -73,9 +76,16 @@ fun TokenItem( modifier = modifier, reorderableTokenListState = null, itemPaddingValues = itemPaddingValues, + composables = composables, ) } +@Stable +class TokenItemComposables( + val title: @Composable (Modifier) -> Unit, + val icon: @Composable (Modifier) -> Unit, +) + /** * Token item for reorderable list * @@ -88,6 +98,7 @@ fun TokenItem( * @see Figma Component */ +@Suppress("LongMethod") @Composable fun TokenItem( state: TokenItemState, @@ -95,6 +106,7 @@ fun TokenItem( reorderableTokenListState: ReorderableLazyListState?, modifier: Modifier = Modifier, itemPaddingValues: PaddingValues = PaddingValues(horizontal = TangemTheme.dimens.spacing12), + composables: TokenItemComposables? = null, ) { val betweenRowsMargin = TangemTheme.dimens.spacing2 @@ -104,13 +116,22 @@ fun TokenItem( .tokenClickable(state = state) .padding(itemPaddingValues), ) { - CurrencyIcon( - state = state.iconState, - modifier = Modifier - .layoutId(layoutId = LayoutId.ICON) - .padding(end = TangemTheme.dimens.spacing8) - .testTag(TokenElementsTestTags.TOKEN_ICON), - ) + if (composables != null) { + composables.icon.invoke( + Modifier + .layoutId(layoutId = LayoutId.ICON) + .padding(end = TangemTheme.dimens.spacing8) + .testTag(TokenElementsTestTags.TOKEN_ICON), + ) + } else { + CurrencyIcon( + state = state.iconState, + modifier = Modifier + .layoutId(layoutId = LayoutId.ICON) + .padding(end = TangemTheme.dimens.spacing8) + .testTag(TokenElementsTestTags.TOKEN_ICON), + ) + } YieldSupplyPromoBanner( state = state.promoBannerState, @@ -120,14 +141,24 @@ fun TokenItem( .fillMaxWidth(), ) - TokenTitle( - state = state.titleState, - modifier = Modifier - .layoutId(layoutId = LayoutId.TITLE) - .padding(end = TangemTheme.dimens.spacing8) - .padding(bottom = betweenRowsMargin) - .testTag(TokenElementsTestTags.TOKEN_TITLE), - ) + if (composables != null) { + composables.title.invoke( + Modifier + .layoutId(layoutId = LayoutId.TITLE) + .padding(end = TangemTheme.dimens.spacing8) + .padding(bottom = betweenRowsMargin) + .testTag(TokenElementsTestTags.TOKEN_TITLE), + ) + } else { + TokenTitle( + state = state.titleState, + modifier = Modifier + .layoutId(layoutId = LayoutId.TITLE) + .padding(end = TangemTheme.dimens.spacing8) + .padding(bottom = betweenRowsMargin) + .testTag(TokenElementsTestTags.TOKEN_TITLE), + ) + } TokenFiatAmount( state = state.fiatAmountState, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt index a60f268fcb..2cbd277ce7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt @@ -21,7 +21,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.components.token.state.TokenItemState.TitleState as TokenTitleState @Composable -internal fun TokenTitle(state: TokenTitleState?, modifier: Modifier = Modifier) { +fun TokenTitle(state: TokenTitleState?, modifier: Modifier = Modifier) { when (state) { is TokenTitleState.Content -> { ContentTitle(state = state, modifier = modifier) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt index b5539edab3..797beea50b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt @@ -1,13 +1,20 @@ package com.tangem.core.ui.components.tokenlist +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.BoundsTransform +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -18,9 +25,12 @@ import com.tangem.core.ui.components.SpacerW4 import com.tangem.core.ui.components.account.AccountCharIcon import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.account.AccountResIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.token.TokenItem +import com.tangem.core.ui.components.token.TokenItemComposables +import com.tangem.core.ui.components.token.internal.TokenTitle import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.internal.GroupTitleItem import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM @@ -57,16 +67,80 @@ fun TokenListItem(state: TokensListItemUM, isBalanceHidden: Boolean, modifier: M } } +@Suppress("MagicNumber", "ReusedModifierInstance") @Composable fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { - if (state.isExpanded) { - ExpandedPortfolioHeader(state = state.tokenItemUM, isCollapsable = state.isCollapsable, modifier = modifier) - } else { - TokenItem( - state = state.tokenItemUM, - isBalanceHidden = isBalanceHidden, - modifier = modifier, - ) + // Box is not needed here, but due to a bug in compose + // we use it like a wrapper to properly handle lookahead scope inside this composable + Box(modifier) { + SharedTransitionLayout { + AnimatedContent( + state.isExpanded, + transitionSpec = { + fadeIn(animationSpec = tween(350, delayMillis = 90)) + .togetherWith(fadeOut(animationSpec = tween(350))) + }, + ) { isExpanded -> + val animatedContentScope = this + val iconSharedContentState = rememberSharedContentState(key = "icon") + val titleSharedContentState = rememberSharedContentState(key = "title") + val boundsTransform = BoundsTransform { _, _ -> tween(350) } + + val composables = + TokenItemComposables( + icon = { modifier: Modifier -> + val iconState = when (val icon = state.tokenItemUM.iconState) { + is CurrencyIconState.CryptoPortfolio.Icon -> + icon.copy( + size = if (isExpanded) AccountIconSize.ExtraSmall else AccountIconSize.Default, + ) + is CurrencyIconState.CryptoPortfolio.Letter -> + icon.copy( + size = if (isExpanded) AccountIconSize.ExtraSmall else AccountIconSize.Default, + ) + else -> icon + } + + CurrencyIcon( + state = iconState, + withFixedSize = false, + modifier = modifier + .sharedBounds( + sharedContentState = iconSharedContentState, + animatedVisibilityScope = animatedContentScope, + boundsTransform = boundsTransform, + ), + ) + }, + title = { modifier: Modifier -> + TokenTitle( + state = state.tokenItemUM.titleState, + modifier = modifier + .sharedElement( + sharedContentState = titleSharedContentState, + animatedVisibilityScope = animatedContentScope, + boundsTransform = boundsTransform, + placeholderSize = SharedTransitionScope.PlaceholderSize.AnimatedSize, + ), + ) + }, + ) + + if (isExpanded) { + ExpandedPortfolioHeader( + state = state.tokenItemUM, + isCollapsable = state.isCollapsable, + composables = composables, + ) + } else { + TokenItem( + state = state.tokenItemUM, + isBalanceHidden = isBalanceHidden, + composables = composables, + ) + } + } + } } } @@ -82,52 +156,89 @@ fun PortfolioTokensListItem(state: PortfolioTokensListItemUM, isBalanceHidden: B } } +@Suppress("LongMethod") @Composable -fun ExpandedPortfolioHeader(state: TokenItemState, isCollapsable: Boolean, modifier: Modifier = Modifier) { +fun ExpandedPortfolioHeader( + state: TokenItemState, + isCollapsable: Boolean, + composables: TokenItemComposables?, + modifier: Modifier = Modifier, +) { Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier .fillMaxWidth() .clickable(onClick = { state.onItemClick?.invoke(state) }) .padding( - vertical = TangemTheme.dimens.spacing4, - horizontal = TangemTheme.dimens.spacing12, + vertical = if (composables != null) 8.dp else 4.dp, + horizontal = 12.dp, ), ) { - when (val icon = state.iconState) { - is CurrencyIconState.CryptoPortfolio.Icon -> AccountResIcon( - resId = icon.resId, - color = icon.color, - size = AccountIconSize.ExtraSmall, - ) - is CurrencyIconState.CryptoPortfolio.Letter -> AccountCharIcon( - char = icon.char.resolveReference().first(), - color = icon.color, - size = AccountIconSize.ExtraSmall, - ) - is CurrencyIconState.CoinIcon, - is CurrencyIconState.CustomTokenIcon, - is CurrencyIconState.Empty, - is CurrencyIconState.FiatIcon, - CurrencyIconState.Loading, - CurrencyIconState.Locked, - is CurrencyIconState.TokenIcon, - -> Unit + if (composables != null) { + composables.icon.invoke(Modifier) + } else { + when (val icon = state.iconState) { + is CurrencyIconState.CryptoPortfolio.Icon -> AccountResIcon( + resId = icon.resId, + color = icon.color, + size = AccountIconSize.ExtraSmall, + ) + is CurrencyIconState.CryptoPortfolio.Letter -> AccountCharIcon( + char = icon.char.resolveReference().first(), + color = icon.color, + size = AccountIconSize.ExtraSmall, + ) + is CurrencyIconState.CoinIcon, + is CurrencyIconState.CustomTokenIcon, + is CurrencyIconState.Empty, + is CurrencyIconState.FiatIcon, + CurrencyIconState.Loading, + CurrencyIconState.Locked, + is CurrencyIconState.TokenIcon, + -> Unit + } } SpacerW4() - when (val titleState = state.titleState) { - TokenItemState.TitleState.Loading -> Unit - TokenItemState.TitleState.Locked -> Unit - is TokenItemState.TitleState.Content -> Text( - modifier = Modifier.weight(1f), - text = titleState.text.resolveReference(), - color = TangemTheme.colors.text.primary1, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = TangemTheme.typography.caption1, - ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f), + ) { + if (composables != null) { + composables.title.invoke(Modifier.alignByBaseline()) + } else { + when (val titleState = state.titleState) { + TokenItemState.TitleState.Loading -> Unit + TokenItemState.TitleState.Locked -> Unit + is TokenItemState.TitleState.Content -> Text( + text = titleState.text.resolveReference(), + color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTheme.typography.caption1, + ) + } + } + + val text = when (val fiatAmountState = state.fiatAmountState) { + is TokenItemState.FiatAmountState.Content -> fiatAmountState.text + is TokenItemState.FiatAmountState.TextContent -> fiatAmountState.text + else -> null + } + + AnimatedVisibility(text != null) { + Text( + text = text.orEmpty(), + modifier = Modifier + .alignByBaseline() + .padding(horizontal = 8.dp), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } } if (isCollapsable) { 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 6478fc9f80..08a1123ea0 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 @@ -218,6 +218,7 @@ private fun LazyItemScope.DraggableItem( is DraggableItem.Portfolio -> ExpandedPortfolioHeader( state = item.tokenItemState, isCollapsable = false, + composables = null, modifier = modifierWithBackground .padding(top = 8.dp) .fillMaxWidth(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 803f2d873b..86a1cc06ae 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.* import androidx.compose.runtime.* -import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind @@ -57,7 +56,6 @@ import com.tangem.core.ui.components.rememberIsKeyboardVisible import com.tangem.core.ui.components.sheetscaffold.* import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar import com.tangem.core.ui.components.snackbar.TangemSnackbar -import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.stringResourceSafe @@ -79,7 +77,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistor import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.common.* -import com.tangem.feature.wallet.presentation.wallet.ui.components.fastForEach import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.nftCollections import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock @@ -154,7 +151,6 @@ private fun WalletContent( */ val selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) } val selectedWallet = state.wallets.getOrElse(selectedWalletIndex) { state.wallets[state.selectedWalletIndex] } - val (expandedState, collapsedState) = getExpandPortfolioStates(selectedWallet) val listState = rememberLazyListState() @@ -199,6 +195,7 @@ private fun WalletContent( contentType = state.wallets.map { it.walletCardState.id }, ) { WalletsList( + modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), lazyListState = walletsListState, wallets = state.wallets.map(WalletState::walletCardState).toImmutableList(), isBalanceHidden = state.isHidingMode, @@ -263,13 +260,6 @@ private fun WalletContent( txHistoryItems = txHistoryItems, isBalanceHidden = state.isHidingMode, modifier = movableItemModifier, - portfolioVisibleState = { - findPortfolioVisibleState( - portfolio = it, - expandedState = expandedState, - collapsedState = collapsedState, - ) - }, ) nftCollections(state = selectedWallet, itemModifier = itemModifier) @@ -307,61 +297,6 @@ private fun WalletContent( ) } -private fun findPortfolioVisibleState( - portfolio: TokensListItemUM.Portfolio, - expandedState: SnapshotStateMap>, - collapsedState: SnapshotStateMap>, -): MutableTransitionState { - val portfolioKey = portfolio.id - - fun forceVisible() = MutableTransitionState(true).apply { targetState = true } - val shouldExpand = portfolio.isExpanded - return if (shouldExpand) { - collapsedState[portfolioKey] ?: forceVisible() - } else { - expandedState[portfolioKey] ?: forceVisible() - } -} - -@Composable -private fun getExpandPortfolioStates( - state: WalletState, -): Pair< - SnapshotStateMap>, - SnapshotStateMap>, - > { - val expandedTransitionState = - remember { mutableStateMapOf>() } - val collapsedTransitionState = - remember { mutableStateMapOf>() } - - val portfolioContent = state is WalletState.MultiCurrency.Content && - state.tokensListState is WalletTokensListState.ContentState.PortfolioContent - if (!portfolioContent) { - return expandedTransitionState to collapsedTransitionState - } - - state.tokensListState.items.fastForEach { portfolio -> - val portfolioKey = portfolio.id - val shouldExpand = portfolio.isExpanded - - fun toggleVisible() = MutableTransitionState(false).apply { targetState = true } - fun forceVisible() = MutableTransitionState(true).apply { targetState = true } - - val isFirstCall = collapsedTransitionState[portfolioKey] == null || - expandedTransitionState[portfolioKey] == null - when { - isFirstCall -> { - collapsedTransitionState[portfolioKey] = forceVisible() - expandedTransitionState[portfolioKey] = forceVisible() - } - shouldExpand -> expandedTransitionState[portfolioKey] = toggleVisible() - else -> collapsedTransitionState[portfolioKey] = toggleVisible() - } - } - return expandedTransitionState to collapsedTransitionState -} - @Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod") @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index e09c48d1c0..bddfb23fb1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -47,13 +47,14 @@ internal fun WalletsList( lazyListState: LazyListState, wallets: ImmutableList, isBalanceHidden: Boolean, + modifier: Modifier = Modifier, ) { val horizontalCardPadding = TangemTheme.dimens.spacing16 val screenWidth = LocalWindowSize.current.width val itemWidth by remember(screenWidth) { derivedStateOf { screenWidth - horizontalCardPadding * 2 } } LazyRow( - modifier = Modifier.background(color = TangemTheme.colors.background.secondary), + modifier = modifier.background(color = TangemTheme.colors.background.secondary), state = lazyListState, contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt index 6c19807add..c4ffea5ea7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt @@ -1,10 +1,8 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common -import androidx.compose.animation.core.MutableTransitionState import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.ui.Modifier import androidx.paging.compose.LazyPagingItems -import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.txHistoryItems import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState @@ -24,11 +22,10 @@ internal fun LazyListScope.contentItems( txHistoryItems: LazyPagingItems?, isBalanceHidden: Boolean, modifier: Modifier = Modifier, - portfolioVisibleState: (portfolio: TokensListItemUM.Portfolio) -> MutableTransitionState, ) { when (state) { is WalletState.MultiCurrency -> { - tokensListItems(state.tokensListState, modifier, isBalanceHidden, portfolioVisibleState) + tokensListItems(state.tokensListState, modifier, isBalanceHidden) } is WalletState.SingleCurrency -> { txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt index 88df5c8e1d..74d4b0079b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt @@ -1,13 +1,17 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency import androidx.compose.animation.* -import androidx.compose.animation.core.LinearOutSlowInEasing -import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.semantics @@ -20,12 +24,12 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.test.MainScreenTestTags import com.tangem.core.ui.utils.lazyListItemPosition import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.delay internal fun LazyListScope.portfolioContentItems( items: ImmutableList, modifier: Modifier = Modifier, isBalanceHidden: Boolean, - portfolioVisibleState: (portfolio: TokensListItemUM.Portfolio) -> MutableTransitionState, ) { items.forEachIndexed { index, item -> portfolioTokensList( @@ -33,7 +37,6 @@ internal fun LazyListScope.portfolioContentItems( modifier = modifier, portfolioIndex = index, isBalanceHidden = isBalanceHidden, - portfolioVisibleState = portfolioVisibleState, ) } } @@ -43,35 +46,34 @@ internal fun LazyListScope.portfolioTokensList( modifier: Modifier, portfolioIndex: Int, isBalanceHidden: Boolean, - portfolioVisibleState: (portfolio: TokensListItemUM.Portfolio) -> MutableTransitionState, ) { val tokens = portfolio.tokens val isExpanded = portfolio.isExpanded + val lastIndex = tokens.lastIndex.inc() portfolioItem( portfolio = portfolio, modifier = modifier, portfolioIndex = portfolioIndex, isBalanceHidden = isBalanceHidden, - portfolioVisibleState = portfolioVisibleState, ) - if (!isExpanded) return if (tokens.isEmpty()) { item( key = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}", contentType = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}", ) { - val appear = portfolioVisibleState(portfolio) SlideInItemVisibility( + currentIndex = 1, + lastIndex = lastIndex, modifier = modifier - .animateItem() + .animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null) .roundedShapeItemDecoration( radius = TangemTheme.dimens.radius14, currentIndex = 1, lastIndex = 1, backgroundColor = TangemTheme.colors.background.primary, ), - visibleState = appear, + visible = isExpanded, ) { NonContentItemContent( modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing28), @@ -86,19 +88,19 @@ internal fun LazyListScope.portfolioTokensList( contentType = { _, item -> item::class.java }, itemContent = { tokenIndex, token -> val indexWithHeader = tokenIndex.inc() - val lastIndex = tokens.lastIndex.inc() - val appear = portfolioVisibleState(portfolio) SlideInItemVisibility( + currentIndex = tokenIndex, + lastIndex = lastIndex, modifier = modifier .testModifier(indexWithHeader) - .animateItem() + .animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null) .roundedShapeItemDecoration( radius = TangemTheme.dimens.radius14, currentIndex = indexWithHeader, lastIndex = lastIndex, backgroundColor = TangemTheme.colors.background.primary, ), - visibleState = appear, + visible = isExpanded, ) { val modifier = if (indexWithHeader == lastIndex) Modifier.padding(bottom = 8.dp) else Modifier PortfolioTokensListItem( @@ -116,11 +118,9 @@ private fun LazyListScope.portfolioItem( modifier: Modifier, portfolioIndex: Int, isBalanceHidden: Boolean, - portfolioVisibleState: (portfolio: TokensListItemUM.Portfolio) -> MutableTransitionState, ) { val tokens = portfolio.tokens val isExpanded = portfolio.isExpanded - val lastIndex = when { isExpanded && tokens.isEmpty() -> 1 isExpanded -> tokens.lastIndex.inc() @@ -128,60 +128,61 @@ private fun LazyListScope.portfolioItem( } item( - key = "account-${portfolio.id}-isExpanded$isExpanded", - contentType = "account-isExpanded$isExpanded", + key = "account-${portfolio.id}", + contentType = "account-content", ) { - val anchorModifier = modifier - .testModifier(portfolioIndex) - .animateItem() - .roundedShapeItemDecoration( - currentIndex = 0, - radius = TangemTheme.dimens.radius14, - lastIndex = lastIndex, - backgroundColor = TangemTheme.colors.background.primary, - ) - val appear = portfolioVisibleState(portfolio) - if (isExpanded) { - SlideInItemVisibility( - modifier = anchorModifier, - visibleState = appear, - ) { - PortfolioListItem( - state = portfolio, - isBalanceHidden = isBalanceHidden, - modifier = Modifier.padding(vertical = 8.dp), - ) - } - } else { - AnimatedVisibility( - modifier = anchorModifier, - visibleState = appear, - enter = fadeIn(), - exit = ExitTransition.None, - ) { - PortfolioListItem( - state = portfolio, - isBalanceHidden = isBalanceHidden, - ) + var lastIndexProxy by remember { mutableIntStateOf(lastIndex) } + + // When collapsing the portfolio, we delay updating lastIndexProxy to allow + // shrinking animation to complete before changing the shape. + LaunchedEffect(lastIndex) { + if (lastIndex != 0) { + lastIndexProxy = lastIndex + return@LaunchedEffect } + delay(timeMillis = (50 * tokens.size).toLong()) + lastIndexProxy = 0 } + + PortfolioListItem( + state = portfolio, + isBalanceHidden = isBalanceHidden, + modifier = modifier + .testModifier(portfolioIndex) + .roundedShapeItemDecoration( + currentIndex = 0, + radius = TangemTheme.dimens.radius14, + lastIndex = lastIndexProxy, + backgroundColor = TangemTheme.colors.background.primary, + ), + ) } } +@Suppress("MagicNumber") @Composable private fun SlideInItemVisibility( - visibleState: MutableTransitionState, + visible: Boolean, + currentIndex: Int, + lastIndex: Int, modifier: Modifier = Modifier, - content: @Composable() AnimatedVisibilityScope.() -> Unit, + content: @Composable () -> Unit, ) { AnimatedVisibility( modifier = modifier, - visibleState = visibleState, - enter = slideInVertically( - animationSpec = tween(easing = LinearOutSlowInEasing), - initialOffsetY = { it }, - ) + fadeIn(), - exit = ExitTransition.None, + visible = visible, + enter = fadeIn( + tween(100, delayMillis = 50 * currentIndex), + ) + expandVertically( + tween(100, delayMillis = 50 * currentIndex), + expandFrom = Alignment.Top, + ), + exit = fadeOut( + tween(100, delayMillis = 50 * (lastIndex - currentIndex - 1)), + ) + shrinkVertically( + tween(100, delayMillis = 50 * (lastIndex - currentIndex - 1)), + shrinkTowards = Alignment.Top, + ), ) { content() } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index ef1d8252e5..7d1be99d66 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -1,6 +1,5 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency -import androidx.compose.animation.core.MutableTransitionState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -41,14 +40,12 @@ internal fun LazyListScope.tokensListItems( state: WalletTokensListState, modifier: Modifier = Modifier, isBalanceHidden: Boolean, - portfolioVisibleState: (portfolio: TokensListItemUM.Portfolio) -> MutableTransitionState, ) { when (state) { is WalletTokensListState.ContentState.PortfolioContent -> portfolioContentItems( items = state.items, isBalanceHidden = isBalanceHidden, modifier = modifier, - portfolioVisibleState = portfolioVisibleState, ) is WalletTokensListState.ContentState.Content, is WalletTokensListState.ContentState.Loading,