Updated on 2026-08-14
This commit is contained in:
parent
07f63b47e4
commit
5212c21454
15 changed files with 586 additions and 92 deletions
|
|
@ -12,6 +12,7 @@ import com.tangem.common.ui.amountScreen.models.AmountState
|
|||
import com.tangem.common.ui.amountScreen.preview.AmountScreenClickIntentsStub
|
||||
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
|
||||
import com.tangem.common.ui.amountScreen.ui.amountField
|
||||
import com.tangem.common.ui.amountScreen.ui.amountFieldV2
|
||||
import com.tangem.common.ui.amountScreen.ui.buttons
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
|
@ -40,18 +41,28 @@ fun AmountScreenContent(
|
|||
bottom = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
amountField(
|
||||
amountState = amountState,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
onValueChange = clickIntents::onAmountValueChange,
|
||||
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
|
||||
)
|
||||
buttons(
|
||||
segmentedButtonConfig = amountState.segmentedButtonConfig,
|
||||
clickIntents = clickIntents,
|
||||
isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled,
|
||||
selectedButton = amountState.selectedButton,
|
||||
)
|
||||
if (amountState.isRedesignEnabled) {
|
||||
amountFieldV2(
|
||||
amountState = amountState,
|
||||
onValueChange = clickIntents::onAmountValueChange,
|
||||
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
|
||||
onCurrencyChange = clickIntents::onCurrencyChangeClick,
|
||||
onMaxAmountClick = clickIntents::onMaxValueClick,
|
||||
)
|
||||
} else {
|
||||
amountField(
|
||||
amountState = amountState,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
onValueChange = clickIntents::onAmountValueChange,
|
||||
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
|
||||
)
|
||||
buttons(
|
||||
segmentedButtonConfig = amountState.segmentedButtonConfig,
|
||||
clickIntents = clickIntents,
|
||||
isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled,
|
||||
selectedButton = amountState.selectedButton,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,6 +86,7 @@ private class SendAmountContentPreviewProvider : PreviewParameterProvider<Amount
|
|||
override val values: Sequence<AmountState>
|
||||
get() = sequenceOf(
|
||||
AmountStatePreviewData.amountState,
|
||||
AmountStatePreviewData.amountStateV2,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -58,10 +58,11 @@ class AmountStateConverter(
|
|||
return AmountState.Data(
|
||||
title = value.title,
|
||||
availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)),
|
||||
tokenName = stringReference(status.currency.name),
|
||||
tokenIconState = iconStateConverter.convert(status),
|
||||
amountTextField = amountFieldConverter.convert(value.value),
|
||||
isPrimaryButtonEnabled = false,
|
||||
appCurrencyCode = appCurrency.code,
|
||||
appCurrency = appCurrency,
|
||||
segmentedButtonConfig = persistentListOf(
|
||||
AmountSegmentedButtonsConfig(
|
||||
title = stringReference(status.currency.symbol),
|
||||
|
|
@ -80,6 +81,7 @@ class AmountStateConverter(
|
|||
),
|
||||
isSegmentedButtonsEnabled = !noFeeRate,
|
||||
selectedButton = 0,
|
||||
isRedesignEnabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -99,6 +101,7 @@ class AmountStateConverterV2(
|
|||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val maxEnterAmount: EnterAmountBoundary,
|
||||
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
|
||||
private val isRedesignEnabled: Boolean,
|
||||
) : Converter<AmountParameters, AmountState> {
|
||||
|
||||
private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
|
|
@ -116,11 +119,16 @@ class AmountStateConverterV2(
|
|||
|
||||
return AmountState.Data(
|
||||
title = value.title,
|
||||
availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)),
|
||||
availableBalance = if (isRedesignEnabled) {
|
||||
resourceReference(R.string.common_balance, wrappedList(crypto))
|
||||
} else {
|
||||
resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat))
|
||||
},
|
||||
tokenName = stringReference(cryptoCurrencyStatus.currency.name),
|
||||
tokenIconState = iconStateConverter.convert(cryptoCurrencyStatus),
|
||||
amountTextField = amountFieldConverter.convert(value.value),
|
||||
isPrimaryButtonEnabled = false,
|
||||
appCurrencyCode = appCurrency.code,
|
||||
appCurrency = appCurrency,
|
||||
segmentedButtonConfig = persistentListOf(
|
||||
AmountSegmentedButtonsConfig(
|
||||
title = stringReference(cryptoCurrencyStatus.currency.symbol),
|
||||
|
|
@ -139,6 +147,7 @@ class AmountStateConverterV2(
|
|||
),
|
||||
isSegmentedButtonsEnabled = !noFeeRate,
|
||||
selectedButton = 0,
|
||||
isRedesignEnabled = isRedesignEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.common.ui.amountScreen.models
|
|||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ sealed class AmountState {
|
|||
* @param selectedButton selected currency index
|
||||
* @param isSegmentedButtonsEnabled indicates if currency switches is enabled
|
||||
* @param amountTextField amount field state
|
||||
* @param appCurrencyCode app currency code
|
||||
* @param appCurrency app currency
|
||||
* @param isEditingDisabled indicated whether amount is editable
|
||||
* @param reduceAmountBy reduces amount to be sent by specified value
|
||||
* @param isIgnoreReduce ignores reduce amount value
|
||||
|
|
@ -30,15 +31,17 @@ sealed class AmountState {
|
|||
override val isPrimaryButtonEnabled: Boolean,
|
||||
val title: TextReference,
|
||||
val availableBalance: TextReference,
|
||||
val tokenName: TextReference,
|
||||
val tokenIconState: CurrencyIconState,
|
||||
val segmentedButtonConfig: PersistentList<AmountSegmentedButtonsConfig>,
|
||||
val selectedButton: Int,
|
||||
val isSegmentedButtonsEnabled: Boolean,
|
||||
val amountTextField: AmountFieldModel,
|
||||
val appCurrencyCode: String,
|
||||
val appCurrency: AppCurrency,
|
||||
val isEditingDisabled: Boolean = false,
|
||||
val reduceAmountBy: BigDecimal = BigDecimal.ZERO,
|
||||
val isIgnoreReduce: Boolean = false,
|
||||
val isRedesignEnabled: Boolean,
|
||||
) : AmountState()
|
||||
|
||||
data class Empty(
|
||||
|
|
|
|||
|
|
@ -2,12 +2,16 @@ package com.tangem.common.ui.amountScreen.preview
|
|||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.Amount
|
||||
import com.tangem.domain.tokens.model.AmountType
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -15,6 +19,8 @@ import java.math.BigDecimal
|
|||
|
||||
object AmountStatePreviewData {
|
||||
|
||||
val emptyState = AmountState.Empty()
|
||||
|
||||
val amountState = AmountState.Data(
|
||||
isPrimaryButtonEnabled = false,
|
||||
title = stringReference("Family Wallet"),
|
||||
|
|
@ -31,7 +37,8 @@ object AmountStatePreviewData {
|
|||
isFiat = true,
|
||||
),
|
||||
),
|
||||
appCurrencyCode = "usd",
|
||||
appCurrency = AppCurrency.Default,
|
||||
tokenName = stringReference("Tether"),
|
||||
amountTextField = AmountFieldModel(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
|
|
@ -60,6 +67,7 @@ object AmountStatePreviewData {
|
|||
),
|
||||
isSegmentedButtonsEnabled = true,
|
||||
selectedButton = 0,
|
||||
isRedesignEnabled = false,
|
||||
)
|
||||
|
||||
val amountWithValueState = amountState.copy(
|
||||
|
|
@ -75,6 +83,11 @@ object AmountStatePreviewData {
|
|||
),
|
||||
)
|
||||
|
||||
val amountStateV2 = amountState.copy(
|
||||
isRedesignEnabled = true,
|
||||
availableBalance = resourceReference(R.string.common_balance, wrappedList("2 130,88 USDT")),
|
||||
)
|
||||
|
||||
val amountWithValueFiatState = amountWithValueState.copy(
|
||||
amountTextField = amountWithValueState.amountTextField.copy(isFiatValue = false),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis
|
|||
val fiatAmount = amount.fiatAmount.value.format {
|
||||
fiat(
|
||||
fiatCurrencySymbol = amount.fiatAmount.currencySymbol,
|
||||
fiatCurrencyCode = amountState.appCurrencyCode,
|
||||
fiatCurrencyCode = amountState.appCurrency.code,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,56 +31,6 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Deprecated("Use AmountField with clicks")
|
||||
@Composable
|
||||
internal fun AmountField(amountField: AmountFieldModel, appCurrencyCode: String) {
|
||||
val decimalFormat = rememberDecimalFormat()
|
||||
val isFiatValue = amountField.isFiatValue
|
||||
val currencyCode = if (isFiatValue) appCurrencyCode else null
|
||||
val (primaryAmount, primaryValue) = if (isFiatValue) {
|
||||
amountField.fiatAmount to amountField.fiatValue
|
||||
} else {
|
||||
amountField.cryptoAmount to amountField.value
|
||||
}
|
||||
val requester = remember { FocusRequester() }
|
||||
|
||||
AmountTextField(
|
||||
value = primaryValue,
|
||||
decimals = primaryAmount.decimals,
|
||||
visualTransformation = AmountVisualTransformation(
|
||||
decimals = primaryAmount.decimals,
|
||||
symbol = primaryAmount.currencySymbol,
|
||||
currencyCode = currencyCode,
|
||||
decimalFormat = decimalFormat,
|
||||
),
|
||||
onValueChange = amountField.onValueChange,
|
||||
keyboardOptions = amountField.keyboardOptions,
|
||||
keyboardActions = amountField.keyboardActions,
|
||||
textStyle = TangemTheme.typography.h2.copy(
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
isAutoResize = true,
|
||||
isValuePasted = amountField.isValuePasted,
|
||||
onValuePastedTriggerDismiss = amountField.onValuePastedTriggerDismiss,
|
||||
modifier = Modifier
|
||||
.focusRequester(requester)
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing24,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.requiredHeightIn(min = TangemTheme.dimens.size32),
|
||||
)
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
delay(timeMillis = 200)
|
||||
requester.requestFocus()
|
||||
}
|
||||
|
||||
AmountSecondary(amountField, appCurrencyCode)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun AmountField(
|
||||
amountField: AmountFieldModel,
|
||||
|
|
@ -97,7 +47,7 @@ internal fun AmountField(
|
|||
amountField.cryptoAmount to amountField.value
|
||||
}
|
||||
val requester = remember { FocusRequester() }
|
||||
|
||||
val symbolColor = if (primaryValue.isBlank()) TangemTheme.colors.text.disabled else TangemTheme.colors.text.primary1
|
||||
AmountTextField(
|
||||
value = primaryValue,
|
||||
decimals = primaryAmount.decimals,
|
||||
|
|
@ -106,6 +56,7 @@ internal fun AmountField(
|
|||
symbol = primaryAmount.currencySymbol,
|
||||
currencyCode = currencyCode,
|
||||
decimalFormat = decimalFormat,
|
||||
symbolColor = symbolColor,
|
||||
),
|
||||
onValueChange = onValueChange,
|
||||
keyboardOptions = amountField.keyboardOptions,
|
||||
|
|
|
|||
|
|
@ -2,20 +2,32 @@ package com.tangem.common.ui.amountScreen.ui
|
|||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.atoms.text.TextEllipsis
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.orMaskWithStars
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
private const val AMOUNT_FIELD_KEY = "amountFieldKey"
|
||||
|
|
@ -64,10 +76,142 @@ internal fun LazyListScope.amountField(
|
|||
)
|
||||
AmountField(
|
||||
amountField = amountState.amountTextField,
|
||||
appCurrencyCode = amountState.appCurrencyCode,
|
||||
appCurrencyCode = amountState.appCurrency.code,
|
||||
onValueChange = onValueChange,
|
||||
onValuePastedTriggerDismiss = onValuePastedTriggerDismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun LazyListScope.amountFieldV2(
|
||||
amountState: AmountState,
|
||||
modifier: Modifier = Modifier,
|
||||
onValueChange: (String) -> Unit,
|
||||
onValuePastedTriggerDismiss: () -> Unit,
|
||||
onCurrencyChange: (Boolean) -> Unit,
|
||||
onMaxAmountClick: () -> Unit,
|
||||
) {
|
||||
item(key = AMOUNT_FIELD_KEY) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = modifier
|
||||
.padding(top = 48.dp, bottom = 28.dp),
|
||||
) {
|
||||
if (amountState !is AmountState.Data) {
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.caption2,
|
||||
modifier = Modifier.width(60.dp),
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = amountState.title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
AmountFieldV2(
|
||||
amountUM = amountState,
|
||||
onValueChange = onValueChange,
|
||||
onValuePastedTriggerDismiss = onValuePastedTriggerDismiss,
|
||||
onCurrencyChange = onCurrencyChange,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
thickness = 0.5.dp,
|
||||
color = TangemTheme.colors.stroke.primary,
|
||||
)
|
||||
AmountInfo(
|
||||
amountUM = amountState,
|
||||
onMaxAmountClick = onMaxAmountClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountInfo(amountUM: AmountState, onMaxAmountClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
val tokenIconState = (amountUM as? AmountState.Data)?.tokenIconState ?: CurrencyIconState.Loading
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
CurrencyIcon(
|
||||
state = tokenIconState,
|
||||
shouldDisplayNetwork = true,
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
top = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
AmountInfoMain(amountUM = amountUM)
|
||||
SpacerWMax()
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.send_max_amount),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(TangemTheme.colors.background.secondary)
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(),
|
||||
onClick = onMaxAmountClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountInfoMain(amountUM: AmountState, modifier: Modifier = Modifier) {
|
||||
AnimatedContent(
|
||||
targetState = amountUM !is AmountState.Data,
|
||||
modifier = modifier,
|
||||
) { isContent ->
|
||||
if (isContent) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
modifier = Modifier.width(56.dp),
|
||||
)
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.caption2,
|
||||
modifier = Modifier.width(72.dp),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val amountUM = amountUM as AmountState.Data
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
Text(
|
||||
text = amountUM.tokenName.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 1,
|
||||
)
|
||||
EllipsisText(
|
||||
text = amountUM.availableBalance.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
ellipsis = TextEllipsis.OffsetEnd(amountUM.amountTextField.cryptoAmount.currencySymbol.length),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,329 @@
|
|||
package com.tangem.common.ui.amountScreen.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.BottomCenter
|
||||
import androidx.compose.ui.Alignment.Companion.TopCenter
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.currency.fiaticon.FiatIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
private const val ROTATED_DEGREE = 180f
|
||||
private const val INITIAL_DEGREE = 0f
|
||||
|
||||
@Composable
|
||||
fun AmountFieldV2(
|
||||
amountUM: AmountState,
|
||||
onValueChange: (String) -> Unit,
|
||||
onValuePastedTriggerDismiss: () -> Unit,
|
||||
onCurrencyChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val decimalFormat = rememberDecimalFormat()
|
||||
|
||||
val requester = remember { FocusRequester() }
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier,
|
||||
) {
|
||||
if (amountUM is AmountState.Empty) {
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.head,
|
||||
modifier = Modifier.width(150.dp),
|
||||
)
|
||||
} else if (amountUM is AmountState.Data) {
|
||||
AnimatedContent(
|
||||
targetState = amountUM.amountTextField.isFiatValue,
|
||||
transitionSpec = { primaryFieldCurrencyAnimation() },
|
||||
label = "Primary field change animation",
|
||||
) { isFiatValue ->
|
||||
val currencyCode = if (isFiatValue) amountUM.appCurrency.code else null
|
||||
val (primaryAmount, primaryValue) = if (isFiatValue) {
|
||||
amountUM.amountTextField.fiatAmount to amountUM.amountTextField.fiatValue
|
||||
} else {
|
||||
amountUM.amountTextField.cryptoAmount to amountUM.amountTextField.value
|
||||
}
|
||||
AmountTextField(
|
||||
value = primaryValue,
|
||||
decimals = primaryAmount.decimals,
|
||||
visualTransformation = AmountVisualTransformation(
|
||||
decimals = primaryAmount.decimals,
|
||||
symbol = primaryAmount.currencySymbol,
|
||||
currencyCode = currencyCode,
|
||||
decimalFormat = decimalFormat,
|
||||
symbolColor = TangemTheme.colors.text.disabled,
|
||||
),
|
||||
onValueChange = onValueChange,
|
||||
keyboardOptions = amountUM.amountTextField.keyboardOptions,
|
||||
keyboardActions = amountUM.amountTextField.keyboardActions,
|
||||
textStyle = TangemTheme.typography.head.copy(
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
isAutoResize = true,
|
||||
isValuePasted = amountUM.amountTextField.isValuePasted,
|
||||
onValuePastedTriggerDismiss = onValuePastedTriggerDismiss,
|
||||
modifier = Modifier
|
||||
.focusRequester(requester)
|
||||
.requiredHeightIn(min = 44.dp),
|
||||
)
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
delay(timeMillis = 200)
|
||||
requester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
AmountSecondary(
|
||||
amountUM = amountUM,
|
||||
onCurrencyChange = onCurrencyChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize()
|
||||
.padding(top = 8.dp),
|
||||
) {
|
||||
if (amountUM is AmountState.Empty) {
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.caption2,
|
||||
modifier = Modifier
|
||||
.width(90.dp)
|
||||
.padding(bottom = 20.dp)
|
||||
.align(TopCenter),
|
||||
)
|
||||
} else if (amountUM is AmountState.Data) {
|
||||
AmountFieldCurrencyInfo(
|
||||
amountUM = amountUM,
|
||||
onCurrencyChange = onCurrencyChange,
|
||||
)
|
||||
AmountFieldError(
|
||||
isError = amountUM.amountTextField.isError,
|
||||
isWarning = amountUM.amountTextField.isWarning,
|
||||
error = amountUM.amountTextField.error,
|
||||
modifier = Modifier
|
||||
.align(BottomCenter)
|
||||
.padding(top = 20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.AmountFieldCurrencyInfo(amountUM: AmountState.Data, onCurrencyChange: (Boolean) -> Unit) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier
|
||||
.align(TopCenter)
|
||||
.padding(bottom = 20.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = { onCurrencyChange(!amountUM.amountTextField.isFiatValue) },
|
||||
),
|
||||
) {
|
||||
val iconRotateState by animateFloatAsState(
|
||||
targetValue = if (amountUM.amountTextField.isFiatValue) ROTATED_DEGREE else INITIAL_DEGREE,
|
||||
animationSpec = tween(
|
||||
durationMillis = 400,
|
||||
easing = FastOutSlowInEasing,
|
||||
),
|
||||
label = "Currency change icon animation",
|
||||
)
|
||||
Icon(
|
||||
painter = rememberVectorPainter(
|
||||
ImageVector.vectorResource(R.drawable.ic_exchange_vertical_24),
|
||||
),
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.graphicsLayer {
|
||||
rotationY = iconRotateState
|
||||
},
|
||||
)
|
||||
AmountFieldCurrencyIcon(
|
||||
amountUM = amountUM,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountFieldCurrencyIcon(amountUM: AmountState.Data) {
|
||||
AnimatedContent(
|
||||
targetState = amountUM.amountTextField.isFiatValue,
|
||||
transitionSpec = { secondaryFieldCurrencyAnimation() },
|
||||
label = "Secondary field change animation",
|
||||
) { isFiatValue ->
|
||||
val secondaryAmount =
|
||||
if (isFiatValue) amountUM.amountTextField.cryptoAmount else amountUM.amountTextField.fiatAmount
|
||||
val text = secondaryAmount.value.format {
|
||||
if (isFiatValue) {
|
||||
crypto(secondaryAmount.currencySymbol, secondaryAmount.decimals)
|
||||
} else {
|
||||
fiat(
|
||||
fiatCurrencySymbol = amountUM.appCurrency.symbol,
|
||||
fiatCurrencyCode = amountUM.appCurrency.code,
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
if (isFiatValue) {
|
||||
CurrencyIcon(
|
||||
state = amountUM.tokenIconState,
|
||||
shouldDisplayNetwork = false,
|
||||
modifier = Modifier.size(12.dp),
|
||||
)
|
||||
} else {
|
||||
FiatIcon(
|
||||
url = amountUM.appCurrency.iconSmallUrl,
|
||||
size = 12.dp,
|
||||
isGrayscale = false,
|
||||
modifier = Modifier.size(12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountFieldError(
|
||||
isError: Boolean,
|
||||
isWarning: Boolean,
|
||||
error: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isError || isWarning,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
modifier = modifier,
|
||||
label = "Error field appearance animation",
|
||||
) {
|
||||
val errorText = remember(this, error) { error }
|
||||
val color = if (isError) TangemTheme.colors.text.warning else TangemTheme.colors.text.attention
|
||||
Text(
|
||||
text = errorText.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = color,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <S> AnimatedContentTransitionScope<S>.secondaryFieldCurrencyAnimation(): ContentTransform {
|
||||
return (
|
||||
fadeIn(
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
) + scaleIn(
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
initialScale = 0.9f,
|
||||
)
|
||||
).togetherWith(
|
||||
fadeOut(
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
) + scaleOut(
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
targetScale = 0.9f,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun <S> AnimatedContentTransitionScope<S>.primaryFieldCurrencyAnimation(): ContentTransform {
|
||||
return (
|
||||
fadeIn(
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
) + scaleIn(
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
initialScale = 0.9f,
|
||||
) + slideInVertically(initialOffsetY = { it / 2 })
|
||||
)
|
||||
.togetherWith(
|
||||
fadeOut(
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
) + scaleOut(
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
targetScale = 0.9f,
|
||||
) + slideOutVertically(targetOffsetY = { it / 2 }),
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun AmountFieldV2_Preview(@PreviewParameter(AmountFieldV2PreviewProvider::class) params: AmountState) {
|
||||
TangemThemePreview {
|
||||
AmountFieldV2(
|
||||
amountUM = params,
|
||||
onValueChange = {},
|
||||
onValuePastedTriggerDismiss = { },
|
||||
onCurrencyChange = {},
|
||||
modifier = Modifier.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class AmountFieldV2PreviewProvider : PreviewParameterProvider<AmountState> {
|
||||
override val values: Sequence<AmountState>
|
||||
get() = sequenceOf(
|
||||
AmountStatePreviewData.emptyState,
|
||||
AmountStatePreviewData.amountState,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -60,7 +60,10 @@ fun AmountTextField(
|
|||
modifier: Modifier = Modifier,
|
||||
color: Color = TangemTheme.colors.text.primary1,
|
||||
backgroundColor: Color = TangemTheme.colors.background.action,
|
||||
visualTransformation: VisualTransformation = AmountVisualTransformation(decimals),
|
||||
visualTransformation: VisualTransformation = AmountVisualTransformation(
|
||||
decimals = decimals,
|
||||
symbolColor = if (value.isBlank()) TangemTheme.colors.text.disabled else color,
|
||||
),
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
package com.tangem.core.ui.components.fields.visualtransformations
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.input.OffsetMapping
|
||||
import androidx.compose.ui.text.input.TransformedText
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import com.tangem.core.ui.extensions.appendColored
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE
|
||||
import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode
|
||||
|
|
@ -19,6 +22,7 @@ class AmountVisualTransformation(
|
|||
private val symbol: String? = null,
|
||||
private val currencyCode: String? = null,
|
||||
private val decimalFormat: DecimalFormat = DecimalFormat(),
|
||||
private val symbolColor: Color,
|
||||
) : VisualTransformation {
|
||||
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
|
|
@ -28,17 +32,22 @@ class AmountVisualTransformation(
|
|||
)
|
||||
formattedAmount = formattedAmount.ifEmpty { decimalFormat.defaultFormat() }
|
||||
val formattedText = if (formattedAmount.isNotEmpty() && symbol != null) {
|
||||
AnnotatedString(
|
||||
buildAnnotatedString {
|
||||
if (currencyCode != null) {
|
||||
formatFiatEditableAmount(
|
||||
fiatAmount = formattedAmount,
|
||||
fiatCurrencyCode = currencyCode,
|
||||
fiatCurrencySymbol = symbol,
|
||||
append(
|
||||
formatFiatEditableAmount(
|
||||
fiatAmount = formattedAmount,
|
||||
fiatCurrencyCode = currencyCode,
|
||||
fiatCurrencySymbol = symbol,
|
||||
fiatCurrencySymbolColor = symbolColor,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
formatWithSymbol(formattedAmount, symbol)
|
||||
},
|
||||
)
|
||||
append(formattedAmount)
|
||||
append(CURRENCY_SPACE)
|
||||
appendColored(symbol, symbolColor)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AnnotatedString(decimalFormat.defaultFormat())
|
||||
}
|
||||
|
|
@ -54,9 +63,10 @@ class AmountVisualTransformation(
|
|||
fiatAmount: String?,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
fiatCurrencySymbolColor: Color,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (fiatAmount == null) return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
|
||||
): AnnotatedString {
|
||||
if (fiatAmount == null) return AnnotatedString(BigDecimalFormatConstants.EMPTY_BALANCE_SIGN)
|
||||
|
||||
val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode)
|
||||
val numberFormatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
|
|
@ -64,14 +74,21 @@ class AmountVisualTransformation(
|
|||
}
|
||||
val formatter = requireNotNull(numberFormatter as? DecimalFormat) {
|
||||
Timber.e("NumberFormat is null")
|
||||
return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
|
||||
return AnnotatedString(BigDecimalFormatConstants.EMPTY_BALANCE_SIGN)
|
||||
}
|
||||
return buildAnnotatedString {
|
||||
appendColored(
|
||||
formatter.positivePrefix.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol),
|
||||
fiatCurrencySymbolColor,
|
||||
)
|
||||
append(fiatAmount)
|
||||
appendColored(
|
||||
formatter.positiveSuffix.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol),
|
||||
fiatCurrencySymbolColor,
|
||||
)
|
||||
}
|
||||
return "${formatter.positivePrefix}$fiatAmount${formatter.positiveSuffix}"
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
|
||||
private fun formatWithSymbol(amount: String, symbol: String) = "$amount$CURRENCY_SPACE$symbol"
|
||||
|
||||
private class OffsetMappingImpl(
|
||||
private val text: String,
|
||||
private val formattedText: AnnotatedString,
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ fun InputRowEnterAmount(
|
|||
decimals = decimals,
|
||||
symbol = symbol,
|
||||
decimalFormat = rememberDecimalFormat(),
|
||||
symbolColor = textColor,
|
||||
),
|
||||
onValueChange = onValueChange,
|
||||
color = textColor,
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ fun InputRowEnterInfoAmount(
|
|||
decimals = decimals,
|
||||
symbol = symbol,
|
||||
decimalFormat = rememberDecimalFormat(),
|
||||
symbolColor = textColor,
|
||||
),
|
||||
onValueChange = onValueChange,
|
||||
color = textColor,
|
||||
|
|
@ -120,6 +121,7 @@ fun InputRowEnterInfoAmountV2(
|
|||
titleColor: Color = TangemTheme.colors.text.secondary,
|
||||
textColor: Color = TangemTheme.colors.text.primary1,
|
||||
infoColor: Color = TangemTheme.colors.text.tertiary,
|
||||
symbolColor: Color = TangemTheme.colors.text.primary1,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
showDivider: Boolean = false,
|
||||
|
|
@ -154,6 +156,7 @@ fun InputRowEnterInfoAmountV2(
|
|||
decimals = decimals,
|
||||
symbol = symbol,
|
||||
decimalFormat = rememberDecimalFormat(),
|
||||
symbolColor = symbolColor,
|
||||
),
|
||||
onValueChange = onValueChange,
|
||||
color = textColor,
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ fun Modifier.selectedBorder(
|
|||
color = color,
|
||||
shape = RoundedCornerShape(radius),
|
||||
)
|
||||
.padding(2.5.dp)
|
||||
.padding(width)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = TangemTheme.colors.text.accent,
|
||||
|
|
@ -91,7 +91,7 @@ fun Modifier.selectedBorder(
|
|||
.clip(RoundedCornerShape(radius - 2.dp))
|
||||
},
|
||||
otherModifier = {
|
||||
padding(2.dp)
|
||||
padding(width)
|
||||
.clip(RoundedCornerShape(radius - 2.dp))
|
||||
},
|
||||
)
|
||||
|
|
@ -51,6 +51,11 @@ internal fun OnrampAmountContent(state: OnrampAmountBlockUM, modifier: Modifier
|
|||
private fun OnrampAmountField(amountField: AmountFieldModel) {
|
||||
val decimalFormat = rememberDecimalFormat()
|
||||
val requester = remember { FocusRequester() }
|
||||
val symbolColor = if (amountField.fiatValue.isBlank()) {
|
||||
TangemTheme.colors.text.disabled
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
}
|
||||
AmountTextField(
|
||||
value = amountField.fiatValue,
|
||||
decimals = amountField.fiatAmount.decimals,
|
||||
|
|
@ -59,6 +64,7 @@ private fun OnrampAmountField(amountField: AmountFieldModel) {
|
|||
symbol = amountField.fiatAmount.currencySymbol,
|
||||
currencyCode = amountField.fiatAmount.currencySymbol,
|
||||
decimalFormat = decimalFormat,
|
||||
symbolColor = symbolColor,
|
||||
),
|
||||
onValueChange = amountField.onValueChange,
|
||||
keyboardOptions = amountField.keyboardOptions,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
|
||||
import com.tangem.features.send.v2.api.SendFeatureToggles
|
||||
import com.tangem.features.send.v2.common.PredefinedValues
|
||||
import com.tangem.features.send.v2.common.ui.state.NavigationUM
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
|
|
@ -47,6 +48,7 @@ internal class SendAmountModel @Inject constructor(
|
|||
private val feeReloadTrigger: SendFeeReloadTrigger,
|
||||
private val sendAmountUpdateQRListener: SendAmountUpdateQRListener,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val sendFeatureToggles: SendFeatureToggles,
|
||||
) : Model(), AmountScreenClickIntents {
|
||||
|
||||
private val params: SendAmountComponentParams = paramsContainer.require()
|
||||
|
|
@ -95,6 +97,7 @@ internal class SendAmountModel @Inject constructor(
|
|||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxEnterAmount = maxAmountBoundary,
|
||||
iconStateConverter = CryptoCurrencyToIconStateConverter(),
|
||||
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
|
||||
).convert(
|
||||
AmountParameters(
|
||||
title = stringReference(userWallet.name),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue