Updated on 2026-08-14
This commit is contained in:
parent
eec3307df3
commit
8bd68643e7
15 changed files with 254 additions and 176 deletions
|
|
@ -0,0 +1,60 @@
|
|||
package com.tangem.core.ui.components.currency
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
internal inline fun DefaultCurrencyIcon(
|
||||
iconData: Any,
|
||||
alpha: Float,
|
||||
colorFilter: ColorFilter?,
|
||||
crossinline errorIcon: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
|
||||
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
SubcomposeAsyncImage(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = iconBackgroundColor,
|
||||
shape = TangemTheme.shapes.roundedCorners8,
|
||||
),
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(iconData)
|
||||
.crossfade(enable = true)
|
||||
.allowHardware(false)
|
||||
.listener(
|
||||
onSuccess = { _, result ->
|
||||
if (isDarkTheme) {
|
||||
coroutineScope.launch {
|
||||
val color = ImageBackgroundContrastChecker(
|
||||
drawable = result.drawable,
|
||||
backgroundColor = itemBackgroundColor,
|
||||
).getContrastColorIfNeeded(isDarkTheme)
|
||||
iconBackgroundColor = color
|
||||
}
|
||||
}
|
||||
},
|
||||
).build(),
|
||||
loading = { LoadingIcon() },
|
||||
error = { errorIcon() },
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.core.ui.components.currency.fiaticon
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
|
||||
|
||||
/**
|
||||
* Simple icon from network
|
||||
*
|
||||
* @param url link to icon
|
||||
* @param fallbackResId fallback icon
|
||||
* @param modifier component modifier
|
||||
*/
|
||||
@Composable
|
||||
fun FiatIcon(
|
||||
url: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
@DrawableRes fallbackResId: Int = R.drawable.ic_shape_circle,
|
||||
) {
|
||||
val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url
|
||||
|
||||
DefaultCurrencyIcon(
|
||||
modifier = modifier,
|
||||
iconData = iconData,
|
||||
errorIcon = {
|
||||
Image(
|
||||
painter = painterResource(id = fallbackResId),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
alpha = 1f,
|
||||
colorFilter = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
package com.tangem.core.ui.components.currency.tokenicon
|
||||
|
||||
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.res.painterResource
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
|
||||
|
||||
@Composable
|
||||
internal fun ContentIcon(
|
||||
icon: TokenIconState,
|
||||
alpha: Float,
|
||||
colorFilter: ColorFilter?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (icon) {
|
||||
is TokenIconState.CoinIcon -> CoinIcon(
|
||||
modifier = modifier,
|
||||
url = icon.url,
|
||||
fallbackResId = icon.fallbackResId,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
)
|
||||
is TokenIconState.TokenIcon -> TokenIcon(
|
||||
modifier = modifier,
|
||||
url = icon.url,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
errorIcon = {
|
||||
CustomTokenIcon(
|
||||
modifier = modifier,
|
||||
tint = icon.fallbackTint,
|
||||
background = icon.fallbackBackground,
|
||||
alpha = alpha,
|
||||
)
|
||||
},
|
||||
)
|
||||
is TokenIconState.CustomTokenIcon -> CustomTokenIcon(
|
||||
modifier = modifier,
|
||||
tint = icon.tint,
|
||||
background = icon.background,
|
||||
alpha = alpha,
|
||||
)
|
||||
TokenIconState.Loading,
|
||||
TokenIconState.Locked,
|
||||
-> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CoinIcon(
|
||||
url: String?,
|
||||
@DrawableRes fallbackResId: Int,
|
||||
alpha: Float,
|
||||
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),
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenIcon(
|
||||
url: String?,
|
||||
alpha: Float,
|
||||
colorFilter: ColorFilter?,
|
||||
errorIcon: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (url == null) {
|
||||
errorIcon()
|
||||
} else {
|
||||
DefaultCurrencyIcon(
|
||||
modifier = modifier,
|
||||
iconData = url,
|
||||
errorIcon = errorIcon,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CustomTokenIcon(tint: Color, background: Color, alpha: Float, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = background.copy(alpha = alpha),
|
||||
shape = CircleShape,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.matchParentSize(),
|
||||
painter = painterResource(id = R.drawable.ic_custom_token_44),
|
||||
tint = tint.copy(alpha = alpha),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.core.ui.components.currency.tokenicon
|
||||
|
||||
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,
|
||||
alpha: Float,
|
||||
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,
|
||||
alpha = alpha,
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package com.tangem.core.ui.components.currency.tokenicon
|
||||
|
||||
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
|
||||
|
||||
private const val GRAY_SCALE_SATURATION = 0f
|
||||
private const val GRAY_SCALE_ALPHA = 0.4f
|
||||
private const val NORMAL_ALPHA = 1f
|
||||
|
||||
/**
|
||||
* Cryptocurrency icon with network badge
|
||||
*
|
||||
* TODO [separate domain from ui]([REDACTED_JIRA])
|
||||
*
|
||||
* @param state cryptocurrency icon config
|
||||
* @param modifier component modifier
|
||||
* @param shouldDisplayNetwork specifies whether to display network badge
|
||||
*/
|
||||
@Composable
|
||||
fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier, shouldDisplayNetwork: Boolean = true) {
|
||||
BaseContainer(modifier = modifier) {
|
||||
val iconModifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(TangemTheme.dimens.size36)
|
||||
|
||||
when (state) {
|
||||
is TokenIconState.Loading -> LoadingIcon(modifier = iconModifier)
|
||||
is TokenIconState.Locked -> LockedIcon(modifier = iconModifier)
|
||||
is TokenIconState.CoinIcon,
|
||||
is TokenIconState.CustomTokenIcon,
|
||||
is TokenIconState.TokenIcon,
|
||||
-> {
|
||||
ContentIconContainer(
|
||||
icon = state,
|
||||
modifier = iconModifier,
|
||||
shouldDisplayNetwork = shouldDisplayNetwork,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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.field.primary,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.ContentIconContainer(
|
||||
icon: TokenIconState,
|
||||
modifier: Modifier = Modifier,
|
||||
shouldDisplayNetwork: Boolean = true,
|
||||
) {
|
||||
val networkBadgeOffset = TangemTheme.dimens.spacing4
|
||||
val (alpha, colorFilter) = remember(icon.isGrayscale) {
|
||||
if (icon.isGrayscale) {
|
||||
GRAY_SCALE_ALPHA to GrayscaleColorFilter
|
||||
} else {
|
||||
NORMAL_ALPHA to null
|
||||
}
|
||||
}
|
||||
|
||||
ContentIcon(
|
||||
modifier = modifier,
|
||||
icon = icon,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
)
|
||||
|
||||
if (icon.networkBadgeIconResId != null && shouldDisplayNetwork) {
|
||||
NetworkBadge(
|
||||
modifier = Modifier
|
||||
.offset(x = networkBadgeOffset, y = -networkBadgeOffset)
|
||||
.align(Alignment.TopEnd),
|
||||
iconResId = requireNotNull(icon.networkBadgeIconResId),
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
)
|
||||
}
|
||||
|
||||
if (icon.showCustomBadge) {
|
||||
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)
|
||||
}
|
||||
|
||||
private val GrayscaleColorFilter: ColorFilter
|
||||
get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) })
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.tangem.core.ui.components.currency.tokenicon
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Represents the various states an icon can be in.
|
||||
*
|
||||
* [REDACTED_TODO_COMMENT]
|
||||
*/
|
||||
@Immutable
|
||||
sealed class TokenIconState {
|
||||
|
||||
abstract val isGrayscale: Boolean
|
||||
abstract val showCustomBadge: Boolean
|
||||
abstract val networkBadgeIconResId: Int?
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @property showCustomBadge Specifies whether to show the custom token badge.
|
||||
*/
|
||||
data class CoinIcon(
|
||||
val url: String?,
|
||||
@DrawableRes val fallbackResId: Int,
|
||||
override val isGrayscale: Boolean,
|
||||
override val showCustomBadge: Boolean,
|
||||
) : TokenIconState() {
|
||||
|
||||
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 showCustomBadge Specifies whether to show the custom token badge.
|
||||
* @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,
|
||||
override val showCustomBadge: Boolean,
|
||||
val fallbackTint: Color,
|
||||
val fallbackBackground: Color,
|
||||
) : TokenIconState()
|
||||
|
||||
/**
|
||||
* 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,
|
||||
) : TokenIconState() {
|
||||
|
||||
override val showCustomBadge: Boolean = true
|
||||
}
|
||||
|
||||
object Loading : TokenIconState() {
|
||||
override val isGrayscale: Boolean = false
|
||||
override val showCustomBadge: Boolean = false
|
||||
override val networkBadgeIconResId: Int? = null
|
||||
}
|
||||
|
||||
object Locked : TokenIconState() {
|
||||
override val isGrayscale: Boolean = false
|
||||
override val showCustomBadge: Boolean = false
|
||||
override val networkBadgeIconResId: Int? = null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.core.ui.components.currency.tokenicon.converter
|
||||
|
||||
import com.tangem.common.Converter
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
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.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
|
||||
/**
|
||||
* Converts [CryptoCurrencyStatus] to [TokenIconState]
|
||||
*/
|
||||
class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrencyStatus, TokenIconState> {
|
||||
|
||||
override fun convert(value: CryptoCurrencyStatus): TokenIconState {
|
||||
return when (val currency = value.currency) {
|
||||
is CryptoCurrency.Coin -> getIconStateForCoin(currency, value.value.isError)
|
||||
is CryptoCurrency.Token -> getIconStateForToken(currency, value.value.isError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getIconStateForCoin(coin: CryptoCurrency.Coin, isUnreachable: Boolean): TokenIconState.CoinIcon {
|
||||
return TokenIconState.CoinIcon(
|
||||
url = coin.iconUrl,
|
||||
fallbackResId = coin.networkIconResId,
|
||||
isGrayscale = coin.network.isTestnet || isUnreachable,
|
||||
showCustomBadge = coin.isCustom,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getIconStateForToken(token: CryptoCurrency.Token, isErrorStatus: Boolean): TokenIconState {
|
||||
val isGrayscale = token.network.isTestnet || isErrorStatus
|
||||
val background = token.tryGetBackgroundForTokenIcon(isGrayscale)
|
||||
val tint = getTintForTokenIcon(background)
|
||||
|
||||
return if (token.isCustom && token.iconUrl == null) {
|
||||
TokenIconState.CustomTokenIcon(
|
||||
tint = tint,
|
||||
background = background,
|
||||
networkBadgeIconResId = token.networkIconResId,
|
||||
isGrayscale = isGrayscale,
|
||||
)
|
||||
} else {
|
||||
TokenIconState.TokenIcon(
|
||||
url = token.iconUrl,
|
||||
networkBadgeIconResId = token.networkIconResId,
|
||||
isGrayscale = isGrayscale,
|
||||
fallbackTint = tint,
|
||||
fallbackBackground = background,
|
||||
showCustomBadge = token.isCustom, // `true` for tokens with custom derivation
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -170,7 +170,6 @@ class TangemColors internal constructor(
|
|||
plain = other.plain
|
||||
action = other.action
|
||||
fade = other.fade
|
||||
tertiary = other.tertiary
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
9
core/ui/src/main/res/drawable/ic_shape_circle.xml
Normal file
9
core/ui/src/main/res/drawable/ic_shape_circle.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
|
||||
</shape>
|
||||
Loading…
Add table
Add a link
Reference in a new issue