Updated on 2026-08-14

This commit is contained in:
Tangem 2023-02-20 20:29:01 +03:00
parent a6cff6210b
commit 4c3312186d
6 changed files with 413 additions and 144 deletions

View file

@ -0,0 +1,337 @@
package com.tangem.tap.features.wallet.ui.view
import android.content.Context
import android.util.AttributeSet
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Divider
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SelectorButton
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.components.SpacerW16
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.formatWithSpaces
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.wallet.R
import com.valentinilk.shimmer.shimmer
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import java.util.*
internal class TotalBalanceCard @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : AbstractComposeView(context, attrs, defStyleAttr) {
private var state by mutableStateOf<TotalBalanceCardState>(TotalBalanceCardState.Empty)
var status: TotalBalance? = null
set(value) {
if (field == value) return
field = value
updateState(value, onChangeFiatCurrencyClick)
}
var onChangeFiatCurrencyClick: () -> Unit = { /* no-op */ }
set(value) {
if (field == value) return
field = value
updateState(status, value)
}
@Composable
override fun Content() {
TangemTheme {
TotalBalanceCardContent(state = state)
}
}
override fun getAccessibilityClassName(): CharSequence {
return javaClass.name
}
private fun updateState(status: TotalBalance?, onChangeCurrencyClick: () -> Unit) {
state = when (status?.state) {
null -> TotalBalanceCardState.Empty
ProgressState.Loading -> TotalBalanceCardState.Loading(
fiatCurrency = status.fiatCurrency,
onChangeFiatCurrencyClick = onChangeCurrencyClick,
)
ProgressState.Error -> TotalBalanceCardState.Failure(
amount = status.fiatAmount,
fiatCurrency = status.fiatCurrency,
onChangeFiatCurrencyClick = onChangeCurrencyClick,
)
ProgressState.Refreshing,
ProgressState.Done,
-> TotalBalanceCardState.Success(
amount = status.fiatAmount,
fiatCurrency = status.fiatCurrency,
onChangeFiatCurrencyClick = onChangeCurrencyClick,
)
}
}
}
@Composable
private fun TotalBalanceCardContent(
state: TotalBalanceCardState,
modifier: Modifier = Modifier,
) {
TotalBalanceCardScaffold(
modifier = modifier,
title = {
Text(
text = stringResource(id = R.string.main_page_balance),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
},
amount = {
when (state) {
is TotalBalanceCardState.Empty,
is TotalBalanceCardState.Loading,
-> LoadingAmount()
is TotalBalanceCardState.Failure,
is TotalBalanceCardState.Success,
-> LoadedAmount(
amount = buildAmountString(
amount = state.amount,
fiatCurrencySymbol = state.fiatCurrency.symbol,
),
)
}
},
currencySelector = {
if (state !is TotalBalanceCardState.Empty) {
SelectorButton(
text = state.fiatCurrency.code,
onClick = state.onChangeFiatCurrencyClick,
)
}
},
failureText = {
AnimatedVisibility(visible = state is TotalBalanceCardState.Failure) {
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.main_processing_full_amount),
style = TangemTheme.typography.caption,
color = TangemTheme.colors.text.attention,
)
}
},
)
}
@Composable
private fun TotalBalanceCardScaffold(
title: @Composable () -> Unit,
amount: @Composable () -> Unit,
currencySelector: @Composable () -> Unit,
failureText: @Composable () -> Unit,
modifier: Modifier = Modifier,
amountWeight: Float = 0.8f,
) {
Surface(
modifier = modifier,
shape = TangemTheme.shapes.roundedCornersMedium,
color = TangemTheme.colors.background.plain,
elevation = TangemTheme.dimens.elevation1,
) {
Column(
modifier = Modifier.fillMaxWidth(),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top,
) {
SpacerW16()
Column(
modifier = Modifier.weight(amountWeight),
) {
SpacerH12()
title()
SpacerH4()
amount()
}
currencySelector()
SpacerW4()
}
SpacerH4()
Box(
modifier = Modifier.padding(
horizontal = TangemTheme.dimens.spacing16,
),
) {
failureText()
}
SpacerH12()
}
}
}
@Composable
private fun LoadingAmount(
modifier: Modifier = Modifier,
) {
Box(modifier = modifier.shimmer()) {
Box(
modifier = Modifier
.width(TangemTheme.dimens.size116)
.height(TangemTheme.dimens.size32)
.background(
color = TangemTheme.colors.stroke.primary,
shape = TangemTheme.shapes.roundedCornersSmall2,
),
)
}
}
@Composable
private fun LoadedAmount(
amount: AnnotatedString,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
Text(
text = amount,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
@Composable
private fun buildAmountString(
amount: BigDecimal,
fiatCurrencySymbol: String,
): AnnotatedString {
val format = DecimalFormat.getInstance(Locale.getDefault()) as DecimalFormat
val scaledAmount = amount
.setScale(2, RoundingMode.HALF_UP)
.formatWithSpaces()
val integer = scaledAmount.substringBefore('.')
val reminder = scaledAmount.substringAfter('.')
return buildAnnotatedString {
append(integer)
append(format.decimalFormatSymbols.decimalSeparator)
append(
AnnotatedString(
text = "$reminder $fiatCurrencySymbol",
spanStyle = TangemTheme.typography.h3.toSpanStyle(),
),
)
}
}
private sealed interface TotalBalanceCardState {
val amount: BigDecimal
val onChangeFiatCurrencyClick: () -> Unit
val fiatCurrency: FiatCurrency
object Empty : TotalBalanceCardState {
override val amount: BigDecimal = BigDecimal.ZERO
override val fiatCurrency: FiatCurrency = FiatCurrency.Default
override val onChangeFiatCurrencyClick: () -> Unit = { /* no-op */ }
}
data class Loading(
override val fiatCurrency: FiatCurrency,
override val onChangeFiatCurrencyClick: () -> Unit,
) : TotalBalanceCardState {
override val amount: BigDecimal = BigDecimal.ZERO
}
data class Failure(
override val amount: BigDecimal,
override val fiatCurrency: FiatCurrency,
override val onChangeFiatCurrencyClick: () -> Unit,
) : TotalBalanceCardState
data class Success(
override val amount: BigDecimal,
override val fiatCurrency: FiatCurrency,
override val onChangeFiatCurrencyClick: () -> Unit,
) : TotalBalanceCardState
}
// region Preview
@Composable
private fun TotalBalanceCardContentSample(
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.padding(all = TangemTheme.dimens.spacing16),
) {
TotalBalanceCardContent(state = TotalBalanceCardState.Empty)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
TotalBalanceCardContent(
state = TotalBalanceCardState.Loading(FiatCurrency("USD", "USD", "$")) {},
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
TotalBalanceCardContent(
state = TotalBalanceCardState.Failure(
amount = BigDecimal("9917.72"),
onChangeFiatCurrencyClick = {},
fiatCurrency = FiatCurrency("USD", "USD", "$"),
),
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
TotalBalanceCardContent(
state = TotalBalanceCardState.Success(
amount = BigDecimal("9917.72"),
onChangeFiatCurrencyClick = {},
fiatCurrency = FiatCurrency("USD", "USD", "$"),
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun TotalBalanceCardContentPreview_Light() {
TangemTheme {
TotalBalanceCardContentSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun TotalBalanceCardContentPreview_Dark() {
TangemTheme(isDark = true) {
TotalBalanceCardContentSample()
}
}
// endregion Preview

View file

@ -3,7 +3,6 @@ package com.tangem.tap.features.wallet.ui.wallet
import android.widget.Button
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.badoo.mvicore.DiffStrategy
import com.badoo.mvicore.modelWatcher
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds.derivationStyle
@ -11,8 +10,6 @@ import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.common.analytics.events.Portfolio
import com.tangem.tap.common.extensions.animateVisibility
import com.tangem.tap.common.extensions.formatAmountAsSpannedString
import com.tangem.tap.common.extensions.getQuantityString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
@ -21,7 +18,6 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.BalanceStatus
@ -36,14 +32,7 @@ class MultiWalletView : WalletView() {
private lateinit var walletsAdapter: WalletAdapter
private val watcher = modelWatcher<WalletState> {
val totalBalanceStrategy: DiffStrategy<WalletState> = { old, new ->
old.cardId != new.cardId ||
old.totalBalance != new.totalBalance ||
old.state != new.state ||
old.walletsStores.size != new.walletsStores.size
}
private val watcher = modelWatcher {
// !!! Workaround !!!
// Checking state properties instead of state params can reduce application performance,
// but here it is necessary because the WalletStore has an unsuitable equals method
@ -68,13 +57,11 @@ class MultiWalletView : WalletView() {
handleBackupWarning(it, showBackupWarnings)
}
}
watch({ it }, totalBalanceStrategy) { walletState ->
WalletState::totalBalance { totalBalance ->
binding?.let {
handleTotalBalance(
binding = it,
totalBalance = walletState.totalBalance,
progressState = walletState.state,
walletsCount = walletState.walletsDataFromStores.size,
totalBalance = totalBalance,
)
}
}
@ -95,7 +82,7 @@ class MultiWalletView : WalletView() {
lAddress.root.hide()
rowButtons.hide()
lSingleWalletBalance.root.hide()
lCardTotalBalance.root.show()
lCardTotalBalance.show()
rvMultiwallet.show()
btnAddToken.show()
}
@ -180,40 +167,11 @@ class MultiWalletView : WalletView() {
private fun handleTotalBalance(
binding: FragmentWalletBinding,
totalBalance: TotalBalance?,
progressState: ProgressState,
walletsCount: Int,
) = with(binding.lCardTotalBalance) {
if (walletsCount == 0) {
root.isVisible = false
} else {
if (totalBalance == null) {
if (progressState != ProgressState.Loading) {
root.isVisible = false
}
} else {
root.isVisible = true
// Skip changes when on refreshing state
if (totalBalance.state == ProgressState.Refreshing || progressState == ProgressState.Refreshing) {
return@with
}
if (totalBalance.state == ProgressState.Loading) {
veilBalance.veil()
} else {
veilBalance.unVeil()
}
tvProcessing.animateVisibility(show = totalBalance.state == ProgressState.Error)
tvBalance.text = totalBalance.fiatAmount.formatAmountAsSpannedString(
currencySymbol = totalBalance.fiatCurrency.symbol,
)
tvCurrencyName.text = totalBalance.fiatCurrency.code
tvCurrencyName.setOnClickListener {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
}
onChangeFiatCurrencyClick = {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
status = totalBalance
}
private fun handleErrorStates(

View file

@ -41,7 +41,7 @@ class SingleWalletView : WalletView() {
btnAddToken.hide()
rvPendingTransaction.hide()
pbLoadingUserTokens.hide()
lCardTotalBalance.root.hide()
lCardTotalBalance.hide()
lSingleWalletBalance.root.hide()
lWalletRescanWarning.root.hide()
lWalletBackupWarning.root.hide()

View file

@ -47,6 +47,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:clipChildren="false"
android:paddingBottom="92dp">
<ImageView
@ -136,9 +137,8 @@
</LinearLayout>
<include
<com.tangem.tap.features.wallet.ui.view.TotalBalanceCard
android:id="@+id/l_card_total_balance"
layout="@layout/layout_card_total_balance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"

View file

@ -1,84 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card_balance"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="18dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="18dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/main_page_balance"
android:textColor="@color/text_tertiary"
android:textSize="14sp"
app:layout_constraintEnd_toStartOf="@id/tv_currency_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.skydoves.androidveil.VeilLayout
android:id="@+id/veil_balance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="@id/tv_processing"
app:layout_constraintTop_toBottomOf="@id/tv_title"
app:veilLayout_baseColor="@color/lightGray0"
app:veilLayout_highlightColor="@color/lightGray1"
app:veilLayout_layout="@layout/card_total_balance_shimmer"
app:veilLayout_radius="4dp"
app:veilLayout_shimmerEnable="true"
app:veilLayout_veiled="true">
<TextView
android:id="@+id/tv_balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:minWidth="152dp"
android:textColor="@color/text_primary_1"
android:textSize="26sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="22 325.40 $"
tools:visibility="visible" />
</com.skydoves.androidveil.VeilLayout>
<TextView
android:id="@+id/tv_processing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="@string/main_processing_full_amount"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/veil_balance"
tools:visibility="visible" />
<TextView
android:id="@+id/tv_currency_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_tertiary"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_arrow_angle_down"
app:drawableTint="@color/icon_informative"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="USD" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -1,13 +1,12 @@
package com.tangem.core.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -31,11 +30,14 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
// region TextButton
/**
* [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=97%3A103&t=TmfD6UBHPg9uYfev-4)
* */
@ -99,7 +101,9 @@ fun WarningTextButton(
size = TangemButtonSize.Text,
)
}
// endregion TextButton
// region PrimaryButton
@Composable
fun PrimaryButton(
text: String,
@ -164,7 +168,9 @@ fun PrimaryButtonIconLeft(
showProgress = showProgress,
)
}
// endregion PrimaryButton
// region SecondaryButton
@Composable
fun SecondaryButton(
text: String,
@ -229,6 +235,29 @@ fun SecondaryButtonIconLeft(
showProgress = showProgress,
)
}
// endregion SecondaryButton
// region Other
@Composable
fun SelectorButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
TangemButton(
modifier = modifier,
text = text,
textStyle = TangemTheme.typography.subtitle2,
icon = TangemButtonIcon.Right(painterResource(id = R.drawable.ic_chevron_24)),
onClick = onClick,
colors = TangemButtonsDefaults.selectorButtonColors,
showProgress = false,
enabled = enabled,
size = TangemButtonSize.Selector,
)
}
// endregion Other
// region Defaults
@Suppress("LongParameterList")
@ -243,12 +272,12 @@ private fun TangemButton(
modifier: Modifier = Modifier,
size: TangemButtonSize = TangemButtonSize.Default,
elevation: ButtonElevation = TangemButtonsDefaults.elevation,
textStyle: TextStyle = TangemTheme.typography.button,
) {
Button(
modifier = modifier
.width(IntrinsicSize.Min)
.height(IntrinsicSize.Min)
.heightIn(size.toHeightDp()),
.heightIn(min = size.toHeightDp()),
onClick = {
if (!showProgress) {
onClick()
@ -261,19 +290,24 @@ private fun TangemButton(
) {
ButtonContent(
text = text,
textStyle = textStyle,
buttonIcon = icon,
colors = colors,
showProgress = showProgress,
enabled = enabled,
size = size,
)
}
}
@Suppress("LongParameterList")
@Composable
private fun ButtonContent(
text: String,
textStyle: TextStyle,
buttonIcon: TangemButtonIcon,
colors: ButtonColors,
size: TangemButtonSize,
enabled: Boolean,
showProgress: Boolean,
) {
@ -297,18 +331,19 @@ private fun ButtonContent(
)
}
} else {
Row {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(size.toIconPadding()),
) {
if (buttonIcon is TangemButtonIcon.Left) {
icon(buttonIcon.painter)
Spacer(modifier = Modifier.width(TangemTheme.dimens.size8))
}
Text(
text = text,
style = TangemTheme.typography.button,
style = textStyle,
color = colors.contentColor(enabled = enabled).value,
)
if (buttonIcon is TangemButtonIcon.Right) {
Spacer(modifier = Modifier.width(TangemTheme.dimens.size8))
icon(buttonIcon.painter)
}
}
@ -332,18 +367,28 @@ sealed interface TangemButtonIcon {
enum class TangemButtonSize {
Default,
Text,
Selector,
}
@Composable
private fun TangemButtonSize.toHeightDp(): Dp = when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.size48
TangemButtonSize.Text -> TangemTheme.dimens.size40
TangemButtonSize.Selector -> TangemTheme.dimens.size24
}
@Composable
private fun TangemButtonSize.toShape(): Shape = when (this) {
TangemButtonSize.Default -> TangemTheme.shapes.roundedCornersMedium
TangemButtonSize.Text -> TangemTheme.shapes.roundedCornersSmall
TangemButtonSize.Selector -> TangemTheme.shapes.roundedCornersSmall
}
@Composable
private fun TangemButtonSize.toIconPadding(): Dp = when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.spacing8
TangemButtonSize.Text -> TangemTheme.dimens.spacing8
TangemButtonSize.Selector -> 0.dp
}
object TangemButtonsDefaults {
@ -385,6 +430,14 @@ object TangemButtonsDefaults {
disabledBackgroundColor = Color.Transparent,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val selectorButtonColors: ButtonColors
@Composable get() = TangemButtonColors(
backgroundColor = Color.Transparent,
contentColor = TangemTheme.colors.text.tertiary,
disabledBackgroundColor = Color.Transparent,
disabledContentColor = TangemTheme.colors.text.disabled,
)
}
@Immutable
@ -595,6 +648,11 @@ private fun TextButtonSample(
text = "Delete",
onClick = { /* no-op */ },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
SelectorButton(
text = "USD",
onClick = { /* no-op */ },
)
}
}