Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-24 13:48:16 +03:00
parent 9836b18ff4
commit 050c7b6106
13 changed files with 369 additions and 222 deletions

View file

@ -2,12 +2,21 @@ package com.tangem.core.ui.components.account
import android.content.res.Configuration import android.content.res.Configuration
import androidx.annotation.DrawableRes 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.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
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.R
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
@ -36,14 +47,28 @@ enum class AccountIconSize {
*/ */
@Composable @Composable
fun AccountResIcon(@DrawableRes resId: Int, color: Color, size: AccountIconSize, modifier: Modifier = Modifier) { fun AccountResIcon(@DrawableRes resId: Int, color: Color, size: AccountIconSize, modifier: Modifier = Modifier) {
val boxModifier = modifier.selectBoxModifier(size) val boxSize by animateDpAsState(
val iconSize = Modifier.selectIconSize(size) targetValue = size.boxSizeInDp(),
animationSpec = animation(),
)
val boxShapeCornerSize by animateDpAsState(
targetValue = size.boxShapeSizeInDp(),
animationSpec = animation(),
)
val iconSize by animateDpAsState(
targetValue = size.iconSizeInDp(),
animationSpec = animation(),
)
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = boxModifier.background(color), modifier = modifier
.size(boxSize)
.clip(RoundedCornerShape(boxShapeCornerSize))
.background(color),
) { ) {
Icon( Icon(
modifier = iconSize, modifier = Modifier.size(iconSize),
tint = TangemTheme.colors.text.constantWhite, tint = TangemTheme.colors.text.constantWhite,
imageVector = ImageVector.vectorResource(id = resId), imageVector = ImageVector.vectorResource(id = resId),
contentDescription = null, contentDescription = null,
@ -64,7 +89,14 @@ fun AccountResIcon(@DrawableRes resId: Int, color: Color, size: AccountIconSize,
*/ */
@Composable @Composable
fun AccountCharIcon(char: Char, color: Color, size: AccountIconSize, modifier: Modifier = Modifier) { 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) { val textStyle = when (size) {
AccountIconSize.Default -> TangemTheme.typography.h3 AccountIconSize.Default -> TangemTheme.typography.h3
AccountIconSize.Large -> TangemTheme.typography.h1 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.Small -> TangemTheme.typography.subtitle2
AccountIconSize.ExtraSmall -> TangemTheme.typography.caption1 AccountIconSize.ExtraSmall -> TangemTheme.typography.caption1
} }
val textSize by animateFloatAsState(
textStyle.fontSize.value,
animationSpec = animation(),
)
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = boxModifier.background(color), modifier = modifier
.size(boxSize)
.clip(RoundedCornerShape(boxShapeCornerSize))
.background(color),
) { ) {
Text( Text(
modifier = Modifier.animateContentSize(animationSpec = animation()),
text = char.uppercase(), text = char.uppercase(),
style = textStyle, style = textStyle.copy(fontSize = textSize.sp),
color = TangemTheme.colors.text.constantWhite, color = TangemTheme.colors.text.constantWhite,
) )
} }
} }
private fun Modifier.selectIconSize(size: AccountIconSize): Modifier = when (size) { private fun <T> animation() = tween<T>(durationMillis = 350)
AccountIconSize.Default -> this.size(20.dp)
AccountIconSize.Large -> this.size(40.dp) private fun AccountIconSize.iconSizeInDp(): Dp = when (this) {
AccountIconSize.Medium -> this.size(16.dp) AccountIconSize.Default -> 20.dp
AccountIconSize.Small -> this.size(12.dp) AccountIconSize.Large -> 40.dp
AccountIconSize.ExtraSmall -> this.size(8.dp) AccountIconSize.Medium -> 16.dp
AccountIconSize.Small -> 12.dp
AccountIconSize.ExtraSmall -> 8.dp
} }
private fun Modifier.selectBoxModifier(size: AccountIconSize): Modifier = when (size) { private fun AccountIconSize.boxSizeInDp(): Dp = when (this) {
AccountIconSize.Default -> size(36.dp).clip(RoundedCornerShape(10.dp)) AccountIconSize.Default -> 36.dp
AccountIconSize.Large -> size(88.dp).clip(RoundedCornerShape(24.dp)) AccountIconSize.Large -> 88.dp
AccountIconSize.Medium -> size(28.dp).clip(RoundedCornerShape(8.dp)) AccountIconSize.Medium -> 28.dp
AccountIconSize.Small -> size(20.dp).clip(RoundedCornerShape(6.dp)) AccountIconSize.Small -> 20.dp
AccountIconSize.ExtraSmall -> size(14.dp).clip(RoundedCornerShape(4.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) @Preview(showBackground = true)
@ -115,7 +167,20 @@ private fun Preview() {
@Composable @Composable
private fun Sample() { private fun Sample() {
var sizeState by remember { mutableStateOf(AccountIconSize.ExtraSmall) }
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { 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_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_rounded_star_24, color = Color.Blue, size = AccountIconSize.Large)
AccountResIcon(resId = R.drawable.ic_user_24, color = Color.Magenta, size = AccountIconSize.Medium) 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) AccountResIcon(resId = R.drawable.ic_wallet_24, color = Color.Green, size = AccountIconSize.ExtraSmall)
} }
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { 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 = 'D', color = Color.Red, size = AccountIconSize.Default)
AccountCharIcon(char = 'L', color = Color.Blue, size = AccountIconSize.Large) AccountCharIcon(char = 'L', color = Color.Blue, size = AccountIconSize.Large)
AccountCharIcon(char = 'M', color = Color.Magenta, size = AccountIconSize.Medium) AccountCharIcon(char = 'M', color = Color.Magenta, size = AccountIconSize.Medium)

View file

@ -14,7 +14,6 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.components.account.AccountCharIcon 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.account.AccountResIcon
import com.tangem.core.ui.components.currency.DefaultCurrencyIcon import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
@ -66,13 +65,13 @@ internal fun ContentIcon(
modifier = modifier, modifier = modifier,
resId = icon.resId, resId = icon.resId,
color = icon.color, color = icon.color,
size = AccountIconSize.Default, size = icon.size,
) )
is CurrencyIconState.CryptoPortfolio.Letter -> AccountCharIcon( is CurrencyIconState.CryptoPortfolio.Letter -> AccountCharIcon(
modifier = modifier, modifier = modifier,
char = icon.char.resolveReference().first(), char = icon.char.resolveReference().first(),
color = icon.color, color = icon.color,
size = AccountIconSize.Default, size = icon.size,
) )
CurrencyIconState.Loading, CurrencyIconState.Loading,
CurrencyIconState.Locked, CurrencyIconState.Locked,

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.CircleShimmer 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.res.TangemTheme
import com.tangem.core.ui.utils.getGreyScaleColorFilter import com.tangem.core.ui.utils.getGreyScaleColorFilter
@ -33,12 +34,20 @@ fun CurrencyIcon(
state: CurrencyIconState, state: CurrencyIconState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
shouldDisplayNetwork: Boolean = true, shouldDisplayNetwork: Boolean = true,
withFixedSize: Boolean = true,
iconSize: Dp = 36.dp, iconSize: Dp = 36.dp,
) { ) {
BaseContainer(modifier = modifier) { Box(
modifier = modifier
.conditional(withFixedSize) {
size(size = 40.dp)
},
) {
val iconModifier = Modifier val iconModifier = Modifier
.align(Alignment.Center) .align(Alignment.Center)
.size(iconSize) .conditional(withFixedSize) {
size(iconSize)
}
when (state) { when (state) {
is CurrencyIconState.Loading -> LoadingIcon(modifier = iconModifier) is CurrencyIconState.Loading -> LoadingIcon(modifier = iconModifier)
@ -132,9 +141,4 @@ private fun BoxScope.ContentIconContainer(
modifier = Modifier.align(Alignment.BottomEnd), 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)
} }

View file

@ -4,6 +4,7 @@ import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
/** /**
@ -92,17 +93,20 @@ sealed class CurrencyIconState {
override val shouldShowCustomBadge: Boolean = false override val shouldShowCustomBadge: Boolean = false
override val topBadgeIconResId: Int? = null override val topBadgeIconResId: Int? = null
abstract val color: Color abstract val color: Color
abstract val size: AccountIconSize
data class Icon( data class Icon(
@DrawableRes val resId: Int, @DrawableRes val resId: Int,
override val color: Color, override val color: Color,
override val isGrayscale: Boolean, override val isGrayscale: Boolean,
override val size: AccountIconSize = AccountIconSize.Default,
) : CryptoPortfolio() ) : CryptoPortfolio()
data class Letter( data class Letter(
val char: TextReference, val char: TextReference,
override val color: Color, override val color: Color,
override val isGrayscale: Boolean, override val isGrayscale: Boolean,
override val size: AccountIconSize = AccountIconSize.Default,
) : CryptoPortfolio() ) : CryptoPortfolio()
} }

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color 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.FiatAmountState
import com.tangem.core.ui.components.token.state.TokenItemState.Subtitle2State 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.PromoBannerState
import com.tangem.core.ui.components.token.state.TokenItemState.TitleState
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
@ -65,6 +67,7 @@ fun TokenItem(
state: TokenItemState, state: TokenItemState,
isBalanceHidden: Boolean, isBalanceHidden: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
composables: TokenItemComposables? = null,
itemPaddingValues: PaddingValues = PaddingValues(horizontal = TangemTheme.dimens.spacing12), itemPaddingValues: PaddingValues = PaddingValues(horizontal = TangemTheme.dimens.spacing12),
) { ) {
TokenItem( TokenItem(
@ -73,9 +76,16 @@ fun TokenItem(
modifier = modifier, modifier = modifier,
reorderableTokenListState = null, reorderableTokenListState = null,
itemPaddingValues = itemPaddingValues, itemPaddingValues = itemPaddingValues,
composables = composables,
) )
} }
@Stable
class TokenItemComposables(
val title: @Composable (Modifier) -> Unit,
val icon: @Composable (Modifier) -> Unit,
)
/** /**
* Token item for reorderable list * Token item for reorderable list
* *
@ -88,6 +98,7 @@ fun TokenItem(
* @see <a href = "https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1051-866&t=ew8mbGp2lacuJfFm-4" * @see <a href = "https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1051-866&t=ew8mbGp2lacuJfFm-4"
* >Figma Component</a> * >Figma Component</a>
*/ */
@Suppress("LongMethod")
@Composable @Composable
fun TokenItem( fun TokenItem(
state: TokenItemState, state: TokenItemState,
@ -95,6 +106,7 @@ fun TokenItem(
reorderableTokenListState: ReorderableLazyListState?, reorderableTokenListState: ReorderableLazyListState?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
itemPaddingValues: PaddingValues = PaddingValues(horizontal = TangemTheme.dimens.spacing12), itemPaddingValues: PaddingValues = PaddingValues(horizontal = TangemTheme.dimens.spacing12),
composables: TokenItemComposables? = null,
) { ) {
val betweenRowsMargin = TangemTheme.dimens.spacing2 val betweenRowsMargin = TangemTheme.dimens.spacing2
@ -104,13 +116,22 @@ fun TokenItem(
.tokenClickable(state = state) .tokenClickable(state = state)
.padding(itemPaddingValues), .padding(itemPaddingValues),
) { ) {
CurrencyIcon( if (composables != null) {
state = state.iconState, composables.icon.invoke(
modifier = Modifier Modifier
.layoutId(layoutId = LayoutId.ICON) .layoutId(layoutId = LayoutId.ICON)
.padding(end = TangemTheme.dimens.spacing8) .padding(end = TangemTheme.dimens.spacing8)
.testTag(TokenElementsTestTags.TOKEN_ICON), .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( YieldSupplyPromoBanner(
state = state.promoBannerState, state = state.promoBannerState,
@ -120,14 +141,24 @@ fun TokenItem(
.fillMaxWidth(), .fillMaxWidth(),
) )
TokenTitle( if (composables != null) {
state = state.titleState, composables.title.invoke(
modifier = Modifier Modifier
.layoutId(layoutId = LayoutId.TITLE) .layoutId(layoutId = LayoutId.TITLE)
.padding(end = TangemTheme.dimens.spacing8) .padding(end = TangemTheme.dimens.spacing8)
.padding(bottom = betweenRowsMargin) .padding(bottom = betweenRowsMargin)
.testTag(TokenElementsTestTags.TOKEN_TITLE), .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( TokenFiatAmount(
state = state.fiatAmountState, state = state.fiatAmountState,

View file

@ -21,7 +21,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.components.token.state.TokenItemState.TitleState as TokenTitleState import com.tangem.core.ui.components.token.state.TokenItemState.TitleState as TokenTitleState
@Composable @Composable
internal fun TokenTitle(state: TokenTitleState?, modifier: Modifier = Modifier) { fun TokenTitle(state: TokenTitleState?, modifier: Modifier = Modifier) {
when (state) { when (state) {
is TokenTitleState.Content -> { is TokenTitleState.Content -> {
ContentTitle(state = state, modifier = modifier) ContentTitle(state = state, modifier = modifier)

View file

@ -1,13 +1,20 @@
package com.tangem.core.ui.components.tokenlist 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.clickable
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource 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.AccountCharIcon
import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.account.AccountResIcon 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.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.token.TokenItem 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.token.state.TokenItemState
import com.tangem.core.ui.components.tokenlist.internal.GroupTitleItem import com.tangem.core.ui.components.tokenlist.internal.GroupTitleItem
import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM 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 @Composable
fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
if (state.isExpanded) { // Box is not needed here, but due to a bug in compose
ExpandedPortfolioHeader(state = state.tokenItemUM, isCollapsable = state.isCollapsable, modifier = modifier) // we use it like a wrapper to properly handle lookahead scope inside this composable
} else { Box(modifier) {
TokenItem( SharedTransitionLayout {
state = state.tokenItemUM, AnimatedContent(
isBalanceHidden = isBalanceHidden, state.isExpanded,
modifier = modifier, 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 @Composable
fun ExpandedPortfolioHeader(state: TokenItemState, isCollapsable: Boolean, modifier: Modifier = Modifier) { fun ExpandedPortfolioHeader(
state: TokenItemState,
isCollapsable: Boolean,
composables: TokenItemComposables?,
modifier: Modifier = Modifier,
) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = { state.onItemClick?.invoke(state) }) .clickable(onClick = { state.onItemClick?.invoke(state) })
.padding( .padding(
vertical = TangemTheme.dimens.spacing4, vertical = if (composables != null) 8.dp else 4.dp,
horizontal = TangemTheme.dimens.spacing12, horizontal = 12.dp,
), ),
) { ) {
when (val icon = state.iconState) { if (composables != null) {
is CurrencyIconState.CryptoPortfolio.Icon -> AccountResIcon( composables.icon.invoke(Modifier)
resId = icon.resId, } else {
color = icon.color, when (val icon = state.iconState) {
size = AccountIconSize.ExtraSmall, is CurrencyIconState.CryptoPortfolio.Icon -> AccountResIcon(
) resId = icon.resId,
is CurrencyIconState.CryptoPortfolio.Letter -> AccountCharIcon( color = icon.color,
char = icon.char.resolveReference().first(), size = AccountIconSize.ExtraSmall,
color = icon.color, )
size = AccountIconSize.ExtraSmall, is CurrencyIconState.CryptoPortfolio.Letter -> AccountCharIcon(
) char = icon.char.resolveReference().first(),
is CurrencyIconState.CoinIcon, color = icon.color,
is CurrencyIconState.CustomTokenIcon, size = AccountIconSize.ExtraSmall,
is CurrencyIconState.Empty, )
is CurrencyIconState.FiatIcon, is CurrencyIconState.CoinIcon,
CurrencyIconState.Loading, is CurrencyIconState.CustomTokenIcon,
CurrencyIconState.Locked, is CurrencyIconState.Empty,
is CurrencyIconState.TokenIcon, is CurrencyIconState.FiatIcon,
-> Unit CurrencyIconState.Loading,
CurrencyIconState.Locked,
is CurrencyIconState.TokenIcon,
-> Unit
}
} }
SpacerW4() SpacerW4()
when (val titleState = state.titleState) { Row(
TokenItemState.TitleState.Loading -> Unit verticalAlignment = Alignment.CenterVertically,
TokenItemState.TitleState.Locked -> Unit modifier = Modifier.weight(1f),
is TokenItemState.TitleState.Content -> Text( ) {
modifier = Modifier.weight(1f), if (composables != null) {
text = titleState.text.resolveReference(), composables.title.invoke(Modifier.alignByBaseline())
color = TangemTheme.colors.text.primary1, } else {
overflow = TextOverflow.Ellipsis, when (val titleState = state.titleState) {
maxLines = 1, TokenItemState.TitleState.Loading -> Unit
style = TangemTheme.typography.caption1, 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) { if (isCollapsable) {

View file

@ -218,6 +218,7 @@ private fun LazyItemScope.DraggableItem(
is DraggableItem.Portfolio -> ExpandedPortfolioHeader( is DraggableItem.Portfolio -> ExpandedPortfolioHeader(
state = item.tokenItemState, state = item.tokenItemState,
isCollapsable = false, isCollapsable = false,
composables = null,
modifier = modifierWithBackground modifier = modifierWithBackground
.padding(top = 8.dp) .padding(top = 8.dp)
.fillMaxWidth(), .fillMaxWidth(),

View file

@ -18,7 +18,6 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind 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.sheetscaffold.*
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar
import com.tangem.core.ui.components.snackbar.TangemSnackbar 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.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.stringResourceSafe 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.TokenActionsBottomSheet
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList 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.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.nftCollections
import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock 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 selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) }
val selectedWallet = state.wallets.getOrElse(selectedWalletIndex) { state.wallets[state.selectedWalletIndex] } val selectedWallet = state.wallets.getOrElse(selectedWalletIndex) { state.wallets[state.selectedWalletIndex] }
val (expandedState, collapsedState) = getExpandPortfolioStates(selectedWallet)
val listState = rememberLazyListState() val listState = rememberLazyListState()
@ -199,6 +195,7 @@ private fun WalletContent(
contentType = state.wallets.map { it.walletCardState.id }, contentType = state.wallets.map { it.walletCardState.id },
) { ) {
WalletsList( WalletsList(
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
lazyListState = walletsListState, lazyListState = walletsListState,
wallets = state.wallets.map(WalletState::walletCardState).toImmutableList(), wallets = state.wallets.map(WalletState::walletCardState).toImmutableList(),
isBalanceHidden = state.isHidingMode, isBalanceHidden = state.isHidingMode,
@ -263,13 +260,6 @@ private fun WalletContent(
txHistoryItems = txHistoryItems, txHistoryItems = txHistoryItems,
isBalanceHidden = state.isHidingMode, isBalanceHidden = state.isHidingMode,
modifier = movableItemModifier, modifier = movableItemModifier,
portfolioVisibleState = {
findPortfolioVisibleState(
portfolio = it,
expandedState = expandedState,
collapsedState = collapsedState,
)
},
) )
nftCollections(state = selectedWallet, itemModifier = itemModifier) nftCollections(state = selectedWallet, itemModifier = itemModifier)
@ -307,61 +297,6 @@ private fun WalletContent(
) )
} }
private fun findPortfolioVisibleState(
portfolio: TokensListItemUM.Portfolio,
expandedState: SnapshotStateMap<String, MutableTransitionState<Boolean>>,
collapsedState: SnapshotStateMap<String, MutableTransitionState<Boolean>>,
): MutableTransitionState<Boolean> {
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<String, MutableTransitionState<Boolean>>,
SnapshotStateMap<String, MutableTransitionState<Boolean>>,
> {
val expandedTransitionState =
remember { mutableStateMapOf<String, MutableTransitionState<Boolean>>() }
val collapsedTransitionState =
remember { mutableStateMapOf<String, MutableTransitionState<Boolean>>() }
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") @Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable

View file

@ -47,13 +47,14 @@ internal fun WalletsList(
lazyListState: LazyListState, lazyListState: LazyListState,
wallets: ImmutableList<WalletCardState>, wallets: ImmutableList<WalletCardState>,
isBalanceHidden: Boolean, isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) { ) {
val horizontalCardPadding = TangemTheme.dimens.spacing16 val horizontalCardPadding = TangemTheme.dimens.spacing16
val screenWidth = LocalWindowSize.current.width val screenWidth = LocalWindowSize.current.width
val itemWidth by remember(screenWidth) { derivedStateOf { screenWidth - horizontalCardPadding * 2 } } val itemWidth by remember(screenWidth) { derivedStateOf { screenWidth - horizontalCardPadding * 2 } }
LazyRow( LazyRow(
modifier = Modifier.background(color = TangemTheme.colors.background.secondary), modifier = modifier.background(color = TangemTheme.colors.background.secondary),
state = lazyListState, state = lazyListState,
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),

View file

@ -1,10 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.common 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.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.paging.compose.LazyPagingItems 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.state.TxHistoryState
import com.tangem.core.ui.components.transactions.txHistoryItems import com.tangem.core.ui.components.transactions.txHistoryItems
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
@ -24,11 +22,10 @@ internal fun LazyListScope.contentItems(
txHistoryItems: LazyPagingItems<TxHistoryState.TxHistoryItemState>?, txHistoryItems: LazyPagingItems<TxHistoryState.TxHistoryItemState>?,
isBalanceHidden: Boolean, isBalanceHidden: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
portfolioVisibleState: (portfolio: TokensListItemUM.Portfolio) -> MutableTransitionState<Boolean>,
) { ) {
when (state) { when (state) {
is WalletState.MultiCurrency -> { is WalletState.MultiCurrency -> {
tokensListItems(state.tokensListState, modifier, isBalanceHidden, portfolioVisibleState) tokensListItems(state.tokensListState, modifier, isBalanceHidden)
} }
is WalletState.SingleCurrency -> { is WalletState.SingleCurrency -> {
txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier) txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier)

View file

@ -1,13 +1,17 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency
import androidx.compose.animation.* 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.animation.core.tween
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable 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.Modifier
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics 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.test.MainScreenTestTags
import com.tangem.core.ui.utils.lazyListItemPosition import com.tangem.core.ui.utils.lazyListItemPosition
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.delay
internal fun LazyListScope.portfolioContentItems( internal fun LazyListScope.portfolioContentItems(
items: ImmutableList<TokensListItemUM.Portfolio>, items: ImmutableList<TokensListItemUM.Portfolio>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isBalanceHidden: Boolean, isBalanceHidden: Boolean,
portfolioVisibleState: (portfolio: TokensListItemUM.Portfolio) -> MutableTransitionState<Boolean>,
) { ) {
items.forEachIndexed { index, item -> items.forEachIndexed { index, item ->
portfolioTokensList( portfolioTokensList(
@ -33,7 +37,6 @@ internal fun LazyListScope.portfolioContentItems(
modifier = modifier, modifier = modifier,
portfolioIndex = index, portfolioIndex = index,
isBalanceHidden = isBalanceHidden, isBalanceHidden = isBalanceHidden,
portfolioVisibleState = portfolioVisibleState,
) )
} }
} }
@ -43,35 +46,34 @@ internal fun LazyListScope.portfolioTokensList(
modifier: Modifier, modifier: Modifier,
portfolioIndex: Int, portfolioIndex: Int,
isBalanceHidden: Boolean, isBalanceHidden: Boolean,
portfolioVisibleState: (portfolio: TokensListItemUM.Portfolio) -> MutableTransitionState<Boolean>,
) { ) {
val tokens = portfolio.tokens val tokens = portfolio.tokens
val isExpanded = portfolio.isExpanded val isExpanded = portfolio.isExpanded
val lastIndex = tokens.lastIndex.inc()
portfolioItem( portfolioItem(
portfolio = portfolio, portfolio = portfolio,
modifier = modifier, modifier = modifier,
portfolioIndex = portfolioIndex, portfolioIndex = portfolioIndex,
isBalanceHidden = isBalanceHidden, isBalanceHidden = isBalanceHidden,
portfolioVisibleState = portfolioVisibleState,
) )
if (!isExpanded) return
if (tokens.isEmpty()) { if (tokens.isEmpty()) {
item( item(
key = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}", key = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}",
contentType = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}", contentType = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}",
) { ) {
val appear = portfolioVisibleState(portfolio)
SlideInItemVisibility( SlideInItemVisibility(
currentIndex = 1,
lastIndex = lastIndex,
modifier = modifier modifier = modifier
.animateItem() .animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null)
.roundedShapeItemDecoration( .roundedShapeItemDecoration(
radius = TangemTheme.dimens.radius14, radius = TangemTheme.dimens.radius14,
currentIndex = 1, currentIndex = 1,
lastIndex = 1, lastIndex = 1,
backgroundColor = TangemTheme.colors.background.primary, backgroundColor = TangemTheme.colors.background.primary,
), ),
visibleState = appear, visible = isExpanded,
) { ) {
NonContentItemContent( NonContentItemContent(
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing28), modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing28),
@ -86,19 +88,19 @@ internal fun LazyListScope.portfolioTokensList(
contentType = { _, item -> item::class.java }, contentType = { _, item -> item::class.java },
itemContent = { tokenIndex, token -> itemContent = { tokenIndex, token ->
val indexWithHeader = tokenIndex.inc() val indexWithHeader = tokenIndex.inc()
val lastIndex = tokens.lastIndex.inc()
val appear = portfolioVisibleState(portfolio)
SlideInItemVisibility( SlideInItemVisibility(
currentIndex = tokenIndex,
lastIndex = lastIndex,
modifier = modifier modifier = modifier
.testModifier(indexWithHeader) .testModifier(indexWithHeader)
.animateItem() .animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null)
.roundedShapeItemDecoration( .roundedShapeItemDecoration(
radius = TangemTheme.dimens.radius14, radius = TangemTheme.dimens.radius14,
currentIndex = indexWithHeader, currentIndex = indexWithHeader,
lastIndex = lastIndex, lastIndex = lastIndex,
backgroundColor = TangemTheme.colors.background.primary, backgroundColor = TangemTheme.colors.background.primary,
), ),
visibleState = appear, visible = isExpanded,
) { ) {
val modifier = if (indexWithHeader == lastIndex) Modifier.padding(bottom = 8.dp) else Modifier val modifier = if (indexWithHeader == lastIndex) Modifier.padding(bottom = 8.dp) else Modifier
PortfolioTokensListItem( PortfolioTokensListItem(
@ -116,11 +118,9 @@ private fun LazyListScope.portfolioItem(
modifier: Modifier, modifier: Modifier,
portfolioIndex: Int, portfolioIndex: Int,
isBalanceHidden: Boolean, isBalanceHidden: Boolean,
portfolioVisibleState: (portfolio: TokensListItemUM.Portfolio) -> MutableTransitionState<Boolean>,
) { ) {
val tokens = portfolio.tokens val tokens = portfolio.tokens
val isExpanded = portfolio.isExpanded val isExpanded = portfolio.isExpanded
val lastIndex = when { val lastIndex = when {
isExpanded && tokens.isEmpty() -> 1 isExpanded && tokens.isEmpty() -> 1
isExpanded -> tokens.lastIndex.inc() isExpanded -> tokens.lastIndex.inc()
@ -128,60 +128,61 @@ private fun LazyListScope.portfolioItem(
} }
item( item(
key = "account-${portfolio.id}-isExpanded$isExpanded", key = "account-${portfolio.id}",
contentType = "account-isExpanded$isExpanded", contentType = "account-content",
) { ) {
val anchorModifier = modifier var lastIndexProxy by remember { mutableIntStateOf(lastIndex) }
.testModifier(portfolioIndex)
.animateItem() // When collapsing the portfolio, we delay updating lastIndexProxy to allow
.roundedShapeItemDecoration( // shrinking animation to complete before changing the shape.
currentIndex = 0, LaunchedEffect(lastIndex) {
radius = TangemTheme.dimens.radius14, if (lastIndex != 0) {
lastIndex = lastIndex, lastIndexProxy = lastIndex
backgroundColor = TangemTheme.colors.background.primary, return@LaunchedEffect
)
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,
)
} }
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 @Composable
private fun SlideInItemVisibility( private fun SlideInItemVisibility(
visibleState: MutableTransitionState<Boolean>, visible: Boolean,
currentIndex: Int,
lastIndex: Int,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
content: @Composable() AnimatedVisibilityScope.() -> Unit, content: @Composable () -> Unit,
) { ) {
AnimatedVisibility( AnimatedVisibility(
modifier = modifier, modifier = modifier,
visibleState = visibleState, visible = visible,
enter = slideInVertically( enter = fadeIn(
animationSpec = tween(easing = LinearOutSlowInEasing), tween(100, delayMillis = 50 * currentIndex),
initialOffsetY = { it }, ) + expandVertically(
) + fadeIn(), tween(100, delayMillis = 50 * currentIndex),
exit = ExitTransition.None, expandFrom = Alignment.Top,
),
exit = fadeOut(
tween(100, delayMillis = 50 * (lastIndex - currentIndex - 1)),
) + shrinkVertically(
tween(100, delayMillis = 50 * (lastIndex - currentIndex - 1)),
shrinkTowards = Alignment.Top,
),
) { ) {
content() content()
} }

View file

@ -1,6 +1,5 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -41,14 +40,12 @@ internal fun LazyListScope.tokensListItems(
state: WalletTokensListState, state: WalletTokensListState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isBalanceHidden: Boolean, isBalanceHidden: Boolean,
portfolioVisibleState: (portfolio: TokensListItemUM.Portfolio) -> MutableTransitionState<Boolean>,
) { ) {
when (state) { when (state) {
is WalletTokensListState.ContentState.PortfolioContent -> portfolioContentItems( is WalletTokensListState.ContentState.PortfolioContent -> portfolioContentItems(
items = state.items, items = state.items,
isBalanceHidden = isBalanceHidden, isBalanceHidden = isBalanceHidden,
modifier = modifier, modifier = modifier,
portfolioVisibleState = portfolioVisibleState,
) )
is WalletTokensListState.ContentState.Content, is WalletTokensListState.ContentState.Content,
is WalletTokensListState.ContentState.Loading, is WalletTokensListState.ContentState.Loading,