Updated on 2026-08-14
This commit is contained in:
parent
b0f23e3660
commit
938d7baee1
14 changed files with 569 additions and 24 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.feature.wallet.presentation.ui.state
|
||||
|
||||
enum class TokenUIState {
|
||||
LOADED, LOADING
|
||||
}
|
||||
|
||||
enum class TokenOptionsUIState {
|
||||
VISIBLE, HIDDEN, DRAG, UNREACHABLE
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue