Updated on 2026-08-14

This commit is contained in:
Tangem 2023-05-04 12:49:25 +03:00
parent b0f23e3660
commit 938d7baee1
14 changed files with 569 additions and 24 deletions

View file

@ -11,6 +11,8 @@ dependencies {
implementation(deps.androidx.activity.compose)
/** Compose */
implementation(deps.compose.coil)
implementation(deps.compose.constraintLayout)
implementation(deps.compose.material)
implementation(deps.compose.foundation)
implementation(deps.compose.navigation)

View file

@ -0,0 +1,355 @@
package com.tangem.feature.wallet.presentation.ui
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.material.Text
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
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.res.TangemTypography
import com.tangem.feature.wallet.presentation.ui.config.PriceChange
import com.tangem.feature.wallet.presentation.ui.config.PriceChangeType
import com.tangem.feature.wallet.presentation.ui.config.TokenV2Config
import com.tangem.feature.wallet.presentation.ui.state.TokenOptionsUIState
import com.tangem.feature.wallet.presentation.ui.state.TokenUIState
private const val DOTS = "•••"
@Composable
fun TokenItemV2(config: TokenV2Config) {
when (config.tokenUIState) {
TokenUIState.LOADED -> LoadedTokenState(config)
TokenUIState.LOADING -> LoadingTokenState()
}
}
@Composable
private fun LoadedTokenState(config: TokenV2Config) {
BaseSurface {
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = TangemTheme.dimens.spacing12,
vertical = TangemTheme.dimens.spacing4,
),
) {
val (iconItem, tokenNameItem, fiatItem) = createRefs()
TokenIcon(
modifier = Modifier.constrainAs(iconItem) {
centerVerticallyTo(parent)
start.linkTo(parent.start)
},
tokenIconUrl = config.iconUrl,
icon = config.icon,
networkIconRes = config.networkIcon,
)
TokenTitleAmountBlock(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing12)
.constrainAs(tokenNameItem) {
centerVerticallyTo(parent)
start.linkTo(iconItem.end)
end.linkTo(fiatItem.start)
width = Dimension.fillToConstraints
},
title = config.name,
amount = config.amount,
hasPending = config.hasPending,
)
TokenOptionsBlock(
config = config,
modifier = Modifier.constrainAs(fiatItem) {
centerVerticallyTo(parent)
end.linkTo(parent.end)
},
)
}
}
}
/**
* Block for end part of token item
* shows status is reachable, is drag, hidden or show balance
*/
@Composable
private fun TokenOptionsBlock(config: TokenV2Config, modifier: Modifier = Modifier) {
when (config.tokenOptionsUIState) {
TokenOptionsUIState.VISIBLE -> TokenFiatPercentageBlock(
modifier = modifier,
fiatAmount = config.fiatAmount,
priceChange = config.priceChange,
)
TokenOptionsUIState.UNREACHABLE -> Text(
modifier = modifier,
text = "Unreachable", // TODO (conform this text)
style = TangemTypography.body2,
color = TangemTheme.colors.text.tertiary,
)
TokenOptionsUIState.HIDDEN -> TokenFiatPercentageBlock(
modifier = modifier,
fiatAmount = DOTS,
priceChange = config.priceChange,
)
TokenOptionsUIState.DRAG -> Icon(
modifier = modifier,
painter = painterResource(id = R.drawable.ic_drag_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
}
@Composable
private fun LoadingTokenState() {
BaseSurface {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = TangemTheme.dimens.spacing12,
vertical = TangemTheme.dimens.spacing4,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42))
SpacerW12()
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
ShimmerRectangle(
modifier = Modifier.size(
width = TangemTheme.dimens.size72,
height = TangemTheme.dimens.size12,
),
)
ShimmerRectangle(
modifier = Modifier.size(
width = TangemTheme.dimens.size50,
height = TangemTheme.dimens.size12,
),
)
}
}
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
ShimmerRectangle(
modifier = Modifier.size(
width = TangemTheme.dimens.size40,
height = TangemTheme.dimens.size12,
),
)
ShimmerRectangle(
modifier = Modifier.size(
width = TangemTheme.dimens.size40,
height = TangemTheme.dimens.size12,
),
)
}
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun BaseSurface(onClick: (() -> Unit)? = null, content: @Composable () -> Unit) {
Surface(
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size68),
color = TangemTheme.colors.background.primary,
onClick = onClick ?: {},
enabled = onClick != null,
) {
content()
}
}
@Composable
private fun TokenTitleAmountBlock(title: String, amount: String, hasPending: Boolean, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
) {
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
Text(
text = title,
style = TangemTypography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
if (hasPending) {
Image(
modifier = Modifier.align(Alignment.CenterVertically),
painter = painterResource(id = R.drawable.img_loader_15),
contentDescription = null,
)
}
}
Text(
text = amount,
style = TangemTypography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Composable
private fun TokenFiatPercentageBlock(fiatAmount: String, priceChange: PriceChange, modifier: Modifier = Modifier) {
Column(modifier = modifier.requiredWidth(IntrinsicSize.Max)) {
Text(
modifier = Modifier.align(Alignment.End),
text = fiatAmount,
style = TangemTypography.body2,
color = TangemTheme.colors.text.primary1,
)
SpacerH2()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
val iconChangeArrow: Int
val changeTextColor: Color
if (priceChange.type == PriceChangeType.UP) {
iconChangeArrow = R.drawable.img_arrow_up_8
changeTextColor = TangemTheme.colors.text.accent
} else {
iconChangeArrow = R.drawable.img_arrow_down_8
changeTextColor = TangemTheme.colors.text.warning
}
Image(
modifier = Modifier.align(Alignment.CenterVertically),
painter = painterResource(id = iconChangeArrow),
contentDescription = null,
)
SpacerW4()
Text(
modifier = Modifier.align(Alignment.CenterVertically),
text = priceChange.valuePercent,
style = TangemTypography.body2,
color = changeTextColor,
)
}
}
}
@Composable
private fun TokenIcon(
tokenIconUrl: String?,
modifier: Modifier = Modifier,
@DrawableRes icon: Int? = null,
@DrawableRes networkIconRes: Int? = null,
) {
Box(
modifier = modifier
.padding(end = TangemTheme.dimens.spacing16)
.size(TangemTheme.dimens.size42),
) {
val tokenImageModifier = Modifier
.align(Alignment.BottomStart)
.size(TangemTheme.dimens.size36)
val data = if (tokenIconUrl.isNullOrEmpty()) {
icon
} else {
tokenIconUrl
}
SubcomposeAsyncImage(
modifier = tokenImageModifier,
model = ImageRequest.Builder(LocalContext.current)
.data(data)
.crossfade(true)
.build(),
loading = { CircleShimmer(modifier = tokenImageModifier) },
contentDescription = null,
)
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,
)
}
}
}
}
// region preview
@Composable
private fun TokensPreview() {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18),
) {
val config = TokenV2Config(
name = "Polygon",
amount = "5,412 MATIC",
fiatAmount = "321 $",
priceChange = PriceChange(
valuePercent = "2%",
type = PriceChangeType.UP,
),
iconUrl = null,
icon = R.drawable.img_polygon_22,
networkIcon = R.drawable.img_polygon_22,
tokenUIState = TokenUIState.LOADED,
tokenOptionsUIState = TokenOptionsUIState.VISIBLE,
hasPending = true,
)
// Loaded item
TokenItemV2(config)
// Unreachable item
TokenItemV2(config.copy(tokenOptionsUIState = TokenOptionsUIState.UNREACHABLE))
// Drag item
TokenItemV2(config.copy(tokenOptionsUIState = TokenOptionsUIState.DRAG))
// Hidden item
TokenItemV2(config.copy(tokenOptionsUIState = TokenOptionsUIState.UNREACHABLE))
// Loading item
TokenItemV2(
config.copy(
tokenUIState = TokenUIState.LOADING,
),
)
}
}
@Preview(showBackground = true)
@Composable
private fun Preview_Tokens_InLightTheme() {
TangemTheme(isDark = false) {
TokensPreview()
}
}
@Preview(showBackground = true)
@Composable
private fun Preview_Tokens_InDarkTheme() {
TangemTheme(isDark = true) {
TokensPreview()
}
}
// endregion preview

