Updated on 2026-08-14

This commit is contained in:
Tangem 2023-08-14 15:18:02 +08:00
parent db0c6cc933
commit 606ae0918b
13 changed files with 129 additions and 77 deletions

View file

@ -31,7 +31,11 @@ fun HorizontalActionChips(
verticalAlignment = Alignment.CenterVertically,
contentPadding = contentPadding,
) {
items(items = buttons, itemContent = { ActionButton(config = it) })
items(
items = buttons,
key = { config -> "${config.text.hashCode()} ${config.iconResId}" },
itemContent = { ActionButton(config = it) },
)
}
}

View file

@ -5,8 +5,8 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState
@ -142,7 +142,7 @@ internal object WalletPreviewData {
)
}
val loadingTokenItemState by lazy { TokenItemState.Loading }
val loadingTokenItemState by lazy { TokenItemState.Loading(id = "Loading#1") }
private const val networksSize = 10
private const val tokensSize = 3

View file

@ -1,6 +1,9 @@
package com.tangem.feature.wallet.presentation.common.component
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@ -16,11 +19,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstrainedLayoutReference
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.ConstraintLayoutScope
@ -124,7 +127,7 @@ internal fun UnreachableTokenItem(state: TokenItemState.Unreachable, modifier: M
options = { ref ->
Text(
modifier = Modifier.constrainAsOptionsItem(scope = this, ref),
text = "Unreachable", // TODO (conform this text)
text = stringResource(id = R.string.common_unreachable),
style = TangemTypography.body2,
color = TangemTheme.colors.text.tertiary,
)
@ -143,7 +146,7 @@ private fun LoadingTokenItem(modifier: Modifier = Modifier) {
vertical = TangemTheme.dimens.spacing4,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24),
) {
CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42))
Row(
@ -188,22 +191,17 @@ private fun LoadingTokenItem(modifier: Modifier = Modifier) {
* Block for end part of token item
* shows status is reachable, is drag, hidden or show balance
*/
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun TokenOptionsBlock(state: TokenOptionsState, modifier: Modifier = Modifier) {
when (state) {
is TokenOptionsState.Visible -> {
TokenFiatPercentageBlock(
modifier = modifier,
fiatAmount = state.fiatAmount,
priceChange = state.priceChange,
)
}
is TokenOptionsState.Hidden -> {
TokenFiatPercentageBlock(
modifier = modifier,
fiatAmount = DOTS,
priceChange = state.priceChange,
)
AnimatedContent(targetState = state, label = "Update the options", modifier = modifier) { options ->
when (options) {
is TokenOptionsState.Visible -> {
TokenFiatPercentageBlock(fiatAmount = options.fiatAmount, priceChange = options.priceChange)
}
is TokenOptionsState.Hidden -> {
TokenFiatPercentageBlock(fiatAmount = DOTS, priceChange = options.priceChange)
}
}
}
}
@ -243,7 +241,7 @@ private fun InternalTokenItem(
TokenTitleAmountBlock(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing8)
.constrainAs(tokenNameItem) {
centerVerticallyTo(parent)
start.linkTo(iconItem.end)
@ -298,17 +296,18 @@ private fun TokenTitleAmountBlock(title: String, amount: String?, hasPending: Bo
style = TangemTypography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
if (hasPending) {
AnimatedVisibility(visible = hasPending, modifier = Modifier.align(Alignment.CenterVertically)) {
Image(
modifier = Modifier.align(Alignment.CenterVertically),
painter = painterResource(id = R.drawable.img_loader_15),
contentDescription = null,
)
}
}
if (!amount.isNullOrBlank()) {
AnimatedVisibility(visible = !amount.isNullOrBlank()) {
Text(
text = amount,
text = requireNotNull(amount),
style = TangemTypography.body2,
color = TangemTheme.colors.text.tertiary,
)
@ -316,6 +315,7 @@ private fun TokenTitleAmountBlock(title: String, amount: String?, hasPending: Bo
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun TokenFiatPercentageBlock(
fiatAmount: String,
@ -334,30 +334,39 @@ private fun TokenFiatPercentageBlock(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
val iconChangeArrow: Int
val changeTextColor: Color
when (priceChange.type) {
PriceChangeConfig.Type.UP -> {
iconChangeArrow = R.drawable.img_arrow_up_8
changeTextColor = TangemTheme.colors.text.accent
}
PriceChangeConfig.Type.DOWN -> {
iconChangeArrow = R.drawable.img_arrow_down_8
changeTextColor = TangemTheme.colors.text.warning
}
AnimatedContent(
targetState = priceChange.type,
label = "Update the price change's arrow",
modifier = Modifier.align(Alignment.CenterVertically),
) {
Image(
painter = painterResource(
id = when (priceChange.type) {
PriceChangeConfig.Type.UP -> R.drawable.img_arrow_up_8
PriceChangeConfig.Type.DOWN -> R.drawable.img_arrow_down_8
},
),
contentDescription = null,
)
}
Image(
modifier = Modifier.align(Alignment.CenterVertically),
painter = painterResource(id = iconChangeArrow),
contentDescription = null,
)
SpacerW4()
Text(
AnimatedContent(
targetState = priceChange.type,
label = "Update the price change's arrow",
modifier = Modifier.align(Alignment.CenterVertically),
text = priceChange.valueInPercent,
style = TangemTypography.body2,
color = changeTextColor,
)
) {
Text(
modifier = Modifier.align(Alignment.CenterVertically),
text = priceChange.valueInPercent,
style = TangemTypography.body2,
color = when (priceChange.type) {
PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent
PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning
},
)
}
}
}
}
@ -389,20 +398,20 @@ private fun TokenIcon(
contentDescription = null,
)
if (networkIconRes != null) {
Box(
AnimatedVisibility(
visible = networkIconRes != null,
modifier = Modifier
.align(Alignment.TopEnd)
.size(TangemTheme.dimens.size18)
.background(color = Color.White, shape = CircleShape),
) {
Image(
modifier = Modifier
.align(Alignment.TopEnd)
.size(TangemTheme.dimens.size18)
.background(color = Color.White, shape = CircleShape),
contentAlignment = Alignment.Center,
) {
Image(
modifier = Modifier.padding(all = 0.5.dp),
painter = painterResource(id = networkIconRes),
contentDescription = null,
)
}
.padding(all = TangemTheme.dimens.spacing0_5)
.align(Alignment.Center),
painter = painterResource(id = requireNotNull(networkIconRes)),
contentDescription = null,
)
}
}
}

View file

@ -8,12 +8,16 @@ import com.tangem.core.ui.components.marketprice.PriceChangeConfig
@Immutable
internal sealed interface TokenItemState {
/** Unique id */
val id: String
/** Loading token state */
object Loading : TokenItemState
data class Loading(override val id: String) : TokenItemState
/**
* Content token state
*
* @property id unique id
* @property tokenIconUrl token icon url
* @property tokenIconResId token icon resource id
* @property networkIconResId network icon resource id, may be null if it is a coin
@ -23,7 +27,7 @@ internal sealed interface TokenItemState {
* @property tokenOptions state for token options
*/
data class Content(
val id: String,
override val id: String,
val tokenIconUrl: String?,
@DrawableRes val tokenIconResId: Int,
@DrawableRes val networkIconResId: Int?,
@ -36,13 +40,15 @@ internal sealed interface TokenItemState {
/**
* Draggable token state
*
* @property id unique id
* @property tokenIconUrl token icon url
* @property tokenIconResId token icon resource id
* @property networkIconResId network icon resource id, may be null if it is a coin
* @property name token name
* @property fiatAmount fiat amount of token
*/
data class Draggable(
val id: String,
override val id: String,
val tokenIconUrl: String?,
@DrawableRes val tokenIconResId: Int,
@DrawableRes val networkIconResId: Int?,
@ -60,7 +66,7 @@ internal sealed interface TokenItemState {
* @property name token name
*/
data class Unreachable(
val id: String,
override val id: String,
val tokenIconUrl: String?,
@DrawableRes val tokenIconResId: Int,
@DrawableRes val networkIconResId: Int?,

View file

@ -1,8 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.state.components
import com.tangem.core.ui.components.wallet.WalletLockedContentState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.wallet.impl.R
import com.tangem.core.ui.components.wallet.WalletLockedContentState
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -23,9 +23,8 @@ internal sealed class WalletTokensListState(
/** Loading content state */
object Loading : WalletTokensListState(
items = persistentListOf(
TokensListItemState.NetworkGroupTitle(value = TextReference.Res(id = R.string.main_tokens)),
TokensListItemState.Token(state = TokenItemState.Loading),
TokensListItemState.Token(state = TokenItemState.Loading),
TokensListItemState.Token(state = TokenItemState.Loading(id = FIRST_LOADING_TOKEN_ID)),
TokensListItemState.Token(state = TokenItemState.Loading(id = SECOND_LOADING_TOKEN_ID)),
),
onOrganizeTokensClick = null,
)
@ -46,7 +45,7 @@ internal sealed class WalletTokensListState(
WalletTokensListState(
items = persistentListOf(
TokensListItemState.NetworkGroupTitle(value = TextReference.Res(id = R.string.main_tokens)),
TokensListItemState.Token(state = TokenItemState.Loading),
TokensListItemState.Token(state = TokenItemState.Loading(id = LOCKED_TOKEN_ID)),
),
onOrganizeTokensClick = null,
),
@ -69,4 +68,10 @@ internal sealed class WalletTokensListState(
*/
data class Token(val state: TokenItemState) : TokensListItemState
}
private companion object {
const val FIRST_LOADING_TOKEN_ID = "Loading#1"
const val SECOND_LOADING_TOKEN_ID = "Loading#2"
const val LOCKED_TOKEN_ID = "Locked#1"
}
}

View file

@ -45,7 +45,11 @@ internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState),
) {
items(items = config.wallets, key = { it.id.stringValue }) { state ->
items(
items = config.wallets,
key = { it.id.stringValue },
contentType = { it::class.java },
) { state ->
WalletCard(
state = state,
modifier = Modifier

View file

@ -21,6 +21,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
items(
items = configs,
key = { it.state.title.hashCode() },
contentType = { it.state::class.java },
itemContent = { Notification(state = it.state, modifier = modifier.animateItemPlacement()) },
)
}

View file

@ -19,7 +19,13 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletToke
internal fun LazyListScope.tokensListItems(state: WalletTokensListState, modifier: Modifier = Modifier) {
itemsIndexed(
items = state.items,
key = { index, _ -> index },
key = { _, item ->
when (item) {
is WalletTokensListState.TokensListItemState.NetworkGroupTitle -> item.value.hashCode()
is WalletTokensListState.TokensListItemState.Token -> item.state.id
}
},
contentType = { _, item -> item::class.java },
itemContent = { index, item ->
MultiCurrencyContentItem(
state = item,

View file

@ -4,6 +4,8 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier
private const val ORGANIZE_BUTTON_CONTENT_TYPE = "OrganizeTokensButton"
/**
* Organize tokens button
*
@ -14,5 +16,7 @@ import androidx.compose.ui.Modifier
*/
@OptIn(ExperimentalFoundationApi::class)
internal fun LazyListScope.organizeButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) {
item { OrganizeTokensButton(onClick = onClick, modifier = modifier.animateItemPlacement()) }
item(key = ORGANIZE_BUTTON_CONTENT_TYPE, contentType = ORGANIZE_BUTTON_CONTENT_TYPE) {
OrganizeTokensButton(onClick = onClick, modifier = modifier.animateItemPlacement())
}
}

View file

@ -10,6 +10,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletMana
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
private const val CONTROL_BUTTONS_CONTENT_TYPE = "ControlButtons"
/**
* Single currency control buttons. Like, "Buy", "Sell", etc
*
@ -20,7 +22,7 @@ import kotlinx.collections.immutable.toImmutableList
*/
@OptIn(ExperimentalFoundationApi::class)
internal fun LazyListScope.controlButtons(configs: ImmutableList<WalletManageButton>, modifier: Modifier = Modifier) {
item {
item(key = CONTROL_BUTTONS_CONTENT_TYPE, contentType = CONTROL_BUTTONS_CONTENT_TYPE) {
HorizontalActionChips(
buttons = configs.map(WalletManageButton::config).toImmutableList(),
modifier = modifier.animateItemPlacement(),

View file

@ -16,5 +16,10 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
*/
@OptIn(ExperimentalFoundationApi::class)
internal fun LazyListScope.marketPriceBlock(state: MarketPriceBlockState, modifier: Modifier = Modifier) {
item { MarketPriceBlock(state = state, modifier = modifier.animateItemPlacement()) }
item(
key = MarketPriceBlockState::class.java,
contentType = MarketPriceBlockState::class.java,
) {
MarketPriceBlock(state = state, modifier = modifier.animateItemPlacement())
}
}

View file

@ -30,7 +30,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
return when (value.value) {
is CryptoCurrencyStatus.Loading -> TokenItemState.Loading
is CryptoCurrencyStatus.Loading -> TokenItemState.Loading(id = value.currency.id.value)
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoQuote,

View file

@ -8,8 +8,14 @@ import kotlinx.collections.immutable.toImmutableList
internal object LoadingItemsProvider {
fun getLoadingMultiCurrencyTokens(): ImmutableList<WalletTokensListState.TokensListItemState.Token> {
return buildList(capacity = 5) {
add(WalletTokensListState.TokensListItemState.Token(state = TokenItemState.Loading))
}.toImmutableList()
val items = mutableListOf<WalletTokensListState.TokensListItemState.Token>()
repeat(times = 5) {
items.add(
WalletTokensListState.TokensListItemState.Token(
state = TokenItemState.Loading(id = "Loading#$it"),
),
)
}
return items.toImmutableList()
}
}