Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-29 14:54:15 +08:00
parent c1302681a4
commit e1f80e7e41
15 changed files with 356 additions and 297 deletions

View file

@ -87,6 +87,7 @@ data class TangemDimens internal constructor(
val spacing0: Dp = 0.dp,
val spacing0_5: Dp = 0.5.dp,
val spacing2: Dp = 2.dp,
val spacing3: Dp = 3.dp,
val spacing4: Dp = 4.dp,
val spacing6: Dp = 6.dp,
val spacing8: Dp = 8.dp,

View file

@ -40,10 +40,16 @@ object BigDecimalFormatter {
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
}
fun formatPercent(percent: BigDecimal, useAbsoluteValue: Boolean, locale: Locale = Locale.getDefault()): String {
fun formatPercent(
percent: BigDecimal,
useAbsoluteValue: Boolean,
locale: Locale = Locale.getDefault(),
maxFractionDigits: Int = 2,
minFractionDigits: Int = 2,
): String {
val formatter = NumberFormat.getPercentInstance(locale).apply {
maximumFractionDigits = 2
minimumFractionDigits = 2
maximumFractionDigits = maxFractionDigits
minimumFractionDigits = minFractionDigits
roundingMode = RoundingMode.HALF_UP
}
val value = if (useAbsoluteValue) percent.abs() else percent

View file

@ -14,7 +14,6 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemColorPalette
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
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
@ -124,15 +123,12 @@ internal object WalletPreviewData {
val tokenItemVisibleState by lazy {
TokenItemState.Content(
id = UUID.randomUUID().toString(),
icon = coinIconState,
name = "Polygon",
amount = "5,412 MATIC",
hasPending = true,
tokenOptions = TokenOptionsState(
fiatAmount = "321 $",
priceChangeState = PriceChangeState.Unknown,
isBalanceHidden = false,
),
iconState = coinIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon", hasPending = true),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $"),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,412 MATIC"),
priceChangeState = TokenItemState.PriceChangeState.Unknown,
isBalanceHidden = false,
onItemClick = {},
onItemLongClick = {},
)
@ -140,26 +136,23 @@ internal object WalletPreviewData {
val testnetTokenItemVisibleState by lazy {
tokenItemVisibleState.copy(
name = "Polygon testnet",
icon = tokenIconState.copy(isGrayscale = true),
titleState = TokenItemState.TitleState.Content(text = "Polygon testnet"),
iconState = tokenIconState.copy(isGrayscale = true),
)
}
val tokenItemHiddenState by lazy {
TokenItemState.Content(
id = UUID.randomUUID().toString(),
icon = tokenIconState,
name = "Polygon",
amount = "5,412 MATIC",
hasPending = false,
tokenOptions = TokenOptionsState(
priceChangeState = PriceChangeState.Content(
valueInPercent = "2%",
type = PriceChangeType.UP,
),
fiatAmount = "321 $",
isBalanceHidden = false,
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $"),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,412 MATIC"),
priceChangeState = TokenItemState.PriceChangeState.Content(
valueInPercent = "2.0%",
type = PriceChangeType.UP,
),
isBalanceHidden = false,
onItemClick = {},
onItemLongClick = {},
)
@ -168,17 +161,17 @@ internal object WalletPreviewData {
val tokenItemDragState by lazy {
TokenItemState.Draggable(
id = UUID.randomUUID().toString(),
icon = tokenIconState,
name = "Polygon",
info = stringReference(value = "3 172,14 $"),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "3 172,14 $"),
)
}
val tokenItemUnreachableState by lazy {
TokenItemState.Unreachable(
id = UUID.randomUUID().toString(),
icon = tokenIconState,
name = "Polygon",
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
onItemClick = {},
onItemLongClick = {},
)
@ -187,16 +180,16 @@ internal object WalletPreviewData {
val tokenItemNoAddressState by lazy {
TokenItemState.NoAddress(
id = UUID.randomUUID().toString(),
icon = tokenIconState,
name = "Polygon",
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
onItemLongClick = {},
)
}
val customTokenItemVisibleState by lazy {
tokenItemVisibleState.copy(
name = "Polygon custom",
icon = customTokenIconState.copy(
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
iconState = customTokenIconState.copy(
tint = TangemColorPalette.White,
background = TangemColorPalette.Black,
),
@ -205,8 +198,8 @@ internal object WalletPreviewData {
val customTestnetTokenItemVisibleState by lazy {
tokenItemVisibleState.copy(
name = "Polygon custom testnet",
icon = customTokenIconState.copy(isGrayscale = true),
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
iconState = customTokenIconState.copy(isGrayscale = true),
)
}
@ -238,7 +231,9 @@ internal object WalletPreviewData {
DraggableItem.Token(
tokenItemState = tokenItemDragState.copy(
id = "${group.id}_token_$tokenNumber",
name = "Token $tokenNumber from $networkNumber network",
titleState = TokenItemState.TitleState.Content(
text = "Token $tokenNumber from $networkNumber network",
),
),
groupId = group.id,
roundingMode = when {
@ -348,37 +343,37 @@ internal object WalletPreviewData {
TokensListItemState.Token(
tokenItemVisibleState.copy(
id = "token_1",
name = "Ethereum",
amount = "1,89340821 ETH",
titleState = TokenItemState.TitleState.Content(text = "Ethereum"),
cryptoAmountState = TokenItemState.CryptoAmountState.Content("1,89340821 ETH"),
),
),
TokensListItemState.Token(
tokenItemVisibleState.copy(
id = "token_2",
name = "Ethereum",
amount = "1,89340821 ETH",
titleState = TokenItemState.TitleState.Content(text = "Ethereum"),
cryptoAmountState = TokenItemState.CryptoAmountState.Content("1,89340821 ETH"),
),
),
TokensListItemState.Token(
tokenItemVisibleState.copy(
id = "token_3",
name = "Ethereum",
amount = "1,89340821 ETH",
titleState = TokenItemState.TitleState.Content(text = "Ethereum"),
cryptoAmountState = TokenItemState.CryptoAmountState.Content("1,89340821 ETH"),
),
),
TokensListItemState.Token(
tokenItemVisibleState.copy(
id = "token_4",
name = "Ethereum",
amount = "1,89340821 ETH",
titleState = TokenItemState.TitleState.Content(text = "Ethereum"),
cryptoAmountState = TokenItemState.CryptoAmountState.Content("1,89340821 ETH"),
),
),
TokensListItemState.NetworkGroupTitle(id = 1, stringReference("Ethereum")),
TokensListItemState.Token(
tokenItemVisibleState.copy(
id = "token_5",
name = "Ethereum",
amount = "1,89340821 ETH",
titleState = TokenItemState.TitleState.Content(text = "Ethereum"),
cryptoAmountState = TokenItemState.CryptoAmountState.Content("1,89340821 ETH"),
),
),
),

View file

@ -11,7 +11,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.constraintlayout.compose.*
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.res.TangemTheme
@ -38,8 +37,10 @@ internal fun TokenItem(
) {
val (iconRef, titleRef, cryptoAmountRef, fiatAmountRef, priceChangeRef, nonFiatContentRef) = createRefs()
val isBalanceHidden = (state as? TokenItemState.Content)?.isBalanceHidden ?: false
TokenIcon(
state = state,
state = state.iconState,
modifier = Modifier.constrainAs(iconRef) {
centerVerticallyTo(parent)
start.linkTo(parent.start)
@ -52,7 +53,7 @@ internal fun TokenItem(
}
TokenTitle(
state = state,
state = state.titleState,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing8)
.constrainAs(titleRef) {
@ -76,7 +77,8 @@ internal fun TokenItem(
)
TokenFiatAmount(
state = state,
state = state.fiatAmountState,
isBalanceHidden = isBalanceHidden,
modifier = Modifier.constrainAs(fiatAmountRef) {
top.linkTo(parent.top)
end.linkTo(parent.end)
@ -91,7 +93,8 @@ internal fun TokenItem(
val marginBetweenRows = TangemTheme.dimens.spacing2
TokenCryptoAmount(
state = state,
state = state.cryptoAmountState,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing8)
.constrainAs(cryptoAmountRef) {
@ -117,16 +120,21 @@ internal fun TokenItem(
derivedStateOf { with(density) { rootWidth.toDp().times(other = 0.16f) } }
}
TokenPriceChange(
state = state,
state = state.priceChangeState,
modifier = Modifier.constrainAs(priceChangeRef) {
top.linkTo(fiatAmountRef.bottom, marginBetweenRows)
end.linkTo(anchor = parent.end)
bottom.linkTo(parent.bottom)
if (state is TokenItemState.ContentState) {
start.linkTo(cryptoAmountRef.end)
width = Dimension.fillToConstraints
.atLeast(priceChangeRequiredMinWidth)
when (state.priceChangeState) {
is TokenItemState.PriceChangeState.Content,
is TokenItemState.PriceChangeState.Unknown,
-> {
start.linkTo(cryptoAmountRef.end)
width = Dimension.fillToConstraints
.atLeast(priceChangeRequiredMinWidth)
}
else -> Unit
}
},
)
@ -202,22 +210,24 @@ private fun Preview_Tokens_DarkTheme(@PreviewParameter(TokenConfigProvider::clas
private class TokenConfigProvider : CollectionPreviewParameterProvider<TokenItemState>(
collection = listOf(
WalletPreviewData.tokenItemVisibleState.copy(amount = "5,41221467146712416241274127841274174213421 MATIC"),
WalletPreviewData.tokenItemVisibleState.copy(
tokenOptions = WalletPreviewData.tokenItemVisibleState.tokenOptions.copy(
priceChangeState = PriceChangeState.Content(
valueInPercent = "31231231231231231231223123123123212312312312.00%",
type = PriceChangeType.UP,
),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(
text = "5,41221467146712416241274127841274174213421 MATIC",
),
),
WalletPreviewData.tokenItemVisibleState.copy(
amount = "5,41221467146712416241274127841274174213421 MATIC",
tokenOptions = WalletPreviewData.tokenItemVisibleState.tokenOptions.copy(
priceChangeState = PriceChangeState.Content(
valueInPercent = "31231231231231231231223123123123212312312312.00%",
type = PriceChangeType.UP,
),
priceChangeState = TokenItemState.PriceChangeState.Content(
valueInPercent = "31231231231231231231223123123123212312312312.0%",
type = PriceChangeType.UP,
),
),
WalletPreviewData.tokenItemVisibleState.copy(
cryptoAmountState = TokenItemState.CryptoAmountState.Content(
text = "5,41221467146712416241274127841274174213421 MATIC",
),
priceChangeState = TokenItemState.PriceChangeState.Content(
valueInPercent = "31231231231231231231223123123123212312312312.0%",
type = PriceChangeType.UP,
),
),
WalletPreviewData.tokenItemVisibleState,

View file

@ -1,51 +1,51 @@
package com.tangem.feature.wallet.presentation.common.component.token
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.common.Strings
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemTypography
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.CryptoAmountState as TokenCryptoAmountState
@OptIn(ExperimentalAnimationApi::class)
@Composable
internal fun TokenCryptoAmount(state: TokenItemState, modifier: Modifier = Modifier) {
AnimatedContent(targetState = state, label = "Update crypto amount", modifier = modifier) { animatedState ->
when (animatedState) {
is TokenItemState.Content -> {
CryptoAmountText(
amount = if (animatedState.tokenOptions.isBalanceHidden) Strings.STARS else animatedState.amount,
)
}
is TokenItemState.Draggable -> {
CryptoAmountText(amount = animatedState.info.resolveReference())
}
is TokenItemState.Loading -> {
RectangleShimmer(modifier = Modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenItemState.Locked -> {
LockedRectangle(modifier = Modifier.placeholderSize())
}
is TokenItemState.Unreachable,
is TokenItemState.NoAddress,
-> Unit
internal fun TokenCryptoAmount(
state: TokenCryptoAmountState?,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
when (state) {
is TokenCryptoAmountState.Content -> {
CryptoAmountText(
amount = if (isBalanceHidden) Strings.STARS else state.text,
modifier = modifier,
)
}
is TokenCryptoAmountState.Unreachable -> {
CryptoAmountText(amount = stringResource(id = R.string.common_unreachable), modifier = modifier)
}
is TokenCryptoAmountState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenCryptoAmountState.Locked -> {
LockedRectangle(modifier = modifier.placeholderSize())
}
null -> Unit
}
}
@Composable
private fun CryptoAmountText(amount: String) {
private fun CryptoAmountText(amount: String, modifier: Modifier = Modifier) {
Text(
text = amount,
modifier = modifier,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -55,6 +55,6 @@ private fun CryptoAmountText(amount: String) {
private fun Modifier.placeholderSize(): Modifier = composed {
return@composed this
.padding(vertical = TangemTheme.dimens.spacing4)
.padding(vertical = TangemTheme.dimens.spacing3)
.size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12)
}

View file

@ -1,7 +1,5 @@
package com.tangem.feature.wallet.presentation.common.component.token
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
@ -13,40 +11,39 @@ import com.tangem.common.Strings
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemTypography
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.FiatAmountState as TokenFiatAmountState
@OptIn(ExperimentalAnimationApi::class)
@Composable
internal fun TokenFiatAmount(state: TokenItemState, modifier: Modifier = Modifier) {
AnimatedContent(targetState = state, label = "Update fiat amount", modifier = modifier) { animatedState ->
when (animatedState) {
is TokenItemState.Content -> {
Text(
text = if (animatedState.tokenOptions.isBalanceHidden) {
Strings.STARS
} else {
animatedState.tokenOptions.fiatAmount
},
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTypography.body2,
)
}
is TokenItemState.Loading -> {
RectangleShimmer(modifier = Modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenItemState.Locked -> {
LockedRectangle(modifier = Modifier.placeholderSize())
}
is TokenItemState.Unreachable,
is TokenItemState.Draggable,
is TokenItemState.NoAddress,
-> Unit
internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
when (state) {
is TokenFiatAmountState.Content -> {
FiatAmountText(
text = if (isBalanceHidden) Strings.STARS else state.text,
modifier,
)
}
is TokenFiatAmountState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenFiatAmountState.Locked -> {
LockedRectangle(modifier = modifier.placeholderSize())
}
null -> Unit
}
}
@Composable
private fun FiatAmountText(text: String, modifier: Modifier = Modifier) {
Text(
text = text,
modifier,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTypography.body2,
)
}
private fun Modifier.placeholderSize(): Modifier = composed {
return@composed this
.padding(vertical = TangemTheme.dimens.spacing4)

View file

@ -13,55 +13,49 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemTypography
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.PriceChangeState as TokenPriceChangeState
@OptIn(ExperimentalAnimationApi::class)
@Composable
internal fun TokenPriceChange(state: TokenItemState, modifier: Modifier = Modifier) {
AnimatedContent(targetState = state, label = "Update the price change", modifier = modifier) { animatedState ->
when (animatedState) {
is TokenItemState.Content -> {
PriceChangeBlock(state = animatedState.tokenOptions.priceChangeState)
}
is TokenItemState.Loading -> {
RectangleShimmer(modifier = Modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenItemState.Locked -> {
LockedRectangle(modifier = Modifier.placeholderSize())
}
is TokenItemState.Unreachable,
is TokenItemState.Draggable,
is TokenItemState.NoAddress,
-> Unit
internal fun TokenPriceChange(state: TokenPriceChangeState?, modifier: Modifier = Modifier) {
when (state) {
is TokenPriceChangeState.Content -> {
PriceChangeBlock(modifier = modifier, type = state.type, text = state.valueInPercent)
}
is TokenPriceChangeState.Unknown -> {
PriceChangeBlock(modifier = modifier)
}
is TokenPriceChangeState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenPriceChangeState.Locked -> {
LockedRectangle(modifier = modifier.placeholderSize())
}
null -> Unit
}
}
@Composable
private fun PriceChangeBlock(state: PriceChangeState) {
Row(horizontalArrangement = Arrangement.End) {
PriceChangeIcon(
type = (state as? PriceChangeState.Content)?.type,
modifier = Modifier.align(Alignment.CenterVertically),
)
private fun PriceChangeBlock(modifier: Modifier = Modifier, type: PriceChangeType? = null, text: String? = null) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
PriceChangeIcon(type = type)
SpacerW4()
PriceChangeText(state = state, modifier = Modifier.align(Alignment.CenterVertically))
PriceChangeText(type = type, text = text)
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun PriceChangeIcon(type: PriceChangeType?, modifier: Modifier = Modifier) {
AnimatedContent(
targetState = type,
label = "Update the price change's arrow",
modifier = modifier,
) { animatedType ->
private fun PriceChangeIcon(type: PriceChangeType?) {
AnimatedContent(targetState = type, label = "Update the price change's arrow") { animatedType ->
animatedType ?: return@AnimatedContent
Icon(
@ -82,25 +76,14 @@ private fun PriceChangeIcon(type: PriceChangeType?, modifier: Modifier = Modifie
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun PriceChangeText(state: PriceChangeState, modifier: Modifier = Modifier) {
AnimatedContent(
targetState = state,
label = "Update the price change's text",
modifier = modifier,
) { animatedState ->
private fun PriceChangeText(type: PriceChangeType?, text: String?) {
AnimatedContent(targetState = text, label = "Update the price change's text") { animatedText ->
Text(
text = when (animatedState) {
is PriceChangeState.Content -> animatedState.valueInPercent
is PriceChangeState.Unknown -> TokenItemState.UNKNOWN_AMOUNT_SIGN
},
color = when (animatedState) {
is PriceChangeState.Content -> {
when (animatedState.type) {
PriceChangeType.UP -> TangemTheme.colors.text.accent
PriceChangeType.DOWN -> TangemTheme.colors.text.warning
}
}
PriceChangeState.Unknown -> TangemTheme.colors.text.primary1
text = animatedText ?: TokenItemState.UNKNOWN_AMOUNT_SIGN,
color = when (type) {
PriceChangeType.UP -> TangemTheme.colors.text.accent
PriceChangeType.DOWN -> TangemTheme.colors.text.warning
null -> TangemTheme.colors.text.primary1
},
overflow = TextOverflow.Ellipsis,
maxLines = 1,
@ -111,6 +94,6 @@ private fun PriceChangeText(state: PriceChangeState, modifier: Modifier = Modifi
private fun Modifier.placeholderSize(): Modifier = composed {
return@composed this
.padding(vertical = TangemTheme.dimens.spacing4)
.padding(vertical = TangemTheme.dimens.spacing3)
.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12)
}

View file

@ -1,12 +1,13 @@
package com.tangem.feature.wallet.presentation.common.component.token
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
@ -16,32 +17,34 @@ import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemTypography
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TitleState as TokenTitleState
@OptIn(ExperimentalAnimationApi::class)
@Composable
internal fun TokenTitle(state: TokenItemState, modifier: Modifier = Modifier) {
AnimatedContent(targetState = state, label = "Update title", modifier = modifier) { animatedState ->
when (animatedState) {
is TokenItemState.ContentState -> {
ContentTitle(
name = animatedState.name,
hasPending = (animatedState as? TokenItemState.Content)?.hasPending == true,
)
}
is TokenItemState.Loading -> {
RectangleShimmer(modifier = Modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenItemState.Locked -> {
LockedRectangle(modifier = Modifier.placeholderSize())
}
internal fun TokenTitle(state: TokenTitleState?, modifier: Modifier = Modifier) {
when (state) {
is TokenTitleState.Content -> {
ContentTitle(name = state.text, hasPending = state.hasPending, modifier = modifier)
}
is TokenTitleState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenTitleState.Locked -> {
LockedRectangle(modifier = modifier.placeholderSize())
}
null -> Unit
}
}
@Composable
private fun ContentTitle(name: String, hasPending: Boolean) {
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6)) {
private fun ContentTitle(name: String, hasPending: Boolean, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing6),
) {
/*
* If currency name has a long width, then it will completely displace the image.
* So we need to use [weight] to avoid displacement.
*/
CurrencyNameText(name = name, modifier = Modifier.weight(weight = 1f, fill = false))
PendingTransactionImage(

View file

@ -53,6 +53,9 @@ internal fun ContentIcon(
background = icon.background,
alpha = alpha,
)
TokenItemState.IconState.Loading,
TokenItemState.IconState.Locked,
-> Unit
}
}

View file

@ -14,26 +14,28 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.IconState as TokenIconState
private const val GRAY_SCALE_SATURATION = 0f
private const val GRAY_SCALE_ALPHA = 0.4f
private const val NORMAL_ALPHA = 1f
@Composable
internal fun TokenIcon(state: TokenItemState, modifier: Modifier = Modifier) {
internal fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier) {
BaseContainer(modifier = modifier) {
val iconModifier = Modifier
.align(Alignment.Center)
.size(TangemTheme.dimens.size36)
when (state) {
is TokenItemState.Loading -> LoadingIcon(modifier = iconModifier)
is TokenItemState.Locked -> LockedIcon(modifier = iconModifier)
is TokenItemState.ContentState -> ContentIconContainer(
modifier = iconModifier,
icon = state.icon,
)
is TokenIconState.Loading -> LoadingIcon(modifier = iconModifier)
is TokenIconState.Locked -> LockedIcon(modifier = iconModifier)
is TokenIconState.CoinIcon,
is TokenIconState.CustomTokenIcon,
is TokenIconState.TokenIcon,
-> {
ContentIconContainer(modifier = iconModifier, icon = state)
}
}
}
}
@ -58,7 +60,7 @@ private fun LockedIcon(modifier: Modifier = Modifier) {
}
@Composable
private fun BoxScope.ContentIconContainer(icon: TokenItemState.IconState, modifier: Modifier = Modifier) {
private fun BoxScope.ContentIconContainer(icon: TokenIconState, modifier: Modifier = Modifier) {
val networkBadgeOffset = TangemTheme.dimens.spacing4
val (alpha, colorFilter) = remember(icon.isGrayscale) {
if (icon.isGrayscale) {

View file

@ -3,99 +3,119 @@ package com.tangem.feature.wallet.presentation.common.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.components.marketprice.PriceChangeType
/** Token item state */
@Immutable
internal sealed interface TokenItemState {
internal sealed class TokenItemState {
/** Unique id */
val id: String
abstract val id: String
abstract val iconState: IconState
abstract val titleState: TitleState
abstract val fiatAmountState: FiatAmountState?
abstract val cryptoAmountState: CryptoAmountState?
abstract val priceChangeState: PriceChangeState?
/** Loading token state */
data class Loading(override val id: String) : TokenItemState
data class Loading(override val id: String) : TokenItemState() {
override val iconState: IconState = IconState.Loading
override val titleState: TitleState = TitleState.Loading
override val fiatAmountState: FiatAmountState = FiatAmountState.Loading
override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Loading
override val priceChangeState: PriceChangeState = PriceChangeState.Loading
}
/** Locked token state */
data class Locked(override val id: String) : TokenItemState
/** Content state */
@Immutable
sealed class ContentState : TokenItemState {
abstract val icon: IconState
abstract val name: String
data class Locked(override val id: String) : TokenItemState() {
override val iconState: IconState = IconState.Locked
override val titleState: TitleState = TitleState.Locked
override val fiatAmountState: FiatAmountState = FiatAmountState.Locked
override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Locked
override val priceChangeState: PriceChangeState = PriceChangeState.Locked
}
/**
* Content token state
*
* @property id unique id
* @property icon token icon state
* @property name token name
* @property amount amount of token
* @property hasPending pending tx in blockchain
* @property tokenOptions state for token options
* @property iconState token icon state
* @property titleState token name
* @property onItemClick callback which will be called when an item is clicked
* @property onItemLongClick callback which will be called when an item is long clicked
*/
data class Content(
override val id: String,
override val icon: IconState,
override val name: String,
val amount: String,
val hasPending: Boolean,
val tokenOptions: TokenOptionsState,
override val iconState: IconState,
override val titleState: TitleState,
override val fiatAmountState: FiatAmountState?,
override val cryptoAmountState: CryptoAmountState.Content,
override val priceChangeState: PriceChangeState?,
val isBalanceHidden: Boolean,
val onItemClick: () -> Unit,
val onItemLongClick: () -> Unit,
) : ContentState()
) : TokenItemState()
/**
* Draggable token state
*
* @property id unique id
* @property icon token icon state
* @property name token name
* @property info token info (e.g. fiat balance or status)
* @property iconState token icon state
* @property titleState token name
*/
data class Draggable(
override val id: String,
override val icon: IconState,
override val name: String,
val info: TextReference,
) : ContentState()
override val iconState: IconState,
override val titleState: TitleState,
override val cryptoAmountState: CryptoAmountState,
) : TokenItemState() {
override val fiatAmountState: FiatAmountState? = null
override val priceChangeState: PriceChangeState? = null
}
/**
* Unreachable token state
*
* @property id token id
* @property icon token icon state
* @property name token name
* @property iconState token icon state
* @property titleState token name
* @property onItemClick callback which will be called when an item is clicked
* @property onItemLongClick callback which will be called when an item is long clicked
*/
data class Unreachable(
override val id: String,
override val icon: IconState,
override val name: String,
override val iconState: IconState,
override val titleState: TitleState,
val onItemClick: () -> Unit,
val onItemLongClick: () -> Unit,
) : ContentState()
) : TokenItemState() {
override val fiatAmountState: FiatAmountState? = null
override val cryptoAmountState: CryptoAmountState? = null
override val priceChangeState: PriceChangeState? = null
}
/**
* No derivation address state
*
* @property id token id
* @property icon token icon state
* @property name token name
* @property iconState token icon state
* @property titleState token name
* @property onItemLongClick callback which will be called when an item is long clicked
*/
data class NoAddress(
override val id: String,
override val icon: IconState,
override val name: String,
override val iconState: IconState,
override val titleState: TitleState,
val onItemLongClick: () -> Unit,
) : ContentState()
) : TokenItemState() {
override val fiatAmountState: FiatAmountState? = null
override val cryptoAmountState: CryptoAmountState? = null
override val priceChangeState: PriceChangeState? = null
}
/**
* Represents the various states an icon can be in.
@ -161,15 +181,60 @@ internal sealed interface TokenItemState {
override val isCustom: Boolean = false
}
object Loading : IconState() {
override val isGrayscale: Boolean = false
override val isCustom: Boolean = false
override val networkBadgeIconResId: Int? = null
}
object Locked : IconState() {
override val isGrayscale: Boolean = false
override val isCustom: Boolean = false
override val networkBadgeIconResId: Int? = null
}
}
/** Token options state */
@Immutable
data class TokenOptionsState(
val priceChangeState: PriceChangeState,
val fiatAmount: String,
val isBalanceHidden: Boolean,
)
sealed class TitleState {
data class Content(val text: String, val hasPending: Boolean = false) : TitleState()
object Loading : TitleState()
object Locked : TitleState()
}
@Immutable
sealed class FiatAmountState {
data class Content(val text: String) : FiatAmountState()
object Loading : FiatAmountState()
object Locked : FiatAmountState()
}
@Immutable
sealed class CryptoAmountState {
data class Content(val text: String) : CryptoAmountState()
object Unreachable : CryptoAmountState()
object Loading : CryptoAmountState()
object Locked : CryptoAmountState()
}
sealed class PriceChangeState {
data class Content(val valueInPercent: String, val type: PriceChangeType) : PriceChangeState()
object Unknown : PriceChangeState()
object Loading : PriceChangeState()
object Locked : PriceChangeState()
}
companion object {
const val UNKNOWN_AMOUNT_SIGN = ""

View file

@ -1,21 +1,12 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
internal class TokenItemHiddenStateConverter {
fun updateHiddenState(
optionsState: TokenItemState.TokenOptionsState,
isBalanceHidden: Boolean,
): TokenItemState.TokenOptionsState {
fun updateHiddenState(wasBalanceHidden: Boolean, isBalanceHidden: Boolean): Boolean {
return when {
!optionsState.isBalanceHidden && isBalanceHidden -> {
optionsState.copy(isBalanceHidden = true)
}
optionsState.isBalanceHidden && !isBalanceHidden -> {
optionsState.copy(isBalanceHidden = false)
}
else -> optionsState
!wasBalanceHidden && isBalanceHidden -> true
wasBalanceHidden && !isBalanceHidden -> false
else -> wasBalanceHidden
}
}
}

View file

@ -1,12 +1,9 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
import com.tangem.common.Provider
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
@ -48,12 +45,12 @@ internal class CryptoCurrencyToDraggableItemConverter(
return TokenItemState.Draggable(
id = getTokenItemId(currency.id),
icon = iconStateConverter.convert(currencyStatus),
name = currency.name,
info = if (currencyStatus.value.isError) {
resourceReference(id = R.string.common_unreachable)
iconState = iconStateConverter.convert(currencyStatus),
titleState = TokenItemState.TitleState.Content(text = currency.name),
cryptoAmountState = if (currencyStatus.value.isError) {
TokenItemState.CryptoAmountState.Unreachable
} else {
stringReference(getFormattedFiatAmount(currencyStatus, appCurrency))
TokenItemState.CryptoAmountState.Content(text = getFormattedFiatAmount(currencyStatus, appCurrency))
},
)
}

View file

@ -1,7 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.utils
import com.tangem.common.Provider
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
@ -39,15 +38,17 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
private fun CryptoCurrencyStatus.mapToTokenItemState(): TokenItemState.Content {
return TokenItemState.Content(
id = currency.id.value,
name = currency.name,
icon = iconStateConverter.convert(value = this),
amount = getFormattedAmount(),
hasPending = value.hasCurrentNetworkTransactions,
tokenOptions = TokenItemState.TokenOptionsState(
fiatAmount = getFormattedFiatAmount(),
priceChangeState = getPriceChangeConfig(),
isBalanceHidden = isBalanceHiddenProvider(),
iconState = iconStateConverter.convert(value = this),
titleState = TokenItemState.TitleState.Content(
text = currency.name,
hasPending = value.hasCurrentNetworkTransactions,
),
fiatAmountState = TokenItemState.FiatAmountState.Content(
text = getFormattedFiatAmount(),
),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = getFormattedAmount()),
priceChangeState = getPriceChangeConfig(),
isBalanceHidden = isBalanceHiddenProvider(),
onItemClick = { clickIntents.onTokenItemClick(currency) },
onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) },
)
@ -68,29 +69,34 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable(
id = currency.id.value,
name = currency.name,
icon = iconStateConverter.convert(value = this),
iconState = iconStateConverter.convert(value = this),
titleState = TokenItemState.TitleState.Content(text = currency.name),
onItemClick = { clickIntents.onTokenItemClick(currency) },
onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) },
)
private fun CryptoCurrencyStatus.mapToNoAddressTokenItemState() = TokenItemState.NoAddress(
id = currency.id.value,
name = currency.name,
icon = iconStateConverter.convert(this),
iconState = iconStateConverter.convert(this),
titleState = TokenItemState.TitleState.Content(text = currency.name),
onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) },
)
private fun CryptoCurrencyStatus.getPriceChangeConfig(): PriceChangeState {
private fun CryptoCurrencyStatus.getPriceChangeConfig(): TokenItemState.PriceChangeState {
val priceChange = value.priceChange
return if (priceChange != null) {
PriceChangeState.Content(
valueInPercent = BigDecimalFormatter.formatPercent(percent = priceChange, useAbsoluteValue = true),
TokenItemState.PriceChangeState.Content(
valueInPercent = BigDecimalFormatter.formatPercent(
percent = priceChange,
useAbsoluteValue = true,
maxFractionDigits = 1,
minFractionDigits = 1,
),
type = priceChange.getPriceChangeType(),
)
} else {
PriceChangeState.Unknown
TokenItemState.PriceChangeState.Unknown
}
}

View file

@ -28,8 +28,8 @@ internal class HiddenStateConverter(
if (tokenListItemState.state is TokenItemState.Content) {
tokenListItemState.copy(
state = tokenListItemState.state.copy(
tokenOptions = tokenItemHiddenStateConverter.updateHiddenState(
optionsState = tokenListItemState.state.tokenOptions,
isBalanceHidden = tokenItemHiddenStateConverter.updateHiddenState(
wasBalanceHidden = tokenListItemState.state.isBalanceHidden,
isBalanceHidden = value,
),
),