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

@ -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(),

View file

@ -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<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")
@OptIn(ExperimentalMaterial3Api::class)
@Composable

View file

@ -47,13 +47,14 @@ internal fun WalletsList(
lazyListState: LazyListState,
wallets: ImmutableList<WalletCardState>,
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),

View file

@ -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<TxHistoryState.TxHistoryItemState>?,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
portfolioVisibleState: (portfolio: TokensListItemUM.Portfolio) -> MutableTransitionState<Boolean>,
) {
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)

View file

@ -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<TokensListItemUM.Portfolio>,
modifier: Modifier = Modifier,
isBalanceHidden: Boolean,
portfolioVisibleState: (portfolio: TokensListItemUM.Portfolio) -> MutableTransitionState<Boolean>,
) {
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<Boolean>,
) {
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<Boolean>,
) {
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<Boolean>,
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()
}

View file

@ -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<Boolean>,
) {
when (state) {
is WalletTokensListState.ContentState.PortfolioContent -> portfolioContentItems(
items = state.items,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
portfolioVisibleState = portfolioVisibleState,
)
is WalletTokensListState.ContentState.Content,
is WalletTokensListState.ContentState.Loading,