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

@ -1,43 +1,31 @@
package com.tangem.common.ui.account
import android.content.res.Configuration
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.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountIconPreviewData.randomAccountIcon
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.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
enum class AccountIconSize {
Default, Large, Medium, Small, ExtraSmall
}
/**
* Displays an account icon that can either show a letter (derived from [name])
* or a predefined vector resource (from [icon]).
* Displays a portfolio account icon that can either show:
* - a single character (derived from the [name]) using [AccountCharIcon], or
* - a predefined vector resource (from [icon]) using [AccountResIcon].
*
* The background color is determined by the icon's [CryptoPortfolioIconUM.color],
* and the icon size, text style, and box modifier are adapted based on the given [size].
* The background color is taken from [CryptoPortfolioIconUM.color],
* and the icon size, text style, and container shape are adapted based on the given [size].
*
* @param name The text reference used to resolve and display the first letter
* when [icon] is set to [CryptoPortfolioIcon.Icon.Letter].
* @param name A [TextReference] used to resolve and display the first letter
* when [icon.value] is set to [CryptoPortfolioIcon.Icon.Letter].
* @param icon The account icon definition, which can be a letter or a drawable resource.
* @param size The size of the icon, defined by [AccountIconSize].
* @param size The icon size, defined by [AccountIconSize].
*
* @see AccountCharIcon
* @see AccountResIcon
* @see AccountIconSize
*/
@Composable
fun AccountIcon(
@ -46,82 +34,20 @@ fun AccountIcon(
size: AccountIconSize,
modifier: Modifier = Modifier,
) {
val boxModifier = modifier.selectBoxModifier(size)
val iconSize = Modifier.selectIconSize(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(icon.color.getUiColor()),
) {
val icon = icon.value
val letter = name.resolveReference().firstOrNull()
when {
icon == CryptoPortfolioIcon.Icon.Letter -> Text(
text = letter?.uppercase() ?: "",
style = textStyle,
color = TangemTheme.colors.text.constantWhite,
)
else -> Icon(
modifier = iconSize,
tint = TangemTheme.colors.text.constantWhite,
imageVector = ImageVector.vectorResource(id = icon.getResId()),
contentDescription = null,
)
}
}
}
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_AccountIcon() {
TangemThemePreview {
Sample()
}
}
@Composable
private fun Sample() {
val name = stringReference("Account Name")
Row(
modifier = Modifier.background(TangemTheme.colors.background.primary),
) {
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Default)
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Large)
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Medium)
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Small)
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.ExtraSmall)
}
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Default)
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Large)
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Medium)
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Small)
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.ExtraSmall)
}
val letter = name.resolveReference().firstOrNull()
when {
icon.value == CryptoPortfolioIcon.Icon.Letter -> AccountCharIcon(
char = letter ?: 'N',
color = icon.color.getUiColor(),
size = size,
modifier = modifier,
)
else -> AccountResIcon(
resId = icon.value.getResId(),
color = icon.color.getUiColor(),
size = size,
modifier = modifier,
)
}
}

View file

@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.R
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference

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 = {},
)
}

View file

@ -31,6 +31,7 @@ import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.fields.AutoSizeTextField
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme