Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-04 09:52:39 +03:00
parent 6d95abf3b8
commit 75e2268d10
15 changed files with 522 additions and 271 deletions

View file

@ -1,45 +1,49 @@
package com.tangem.core.ui.extensions
import androidx.annotation.DrawableRes
import com.tangem.core.ui.R
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.core.graphics.toColorInt
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.domain.tokens.models.CryptoCurrency
private const val LIGHT_LUMINANCE = 0.5f
private const val COLOR_HEX_START_INDEX = 2
private const val COLOR_HEX_END_INDEX = 7
/**
* Retrieves the resource ID for the network badge of a [CryptoCurrency].
* Retrieves the resource ID for the network of a [CryptoCurrency].
*
* This property provides a way to fetch the appropriate drawable resource ID
* for the network badge of a given cryptocurrency. For coins, this will typically
* return null as they do not have network badges, while tokens will fetch the icon
* based on their associated network ID.
*
* @return Drawable resource ID for the network badge or null if the cryptocurrency is a coin.
* @return Drawable resource ID for the network.
*/
@get:DrawableRes
val CryptoCurrency.networkBadgeIconResId: Int?
get() = when (this) {
is CryptoCurrency.Coin -> null
is CryptoCurrency.Token -> getActiveIconRes(network.id.value)
val CryptoCurrency.networkIconResId: Int
get() = getActiveIconRes(network.id.value)
/**
* Tries to extract a background color from the contract address of a token.
*
* @param fallbackColor The color to use as a fallback.
* @return The extracted background color or the fallback color if extraction fails or if it is a test network token.
*/
fun CryptoCurrency.Token.tryGetBackgroundForTokenIcon(fallbackColor: Color = TangemColorPalette.Black): Color {
if (network.isTestnet) return fallbackColor
return try {
val colorHex = "#" + contractAddress.substring(range = COLOR_HEX_START_INDEX..COLOR_HEX_END_INDEX)
Color(colorHex.toColorInt())
} catch (exception: Exception) {
fallbackColor
}
}
/**
* Retrieves the resource ID for the icon of a [CryptoCurrency].
* Determines the tint color to be used for a token icon based on its background color.
* If the icon's background color is light, a dark tint is chosen; otherwise, a light tint is chosen.
*
* This property provides a way to fetch the appropriate drawable resource ID
* for the icon of a given cryptocurrency.
*
* @return Drawable resource ID for the cryptocurrency icon.
* @param iconBackground The background color of the custom token icon.
* @return The tint color to be used for the icon.
*/
@get:DrawableRes
val CryptoCurrency.iconResId: Int
get() = when (this) {
is CryptoCurrency.Coin -> {
val rawCoinId = id.rawCurrencyId
if (rawCoinId != null) {
getActiveIconResByCoinId(rawCoinId, network.id.value)
} else {
R.drawable.ic_alert_24
}
}
is CryptoCurrency.Token -> R.drawable.ic_alert_24
}
fun getTintForTokenIcon(iconBackground: Color): Color {
return if (iconBackground.luminance() > LIGHT_LUMINANCE) TangemColorPalette.Black else TangemColorPalette.White
}

View file

@ -88,6 +88,7 @@ private fun materialThemeColors(colors: TangemColors, isDark: Boolean): Colors {
}
@Composable
@ReadOnlyComposable
private fun lightThemeColors(): TangemColors {
return TangemColors(
text = TangemColors.Text(
@ -135,6 +136,7 @@ private fun lightThemeColors(): TangemColors {
}
@Composable
@ReadOnlyComposable
private fun darkThemeColors(): TangemColors {
return TangemColors(
text = TangemColors.Text(

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="44dp"
android:height="44dp"
android:viewportWidth="44"
android:viewportHeight="44">
<path
android:fillColor="#656565"
android:pathData="M22,34L22.69,29.327C23,27.23 23.155,26.182 23.648,25.352C24.065,24.651 24.651,24.065 25.352,23.648C26.182,23.155 27.23,23 29.327,22.69L34,22L29.327,21.31C27.23,21 26.182,20.845 25.352,20.352C24.651,19.935 24.065,19.349 23.648,18.648C23.155,17.818 23,16.77 22.69,14.673L22,10L21.31,14.673C21,16.77 20.845,17.818 20.352,18.648C19.935,19.349 19.349,19.935 18.648,20.352C17.818,20.845 16.77,21 14.673,21.31L10,22L14.673,22.69C16.77,23 17.818,23.155 18.648,23.648C19.349,24.065 19.935,24.651 20.352,25.352C20.845,26.182 21,27.23 21.31,29.327L22,34Z" />
</vector>

View file

@ -3,14 +3,10 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.iconResId
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSkeletonStateConverter.SkeletonModel
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
@ -38,7 +34,7 @@ internal class TokenDetailsSkeletonStateConverter(
is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token(
networkName = currency.network.standardType.name,
blockchainName = currency.network.name,
networkIcon = currency.iconResId,
networkIcon = currency.networkIconResId,
)
},
),

View file

@ -8,13 +8,17 @@ import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.consumed
import com.tangem.core.ui.extensions.TextReference
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
import com.tangem.feature.wallet.presentation.wallet.state.*
import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.components.*
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState
import kotlinx.collections.immutable.persistentListOf
@ -87,12 +91,34 @@ internal object WalletPreviewData {
)
}
private val coinIconState
get() = TokenItemState.IconState.CoinIcon(
url = null,
fallbackResId = R.drawable.img_polygon_22,
isGrayscale = false,
)
private val tokenIconState
get() = TokenItemState.IconState.TokenIcon(
url = null,
networkBadgeIconResId = R.drawable.img_polygon_22,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
)
private val customTokenIconState
get() = TokenItemState.IconState.CustomTokenIcon(
tint = TangemColorPalette.Black,
background = TangemColorPalette.Meadow,
networkBadgeIconResId = R.drawable.img_polygon_22,
isGrayscale = false,
)
val tokenItemVisibleState by lazy {
TokenItemState.Content(
id = UUID.randomUUID().toString(),
tokenIconUrl = null,
tokenIconResId = R.drawable.img_polygon_22,
networkBadgeIconResId = R.drawable.img_polygon_22,
icon = coinIconState,
name = "Polygon",
amount = "5,412 MATIC",
hasPending = true,
@ -103,7 +129,6 @@ internal object WalletPreviewData {
type = PriceChangeConfig.Type.UP,
),
),
isTestnet = false,
onItemClick = {},
onItemLongClick = {},
)
@ -112,16 +137,14 @@ internal object WalletPreviewData {
val testnetTokenItemVisibleState by lazy {
tokenItemVisibleState.copy(
name = "Polygon testnet",
isTestnet = true,
icon = tokenIconState.copy(isGrayscale = true),
)
}
val tokenItemHiddenState by lazy {
TokenItemState.Content(
id = UUID.randomUUID().toString(),
tokenIconUrl = null,
tokenIconResId = R.drawable.img_polygon_22,
networkBadgeIconResId = R.drawable.img_polygon_22,
icon = tokenIconState,
name = "Polygon",
amount = "5,412 MATIC",
hasPending = true,
@ -131,7 +154,6 @@ internal object WalletPreviewData {
type = PriceChangeConfig.Type.UP,
),
),
isTestnet = false,
onItemClick = {},
onItemLongClick = {},
)
@ -140,11 +162,8 @@ internal object WalletPreviewData {
val tokenItemDragState by lazy {
TokenItemState.Draggable(
id = UUID.randomUUID().toString(),
tokenIconUrl = null,
tokenIconResId = R.drawable.img_polygon_22,
networkBadgeIconResId = R.drawable.img_polygon_22,
icon = tokenIconState,
name = "Polygon",
isTestnet = false,
fiatAmount = "3 172,14 $",
)
}
@ -152,13 +171,28 @@ internal object WalletPreviewData {
val tokenItemUnreachableState by lazy {
TokenItemState.Unreachable(
id = UUID.randomUUID().toString(),
tokenIconUrl = null,
tokenIconResId = R.drawable.img_polygon_22,
networkBadgeIconResId = R.drawable.img_polygon_22,
icon = tokenIconState,
name = "Polygon",
)
}
val customTokenItemVisibleState by lazy {
tokenItemVisibleState.copy(
name = "Polygon custom",
icon = customTokenIconState.copy(
tint = TangemColorPalette.White,
background = TangemColorPalette.Black,
),
)
}
val customTestnetTokenItemVisibleState by lazy {
tokenItemVisibleState.copy(
name = "Polygon custom testnet",
icon = customTokenIconState.copy(isGrayscale = true),
)
}
val loadingTokenItemState by lazy { TokenItemState.Loading(id = "Loading#1") }
private const val networksSize = 10
@ -188,7 +222,6 @@ internal object WalletPreviewData {
tokenItemState = tokenItemDragState.copy(
id = "${group.id}_token_$tokenNumber",
name = "Token $tokenNumber from $networkNumber network",
networkBadgeIconResId = R.drawable.img_eth_22.takeIf { i != 0 },
),
groupId = group.id,
roundingMode = when {
@ -299,8 +332,6 @@ internal object WalletPreviewData {
tokenItemVisibleState.copy(
id = "token_1",
name = "Ethereum",
tokenIconResId = R.drawable.img_eth_22,
networkBadgeIconResId = null,
amount = "1,89340821 ETH",
),
),
@ -308,8 +339,6 @@ internal object WalletPreviewData {
tokenItemVisibleState.copy(
id = "token_2",
name = "Ethereum",
tokenIconResId = R.drawable.img_eth_22,
networkBadgeIconResId = null,
amount = "1,89340821 ETH",
),
),
@ -317,8 +346,6 @@ internal object WalletPreviewData {
tokenItemVisibleState.copy(
id = "token_3",
name = "Ethereum",
tokenIconResId = R.drawable.img_eth_22,
networkBadgeIconResId = null,
amount = "1,89340821 ETH",
),
),
@ -326,8 +353,6 @@ internal object WalletPreviewData {
tokenItemVisibleState.copy(
id = "token_4",
name = "Ethereum",
tokenIconResId = R.drawable.img_eth_22,
networkBadgeIconResId = null,
amount = "1,89340821 ETH",
),
),
@ -336,8 +361,6 @@ internal object WalletPreviewData {
tokenItemVisibleState.copy(
id = "token_5",
name = "Ethereum",
tokenIconResId = R.drawable.img_eth_22,
networkBadgeIconResId = null,
amount = "1,89340821 ETH",
),
),

View file

@ -17,16 +17,14 @@ import androidx.constraintlayout.compose.ConstrainedLayoutReference
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.ConstraintLayoutScope
import androidx.constraintlayout.compose.Dimension
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.component.token.TokenCryptoInfoBlock
import com.tangem.feature.wallet.presentation.common.component.token.TokenFiatInfoBlock
import com.tangem.feature.wallet.presentation.common.component.token.TokenIcon
import com.tangem.feature.wallet.presentation.common.component.token.icon.TokenIcon
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import org.burnoutcrew.reorderable.ReorderableLazyListState
// TODO: Add custom token state: [REDACTED_JIRA]
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun TokenItem(
@ -90,7 +88,7 @@ private inline fun BaseContainer(
) {
ConstraintLayout(
modifier = Modifier
.fillMaxSize()
.fillMaxWidth()
.padding(
horizontal = TangemTheme.dimens.spacing14,
vertical = TangemTheme.dimens.spacing14,
@ -111,7 +109,6 @@ private fun Modifier.constrainAsOptionsItem(scope: ConstraintLayoutScope, ref: C
}
// region preview
@Preview
@Composable
private fun Preview_Tokens_LightTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) {
@ -136,6 +133,8 @@ private class TokenConfigProvider : CollectionPreviewParameterProvider<TokenItem
WalletPreviewData.tokenItemHiddenState,
WalletPreviewData.loadingTokenItemState,
WalletPreviewData.testnetTokenItemVisibleState,
WalletPreviewData.customTokenItemVisibleState,
WalletPreviewData.customTestnetTokenItemVisibleState,
),
)

View file

@ -1,152 +0,0 @@
package com.tangem.feature.wallet.presentation.common.component.token
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
private const val GRAY_SCALE_SATURATION = 0f
@Composable
internal fun TokenIcon(state: TokenItemState, modifier: Modifier = Modifier) {
when (state) {
is TokenItemState.ContentState -> ContentIcon(content = state, modifier = modifier)
is TokenItemState.Loading -> LoadingIcon(modifier = modifier)
is TokenItemState.Locked -> LockedIcon(modifier = modifier)
}
}
@Composable
private fun ContentIcon(content: TokenItemState.ContentState, modifier: Modifier = Modifier) {
BaseContainer(modifier = modifier) {
val isTestnet = when (content) {
is TokenItemState.Content -> content.isTestnet
is TokenItemState.Draggable -> content.isTestnet
is TokenItemState.Unreachable -> false
}
val colorFilter = remember(isTestnet) {
if (isTestnet) {
ColorFilter.colorMatrix(
colorMatrix = ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) },
)
} else {
null
}
}
Icon(
content = content,
colorFilter = colorFilter,
modifier = Modifier.align(Alignment.BottomStart),
)
NetworkBadge(
iconResId = content.networkBadgeIconResId,
colorFilter = colorFilter,
modifier = Modifier.align(Alignment.TopEnd),
)
}
}
@Composable
private fun Icon(content: TokenItemState.ContentState, colorFilter: ColorFilter?, modifier: Modifier = Modifier) {
val iconUrl = content.tokenIconUrl
val iconData: Any = remember(iconUrl) {
if (iconUrl.isNullOrEmpty()) content.tokenIconResId else iconUrl
}
SubcomposeAsyncImage(
modifier = modifier.iconSize(),
model = ImageRequest.Builder(context = LocalContext.current)
.data(data = iconData)
.placeholder(drawableResId = content.tokenIconResId)
.error(drawableResId = content.tokenIconResId)
.fallback(drawableResId = content.tokenIconResId)
.crossfade(enable = true)
.build(),
colorFilter = colorFilter,
contentDescription = null,
)
}
@Composable
private fun BoxScope.NetworkBadge(
@DrawableRes iconResId: Int?,
colorFilter: ColorFilter?,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = iconResId != null,
modifier = modifier
.size(TangemTheme.dimens.size18)
.background(color = TangemTheme.colors.background.primary, shape = CircleShape),
) {
if (iconResId == null) return@AnimatedVisibility
Image(
modifier = Modifier
.padding(all = TangemTheme.dimens.spacing2)
.align(Alignment.Center),
painter = painterResource(id = iconResId),
colorFilter = colorFilter,
contentDescription = null,
)
}
}
@Composable
private fun LoadingIcon(modifier: Modifier = Modifier) {
BaseContainer(modifier) {
CircleShimmer(
modifier = Modifier
.iconSize()
.align(alignment = Alignment.BottomStart),
)
}
}
@Composable
private fun LockedIcon(modifier: Modifier = Modifier) {
BaseContainer(modifier) {
Box(
modifier = Modifier
.iconSize()
.align(Alignment.BottomStart),
) {
Box(
modifier = Modifier
.matchParentSize()
.background(color = TangemTheme.colors.background.secondary, shape = CircleShape),
)
}
}
}
@Composable
private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) {
Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content)
}
private fun Modifier.iconSize(): Modifier = composed {
return@composed this.size(size = TangemTheme.dimens.size36)
}

View file

@ -0,0 +1,129 @@
package com.tangem.feature.wallet.presentation.common.component.token.icon
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
@Composable
internal fun ContentIcon(icon: TokenItemState.IconState, colorFilter: ColorFilter?, modifier: Modifier = Modifier) {
when (icon) {
is TokenItemState.IconState.CoinIcon -> CoinIcon(
modifier = modifier,
url = icon.url,
fallbackResId = icon.fallbackResId,
colorFilter = colorFilter,
)
is TokenItemState.IconState.TokenIcon -> TokenIcon(
modifier = modifier,
url = icon.url,
colorFilter = colorFilter,
errorIcon = {
CustomTokenIcon(
modifier = modifier,
tint = icon.fallbackTint,
background = icon.fallbackBackground,
)
},
)
is TokenItemState.IconState.CustomTokenIcon -> CustomTokenIcon(
modifier = modifier,
tint = icon.tint,
background = icon.background,
)
}
}
@Composable
private fun CoinIcon(
url: String?,
@DrawableRes fallbackResId: Int,
colorFilter: ColorFilter?,
modifier: Modifier = Modifier,
) {
val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url
DefaultCurrencyIcon(
modifier = modifier,
iconData = iconData,
errorIcon = {
Image(
painter = painterResource(id = fallbackResId),
colorFilter = colorFilter,
contentDescription = null,
)
},
colorFilter = colorFilter,
)
}
@Composable
private fun TokenIcon(
url: String?,
colorFilter: ColorFilter?,
errorIcon: @Composable () -> Unit,
modifier: Modifier = Modifier,
) {
if (url == null) {
errorIcon()
} else {
DefaultCurrencyIcon(
modifier = modifier,
iconData = url,
errorIcon = errorIcon,
colorFilter = colorFilter,
)
}
}
@Composable
private fun CustomTokenIcon(tint: Color, background: Color, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.background(
color = background,
shape = CircleShape,
),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.matchParentSize(),
painter = painterResource(id = R.drawable.ic_custom_token_44),
tint = tint,
contentDescription = null,
)
}
}
@Composable
private inline fun DefaultCurrencyIcon(
iconData: Any,
colorFilter: ColorFilter?,
crossinline errorIcon: @Composable () -> Unit,
modifier: Modifier = Modifier,
) {
SubcomposeAsyncImage(
modifier = modifier,
model = ImageRequest.Builder(context = LocalContext.current)
.data(iconData)
.crossfade(enable = true)
.build(),
loading = { LoadingIcon() },
error = { errorIcon() },
colorFilter = colorFilter,
contentDescription = null,
)
}

View file

@ -0,0 +1,57 @@
package com.tangem.feature.wallet.presentation.common.component.token.icon
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun NetworkBadge(@DrawableRes iconResId: Int, colorFilter: ColorFilter?, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens.size18)
.background(
color = TangemTheme.colors.background.primary,
shape = CircleShape,
),
) {
Image(
modifier = Modifier
.padding(all = TangemTheme.dimens.spacing2)
.matchParentSize(),
painter = painterResource(id = iconResId),
colorFilter = colorFilter,
contentDescription = null,
)
}
}
@Composable
internal fun CustomBadge(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens.size12)
.background(
color = TangemTheme.colors.background.primary,
shape = CircleShape,
),
) {
Box(
modifier = Modifier
.padding(all = TangemTheme.dimens.spacing2)
.matchParentSize()
.background(
color = TangemTheme.colors.icon.informative,
shape = CircleShape,
),
)
}
}

View file

@ -0,0 +1,95 @@
package com.tangem.feature.wallet.presentation.common.component.token.icon
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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
private const val GRAY_SCALE_SATURATION = 0f
@Composable
internal fun TokenIcon(state: TokenItemState, 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,
)
}
}
}
@Composable
internal fun LoadingIcon(modifier: Modifier = Modifier) {
CircleShimmer(modifier = modifier)
}
@Composable
private fun LockedIcon(modifier: Modifier = Modifier) {
Box(modifier = modifier) {
Box(
modifier = Modifier
.matchParentSize()
.background(
color = TangemTheme.colors.background.secondary,
shape = CircleShape,
),
)
}
}
@Composable
private fun BoxScope.ContentIconContainer(icon: TokenItemState.IconState, modifier: Modifier = Modifier) {
val networkBadgeOffset = TangemTheme.dimens.spacing4
val colorFilter = remember(icon.isGrayscale) {
if (icon.isGrayscale) {
ColorFilter.colorMatrix(
colorMatrix = ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) },
)
} else {
null
}
}
ContentIcon(
modifier = modifier,
icon = icon,
colorFilter = colorFilter,
)
if (icon.networkBadgeIconResId != null) {
NetworkBadge(
modifier = Modifier
.offset(x = networkBadgeOffset, y = -networkBadgeOffset)
.align(Alignment.TopEnd),
iconResId = requireNotNull(icon.networkBadgeIconResId),
colorFilter = colorFilter,
)
}
if (icon is TokenItemState.IconState.CustomTokenIcon) {
CustomBadge(modifier = Modifier.align(Alignment.BottomEnd))
}
}
@Composable
private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) {
Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content)
}

View file

@ -2,6 +2,7 @@ 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.PriceChangeConfig
/** Token item state */
@ -18,80 +19,120 @@ internal sealed interface TokenItemState {
data class Locked(override val id: String) : TokenItemState
/** Content state */
sealed class ContentState(
override val id: String,
open val tokenIconUrl: String?,
@DrawableRes open val tokenIconResId: Int,
@DrawableRes open val networkBadgeIconResId: Int?,
open val name: String,
) : TokenItemState
sealed class ContentState : TokenItemState {
abstract val icon: IconState
abstract val name: String
}
/**
* Content token state
*
* @property id unique id
* @property tokenIconUrl token icon url
* @property tokenIconResId token icon resource id
* @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin
* @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 isTestnet indicates whether the token is from test network or not
* @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 tokenIconUrl: String?,
@DrawableRes override val tokenIconResId: Int,
@DrawableRes override val networkBadgeIconResId: Int?,
override val icon: IconState,
override val name: String,
val amount: String,
val hasPending: Boolean,
val tokenOptions: TokenOptionsState,
val isTestnet: Boolean,
val onItemClick: () -> Unit,
val onItemLongClick: () -> Unit,
) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name)
) : ContentState()
/**
* Draggable token state
*
* @property id unique id
* @property tokenIconUrl token icon url
* @property tokenIconResId token icon resource id
* @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin
* @property icon token icon state
* @property name token name
* @property fiatAmount fiat amount of token
* @property isTestnet indicates whether the token is from test network or not
*/
data class Draggable(
override val id: String,
override val tokenIconUrl: String?,
@DrawableRes override val tokenIconResId: Int,
@DrawableRes override val networkBadgeIconResId: Int?,
override val icon: IconState,
override val name: String,
val fiatAmount: String,
val isTestnet: Boolean,
) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name)
) : ContentState()
/**
* Unreachable token state
*
* @property id token id
* @property tokenIconUrl token icon url
* @property tokenIconResId token icon resource id
* @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin
* @property icon token icon state
* @property name token name
*/
data class Unreachable(
override val id: String,
override val tokenIconUrl: String?,
@DrawableRes override val tokenIconResId: Int,
@DrawableRes override val networkBadgeIconResId: Int?,
override val icon: IconState,
override val name: String,
) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name)
) : ContentState()
/**
* Represents the various states an icon can be in.
*/
@Immutable
sealed class IconState {
abstract val networkBadgeIconResId: Int?
abstract val isGrayscale: Boolean
/**
* Represents a coin icon.
*
* @property url The URL where the coin icon can be fetched from. May be `null` if not found.
* @property fallbackResId The drawable resource ID to be used as a fallback if the URL is not available.
* @property isGrayscale Specifies whether to show the icon in grayscale.
*/
data class CoinIcon(
val url: String?,
@DrawableRes val fallbackResId: Int,
override val isGrayscale: Boolean,
) : IconState() {
override val networkBadgeIconResId: Int? = null
}
/**
* Represents a token icon.
*
* @property url The URL where the token icon can be fetched from. May be `null` if not found.
* @property networkBadgeIconResId The drawable resource ID for the network badge.
* @property isGrayscale Specifies whether to show the icon in grayscale.
* @property fallbackTint The color to be used for tinting the fallback icon.
* @property fallbackBackground The background color to be used for the fallback icon.
*/
data class TokenIcon(
val url: String?,
@DrawableRes override val networkBadgeIconResId: Int,
override val isGrayscale: Boolean,
val fallbackTint: Color,
val fallbackBackground: Color,
) : IconState()
/**
* Represents a custom token icon.
*
* @property tint The color to be used for tinting the icon.
* @property background The background color to be used for the icon.
* @property networkBadgeIconResId The drawable resource ID for the network badge.
* @property isGrayscale Specifies whether to show the icon in grayscale.
*/
data class CustomTokenIcon(
val tint: Color,
val background: Color,
@DrawableRes override val networkBadgeIconResId: Int,
override val isGrayscale: Boolean,
) : IconState()
}
/** Token options state */
@Immutable

View file

@ -0,0 +1,48 @@
package com.tangem.feature.wallet.presentation.common.utils
import com.tangem.core.ui.extensions.getTintForTokenIcon
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.utils.converter.Converter
internal class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrency, TokenItemState.IconState> {
override fun convert(value: CryptoCurrency): TokenItemState.IconState {
return when (value) {
is CryptoCurrency.Coin -> getIconStateForCoin(value)
is CryptoCurrency.Token -> getIconStateForToken(value)
}
}
private fun getIconStateForCoin(coin: CryptoCurrency.Coin): TokenItemState.IconState.CoinIcon {
return TokenItemState.IconState.CoinIcon(
url = coin.iconUrl,
fallbackResId = coin.networkIconResId,
isGrayscale = coin.network.isTestnet,
)
}
private fun getIconStateForToken(token: CryptoCurrency.Token): TokenItemState.IconState {
val background = token.tryGetBackgroundForTokenIcon()
val tint = getTintForTokenIcon(background)
return if (token.isCustom) {
TokenItemState.IconState.CustomTokenIcon(
tint = tint,
background = background,
networkBadgeIconResId = token.networkIconResId,
isGrayscale = token.network.isTestnet,
)
} else {
TokenItemState.IconState.TokenIcon(
url = token.iconUrl,
networkBadgeIconResId = token.networkIconResId,
isGrayscale = token.network.isTestnet,
fallbackTint = tint,
fallbackBackground = background,
)
}
}
}

View file

@ -1,7 +1,9 @@
package com.tangem.feature.wallet.presentation.organizetokens
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.*
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@ -37,7 +39,10 @@ import com.tangem.feature.wallet.presentation.common.component.TokenItem
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
import org.burnoutcrew.reorderable.*
import org.burnoutcrew.reorderable.ReorderableItem
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
import org.burnoutcrew.reorderable.reorderable
@Composable
internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier = Modifier) {

View file

@ -1,12 +1,11 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
import com.tangem.common.Provider
import com.tangem.core.ui.extensions.iconResId
import com.tangem.core.ui.extensions.networkBadgeIconResId
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.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId
@ -16,6 +15,8 @@ internal class CryptoCurrencyToDraggableItemConverter(
private val appCurrencyProvider: Provider<AppCurrency>,
) : Converter<CryptoCurrencyStatus, DraggableItem.Token> {
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token {
return createDraggableToken(value, appCurrencyProvider())
}
@ -44,12 +45,9 @@ internal class CryptoCurrencyToDraggableItemConverter(
return TokenItemState.Draggable(
id = getTokenItemId(currency.id),
tokenIconUrl = currency.iconUrl,
tokenIconResId = currencyStatus.currency.iconResId,
networkBadgeIconResId = currencyStatus.currency.networkBadgeIconResId,
icon = iconStateConverter.convert(currency),
name = currency.name,
fiatAmount = getFormattedFiatAmount(currencyStatus, appCurrency),
isTestnet = currencyStatus.currency.network.isTestnet,
)
}

View file

@ -2,12 +2,11 @@ package com.tangem.feature.wallet.presentation.wallet.utils
import com.tangem.common.Provider
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
import com.tangem.core.ui.extensions.iconResId
import com.tangem.core.ui.extensions.networkBadgeIconResId
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.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
@ -18,6 +17,8 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
private val clickIntents: WalletClickIntents,
) : Converter<CryptoCurrencyStatus, TokenItemState> {
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
return when (value.value) {
is CryptoCurrencyStatus.Loading -> TokenItemState.Loading(id = value.currency.id.value)
@ -37,9 +38,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
return TokenItemState.Content(
id = currency.id.value,
name = currency.name,
tokenIconUrl = currency.iconUrl,
tokenIconResId = currency.iconResId,
networkBadgeIconResId = currency.networkBadgeIconResId,
icon = iconStateConverter.convert(currency),
amount = getFormattedAmount(),
hasPending = value.hasCurrentNetworkTransactions,
tokenOptions = if (isWalletContentHidden) {
@ -50,7 +49,6 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
config = getPriceChangeConfig(),
)
},
isTestnet = currency.network.isTestnet,
onItemClick = { clickIntents.onTokenItemClick(currency) },
onItemLongClick = { clickIntents.onTokenItemLongClick(currency) },
)
@ -72,9 +70,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable(
id = currency.id.value,
name = currency.name,
tokenIconUrl = currency.iconUrl,
tokenIconResId = currency.iconResId,
networkBadgeIconResId = currency.networkBadgeIconResId,
icon = iconStateConverter.convert(currency),
)
private fun CryptoCurrencyStatus.getPriceChangeConfig(): PriceChangeConfig {