Updated on 2026-08-14
This commit is contained in:
parent
f9ace58267
commit
9ac2009c89
14 changed files with 250 additions and 149 deletions
|
|
@ -1,10 +1,7 @@
|
|||
package com.tangem.core.ui.components.appbar
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
|
|
@ -66,8 +63,7 @@ fun AppBarWithBackButtonAndIconContent(
|
|||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.weight(1f)
|
||||
.animateContentSize(),
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = !text.isNullOrBlank(),
|
||||
|
|
@ -84,8 +80,8 @@ fun AppBarWithBackButtonAndIconContent(
|
|||
}
|
||||
AnimatedVisibility(
|
||||
visible = !subtitle.isNullOrBlank(),
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
enter = fadeIn().plus(expandVertically()),
|
||||
exit = fadeOut().plus(shrinkVertically()),
|
||||
label = "Toolbar subtitle change",
|
||||
) {
|
||||
Text(
|
||||
|
|
@ -93,6 +89,7 @@ fun AppBarWithBackButtonAndIconContent(
|
|||
color = TangemTheme.colors.text.secondary,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.caption2,
|
||||
modifier = Modifier.animateContentSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,10 @@ package com.tangem.core.ui.components.fields
|
|||
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.TopCenter
|
||||
|
|
@ -21,6 +19,7 @@ import androidx.compose.ui.text.ParagraphIntrinsics
|
|||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.createFontFamilyResolver
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -33,20 +32,21 @@ import java.text.DecimalFormat
|
|||
|
||||
/**
|
||||
* Simple text field for amount input.
|
||||
* Validates and trims input text using [DecimalFormat]. Formats visual output using [AmountVisualTransformation].
|
||||
* Can display aligned placeholder and currency symbol [symbol].
|
||||
* Validates and trims input text using [DecimalFormat]. Formats visual output using [visualTransformation].
|
||||
* Can display aligned placeholder.
|
||||
*
|
||||
* @param value initial text
|
||||
* @param decimals number of decimal places
|
||||
* @param onValueChange callback
|
||||
* @param textStyle text and placeholder styles
|
||||
* @param modifier modifier
|
||||
* @param symbol currency symbol
|
||||
* @param color text color
|
||||
* @param placeholderAlignment alignment of placeholder
|
||||
* @param showPlaceholder show placeholder
|
||||
* @param visualTransformation text visual transformation
|
||||
* @param keyboardOptions keyboard options
|
||||
*
|
||||
* @param keyboardActions keyboard actions
|
||||
* @param isEnabled is field editing enabled
|
||||
* @param isAutoResize is text font auto resize
|
||||
* @param reduceFactor font resize factor
|
||||
* @see [SimpleTextField] for standard text field
|
||||
*/
|
||||
@Composable
|
||||
|
|
@ -56,10 +56,8 @@ fun AmountTextField(
|
|||
onValueChange: (String) -> Unit,
|
||||
textStyle: TextStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
symbol: String? = null,
|
||||
color: Color = TangemTheme.colors.text.primary1,
|
||||
placeholderAlignment: Alignment = TopStart,
|
||||
showPlaceholder: Boolean = true,
|
||||
visualTransformation: VisualTransformation = AmountVisualTransformation(decimals),
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
|
|
@ -70,12 +68,6 @@ fun AmountTextField(
|
|||
reduceFactor: Double = 0.9,
|
||||
) {
|
||||
val decimalFormat = rememberDecimalFormat()
|
||||
val visualTransformation = remember { AmountVisualTransformation(decimals, symbol, decimalFormat) }
|
||||
val placeholderTextAlign = if (placeholderAlignment == TopCenter) {
|
||||
TextAlign.Center
|
||||
} else {
|
||||
TextAlign.Start
|
||||
}
|
||||
BoxWithConstraints(modifier = modifier) {
|
||||
var fontSize = textStyle.fontSize
|
||||
if (isAutoResize) {
|
||||
|
|
@ -96,6 +88,7 @@ fun AmountTextField(
|
|||
}
|
||||
}
|
||||
}
|
||||
val textColor = if (value.isBlank()) TangemTheme.colors.text.disabled else color
|
||||
SimpleTextField(
|
||||
value = value,
|
||||
onValueChange = { newText ->
|
||||
|
|
@ -108,31 +101,13 @@ fun AmountTextField(
|
|||
fontSize = fontSize,
|
||||
textDirection = TextDirection.ContentOrLtr,
|
||||
),
|
||||
color = color,
|
||||
color = textColor,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
singleLine = true,
|
||||
readOnly = !isEnabled,
|
||||
visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat),
|
||||
visualTransformation = visualTransformation,
|
||||
modifier = Modifier.background(TangemTheme.colors.background.action),
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (value.isBlank() && showPlaceholder) {
|
||||
var placeholder = decimalFormat.defaultFormat()
|
||||
if (symbol != null) {
|
||||
placeholder = placeholder.plus(" $symbol")
|
||||
}
|
||||
Text(
|
||||
text = placeholder,
|
||||
style = textStyle.copy(textDirection = TextDirection.ContentOrLtr),
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
textAlign = placeholderTextAlign,
|
||||
modifier = Modifier.align(placeholderAlignment),
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -153,9 +128,6 @@ private fun AmountTextFieldPreview(
|
|||
AmountTextField(
|
||||
value = text,
|
||||
decimals = amount.decimals,
|
||||
symbol = amount.symbol,
|
||||
placeholderAlignment = amount.placeholderAlignment,
|
||||
showPlaceholder = amount.showPlaceholder,
|
||||
onValueChange = { text = it },
|
||||
textStyle = TangemTheme.typography.h2.copy(
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
|
|
@ -169,28 +141,24 @@ private fun AmountTextFieldPreview(
|
|||
private class AmountTextFieldPreviewProvider : PreviewParameterProvider<AmountTextFieldPreviewData> {
|
||||
override val values = sequenceOf(
|
||||
AmountTextFieldPreviewData(
|
||||
symbol = "USD",
|
||||
value = "1000000,123123",
|
||||
decimals = 3,
|
||||
placeholderAlignment = TopStart,
|
||||
showPlaceholder = true,
|
||||
),
|
||||
AmountTextFieldPreviewData(
|
||||
symbol = null,
|
||||
value = "1000000.123123",
|
||||
decimals = 6,
|
||||
placeholderAlignment = TopStart,
|
||||
showPlaceholder = false,
|
||||
),
|
||||
AmountTextFieldPreviewData(
|
||||
symbol = "$",
|
||||
value = null,
|
||||
decimals = 2,
|
||||
showPlaceholder = true,
|
||||
placeholderAlignment = TopCenter,
|
||||
),
|
||||
AmountTextFieldPreviewData(
|
||||
symbol = null,
|
||||
value = null,
|
||||
decimals = 2,
|
||||
showPlaceholder = true,
|
||||
|
|
@ -200,7 +168,6 @@ private class AmountTextFieldPreviewProvider : PreviewParameterProvider<AmountTe
|
|||
}
|
||||
|
||||
private data class AmountTextFieldPreviewData(
|
||||
val symbol: String? = "$",
|
||||
val value: String? = null,
|
||||
val decimals: Int = 2,
|
||||
val showPlaceholder: Boolean,
|
||||
|
|
|
|||
|
|
@ -1,48 +1,82 @@
|
|||
package com.tangem.core.ui.components.fields.visualtransformations
|
||||
|
||||
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.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.defaultFormat
|
||||
import com.tangem.core.ui.utils.formatWithThousands
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import java.text.DecimalFormat
|
||||
|
||||
class AmountVisualTransformation(
|
||||
private val decimals: Int,
|
||||
private val symbol: String? = null,
|
||||
private val currencyCode: String? = null,
|
||||
private val decimalFormat: DecimalFormat = DecimalFormat(),
|
||||
) : VisualTransformation {
|
||||
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
val formattedText = decimalFormat.formatWithThousands(
|
||||
var formattedAmount = decimalFormat.formatWithThousands(
|
||||
text.text,
|
||||
decimals,
|
||||
)
|
||||
formattedAmount = formattedAmount.ifEmpty { decimalFormat.defaultFormat() }
|
||||
val decimalValue = text.text.parseToBigDecimal(decimals)
|
||||
val formattedText = if (formattedAmount.isNotEmpty() && symbol != null) {
|
||||
AnnotatedString(
|
||||
if (currencyCode != null) {
|
||||
BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = decimalValue,
|
||||
fiatCurrencyCode = currencyCode,
|
||||
fiatCurrencySymbol = symbol,
|
||||
)
|
||||
} else {
|
||||
BigDecimalFormatter.formatCryptoAmountUncapped(
|
||||
cryptoAmount = decimalValue,
|
||||
cryptoSymbol = symbol,
|
||||
decimals = decimals,
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
AnnotatedString(decimalFormat.defaultFormat())
|
||||
}
|
||||
|
||||
val groupingSymbol = decimalFormat.decimalFormatSymbols.groupingSeparator
|
||||
return TransformedText(
|
||||
text = buildAnnotatedString {
|
||||
append(formattedText)
|
||||
if (formattedText.isNotEmpty() && symbol != null) {
|
||||
append(" $symbol")
|
||||
}
|
||||
},
|
||||
offsetMapping = OffsetMappingImpl(text.text, formattedText, groupingSymbol),
|
||||
text = formattedText,
|
||||
offsetMapping = OffsetMappingImpl(text.text, formattedText, symbol, groupingSymbol),
|
||||
)
|
||||
}
|
||||
|
||||
private class OffsetMappingImpl(
|
||||
private val text: String,
|
||||
private val formattedText: String,
|
||||
private val formattedText: AnnotatedString,
|
||||
private val currencySymbol: String?,
|
||||
private val gropingSymbol: Char,
|
||||
) : OffsetMapping {
|
||||
override fun originalToTransformed(offset: Int): Int {
|
||||
var noneDigitCount = 0
|
||||
var i = 0
|
||||
val symbolOffset = currencySymbol?.let { formattedText.indexOf(it) } ?: -1
|
||||
while (i < offset + noneDigitCount) {
|
||||
if (formattedText.getOrNull(i++) == gropingSymbol) noneDigitCount++
|
||||
val char = formattedText.getOrNull(i++)
|
||||
if (char == gropingSymbol) noneDigitCount++
|
||||
if (symbolOffset == 0 && char?.isWhitespace() == true) noneDigitCount++
|
||||
}
|
||||
return (offset + noneDigitCount).coerceIn(0, formattedText.length)
|
||||
var transformedOffset = if (symbolOffset == 0 && currencySymbol != null) {
|
||||
currencySymbol.length + offset + noneDigitCount
|
||||
} else {
|
||||
offset + noneDigitCount
|
||||
}
|
||||
transformedOffset = if (symbolOffset > 0) {
|
||||
transformedOffset.coerceIn(0, minOf(symbolOffset, formattedText.length))
|
||||
} else {
|
||||
transformedOffset.coerceIn(0, formattedText.length)
|
||||
}
|
||||
return transformedOffset
|
||||
}
|
||||
|
||||
override fun transformedToOriginal(offset: Int): Int {
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||
|
||||
/**
|
||||
* Input row for entering amount. Manages correct amount format and validation
|
||||
|
|
@ -76,7 +78,11 @@ fun InputRowEnterAmount(
|
|||
AmountTextField(
|
||||
value = text,
|
||||
decimals = decimals,
|
||||
symbol = symbol,
|
||||
visualTransformation = AmountVisualTransformation(
|
||||
decimals = decimals,
|
||||
symbol = symbol,
|
||||
decimalFormat = rememberDecimalFormat(),
|
||||
),
|
||||
onValueChange = onValueChange,
|
||||
color = textColor,
|
||||
textStyle = TangemTheme.typography.body2,
|
||||
|
|
|
|||
|
|
@ -12,23 +12,28 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||
|
||||
/**
|
||||
* `Input Row Enter Info` for entering amount. Manages correct amount format and validation
|
||||
|
||||
* @param title title reference
|
||||
* @param text primary text reference
|
||||
* @param decimals amount text decimal count
|
||||
* @param onValueChange text change callback
|
||||
* @param modifier modifier
|
||||
* @param symbol amount symbol
|
||||
* @param info info text
|
||||
* @param titleColor title color
|
||||
* @param textColor text color
|
||||
* @param isSingleLine text
|
||||
* @param visualTransformation applied transformation to text
|
||||
* @param infoColor info text color
|
||||
* @param keyboardOptions keyboard options for field
|
||||
* @param keyboardActions keyboard actions for field
|
||||
* @param showDivider show divider
|
||||
*
|
||||
* @see [InputRowEnterInfo]
|
||||
|
|
@ -71,7 +76,11 @@ fun InputRowEnterInfoAmount(
|
|||
AmountTextField(
|
||||
value = text,
|
||||
decimals = decimals,
|
||||
symbol = symbol,
|
||||
visualTransformation = AmountVisualTransformation(
|
||||
decimals = decimals,
|
||||
symbol = symbol,
|
||||
decimalFormat = rememberDecimalFormat(),
|
||||
),
|
||||
onValueChange = onValueChange,
|
||||
color = textColor,
|
||||
textStyle = TangemTheme.typography.body2,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,30 @@ object BigDecimalFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
fun formatCryptoAmountUncapped(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoSymbol: String,
|
||||
decimals: Int,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (cryptoAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatter = NumberFormat.getNumberInstance(locale).apply {
|
||||
maximumFractionDigits = decimals
|
||||
minimumFractionDigits = minOf(2, decimals)
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.DOWN
|
||||
}
|
||||
|
||||
return formatter.format(cryptoAmount).let {
|
||||
if (cryptoSymbol.isEmpty()) {
|
||||
it
|
||||
} else {
|
||||
it + "\u2009$cryptoSymbol"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun formatCryptoAmount(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue