Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-27 17:39:23 +08:00
parent 640e6433ff
commit c1561a366c
9 changed files with 498 additions and 377 deletions

View file

@ -153,7 +153,7 @@ internal object WalletPreviewData {
icon = tokenIconState,
name = "Polygon",
amount = "5,412 MATIC",
hasPending = true,
hasPending = false,
tokenOptions = TokenOptionsState(
config = PriceChangeConfig(
valueInPercent = "2%",
@ -161,7 +161,6 @@ internal object WalletPreviewData {
),
fiatAmount = "321 $",
isBalanceHidden = false,
),
onItemClick = {},
onItemLongClick = {},

View file

@ -1,40 +1,40 @@
package com.tangem.feature.wallet.presentation.common.component
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.constraintlayout.compose.ConstrainedLayoutReference
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.ConstraintLayoutScope
import androidx.constraintlayout.compose.Dimension
import androidx.constraintlayout.compose.*
import com.tangem.core.ui.extensions.rememberHapticFeedback
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.*
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
@Suppress("LongMethod")
@Composable
internal fun TokenItem(
state: TokenItemState,
modifier: Modifier = Modifier,
reorderableTokenListState: ReorderableLazyListState? = null,
) {
BaseContainer(modifier = modifier.tokenClickable(state)) {
val (iconRef, cryptoInfoRef, fiatInfoRef) = createRefs()
var rootWidth by remember { mutableStateOf(Int.MIN_VALUE) }
@Suppress("DestructuringDeclarationWithTooManyEntries")
BaseContainer(
modifier = modifier
.tokenClickable(state)
.onSizeChanged { rootWidth = it.width },
) {
val (iconRef, titleRef, cryptoAmountRef, fiatAmountRef, priceChangeRef, nonFiatContentRef) = createRefs()
TokenIcon(
state = state,
@ -44,22 +44,98 @@ internal fun TokenItem(
},
)
TokenCryptoInfoBlock(
val density = LocalDensity.current
val titleRequiredMinWidth by remember(rootWidth) {
derivedStateOf { with(density) { rootWidth.toDp().times(other = 0.22f) } }
}
TokenTitle(
state = state,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing8)
.constrainAs(cryptoInfoRef) {
centerVerticallyTo(parent)
.constrainAs(titleRef) {
start.linkTo(iconRef.end)
end.linkTo(fiatInfoRef.start)
width = Dimension.fillToConstraints
top.linkTo(parent.top)
width = Dimension.fillToConstraints.atLeast(dp = titleRequiredMinWidth)
when (state) {
is TokenItemState.Content -> end.linkTo(fiatAmountRef.start)
is TokenItemState.Draggable -> end.linkTo(nonFiatContentRef.start)
is TokenItemState.Unreachable,
is TokenItemState.NoAddress,
-> {
end.linkTo(nonFiatContentRef.start)
bottom.linkTo(parent.bottom)
}
else -> Unit
}
},
)
TokenFiatInfoBlock(
TokenFiatAmount(
state = state,
modifier = Modifier.constrainAs(fiatAmountRef) {
top.linkTo(parent.top)
end.linkTo(parent.end)
width = Dimension.fillToConstraints.atMostWrapContent
if (state is TokenItemState.Content) {
start.linkTo(titleRef.end)
}
},
)
val marginBetweenRows = TangemTheme.dimens.spacing2
TokenCryptoAmount(
state = state,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing8)
.constrainAs(cryptoAmountRef) {
start.linkTo(iconRef.end)
top.linkTo(titleRef.bottom, marginBetweenRows)
bottom.linkTo(parent.bottom)
when (state) {
is TokenItemState.Content -> {
end.linkTo(priceChangeRef.start)
width = Dimension.fillToConstraints.atMostWrapContent
}
is TokenItemState.Draggable -> {
end.linkTo(nonFiatContentRef.start)
width = Dimension.fillToConstraints
}
else -> Unit
}
},
)
val priceChangeRequiredMinWidth by remember(rootWidth) {
derivedStateOf { with(density) { rootWidth.toDp().times(other = 0.16f) } }
}
TokenPriceChange(
state = state,
modifier = Modifier.constrainAs(priceChangeRef) {
top.linkTo(fiatAmountRef.bottom, marginBetweenRows)
end.linkTo(anchor = parent.end)
bottom.linkTo(parent.bottom)
if (state is TokenItemState.ContentState) {
start.linkTo(cryptoAmountRef.end)
width = Dimension.fillToConstraints
.atLeast(priceChangeRequiredMinWidth)
}
},
)
NonFiatContentBlock(
state = state,
modifier = Modifier.constrainAsOptionsItem(scope = this, ref = fiatInfoRef),
reorderableTokenListState = reorderableTokenListState,
modifier = Modifier.constrainAs(nonFiatContentRef) {
centerVerticallyTo(parent)
end.linkTo(parent.end)
},
)
}
}
@ -77,57 +153,26 @@ private inline fun BaseContainer(
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = TangemTheme.dimens.spacing14,
vertical = TangemTheme.dimens.spacing14,
),
.padding(all = TangemTheme.dimens.spacing14),
content = content,
)
}
}
@Stable
private fun Modifier.constrainAsOptionsItem(scope: ConstraintLayoutScope, ref: ConstrainedLayoutReference): Modifier {
return with(scope) {
this@constrainAsOptionsItem.constrainAs(ref) {
centerVerticallyTo(parent)
end.linkTo(parent.end)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
private fun Modifier.tokenClickable(state: TokenItemState): Modifier = composed {
when (state) {
is TokenItemState.Content -> {
val onLongClick = rememberHapticFeedback(
state = state,
onAction = state.onItemLongClick,
)
this.combinedClickable(
onClick = state.onItemClick,
onLongClick = onLongClick,
)
val onLongClick = rememberHapticFeedback(state = state, onAction = state.onItemLongClick)
combinedClickable(onClick = state.onItemClick, onLongClick = onLongClick)
}
is TokenItemState.Unreachable -> {
val onLongClick = rememberHapticFeedback(
state = state,
onAction = state.onItemLongClick,
)
this.combinedClickable(
onClick = state.onItemClick,
onLongClick = onLongClick,
)
val onLongClick = rememberHapticFeedback(state = state, onAction = state.onItemLongClick)
combinedClickable(onClick = state.onItemClick, onLongClick = onLongClick)
}
is TokenItemState.NoAddress -> {
val onLongClick = rememberHapticFeedback(
state = state,
onAction = state.onItemLongClick,
)
this.combinedClickable(
onClick = {},
onLongClick = onLongClick,
)
val onLongClick = rememberHapticFeedback(state = state, onAction = state.onItemLongClick)
combinedClickable(onClick = {}, onLongClick = onLongClick)
}
is TokenItemState.Draggable,
is TokenItemState.Loading,
@ -155,6 +200,22 @@ private fun Preview_Tokens_DarkTheme(@PreviewParameter(TokenConfigProvider::clas
private class TokenConfigProvider : CollectionPreviewParameterProvider<TokenItemState>(
collection = listOf(
WalletPreviewData.tokenItemVisibleState.copy(amount = "5,41221467146712416241274127841274174213421 MATIC"),
WalletPreviewData.tokenItemVisibleState.copy(
tokenOptions = WalletPreviewData.tokenItemVisibleState.tokenOptions.copy(
config = WalletPreviewData.tokenItemVisibleState.tokenOptions.config.copy(
valueInPercent = "31231231231231231231223123123123212312312312.00%",
),
),
),
WalletPreviewData.tokenItemVisibleState.copy(
amount = "5,41221467146712416241274127841274174213421 MATIC",
tokenOptions = WalletPreviewData.tokenItemVisibleState.tokenOptions.copy(
config = WalletPreviewData.tokenItemVisibleState.tokenOptions.config.copy(
valueInPercent = "31231231231231231231223123123123212312312312.00%",
),
),
),
WalletPreviewData.tokenItemVisibleState,
WalletPreviewData.tokenItemUnreachableState,
WalletPreviewData.tokenItemNoAddressState,

View file

@ -0,0 +1,78 @@
package com.tangem.feature.wallet.presentation.common.component.token
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemTypography
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder
@OptIn(ExperimentalAnimationApi::class)
@Composable
internal fun NonFiatContentBlock(
state: TokenItemState,
reorderableTokenListState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
) {
AnimatedContent(
targetState = state,
label = "Update non content fiat block",
modifier = modifier,
) { animatedState ->
when (animatedState) {
is TokenItemState.Draggable -> DraggableImage(reorderableTokenListState = reorderableTokenListState)
is TokenItemState.Unreachable -> NonFiatContentText(text = R.string.common_unreachable)
is TokenItemState.NoAddress -> NonFiatContentText(text = R.string.common_no_address)
is TokenItemState.Content,
is TokenItemState.Loading,
is TokenItemState.Locked,
-> Unit
}
}
}
@Composable
private fun DraggableImage(reorderableTokenListState: ReorderableLazyListState?) {
Box(
modifier = Modifier
.size(size = TangemTheme.dimens.size32)
.then(
other = if (reorderableTokenListState != null) {
Modifier.detectReorder(reorderableTokenListState)
} else {
Modifier
},
),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_drag_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
}
@Composable
private fun NonFiatContentText(@StringRes text: Int) {
Text(
text = stringResource(id = text),
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTypography.body2,
)
}

View file

@ -0,0 +1,60 @@
package com.tangem.feature.wallet.presentation.common.component.token
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.common.Strings
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemTypography
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
@OptIn(ExperimentalAnimationApi::class)
@Composable
internal fun TokenCryptoAmount(state: TokenItemState, modifier: Modifier = Modifier) {
AnimatedContent(targetState = state, label = "Update crypto amount", modifier = modifier) { animatedState ->
when (animatedState) {
is TokenItemState.Content -> {
CryptoAmountText(
amount = if (animatedState.tokenOptions.isBalanceHidden) Strings.STARS else animatedState.amount,
)
}
is TokenItemState.Draggable -> {
CryptoAmountText(amount = animatedState.info.resolveReference())
}
is TokenItemState.Loading -> {
RectangleShimmer(modifier = Modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenItemState.Locked -> {
LockedRectangle(modifier = Modifier.placeholderSize())
}
is TokenItemState.Unreachable,
is TokenItemState.NoAddress,
-> Unit
}
}
}
@Composable
private fun CryptoAmountText(amount: String) {
Text(
text = amount,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTypography.body2,
)
}
private fun Modifier.placeholderSize(): Modifier = composed {
return@composed this
.padding(vertical = TangemTheme.dimens.spacing4)
.size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12)
}

View file

@ -1,118 +0,0 @@
package com.tangem.feature.wallet.presentation.common.component.token
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.res.painterResource
import com.tangem.common.Strings.STARS
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemTypography
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
@Composable
internal fun TokenCryptoInfoBlock(state: TokenItemState, modifier: Modifier = Modifier) {
when (state) {
is TokenItemState.ContentState -> ContentBlock(state = state, modifier = modifier)
is TokenItemState.Loading -> LoadingBlock(modifier = modifier)
is TokenItemState.Locked -> LockedBlock(modifier = modifier)
}
}
@Composable
private fun ContentBlock(state: TokenItemState.ContentState, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
) {
CurrencyNameText(
name = state.name,
hasPending = (state as? TokenItemState.Content)?.hasPending == true,
)
AmountText(
amount = when (state) {
is TokenItemState.Content -> if (state.tokenOptions.isBalanceHidden) STARS else state.amount
is TokenItemState.Draggable -> state.info.resolveReference()
is TokenItemState.Unreachable,
is TokenItemState.NoAddress,
-> null
},
)
}
}
@Composable
private fun CurrencyNameText(name: String, hasPending: Boolean) {
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
Text(
text = name,
style = TangemTypography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
PendingTransactionImage(hasPending = hasPending, modifier = Modifier.align(Alignment.CenterVertically))
}
}
@Composable
private fun PendingTransactionImage(hasPending: Boolean, modifier: Modifier = Modifier) {
AnimatedVisibility(visible = hasPending, modifier = modifier) {
Image(
painter = painterResource(id = R.drawable.img_loader_15),
contentDescription = null,
)
}
}
@Composable
private fun AmountText(amount: String?) {
AnimatedVisibility(visible = !amount.isNullOrBlank()) {
if (amount == null) return@AnimatedVisibility
Text(
text = amount,
style = TangemTypography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Composable
private fun LoadingBlock(modifier: Modifier = Modifier) {
NonContentContainer(modifier = modifier) {
RectangleShimmer(modifier = Modifier.nameSize(), radius = TangemTheme.dimens.radius4)
RectangleShimmer(modifier = Modifier.amountSize(), radius = TangemTheme.dimens.radius4)
}
}
@Composable
private fun LockedBlock(modifier: Modifier = Modifier) {
NonContentContainer(modifier = modifier) {
LockedRectangle(modifier = Modifier.nameSize())
LockedRectangle(modifier = Modifier.amountSize())
}
}
@Composable
private fun NonContentContainer(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10),
content = content,
)
}
private fun Modifier.nameSize(): Modifier = composed {
return@composed size(width = TangemTheme.dimens.size72, height = TangemTheme.dimens.size12)
}
private fun Modifier.amountSize(): Modifier = composed {
return@composed size(width = TangemTheme.dimens.size50, height = TangemTheme.dimens.size12)
}

View file

@ -0,0 +1,54 @@
package com.tangem.feature.wallet.presentation.common.component.token
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.common.Strings
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemTypography
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
@OptIn(ExperimentalAnimationApi::class)
@Composable
internal fun TokenFiatAmount(state: TokenItemState, modifier: Modifier = Modifier) {
AnimatedContent(targetState = state, label = "Update fiat amount", modifier = modifier) { animatedState ->
when (animatedState) {
is TokenItemState.Content -> {
Text(
text = if (animatedState.tokenOptions.isBalanceHidden) {
Strings.STARS
} else {
animatedState.tokenOptions.fiatAmount
},
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTypography.body2,
)
}
is TokenItemState.Loading -> {
RectangleShimmer(modifier = Modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenItemState.Locked -> {
LockedRectangle(modifier = Modifier.placeholderSize())
}
is TokenItemState.Unreachable,
is TokenItemState.Draggable,
is TokenItemState.NoAddress,
-> Unit
}
}
}
private fun Modifier.placeholderSize(): Modifier = composed {
return@composed this
.padding(vertical = TangemTheme.dimens.spacing4)
.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12)
}

View file

@ -1,195 +0,0 @@
package com.tangem.feature.wallet.presentation.common.component.token
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.tangem.common.Strings.STARS
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemTypography
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder
@Composable
internal fun TokenFiatInfoBlock(
state: TokenItemState,
modifier: Modifier = Modifier,
reorderableTokenListState: ReorderableLazyListState? = null,
) {
when (state) {
is TokenItemState.Content -> ContentBlock(state = state.tokenOptions, modifier = modifier)
is TokenItemState.Draggable -> {
DraggableBlock(
modifier = modifier,
reorderableTokenListState = reorderableTokenListState,
)
}
is TokenItemState.Unreachable -> UnreachableBlock(modifier = modifier)
is TokenItemState.NoAddress -> NoAddressBlock(modifier = modifier)
is TokenItemState.Loading -> LoadingBlock(modifier = modifier)
is TokenItemState.Locked -> LockedBlock(modifier = modifier)
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun ContentBlock(state: TokenOptionsState, modifier: Modifier = Modifier) {
Column(
modifier = modifier.requiredWidth(IntrinsicSize.Max),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
) {
AnimatedContent(
targetState = state,
label = "Update the fiat percentage block",
modifier = Modifier.align(Alignment.End),
) {
Text(
text = if (it.isBalanceHidden) {
STARS
} else {
it.fiatAmount
},
style = TangemTypography.body2,
color = TangemTheme.colors.text.primary1,
)
}
PriceChangeBlock(config = state.config)
}
}
@Composable
private fun PriceChangeBlock(config: PriceChangeConfig) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
PriceChangeIcon(type = config.type, modifier = Modifier.align(Alignment.CenterVertically))
SpacerW4()
PriceChangeText(config = config, modifier = Modifier.align(Alignment.CenterVertically))
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun PriceChangeIcon(type: PriceChangeConfig.Type, modifier: Modifier = Modifier) {
AnimatedContent(
targetState = type,
label = "Update the price change's arrow",
modifier = modifier,
) {
Icon(
painter = painterResource(
id = when (it) {
PriceChangeConfig.Type.UP -> R.drawable.ic_arrow_up_8
PriceChangeConfig.Type.DOWN -> R.drawable.ic_arrow_down_8
},
),
tint = when (it) {
PriceChangeConfig.Type.UP -> TangemTheme.colors.icon.accent
PriceChangeConfig.Type.DOWN -> TangemTheme.colors.icon.warning
},
contentDescription = null,
)
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun PriceChangeText(config: PriceChangeConfig, modifier: Modifier = Modifier) {
AnimatedContent(
targetState = config.type,
label = "Update the price change's arrow",
modifier = modifier,
) {
Text(
text = config.valueInPercent,
style = TangemTypography.body2,
color = when (it) {
PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent
PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning
},
)
}
}
@Composable
private fun DraggableBlock(reorderableTokenListState: ReorderableLazyListState?, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens.size32)
.then(
other = if (reorderableTokenListState != null) {
Modifier.detectReorder(reorderableTokenListState)
} else {
Modifier
},
),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_drag_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
}
@Composable
private fun UnreachableBlock(modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = stringResource(id = R.string.common_unreachable),
style = TangemTypography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
@Composable
private fun NoAddressBlock(modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = stringResource(id = R.string.common_no_address),
style = TangemTypography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
@Composable
private fun LoadingBlock(modifier: Modifier = Modifier) {
NonContentContainer(modifier = modifier) {
RectangleShimmer(modifier = Modifier.viewSize(), radius = TangemTheme.dimens.radius4)
RectangleShimmer(modifier = Modifier.viewSize(), radius = TangemTheme.dimens.radius4)
}
}
@Composable
private fun LockedBlock(modifier: Modifier = Modifier) {
NonContentContainer(modifier = modifier) {
LockedRectangle(modifier = Modifier.viewSize())
LockedRectangle(modifier = Modifier.viewSize())
}
}
@Composable
private fun NonContentContainer(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10),
content = content,
)
}
private fun Modifier.viewSize(): Modifier = composed {
return@composed size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12)
}

View file

@ -0,0 +1,102 @@
package com.tangem.feature.wallet.presentation.common.component.token
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemTypography
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
@OptIn(ExperimentalAnimationApi::class)
@Composable
internal fun TokenPriceChange(state: TokenItemState, modifier: Modifier = Modifier) {
AnimatedContent(targetState = state, label = "Update the price change", modifier = modifier) { animatedState ->
when (animatedState) {
is TokenItemState.Content -> {
PriceChangeBlock(config = animatedState.tokenOptions.config)
}
is TokenItemState.Loading -> {
RectangleShimmer(modifier = Modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenItemState.Locked -> {
LockedRectangle(modifier = Modifier.placeholderSize())
}
is TokenItemState.Unreachable,
is TokenItemState.Draggable,
is TokenItemState.NoAddress,
-> Unit
}
}
}
@Composable
private fun PriceChangeBlock(config: PriceChangeConfig) {
Row(horizontalArrangement = Arrangement.End) {
PriceChangeIcon(
type = config.type,
modifier = Modifier.align(Alignment.CenterVertically),
)
SpacerW4()
PriceChangeText(config = config, modifier = Modifier.align(Alignment.CenterVertically))
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun PriceChangeIcon(type: PriceChangeConfig.Type, modifier: Modifier = Modifier) {
AnimatedContent(
targetState = type,
label = "Update the price change's arrow",
modifier = modifier,
) { animatedType ->
Icon(
painter = painterResource(
id = when (animatedType) {
PriceChangeConfig.Type.UP -> R.drawable.ic_arrow_up_8
PriceChangeConfig.Type.DOWN -> R.drawable.ic_arrow_down_8
},
),
tint = when (animatedType) {
PriceChangeConfig.Type.UP -> TangemTheme.colors.icon.accent
PriceChangeConfig.Type.DOWN -> TangemTheme.colors.icon.warning
},
contentDescription = null,
)
}
}
@Composable
private fun PriceChangeText(config: PriceChangeConfig, modifier: Modifier = Modifier) {
Text(
text = config.valueInPercent,
modifier = modifier,
color = when (config.type) {
PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent
PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning
},
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTypography.body2,
)
}
private fun Modifier.placeholderSize(): Modifier = composed {
return@composed this
.padding(vertical = TangemTheme.dimens.spacing4)
.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12)
}

View file

@ -0,0 +1,80 @@
package com.tangem.feature.wallet.presentation.common.component.token
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemTypography
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
@OptIn(ExperimentalAnimationApi::class)
@Composable
internal fun TokenTitle(state: TokenItemState, modifier: Modifier = Modifier) {
AnimatedContent(targetState = state, label = "Update title", modifier = modifier) { animatedState ->
when (animatedState) {
is TokenItemState.ContentState -> {
ContentTitle(
name = animatedState.name,
hasPending = (animatedState as? TokenItemState.Content)?.hasPending == true,
)
}
is TokenItemState.Loading -> {
RectangleShimmer(modifier = Modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenItemState.Locked -> {
LockedRectangle(modifier = Modifier.placeholderSize())
}
}
}
}
@Composable
private fun ContentTitle(name: String, hasPending: Boolean) {
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6)) {
CurrencyNameText(name = name, modifier = Modifier.weight(weight = 1f, fill = false))
PendingTransactionImage(
hasPending = hasPending,
modifier = Modifier.align(alignment = Alignment.CenterVertically),
)
}
}
@Composable
private fun CurrencyNameText(name: String, modifier: Modifier = Modifier) {
Text(
text = name,
modifier = modifier,
color = TangemTheme.colors.text.primary1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTypography.subtitle2,
)
}
@Composable
private fun PendingTransactionImage(hasPending: Boolean, modifier: Modifier = Modifier) {
AnimatedVisibility(visible = hasPending, modifier = modifier) {
Image(
painter = painterResource(id = R.drawable.img_loader_15),
contentDescription = null,
)
}
}
private fun Modifier.placeholderSize(): Modifier = composed {
return@composed this
.padding(vertical = TangemTheme.dimens.spacing4)
.size(width = TangemTheme.dimens.size70, height = TangemTheme.dimens.size12)
}