Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-09 15:29:49 +07:00
parent b42fa5283c
commit 18474637b9
8 changed files with 249 additions and 105 deletions

View file

@ -0,0 +1,132 @@
package com.tangem.core.ui.components.account
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
enum class AccountIconSize {
Default, Large, Medium, Small, ExtraSmall
}
/**
* Displays a portfolio icon using a predefined vector resource.
*
* The background color is defined by [color], and the icon tint is constant white.
* The icon size and container shape are adapted based on the provided [size].
*
* @param resId The vector drawable resource ID to display.
* @param color The background color of the icon container.
* @param size The size of the icon, defined by [AccountIconSize].
*/
@Composable
fun AccountResIcon(@DrawableRes resId: Int, color: Color, size: AccountIconSize, modifier: Modifier = Modifier) {
val boxModifier = modifier.selectBoxModifier(size)
val iconSize = Modifier.selectIconSize(size)
Box(
contentAlignment = Alignment.Center,
modifier = boxModifier.background(color),
) {
Icon(
modifier = iconSize,
tint = TangemTheme.colors.text.constantWhite,
imageVector = ImageVector.vectorResource(id = resId),
contentDescription = null,
)
}
}
/**
* Displays a portfolio icon using a single character.
*
* The background color is defined by [color], the text is uppercased,
* and its style is chosen based on the provided [size].
* The container shape and size also adapt to [size].
*
* @param char The character to display inside the icon.
* @param color The background color of the icon container.
* @param size The size of the icon, defined by [AccountIconSize].
*/
@Composable
fun AccountCharIcon(char: Char, color: Color, size: AccountIconSize, modifier: Modifier = Modifier) {
val boxModifier = modifier.selectBoxModifier(size)
val textStyle = when (size) {
AccountIconSize.Default -> TangemTheme.typography.h3
AccountIconSize.Large -> TangemTheme.typography.h1
AccountIconSize.Medium -> TangemTheme.typography.subtitle1
AccountIconSize.Small -> TangemTheme.typography.subtitle2
AccountIconSize.ExtraSmall -> TangemTheme.typography.caption1
}
Box(
contentAlignment = Alignment.Center,
modifier = boxModifier.background(color),
) {
Text(
text = char.uppercase(),
style = textStyle,
color = TangemTheme.colors.text.constantWhite,
)
}
}
private fun Modifier.selectIconSize(size: AccountIconSize): Modifier = when (size) {
AccountIconSize.Default -> this.size(20.dp)
AccountIconSize.Large -> this.size(40.dp)
AccountIconSize.Medium -> this.size(16.dp)
AccountIconSize.Small -> this.size(12.dp)
AccountIconSize.ExtraSmall -> this.size(8.dp)
}
private fun Modifier.selectBoxModifier(size: AccountIconSize): Modifier = when (size) {
AccountIconSize.Default -> size(36.dp).clip(RoundedCornerShape(10.dp))
AccountIconSize.Large -> size(88.dp).clip(RoundedCornerShape(24.dp))
AccountIconSize.Medium -> size(28.dp).clip(RoundedCornerShape(8.dp))
AccountIconSize.Small -> size(20.dp).clip(RoundedCornerShape(6.dp))
AccountIconSize.ExtraSmall -> size(14.dp).clip(RoundedCornerShape(4.dp))
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
Row(
modifier = Modifier.background(TangemTheme.colors.background.primary),
) {
Sample()
}
}
}
@Composable
private fun Sample() {
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
AccountResIcon(resId = R.drawable.ic_shirt_24, color = Color.Red, size = AccountIconSize.Default)
AccountResIcon(resId = R.drawable.ic_rounded_star_24, color = Color.Blue, size = AccountIconSize.Large)
AccountResIcon(resId = R.drawable.ic_user_24, color = Color.Magenta, size = AccountIconSize.Medium)
AccountResIcon(resId = R.drawable.ic_family_24, color = Color.DarkGray, size = AccountIconSize.Small)
AccountResIcon(resId = R.drawable.ic_wallet_24, color = Color.Green, size = AccountIconSize.ExtraSmall)
}
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
AccountCharIcon(char = 'D', color = Color.Red, size = AccountIconSize.Default)
AccountCharIcon(char = 'L', color = Color.Blue, size = AccountIconSize.Large)
AccountCharIcon(char = 'M', color = Color.Magenta, size = AccountIconSize.Medium)
AccountCharIcon(char = 'S', color = Color.DarkGray, size = AccountIconSize.Small)
AccountCharIcon(char = 'E', color = Color.Green, size = AccountIconSize.ExtraSmall)
}
}

