From 35f073e493a2b406f78b8b8acb4bcb3248d1d565 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Apr 2026 15:30:56 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../common/ui/tokens/PortfolioListItem.kt | 234 ++++++++++++ .../ui/components/tokenlist/TokenListItem.kt | 5 +- .../choosetoken/impl/ui/ChooseTokenScreen.kt | 350 ++++++++++++++++++ .../swap/choosetoken/impl/ui/ChooseTokenUM.kt | 23 ++ .../MultiCurrencyAccountContent.kt | 200 +--------- .../multicurrency/MultiCurrencyContent.kt | 30 +- 6 files changed, 616 insertions(+), 226 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/tokens/PortfolioListItem.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenUM.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/PortfolioListItem.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/PortfolioListItem.kt new file mode 100644 index 0000000000..226c6e05d6 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/PortfolioListItem.kt @@ -0,0 +1,234 @@ +package com.tangem.common.ui.tokens + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.animateIntAsState +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +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.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.tokenlist.NON_CONTENT_TOKENS_LIST_KEY +import com.tangem.core.ui.components.tokenlist.PortfolioListItem +import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem +import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.conditional +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.lazyListItemPosition + +fun LazyListScope.portfolioTokensList( + portfolio: TokensListItemUM.Portfolio, + portfolioIndex: Int, + testTag: String, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + val tokens = portfolio.tokens + val isExpanded = portfolio.isExpanded + val lastIndex = tokens.lastIndex.inc() + + portfolioItem( + portfolio = portfolio, + modifier = modifier.testModifier(portfolioIndex, testTag), + isBalanceHidden = isBalanceHidden, + ) + val portfolioContent = portfolio.content + if (portfolioContent is PortfolioItemContentUM.Empty) { + item( + key = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}", + contentType = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}", + ) { + SlideInItemVisibility( + currentIndex = 1, + lastIndex = lastIndex, + modifier = modifier + .animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null) + .roundedShapeItemDecoration( + radius = TangemTheme.dimens.radius14, + currentIndex = 1, + lastIndex = 1, + backgroundColor = TangemTheme.colors.background.primary, + ), + visible = isExpanded, + ) { + EmptyAccountContent(portfolioContent) + } + } + return + } + itemsIndexed( + items = tokens, + key = { _, item -> item.id.toString() + "-portfolio-${portfolio.id}" }, + contentType = { _, item -> item::class.java }, + itemContent = { tokenIndex, token -> + val indexWithHeader = tokenIndex.inc() + SlideInItemVisibility( + currentIndex = tokenIndex, + lastIndex = lastIndex, + modifier = modifier + .testModifier(indexWithHeader, testTag) + .animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null) + .roundedShapeItemDecoration( + radius = TangemTheme.dimens.radius14, + currentIndex = indexWithHeader, + lastIndex = lastIndex, + backgroundColor = TangemTheme.colors.background.primary, + ), + visible = isExpanded, + ) { + PortfolioTokensListItem( + state = token, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .conditional(indexWithHeader == lastIndex) { padding(bottom = 8.dp) }, + ) + } + }, + ) +} + +@Suppress("MagicNumber") +fun LazyListScope.portfolioItem(portfolio: TokensListItemUM.Portfolio, modifier: Modifier, isBalanceHidden: Boolean) { + val tokens = portfolio.tokens + val isExpanded = portfolio.isExpanded + val lastIndex = when { + isExpanded && tokens.isEmpty() -> 1 + isExpanded -> tokens.lastIndex.inc() + else -> 0 + } + + item( + key = "account-${portfolio.id}", + contentType = "account-content", + ) { + // Snap immediately on expand; on collapse, hold until all child items finish + // their shrink animation, then snap to fully-rounded shape. + val effectiveLastIndex by animateIntAsState( + targetValue = lastIndex, + animationSpec = if (lastIndex != 0) { + snap() + } else { + snap(delayMillis = minOf(50 * tokens.lastIndex, 250) + 150) + }, + label = "lastIndex", + ) + + PortfolioListItem( + state = portfolio, + isBalanceHidden = isBalanceHidden, + modifier = modifier + .roundedShapeItemDecoration( + currentIndex = 0, + radius = TangemTheme.dimens.radius14, + lastIndex = effectiveLastIndex, + backgroundColor = TangemTheme.colors.background.primary, + ), + ) + } +} + +@Suppress("MagicNumber") +@Composable +fun SlideInItemVisibility( + visible: Boolean, + currentIndex: Int, + lastIndex: Int, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + val maxDelay = 250 + val delayEnter = minOf(50 * currentIndex, maxDelay) + val delayExit = minOf(50 * (lastIndex - currentIndex - 1), maxDelay) + + AnimatedVisibility( + modifier = modifier, + visible = visible, + enter = expandVertically( + tween(200, delayMillis = delayEnter, easing = LinearOutSlowInEasing), + expandFrom = Alignment.Top, + ) + fadeIn(tween(200, delayMillis = delayEnter, easing = LinearOutSlowInEasing)), + exit = shrinkVertically( + tween(150, delayMillis = delayExit, easing = FastOutLinearInEasing), + shrinkTowards = Alignment.Top, + ) + fadeOut(tween(150, delayMillis = delayExit, easing = FastOutLinearInEasing)), + ) { + content() + } +} + +@Composable +private fun EmptyAccountContent(emptyConent: PortfolioItemContentUM.Empty, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SpacerH16() + NonContentItemContent() + val emptyAction = emptyConent.action + if (emptyAction != null) { + SpacerH16() + SecondarySmallButton( + config = SmallButtonConfig( + text = emptyAction.text, + onClick = { emptyAction.onClick() }, + ), + ) + } + SpacerH24() + } +} + +@Composable +fun NonContentItemContent(modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_empty_64), + contentDescription = null, + modifier = Modifier.size(size = TangemTheme.dimens.size64), + tint = TangemTheme.colors.icon.inactive, + ) + + Text( + text = stringResourceSafe(id = R.string.main_empty_tokens_list_message), + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing48), + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.caption2, + ) + } +} + +private fun Modifier.testModifier(index: Int, testTag: String): Modifier = this + .testTag(testTag) + .semantics { lazyListItemPosition = index } \ No newline at end of file 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 214c54f967..0c6291b831 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 @@ -2,8 +2,7 @@ package com.tangem.core.ui.components.tokenlist import androidx.compose.animation.* import androidx.compose.animation.SharedTransitionScope.ResizeMode.Companion.scaleToBounds -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween +import androidx.compose.animation.core.* import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon @@ -40,6 +39,8 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.ProvideSharedTransitionScope +const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST" + /** * Multi-currency content item * diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt new file mode 100644 index 0000000000..d40b8c87ae --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt @@ -0,0 +1,350 @@ +package com.tangem.feature.swap.choosetoken.impl.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.* +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.tokens.portfolioTokensList +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.fields.SearchBar +import com.tangem.core.ui.components.fields.TangemSearchBarDefaults +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.list.InfiniteListHandler +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.token.AccountItemPreviewData +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.TokenListItem +import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM +import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.BuyTokenScreenTestTags +import com.tangem.core.ui.utils.lazyListItemPosition +import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection +import com.tangem.feature.swap.models.TokenListUMData +import com.tangem.feature.swap.models.market.state.SwapMarketState +import com.tangem.feature.swap.presentation.R +import com.tangem.feature.swap.ui.market.swapMarketsListItems +import com.tangem.feature.swap.ui.preview.SwapSelectTokenPreviewProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlin.random.Random + +private const val LOAD_MORE_BUFFER = 25 + +@Composable +internal fun ChooseTokenScreen(state: ChooseTokenUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.tertiary) + .fillMaxSize() + .systemBarsPadding() + .imePadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBar(title = state.screenTitle, onBackClick = state.onCloseClick) + SearchBar( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + state = state.searchBar, + colors = TangemSearchBarDefaults.secondaryTextFieldColors, + ) + + Content( + state = state, + modifier = Modifier.weight(1f), + ) + } +} + +@Composable +private fun AppBar(title: TextReference, onBackClick: () -> Unit) { + AppBarWithBackButton( + text = title.resolveReference(), + onBackClick = onBackClick, + iconRes = com.tangem.common.ui.R.drawable.ic_back_24, + modifier = Modifier.height(TangemTheme.dimens.size56), + ) +} + +@Composable +private fun Content(state: ChooseTokenUM, modifier: Modifier = Modifier) { + val nestedScrollConnection = rememberHideKeyboardNestedScrollConnection() + val lazyListState = rememberLazyListState() + + LazyColumn( + modifier = modifier + .fillMaxSize() + .nestedScroll(nestedScrollConnection), + horizontalAlignment = Alignment.CenterHorizontally, + state = lazyListState, + ) { + assetsTitle() + + walletListItem(state.walletList) + + tokensListItems( + tokensListData = state.tokensListData, + isBalanceHidden = state.isBalanceHidden, + ) + + if (state.marketsState != null) { + item("markets_title_spacer") { SpacerH(height = 20.dp) } + swapMarketsListItems(state.marketsState) + } + } + if (state.marketsState != null) { + SetupMarketScrollTracker(state.marketsState, lazyListState) + } +} + +@Composable +private fun SetupMarketScrollTracker(marketsState: SwapMarketState, lazyListState: LazyListState) { + if (marketsState !is SwapMarketState.Content) return + val onLoadMore = remember(marketsState) { + { + marketsState.loadMore() + true + } + } + VisibleItemsTracker( + lazyListState = lazyListState, + marketState = marketsState, + ) + + InfiniteListHandler( + listState = lazyListState, + buffer = LOAD_MORE_BUFFER, + triggerLoadMoreCheckOnItemsCountChange = true, + onLoadMore = onLoadMore, + ) +} + +@Composable +private fun VisibleItemsTracker(lazyListState: LazyListState, marketState: SwapMarketState.Content) { + val visibleItems by remember { + derivedStateOf { + lazyListState.layoutInfo.visibleItemsInfo.mapNotNull { itemInfo -> + marketState.items.find { it.getComposeKey() == itemInfo.key }?.id + } + } + } + + LaunchedEffect(visibleItems) { + marketState.visibleIdsChanged(visibleItems) + } +} + +private fun LazyListScope.assetsTitle() { + item(key = "assets_title") { + Text( + text = stringResourceSafe(R.string.swap_your_assets_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .fillMaxWidth() + .padding( + top = TangemTheme.dimens.spacing20, + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + ), + ) + } +} + +private fun LazyListScope.walletListItem(walletList: WalletListUM) { + if (walletList.items.isEmpty()) return + item("wallet_list") { + Row( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), + verticalAlignment = Alignment.CenterVertically, + ) { + walletList.items.forEach { um -> + val colors = when (um.type) { + TangemButtonType.Primary -> TangemButtonsDefaults.primaryButtonColors + else -> TangemButtonsDefaults.secondaryButtonColors + } + TangemButton( + text = um.text?.resolveReference().orEmpty(), + icon = TangemButtonIconPosition.None, + size = TangemButtonSize.Action, + colors = colors, + showProgress = false, + onClick = um.onClick, + enabled = true, + ) + } + } + } +} + +private fun LazyListScope.tokensListItems(tokensListData: TokenListUMData, isBalanceHidden: Boolean) { + when (tokensListData) { + is TokenListUMData.AccountList -> { + tokensListData.tokensList.forEachIndexed { index, item -> + portfolioTokensList( + portfolio = item, + portfolioIndex = index, + isBalanceHidden = isBalanceHidden, + testTag = BuyTokenScreenTestTags.LAZY_LIST_ITEM, + ) + } + } + is TokenListUMData.TokenList -> { + tokensList( + items = tokensListData.tokensList, + isBalanceHidden = isBalanceHidden, + ) + } + TokenListUMData.EmptyList -> Unit + } +} + +private fun LazyListScope.tokensList(items: ImmutableList, isBalanceHidden: Boolean) { + itemsIndexed( + items = items, + key = { _, item -> item.id }, + contentType = { _, item -> item::class.java }, + itemContent = { index, item -> + TokenListItem( + state = item, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = items.lastIndex, + backgroundColor = TangemTheme.colors.background.primary, + ) + .testTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM) + .semantics { lazyListItemPosition = index }, + ) + }, + ) +} + +@Preview +@Composable +private fun TokenScreenPreview(@PreviewParameter(ChooseTokenScreenPreviewProvider::class) state: ChooseTokenUM) { + TangemThemePreview { + ChooseTokenScreen( + state = state, + modifier = Modifier, + ) + } +} + +private val searchBar + get() = SearchBarUM( + placeholderText = resourceReference(R.string.common_search), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = {}, + ) + +private val tokenItem + get() = TokenItemState.Content( + id = Random.nextInt().toString(), + iconState = CurrencyIconState.Locked, + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Bitcoin")), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"), + subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "0,35853044 BTC"), + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( + price = "34 496,75 \$", + priceChangePercent = "0,43 %", + type = PriceChangeType.DOWN, + ), + onItemClick = {}, + onItemLongClick = {}, + ) + +private val tokens + get() = persistentListOf( + TokensListItemUM.GroupTitle(id = 111, text = stringReference("Network Bitcoin")), + TokensListItemUM.Token(state = tokenItem), + TokensListItemUM.GroupTitle(id = 222, text = stringReference("Network Ethereum")), + TokensListItemUM.Token(state = tokenItem), + TokensListItemUM.Token(state = tokenItem), + TokensListItemUM.Token(state = tokenItem), + ) + +private val accounts + get() = persistentListOf( + TokensListItemUM.Portfolio( + content = PortfolioItemContentUM.Tokens( + tokens = tokens.filterIsInstance().toPersistentList(), + ), + isExpanded = false, + isCollapsable = true, + tokenItemUM = AccountItemPreviewData.accountItem.copy(iconState = AccountItemPreviewData.accountLetterIcon), + ), + TokensListItemUM.Portfolio( + content = PortfolioItemContentUM.Tokens( + tokens = tokens.filterIsInstance().toPersistentList(), + ), + isExpanded = true, + isCollapsable = true, + tokenItemUM = AccountItemPreviewData.accountItem, + ), + ) + +private val wallets + get() = persistentListOf( + TangemButtonUM( + text = TextReference.Str(value = "Wallet 1"), + type = TangemButtonType.Primary, + onClick = {}, + ), + TangemButtonUM( + text = TextReference.Str(value = "Wallet 2"), + type = TangemButtonType.Secondary, + onClick = {}, + ), + TangemButtonUM( + text = TextReference.Str(value = "Wallet 3"), + type = TangemButtonType.Secondary, + onClick = {}, + ), + ) + +private class ChooseTokenScreenPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + ChooseTokenUM( + screenTitle = stringReference("Choose token"), + onCloseClick = {}, + walletList = WalletListUM(wallets), + searchBar = searchBar, + isBalanceHidden = false, + isAfterSearch = false, + tokensListData = TokenListUMData.AccountList( + tokensList = accounts, + accounts.size, + ), + marketsState = SwapSelectTokenPreviewProvider.defaultState.marketsState, + ), + ) +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenUM.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenUM.kt new file mode 100644 index 0000000000..8b9b2e3c23 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenUM.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.swap.choosetoken.impl.ui + +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.feature.swap.models.TokenListUMData +import com.tangem.feature.swap.models.market.state.SwapMarketState +import kotlinx.collections.immutable.ImmutableList + +internal data class ChooseTokenUM( + val screenTitle: TextReference, + val onCloseClick: () -> Unit, + val walletList: WalletListUM, + val searchBar: SearchBarUM, + val isBalanceHidden: Boolean, + val isAfterSearch: Boolean, + val tokensListData: TokenListUMData, + val marketsState: SwapMarketState?, +) + +internal data class WalletListUM( + val items: ImmutableList, +) \ No newline at end of file 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 e9a092a806..e5b93f0c9c 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,33 +1,10 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency -import androidx.compose.animation.* -import androidx.compose.animation.core.FastOutLinearInEasing -import androidx.compose.animation.core.LinearOutSlowInEasing -import androidx.compose.animation.core.animateIntAsState -import androidx.compose.animation.core.snap -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.SpacerH24 -import com.tangem.core.ui.components.buttons.SecondarySmallButton -import com.tangem.core.ui.components.buttons.SmallButtonConfig -import com.tangem.core.ui.components.tokenlist.PortfolioListItem -import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem -import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM +import com.tangem.common.ui.tokens.portfolioTokensList import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -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 internal fun LazyListScope.portfolioContentItems( @@ -41,178 +18,7 @@ internal fun LazyListScope.portfolioContentItems( modifier = modifier, portfolioIndex = index, isBalanceHidden = isBalanceHidden, + testTag = MainScreenTestTags.TOKEN_LIST_ITEM, ) } -} - -internal fun LazyListScope.portfolioTokensList( - portfolio: TokensListItemUM.Portfolio, - modifier: Modifier, - portfolioIndex: Int, - isBalanceHidden: Boolean, -) { - val tokens = portfolio.tokens - val isExpanded = portfolio.isExpanded - val lastIndex = tokens.lastIndex.inc() - - portfolioItem( - portfolio = portfolio, - modifier = modifier, - portfolioIndex = portfolioIndex, - isBalanceHidden = isBalanceHidden, - ) - val portfolioContent = portfolio.content - if (portfolioContent is PortfolioItemContentUM.Empty) { - item( - key = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}", - contentType = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}", - ) { - SlideInItemVisibility( - currentIndex = 1, - lastIndex = lastIndex, - modifier = modifier - .animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null) - .roundedShapeItemDecoration( - radius = TangemTheme.dimens.radius14, - currentIndex = 1, - lastIndex = 1, - backgroundColor = TangemTheme.colors.background.primary, - ), - visible = isExpanded, - ) { - EmptyAccountContent(portfolioContent) - } - } - return - } - itemsIndexed( - items = tokens, - key = { _, item -> item.id.toString() + "-portfolio-${portfolio.id}" }, - contentType = { _, item -> item::class.java }, - itemContent = { tokenIndex, token -> - val indexWithHeader = tokenIndex.inc() - SlideInItemVisibility( - currentIndex = tokenIndex, - lastIndex = lastIndex, - modifier = modifier - .testModifier(indexWithHeader) - .animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null) - .roundedShapeItemDecoration( - radius = TangemTheme.dimens.radius14, - currentIndex = indexWithHeader, - lastIndex = lastIndex, - backgroundColor = TangemTheme.colors.background.primary, - ), - visible = isExpanded, - ) { - val modifier = if (indexWithHeader == lastIndex) Modifier.padding(bottom = 8.dp) else Modifier - PortfolioTokensListItem( - state = token, - isBalanceHidden = isBalanceHidden, - modifier = modifier, - ) - } - }, - ) -} - -@Composable -private fun EmptyAccountContent(emptyConent: PortfolioItemContentUM.Empty, modifier: Modifier = Modifier) { - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - SpacerH16() - NonContentItemContent() - val emptyAction = emptyConent.action - if (emptyAction != null) { - SpacerH16() - SecondarySmallButton( - config = SmallButtonConfig( - text = emptyAction.text, - onClick = { emptyAction.onClick() }, - ), - ) - } - SpacerH24() - } -} - -@Suppress("MagicNumber") -private fun LazyListScope.portfolioItem( - portfolio: TokensListItemUM.Portfolio, - modifier: Modifier, - portfolioIndex: Int, - isBalanceHidden: Boolean, -) { - val tokens = portfolio.tokens - val isExpanded = portfolio.isExpanded - val lastIndex = when { - isExpanded && tokens.isEmpty() -> 1 - isExpanded -> tokens.lastIndex.inc() - else -> 0 - } - - item( - key = "account-${portfolio.id}", - contentType = "account-content", - ) { - // Snap immediately on expand; on collapse, hold until all child items finish - // their shrink animation, then snap to fully-rounded shape. - val effectiveLastIndex by animateIntAsState( - targetValue = lastIndex, - animationSpec = if (lastIndex != 0) { - snap() - } else { - snap(delayMillis = minOf(50 * tokens.lastIndex, 250) + 150) - }, - label = "lastIndex", - ) - - PortfolioListItem( - state = portfolio, - isBalanceHidden = isBalanceHidden, - modifier = modifier - .testModifier(portfolioIndex) - .roundedShapeItemDecoration( - currentIndex = 0, - radius = TangemTheme.dimens.radius14, - lastIndex = effectiveLastIndex, - backgroundColor = TangemTheme.colors.background.primary, - ), - ) - } -} - -@Suppress("MagicNumber") -@Composable -internal fun SlideInItemVisibility( - visible: Boolean, - currentIndex: Int, - lastIndex: Int, - modifier: Modifier = Modifier, - content: @Composable () -> Unit, -) { - val maxDelay = 250 - val delayEnter = minOf(50 * currentIndex, maxDelay) - val delayExit = minOf(50 * (lastIndex - currentIndex - 1), maxDelay) - - AnimatedVisibility( - modifier = modifier, - visible = visible, - enter = expandVertically( - tween(200, delayMillis = delayEnter, easing = LinearOutSlowInEasing), - expandFrom = Alignment.Top, - ) + fadeIn(tween(200, delayMillis = delayEnter, easing = LinearOutSlowInEasing)), - exit = shrinkVertically( - tween(150, delayMillis = delayExit, easing = FastOutLinearInEasing), - shrinkTowards = Alignment.Top, - ) + fadeOut(tween(150, delayMillis = delayExit, easing = FastOutLinearInEasing)), - ) { - content() - } -} - -private fun Modifier.testModifier(index: Int): Modifier = this - .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) - .semantics { lazyListItemPosition = index } \ No newline at end of file +} \ No newline at end of file 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 a04c4bbd1e..1c4838b7b9 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 @@ -7,7 +7,6 @@ import androidx.compose.animation.core.animateIntAsState import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -31,9 +30,12 @@ import androidx.compose.ui.text.lerp import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.common.ui.tokens.NonContentItemContent +import com.tangem.common.ui.tokens.SlideInItemVisibility import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.tokenlist.NON_CONTENT_TOKENS_LIST_KEY import com.tangem.core.ui.components.tokenlist.TokenListItem import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration @@ -59,8 +61,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensLis import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListUM import kotlinx.collections.immutable.ImmutableList -internal const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST" - /** * LazyList extension for [WalletTokensListState] * @@ -522,30 +522,6 @@ private fun LazyListScope.nonContentAccountItem( } } -@Composable -internal fun NonContentItemContent(modifier: Modifier = Modifier) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_empty_64), - contentDescription = null, - modifier = Modifier.size(size = TangemTheme.dimens.size64), - tint = TangemTheme.colors.icon.inactive, - ) - - Text( - text = stringResourceSafe(id = R.string.main_empty_tokens_list_message), - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing48), - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - style = TangemTheme.typography.caption2, - ) - } -} - @Composable internal fun NonContentItemContentV2(textColor: Color, modifier: Modifier = Modifier, onClick: () -> Unit) { Column(