Updated on 2026-08-14

This commit is contained in:
Tangem 2023-10-11 10:57:11 +04:00
parent 7141c0e6ee
commit 91ec85c3eb
12 changed files with 269 additions and 93 deletions

View file

@ -95,7 +95,6 @@ dependencies {
implementation(deps.lifecycle.common.java8)
implementation(deps.lifecycle.viewModel.ktx)
implementation(deps.lifecycle.compose)
implementation(deps.androidx.palette)
/** Compose libraries */
implementation(deps.compose.constraintLayout)

View file

@ -3,19 +3,20 @@ package com.tangem.tap.features.tokens.impl.presentation.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
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.res.painterResource
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState
@ -69,11 +70,21 @@ internal fun BriefNetworksList(
*/
@Composable
internal fun BriefNetworkItem(model: NetworkItemState, modifier: Modifier = Modifier) {
val isAdded = model is NetworkItemState.ManageContent && model.isAdded.value
Box(modifier = modifier.size(size = TangemTheme.dimens.size20)) {
Image(
if (!isAdded) {
Box(
modifier = Modifier
.size(TangemTheme.dimens.size20)
.clip(CircleShape)
.background(TangemTheme.colors.control.unchecked),
)
}
Icon(
painter = painterResource(id = model.iconResId.value),
contentDescription = null,
modifier = Modifier.size(size = TangemTheme.dimens.size20),
tint = if (isAdded) Color.Unspecified else TangemTheme.colors.text.tertiary,
)
if (model.isMainNetwork) {

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.tokens.impl.presentation.ui
import android.graphics.drawable.Drawable
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
@ -20,16 +19,15 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.drawable.toBitmap
import androidx.palette.graphics.Palette
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.CurrencyPlaceholderIcon
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
import com.tangem.tap.common.compose.extensions.toPx
import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState
import com.tangem.wallet.R
import kotlinx.coroutines.launch
/**
[REDACTED_AUTHOR]
@ -130,6 +128,7 @@ private fun Icon(name: String, iconUrl: String, onContrastCalculate: (Color) ->
val iconModifier = modifier.size(size = TangemTheme.dimens.size46)
val screenBackgroundColor = TangemTheme.colors.background.primary.toArgb()
val isDarkTheme = isSystemInDarkTheme()
val coroutineScope = rememberCoroutineScope()
SubcomposeAsyncImage(
modifier = iconModifier,
@ -141,11 +140,13 @@ private fun Icon(name: String, iconUrl: String, onContrastCalculate: (Color) ->
.listener(
onSuccess = { _, result ->
if (isDarkTheme) {
setContrastBackgroundIfNeeded(
drawable = result.drawable,
screenBackgroundColor = screenBackgroundColor,
onContrastCalculate = onContrastCalculate,
)
coroutineScope.launch {
val color = ImageBackgroundContrastChecker(
drawable = result.drawable,
backgroundColor = screenBackgroundColor,
).getContrastColorIfNeeded(isDarkTheme)
onContrastCalculate(color)
}
}
},
)
@ -156,26 +157,6 @@ private fun Icon(name: String, iconUrl: String, onContrastCalculate: (Color) ->
)
}
private fun setContrastBackgroundIfNeeded(
drawable: Drawable,
screenBackgroundColor: Int,
onContrastCalculate: (Color) -> Unit,
) {
Palette.Builder(drawable.toBitmap()).generate { palette ->
val color = palette?.getDominantColor(screenBackgroundColor) ?: screenBackgroundColor
val contrast = ColorUtils.calculateContrast(color, screenBackgroundColor)
val colorToSet = if (contrast > LOW_CONTRAST_RATIO) {
Color.Transparent
} else {
Color.White
}
onContrastCalculate(colorToSet)
}
}
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
private const val LOW_CONTRAST_RATIO = 1.5f
@Composable
private fun Title(title: String, modifier: Modifier = Modifier) {
Text(

View file

@ -2,8 +2,8 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="90"
android:startColor="#FFFFFF"
android:centerColor="#F0FFFFFF"
android:endColor="#19FFFFFF"
android:centerColor="@color/gradient_center_color"
android:endColor="@color/gradient_end_color"
android:startColor="@color/gradient_start_color"
android:type="linear" />
</shape>

View file

@ -51,4 +51,8 @@
<!--<color name="text_warning">#DE1010</color>-->
<color name="card_background">#333333</color>
<color name="gradient_start_color">#000000</color>
<color name="gradient_center_color">#0F0000</color>
<color name="gradient_end_color">#19000000</color>
</resources>

View file

@ -80,6 +80,10 @@
<color name="background_splash">#000000</color>
<color name="transparent">#00000000</color>
d
<color name="card_background">#1B1D1C</color>
<color name="gradient_start_color">#FFFFFF</color>
<color name="gradient_center_color">#F0FFFF</color>
<color name="gradient_end_color">#19FFFFFF</color>
</resources>

View file

@ -18,6 +18,7 @@ dependencies {
/** AndroidX libraries */
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.paging.runtime)
implementation(deps.androidx.palette)
/** Compose */
implementation(deps.compose.constraintLayout)

View file

@ -0,0 +1,44 @@
package com.tangem.core.ui.utils
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import androidx.compose.ui.graphics.Color
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.drawable.toBitmap
import androidx.palette.graphics.Palette
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class ImageBackgroundContrastChecker(
private val image: Bitmap,
private val backgroundColor: Int,
) {
constructor(drawable: Drawable, backgroundColor: Int) : this(
image = drawable.toBitmap(),
backgroundColor = backgroundColor,
)
suspend fun getContrastColorIfNeeded(isDarkTheme: Boolean): Color {
return if (isLowContrast()) {
if (isDarkTheme) Color.White else Color.Black
} else {
Color.Transparent
}
}
suspend fun isLowContrast(): Boolean {
val palette = generatePaletteAsync(bitmap = image)
val color = palette.getDominantColor(backgroundColor)
val contrast = ColorUtils.calculateContrast(color, backgroundColor)
return contrast <= LOW_CONTRAST_RATIO
}
private suspend fun generatePaletteAsync(bitmap: Bitmap): Palette = withContext(Dispatchers.Default) {
return@withContext Palette.Builder(bitmap).generate()
}
private companion object {
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
private const val LOW_CONTRAST_RATIO = 1.5f
}
}

View file

@ -5,17 +5,20 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.Divider
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
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.graphics.ColorMatrix
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -29,11 +32,13 @@ import com.tangem.core.ui.components.SpacerW2
import com.tangem.core.ui.components.appbar.ExpandableSearchView
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
import com.tangem.feature.swap.models.Network
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
import com.tangem.feature.swap.models.TokenBalanceData
import com.tangem.feature.swap.models.TokenToSelect
import com.tangem.feature.swap.presentation.R
import kotlinx.coroutines.launch
@Composable
fun SwapSelectTokenScreen(
@ -66,9 +71,10 @@ fun SwapSelectTokenScreen(
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = Modifier) {
val screenBackgroundColor = TangemTheme.colors.background.secondary
LazyColumn(
modifier = modifier
.background(color = TangemTheme.colors.background.secondary)
.background(color = screenBackgroundColor)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
@ -77,7 +83,15 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier =
}
itemsIndexed(items = state.addedTokens) { index, item ->
TokenItem(token = item, network = state.network, onTokenClick = { state.onTokenSelected(item.id) })
TokenItem(
token = item,
network = state.network,
screenBackgroundColor = screenBackgroundColor,
onTokenClick = {
state
.onTokenSelected(item.id)
},
)
if (index != state.addedTokens.lastIndex) {
Divider(
@ -92,7 +106,12 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier =
}
itemsIndexed(items = state.otherTokens) { index, item ->
TokenItem(token = item, network = state.network, onTokenClick = { state.onTokenSelected(item.id) })
TokenItem(
token = item,
network = state.network,
screenBackgroundColor = screenBackgroundColor,
onTokenClick = { state.onTokenSelected(item.id) },
)
if (index != state.otherTokens.lastIndex) {
Divider(
color = TangemTheme.colors.stroke.primary,
@ -121,7 +140,7 @@ private fun Header(@StringRes title: Int) {
@Suppress("LongMethod")
@Composable
private fun TokenItem(token: TokenToSelect, network: Network, onTokenClick: () -> Unit) {
private fun TokenItem(token: TokenToSelect, network: Network, screenBackgroundColor: Color, onTokenClick: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
@ -134,6 +153,7 @@ private fun TokenItem(token: TokenToSelect, network: Network, onTokenClick: () -
) {
TokenIcon(
token = token,
screenBackgroundColor = screenBackgroundColor,
iconPlaceholder = if (token.isNative) getActiveIconRes(network.blockchainId) else null,
)
@ -187,13 +207,21 @@ private fun TokenItem(token: TokenToSelect, network: Network, onTokenClick: () -
@Suppress("MagicNumber")
@Composable
private fun TokenIcon(token: TokenToSelect, @DrawableRes iconPlaceholder: Int?) {
private fun TokenIcon(token: TokenToSelect, screenBackgroundColor: Color, @DrawableRes iconPlaceholder: Int?) {
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
val isDarkTheme = isSystemInDarkTheme()
val coroutineScope = rememberCoroutineScope()
val data = token.iconUrl.ifEmpty {
iconPlaceholder
}
Box(
modifier = Modifier
.padding(end = TangemTheme.dimens.spacing12),
.padding(end = TangemTheme.dimens.spacing12)
.background(
color = iconBackgroundColor,
shape = TangemTheme.shapes.roundedCorners8,
),
) {
val iconModifier = Modifier
.size(TangemTheme.dimens.size40)
@ -208,7 +236,20 @@ private fun TokenIcon(token: TokenToSelect, @DrawableRes iconPlaceholder: Int?)
model = ImageRequest.Builder(LocalContext.current)
.data(data)
.crossfade(true)
.build(),
.allowHardware(false)
.listener(
onSuccess = { _, result ->
if (isDarkTheme) {
coroutineScope.launch {
val color = ImageBackgroundContrastChecker(
drawable = result.drawable,
backgroundColor = screenBackgroundColor.toArgb(),
).getContrastColorIfNeeded(isDarkTheme)
iconBackgroundColor = color
}
}
},
).build(),
contentDescription = token.id,
loading = { CircleShimmer(modifier = iconModifier) },
error = { CurrencyPlaceholderIcon(modifier = iconModifier, id = token.id) },

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
@ -13,11 +14,11 @@ import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -27,15 +28,16 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.R
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
import com.tangem.feature.swap.models.SwapWarning
import com.tangem.feature.swap.models.TransactionCardType
import kotlinx.coroutines.launch
@Suppress("LongParameterList")
@Composable
@ -265,47 +267,13 @@ fun Token(
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.End,
) {
Box(
modifier = Modifier
.padding(end = TangemTheme.dimens.spacing16)
.size(TangemTheme.dimens.size42),
) {
val tokenImageModifier = Modifier
.align(Alignment.BottomStart)
.size(TangemTheme.dimens.size36)
val data = tokenIconUrl.ifEmpty {
iconPlaceholder
}
SubcomposeAsyncImage(
modifier = tokenImageModifier,
model = ImageRequest.Builder(LocalContext.current)
.data(data)
.crossfade(true)
.build(),
loading = { CircleShimmer(modifier = tokenImageModifier) },
contentDescription = tokenCurrency,
)
if (networkIconRes != null) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.size(TangemTheme.dimens.size18)
.background(color = Color.White, shape = CircleShape),
contentAlignment = Alignment.Center,
) {
Image(
modifier = Modifier.padding(all = 0.5.dp),
painter = painterResource(id = networkIconRes),
contentDescription = null,
)
}
}
}
TokenIcon(
tokenIconUrl = tokenIconUrl,
tokenCurrency = tokenCurrency,
iconPlaceholder = iconPlaceholder,
networkIconRes = networkIconRes,
)
SpacerH4()
Text(
text = tokenCurrency,
color = TangemTheme.colors.text.primary1,
@ -317,6 +285,75 @@ fun Token(
}
}
@Composable
private fun TokenIcon(
tokenIconUrl: String,
tokenCurrency: String,
@DrawableRes iconPlaceholder: Int? = null,
@DrawableRes networkIconRes: Int? = null,
) {
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
val isDarkTheme = isSystemInDarkTheme()
val coroutineScope = rememberCoroutineScope()
Box(
modifier = Modifier
.padding(end = TangemTheme.dimens.spacing16)
.size(TangemTheme.dimens.size42),
) {
val tokenImageModifier = Modifier
.align(Alignment.BottomStart)
.size(TangemTheme.dimens.size36)
.background(
color = iconBackgroundColor,
shape = TangemTheme.shapes.roundedCorners8,
)
val data = tokenIconUrl.ifEmpty {
iconPlaceholder
}
SubcomposeAsyncImage(
modifier = tokenImageModifier,
model = ImageRequest.Builder(LocalContext.current)
.data(data)
.crossfade(true)
.allowHardware(false)
.listener(
onSuccess = { _, result ->
if (isDarkTheme) {
coroutineScope.launch {
val color = ImageBackgroundContrastChecker(
drawable = result.drawable,
backgroundColor = itemBackgroundColor,
).getContrastColorIfNeeded(isDarkTheme)
iconBackgroundColor = color
}
}
},
).build(),
loading = { CircleShimmer(modifier = tokenImageModifier) },
contentDescription = tokenCurrency,
)
if (networkIconRes != null) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.size(TangemTheme.dimens.size18)
.background(color = TangemTheme.colors.background.primary, shape = CircleShape),
contentAlignment = Alignment.Center,
) {
Image(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing0_5),
painter = painterResource(id = networkIconRes),
contentDescription = null,
)
}
}
}
}
@Composable
fun ChangeTokenSelector() {
Box(

View file

@ -3,21 +3,26 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
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.runtime.*
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.graphics.toArgb
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.core.ui.utils.ImageBackgroundContrastChecker
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState
import com.tangem.features.tokendetails.impl.R
import kotlinx.coroutines.launch
@Composable
internal fun CurrencyIcon(
@ -131,12 +136,34 @@ private inline fun DefaultCurrencyIcon(
crossinline errorIcon: @Composable () -> Unit,
modifier: Modifier = Modifier,
) {
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
val isDarkTheme = isSystemInDarkTheme()
val coroutineScope = rememberCoroutineScope()
SubcomposeAsyncImage(
modifier = modifier,
modifier = modifier
.background(
color = iconBackgroundColor,
shape = TangemTheme.shapes.roundedCorners8,
),
model = ImageRequest.Builder(context = LocalContext.current)
.data(iconData)
.crossfade(enable = true)
.build(),
.allowHardware(false)
.listener(
onSuccess = { _, result ->
if (isDarkTheme) {
coroutineScope.launch {
val color = ImageBackgroundContrastChecker(
drawable = result.drawable,
backgroundColor = itemBackgroundColor,
).getContrastColorIfNeeded(isDarkTheme)
iconBackgroundColor = color
}
}
},
).build(),
loading = { CircleShimmer() },
error = { errorIcon() },
alpha = alpha,

View file

@ -3,20 +3,25 @@ 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.isSystemInDarkTheme
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.runtime.*
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.graphics.toArgb
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.res.TangemTheme
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import kotlinx.coroutines.launch
@Composable
internal fun ContentIcon(
@ -133,12 +138,34 @@ private inline fun DefaultCurrencyIcon(
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,
modifier = modifier
.background(
color = iconBackgroundColor,
shape = TangemTheme.shapes.roundedCorners8,
),
model = ImageRequest.Builder(context = LocalContext.current)
.data(iconData)
.crossfade(enable = true)
.build(),
.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,