From 4c3312186d44105c31dab8a6f1eba053f4c309d1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 20 Feb 2023 20:29:01 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../wallet/ui/view/TotalBalanceCard.kt | 337 ++++++++++++++++++ .../wallet/ui/wallet/MultiWalletView.kt | 56 +-- .../wallet/ui/wallet/SingleWalletView.kt | 2 +- app/src/main/res/layout/fragment_wallet.xml | 4 +- .../res/layout/layout_card_total_balance.xml | 84 ----- .../com/tangem/core/ui/components/Buttons.kt | 74 +++- 6 files changed, 413 insertions(+), 144 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/wallet/ui/view/TotalBalanceCard.kt delete mode 100644 app/src/main/res/layout/layout_card_total_balance.xml diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/view/TotalBalanceCard.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/view/TotalBalanceCard.kt new file mode 100644 index 0000000000..5a39cf2b28 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/view/TotalBalanceCard.kt @@ -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.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 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index 2e9f1b426f..c77e2446d8 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -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 { - val totalBalanceStrategy: DiffStrategy = { 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( diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt index 61c7b614b6..aa2ce84691 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -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() diff --git a/app/src/main/res/layout/fragment_wallet.xml b/app/src/main/res/layout/fragment_wallet.xml index cc67090da2..f2324b80c3 100644 --- a/app/src/main/res/layout/fragment_wallet.xml +++ b/app/src/main/res/layout/fragment_wallet.xml @@ -47,6 +47,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:clipToPadding="false" + android:clipChildren="false" android:paddingBottom="92dp"> - - - - - - - - - - - - - - - - - - diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index 7a262835b6..c720130f16 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -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 */ }, + ) } }