View file

@ -13,6 +13,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.R
import com.tangem.core.ui.components.account.AccountCharIcon
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.account.AccountResIcon
import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
import com.tangem.core.ui.res.TangemTheme
@ -58,6 +61,18 @@ internal fun ContentIcon(
background = icon.background,
alpha = alpha,
)
is CurrencyIconState.CryptoPortfolio.Icon -> AccountResIcon(
modifier = modifier,
resId = icon.resId,
color = icon.color,
size = AccountIconSize.Default,
)
is CurrencyIconState.CryptoPortfolio.Letter -> AccountCharIcon(
modifier = modifier,
char = icon.char,
color = icon.color,
size = AccountIconSize.Default,
)
CurrencyIconState.Loading,
CurrencyIconState.Locked,
is CurrencyIconState.Empty,

View file

@ -48,6 +48,8 @@ fun CurrencyIcon(
is CurrencyIconState.FiatIcon,
is CurrencyIconState.CustomTokenIcon,
is CurrencyIconState.TokenIcon,
is CurrencyIconState.CryptoPortfolio.Icon,
is CurrencyIconState.CryptoPortfolio.Letter,
-> {
ContentIconContainer(
icon = state,

View file

@ -86,6 +86,24 @@ sealed class CurrencyIconState {
override val topBadgeIconResId: Int? = null
}
sealed class CryptoPortfolio : CurrencyIconState() {
override val showCustomBadge: Boolean = false
override val topBadgeIconResId: Int? = null
abstract val color: Color
data class Icon(
@DrawableRes val resId: Int,
override val color: Color,
override val isGrayscale: Boolean,
) : CryptoPortfolio()
data class Letter(
val char: Char,
override val color: Color,
override val isGrayscale: Boolean,
) : CryptoPortfolio()
}
data object Loading : CurrencyIconState() {
override val isGrayscale: Boolean = false
override val showCustomBadge: Boolean = false
@ -125,6 +143,12 @@ sealed class CurrencyIconState {
showCustomBadge = showCustomBadge,
topBadgeIconResId = topBadgeIconResId,
)
is CryptoPortfolio.Icon -> copy(
isGrayscale = isGrayscale,
)
is CryptoPortfolio.Letter -> copy(
isGrayscale = isGrayscale,
)
is Loading,
is Locked,
is Empty,

View file

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.Placeable
@ -309,7 +310,13 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier
},
)
fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width, y = verticalPadding)
fiatAmount?.placeRelative(
x = layoutWidth - fiatAmount.width,
y = when (state.subtitle2State) {
null -> (layoutHeight - fiatAmount.height).div(other = 2)
else -> verticalPadding
},
)
priceChange?.placeRelative(
x = icon.width,
@ -462,7 +469,7 @@ private fun calculateLayoutHeight(
return max(firstColumnHeight, secondColumnHeight).coerceAtLeast(minLayoutHeight)
}
@Preview(widthDp = 360)
@Preview(widthDp = 360, showBackground = true)
@Composable
private fun Preview_TokenItem_InLight(@PreviewParameter(TokenItemStateProvider::class) state: TokenItemState) {
TangemThemePreview(isDark = false) {
@ -589,6 +596,8 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
iconState = customTokenIconState.copy(isGrayscale = true),
),
AccountItemPreviewData.accountItem,
AccountItemPreviewData.accountItem.copy(iconState = AccountItemPreviewData.accountLetterIcon),
),
) {
@ -641,4 +650,38 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
)
}
}
}
object AccountItemPreviewData {
val accountResIcon: CurrencyIconState.CryptoPortfolio.Icon
get() = CurrencyIconState.CryptoPortfolio.Icon(
resId = R.drawable.ic_rounded_star_24,
color = Color.Blue,
isGrayscale = false,
)
val accountLetterIcon: CurrencyIconState.CryptoPortfolio.Letter
get() = CurrencyIconState.CryptoPortfolio.Letter(
char = 'A',
color = Color.Blue,
isGrayscale = false,
)
val accountItem: TokenItemState.Content
get() = TokenItemState.Content(
id = UUID.randomUUID().toString(),
iconState = accountResIcon,
titleState = TokenItemState.TitleState.Content(
text = stringReference(value = "Portfolio"),
),
fiatAmountState = FiatAmountState.Content(
text = "$22,129.65",
),
subtitleState = TokenItemState.SubtitleState.TextContent(
value = stringReference("24 tokens"),
isAvailable = false,
),
subtitle2State = null,
onItemClick = {},
onItemLongClick = {},
)
}