View file

@ -1,9 +1,7 @@
package com.tangem.feature.main.presentation.ui
package com.tangem.feature.wallet.presentation.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.Surface
@ -14,10 +12,15 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import com.tangem.core.ui.R
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemTheme
import com.valentinilk.shimmer.shimmer
import com.tangem.feature.wallet.presentation.ui.config.WalletHeaderConfig
import com.tangem.feature.wallet.presentation.ui.state.BalanceUIState
private const val DOTS = "•••"
@OptIn(ExperimentalMaterialApi::class)
@Composable
@ -29,14 +32,19 @@ fun WalletHeaderCard(config: WalletHeaderConfig, modifier: Modifier = Modifier)
onClick = config.onClick ?: {},
enabled = config.onClick != null,
) {
Row(
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.height(intrinsicSize = IntrinsicSize.Max),
horizontalArrangement = Arrangement.SpaceBetween,
.padding(horizontal = TangemTheme.dimens.spacing12),
) {
val (balanceBlock, imageItem) = createRefs()
Column(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing12),
modifier = Modifier.constrainAs(balanceBlock) {
centerVerticallyTo(parent)
start.linkTo(parent.start)
end.linkTo(imageItem.start)
width = Dimension.fillToConstraints
},
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
HeaderWalletName(
@ -53,11 +61,15 @@ fun WalletHeaderCard(config: WalletHeaderConfig, modifier: Modifier = Modifier)
style = TangemTheme.typography.caption,
)
}
val imageWidth = TangemTheme.dimens.size120
Image(
modifier = Modifier
.width(TangemTheme.dimens.size120)
.padding(end = TangemTheme.dimens.spacing18)
.fillMaxHeight(),
modifier = Modifier.constrainAs(imageItem) {
centerVerticallyTo(parent)
top.linkTo(parent.top)
end.linkTo(parent.end)
height = Dimension.fillToConstraints
width = Dimension.value(imageWidth)
},
painter = config.cardImage,
contentDescription = null,
contentScale = ContentScale.FillWidth,
@ -106,20 +118,16 @@ private fun HeaderBalanceTitle(balance: String, balanceState: BalanceUIState) {
)
}
BalanceUIState.LOADING -> {
Box(
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing4)
.size(width = TangemTheme.dimens.size102, height = TangemTheme.dimens.size24)
.shimmer()
.background(
color = TangemTheme.colors.button.secondary,
shape = RoundedCornerShape(TangemTheme.dimens.radius6),
),
ShimmerRectangle(
modifier = Modifier.size(
width = TangemTheme.dimens.size102,
height = TangemTheme.dimens.size24,
),
)
}
BalanceUIState.HIDDEN -> {
Text(
text = "•••",
text = DOTS,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h2,
)

View file

@ -0,0 +1,41 @@
package com.tangem.feature.wallet.presentation.ui.config
import androidx.annotation.DrawableRes
import com.tangem.feature.wallet.presentation.ui.state.TokenOptionsUIState
import com.tangem.feature.wallet.presentation.ui.state.TokenUIState
/**
* Token config
*
* @property name of token/coin
* @property amount of token
* @property fiatAmount amount in fiat
* @property priceChange value of price changing
* @property iconUrl
* @property icon token/coin icon
* @property networkIcon
* @property tokenUIState token state [TokenUIState] loading etc.
* @property tokenOptionsUIState state for token options like 'unreachable', 'hidden' etc
* @property hasPending pending tx in blockchain
*/
data class TokenV2Config(
val name: String,
val amount: String,
val fiatAmount: String,
val priceChange: PriceChange,
val iconUrl: String?,
@DrawableRes val icon: Int?,
@DrawableRes val networkIcon: Int,
val tokenUIState: TokenUIState,
val tokenOptionsUIState: TokenOptionsUIState,
val hasPending: Boolean,
)
data class PriceChange(
val valuePercent: String,
val type: PriceChangeType,
)
enum class PriceChangeType {
UP, DOWN
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.main.presentation.ui
package com.tangem.feature.wallet.presentation.ui.config
import androidx.compose.ui.graphics.painter.Painter
import com.tangem.feature.wallet.presentation.ui.state.BalanceUIState
data class WalletHeaderConfig(
val walletName: String,

View file

@ -1,4 +1,4 @@
package com.tangem.feature.main.presentation.ui
package com.tangem.feature.wallet.presentation.ui.state
enum class BalanceUIState {
VISIBLE, LOADING, HIDDEN, NONE

View file

@ -0,0 +1,9 @@
package com.tangem.feature.wallet.presentation.ui.state
enum class TokenUIState {
LOADED, LOADING
}
enum class TokenOptionsUIState {
VISIBLE, HIDDEN, DRAG, UNREACHABLE
}