Updated on 2026-08-14
This commit is contained in:
parent
bfeea010d0
commit
35f073e493
6 changed files with 616 additions and 226 deletions
|
|
@ -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 }
|
||||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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<TokensListItemUM>, 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<PortfolioTokensListItemUM>().toPersistentList(),
|
||||
),
|
||||
isExpanded = false,
|
||||
isCollapsable = true,
|
||||
tokenItemUM = AccountItemPreviewData.accountItem.copy(iconState = AccountItemPreviewData.accountLetterIcon),
|
||||
),
|
||||
TokensListItemUM.Portfolio(
|
||||
content = PortfolioItemContentUM.Tokens(
|
||||
tokens = tokens.filterIsInstance<PortfolioTokensListItemUM>().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<ChooseTokenUM> {
|
||||
override val values: Sequence<ChooseTokenUM> = 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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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<TangemButtonUM>,
|
||||
)
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue