Updated on 2026-08-14
This commit is contained in:
parent
0450ce9906
commit
b14caba3cf
9 changed files with 677 additions and 43 deletions
|
|
@ -0,0 +1,154 @@
|
|||
package com.tangem.core.ui.components.fields
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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
|
||||
import androidx.compose.ui.Alignment.Companion.TopStart
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.*
|
||||
import java.text.DecimalFormat
|
||||
|
||||
@Composable
|
||||
fun AmountTextField(
|
||||
value: String,
|
||||
decimals: Int,
|
||||
onValueChange: (String) -> Unit,
|
||||
textStyle: TextStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
symbol: String? = null,
|
||||
color: Color = TangemTheme.colors.text.primary1,
|
||||
placeholderAlignment: Alignment = TopStart,
|
||||
showPlaceholder: Boolean = true,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
) {
|
||||
val decimalFormat = rememberDecimalFormat()
|
||||
|
||||
val placeholderTextAlign = if (placeholderAlignment == TopCenter) {
|
||||
TextAlign.Center
|
||||
} else {
|
||||
TextAlign.Start
|
||||
}
|
||||
SimpleTextField(
|
||||
value = value,
|
||||
onValueChange = { newText ->
|
||||
if (decimalFormat.isValidSymbols(newText)) {
|
||||
val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals)
|
||||
onValueChange(trimmed)
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.action),
|
||||
textStyle = textStyle,
|
||||
color = color,
|
||||
keyboardOptions = keyboardOptions,
|
||||
singleLine = true,
|
||||
visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat),
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (value.isBlank() && showPlaceholder) {
|
||||
val placeholder = if (symbol != null) {
|
||||
decimalFormat.defaultFormat().plus(" $symbol")
|
||||
} else {
|
||||
decimalFormat.defaultFormat()
|
||||
}
|
||||
Text(
|
||||
text = placeholder,
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
textAlign = placeholderTextAlign,
|
||||
modifier = Modifier
|
||||
.align(placeholderAlignment),
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun DecimalFormat.isValidSymbols(text: String): Boolean {
|
||||
return checkDecimalSeparatorDuplicate(text) && checkGroupingSeparator(text)
|
||||
}
|
||||
|
||||
// region preview
|
||||
@Preview(locale = "en", showBackground = true, name = "English")
|
||||
@Preview(locale = "ru", showBackground = true, name = "Russian")
|
||||
@Composable
|
||||
private fun AmountTextFieldPreview(
|
||||
@PreviewParameter(AmountTextFieldPreviewProvider::class) amount: AmountTextFieldPreviewData,
|
||||
) {
|
||||
var text by remember { mutableStateOf(amount.value.orEmpty()) }
|
||||
TangemTheme {
|
||||
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,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
placeholderAlignment = TopStart,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private data class AmountTextFieldPreviewData(
|
||||
val symbol: String? = "$",
|
||||
val value: String? = null,
|
||||
val decimals: Int = 2,
|
||||
val showPlaceholder: Boolean,
|
||||
val placeholderAlignment: Alignment,
|
||||
)
|
||||
// endregion
|
||||
|
|
@ -3,14 +3,18 @@ package com.tangem.core.ui.components.fields
|
|||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||
import androidx.compose.foundation.text.selection.TextSelectionColors
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
|
|
@ -19,6 +23,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
/**
|
||||
* Simple text field with placeholder
|
||||
*/
|
||||
@Suppress("ReusedModifierInstance")
|
||||
@Composable
|
||||
fun SimpleTextField(
|
||||
value: String,
|
||||
|
|
@ -29,32 +34,72 @@ fun SimpleTextField(
|
|||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
color: Color = TangemTheme.colors.text.primary1,
|
||||
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
|
||||
readOnly: Boolean = false,
|
||||
decorationBox: (@Composable (innerTextField: @Composable () -> Unit) -> Unit)? = null,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
textStyle = TangemTheme.typography.body2.copy(color = color),
|
||||
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
|
||||
singleLine = singleLine,
|
||||
readOnly = readOnly,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
decorationBox = { textValue ->
|
||||
Box {
|
||||
if (value.isBlank() && placeholder != null) {
|
||||
Text(
|
||||
text = placeholder.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
textValue()
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
.focusRequester(focusRequester),
|
||||
var textFieldValueState by remember {
|
||||
mutableStateOf(
|
||||
TextFieldValue(
|
||||
text = value,
|
||||
selection = when {
|
||||
value.isEmpty() -> TextRange.Zero
|
||||
else -> TextRange(value.length, value.length)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
val focusRequester = remember { FocusRequester.Default }
|
||||
val customTextSelectionColors = TextSelectionColors(
|
||||
handleColor = TangemTheme.colors.text.secondary,
|
||||
backgroundColor = TangemTheme.colors.text.secondary.copy(alpha = 0.4f),
|
||||
)
|
||||
|
||||
val textFieldValue = textFieldValueState.copy(text = value)
|
||||
|
||||
SideEffect {
|
||||
if (textFieldValue.selection != textFieldValueState.selection ||
|
||||
textFieldValue.composition != textFieldValueState.composition
|
||||
) {
|
||||
textFieldValueState = textFieldValue
|
||||
}
|
||||
}
|
||||
|
||||
var lastTextValue by remember(value) { mutableStateOf(value) }
|
||||
|
||||
CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) {
|
||||
BasicTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = { newTextFieldValueState ->
|
||||
textFieldValueState = newTextFieldValueState
|
||||
|
||||
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
|
||||
lastTextValue = newTextFieldValueState.text
|
||||
|
||||
if (stringChangedSinceLastInvocation) {
|
||||
onValueChange(newTextFieldValueState.text)
|
||||
}
|
||||
},
|
||||
textStyle = textStyle.copy(color = color),
|
||||
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
|
||||
singleLine = singleLine,
|
||||
readOnly = readOnly,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
decorationBox = decorationBox ?: { textValue ->
|
||||
Box {
|
||||
if (value.isBlank() && placeholder != null) {
|
||||
Text(
|
||||
text = placeholder.resolveReference(),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
)
|
||||
}
|
||||
textValue()
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,28 +5,49 @@ 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.formatWithThousands
|
||||
import java.text.DecimalFormat
|
||||
|
||||
class AmountVisualTransformation(
|
||||
private val symbol: String,
|
||||
private val decimals: Int,
|
||||
private val symbol: String? = null,
|
||||
private val decimalFormat: DecimalFormat = DecimalFormat(),
|
||||
) : VisualTransformation {
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
return TransformedText(
|
||||
buildAnnotatedString {
|
||||
append(text)
|
||||
if (text.isNotBlank()) {
|
||||
append(" ")
|
||||
append(symbol)
|
||||
}
|
||||
},
|
||||
object : OffsetMapping {
|
||||
override fun originalToTransformed(offset: Int): Int {
|
||||
return text.length
|
||||
}
|
||||
|
||||
override fun transformedToOriginal(offset: Int): Int {
|
||||
return text.length
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
val formattedText = decimalFormat.formatWithThousands(
|
||||
text.text,
|
||||
decimals,
|
||||
)
|
||||
val groupingSymbol = decimalFormat.decimalFormatSymbols.groupingSeparator
|
||||
return TransformedText(
|
||||
text = buildAnnotatedString {
|
||||
append(formattedText)
|
||||
if (formattedText.isNotEmpty() && symbol != null) {
|
||||
append(" $symbol")
|
||||
}
|
||||
},
|
||||
offsetMapping = OffsetMappingImpl(text.text, formattedText, groupingSymbol),
|
||||
)
|
||||
}
|
||||
|
||||
private class OffsetMappingImpl(
|
||||
private val text: String,
|
||||
private val formattedText: String,
|
||||
private val gropingSymbol: Char,
|
||||
) : OffsetMapping {
|
||||
override fun originalToTransformed(offset: Int): Int {
|
||||
var noneDigitCount = 0
|
||||
var i = 0
|
||||
while (i < offset + noneDigitCount) {
|
||||
if (formattedText.getOrNull(i++) == gropingSymbol) noneDigitCount++
|
||||
}
|
||||
return (offset + noneDigitCount).coerceIn(0, formattedText.length)
|
||||
}
|
||||
|
||||
override fun transformedToOriginal(offset: Int): Int {
|
||||
val noneDigitCount = formattedText.take(offset).count { it == gropingSymbol }
|
||||
return (offset - noneDigitCount).coerceIn(0, text.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package com.tangem.core.ui.components.inputrow
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
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.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Input row for entering amount. Manages correct amount format and validation
|
||||
*
|
||||
* @param title title reference
|
||||
* @param text primary text reference
|
||||
* @param onValueChange text change callback
|
||||
* @param modifier modifier
|
||||
* @param titleColor title color
|
||||
* @param textColor text color
|
||||
* @param keyboardOptions keyboard options for field
|
||||
* @param iconRes action icon
|
||||
* @param iconTint action icon tint
|
||||
* @param onIconClick click on action icon
|
||||
* @param showDivider show divider
|
||||
*
|
||||
* @see [InputRowDefault] for read only version
|
||||
* @see <a href=https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode=design&
|
||||
* t=IQ5lBJEkFGU4WSvi-4>InputRowEnter</a>
|
||||
*/
|
||||
@Composable
|
||||
fun InputRowEnterAmount(
|
||||
title: TextReference,
|
||||
text: String,
|
||||
decimals: Int,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
symbol: String? = null,
|
||||
titleColor: Color = TangemTheme.colors.text.secondary,
|
||||
textColor: Color = TangemTheme.colors.text.primary1,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
iconRes: Int? = null,
|
||||
iconTint: Color = TangemTheme.colors.icon.informative,
|
||||
onIconClick: (() -> Unit)? = null,
|
||||
showDivider: Boolean = false,
|
||||
) {
|
||||
DividerContainer(
|
||||
modifier = modifier,
|
||||
showDivider = showDivider,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = titleColor,
|
||||
)
|
||||
AmountTextField(
|
||||
value = text,
|
||||
decimals = decimals,
|
||||
symbol = symbol,
|
||||
onValueChange = onValueChange,
|
||||
color = textColor,
|
||||
textStyle = TangemTheme.typography.body2,
|
||||
keyboardOptions = keyboardOptions,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8),
|
||||
)
|
||||
}
|
||||
iconRes?.let {
|
||||
Icon(
|
||||
painter = painterResource(id = iconRes),
|
||||
contentDescription = null,
|
||||
tint = iconTint,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing10,
|
||||
bottom = TangemTheme.dimens.spacing10,
|
||||
)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
) { onIconClick?.invoke() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package com.tangem.core.ui.components.inputrow
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* `Input Row Enter Info` for entering amount. Manages correct amount format and validation
|
||||
|
||||
* @param title title reference
|
||||
* @param text primary text reference
|
||||
* @param onValueChange text change callback
|
||||
* @param modifier modifier
|
||||
* @param titleColor title color
|
||||
* @param textColor text color
|
||||
* @param isSingleLine text
|
||||
* @param visualTransformation applied transformation to text
|
||||
* @param keyboardOptions keyboard options for field
|
||||
* @param showDivider show divider
|
||||
*
|
||||
* @see [InputRowEnterInfo]
|
||||
* @see <a href=https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode
|
||||
* =design&t=IQ5lBJEkFGU4WSvi-4>Input Row Enter</a>
|
||||
* @see <a href=https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node
|
||||
* * -id=7854-33577&mode=design&t=6o23sqF8fDQdn4C5-4>Input Row Enter Info</a>
|
||||
*/
|
||||
@Composable
|
||||
fun InputRowEnterInfoAmount(
|
||||
title: TextReference,
|
||||
text: String,
|
||||
decimals: Int,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
symbol: String? = null,
|
||||
info: TextReference? = null,
|
||||
titleColor: Color = TangemTheme.colors.text.secondary,
|
||||
textColor: Color = TangemTheme.colors.text.primary1,
|
||||
infoColor: Color = TangemTheme.colors.text.tertiary,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
showDivider: Boolean = false,
|
||||
) {
|
||||
DividerContainer(
|
||||
modifier = modifier,
|
||||
showDivider = showDivider,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = titleColor,
|
||||
)
|
||||
Row {
|
||||
AmountTextField(
|
||||
value = text,
|
||||
decimals = decimals,
|
||||
symbol = symbol,
|
||||
onValueChange = onValueChange,
|
||||
color = textColor,
|
||||
textStyle = TangemTheme.typography.body2,
|
||||
keyboardOptions = keyboardOptions,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8)
|
||||
.weight(1f),
|
||||
)
|
||||
info?.let {
|
||||
Text(
|
||||
text = it.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = infoColor,
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing8)
|
||||
.align(Alignment.Bottom),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.Locale
|
||||
|
||||
private const val TEXT_CHUNK_THOUSAND = 3
|
||||
private const val POINT_SEPARATOR = '.'
|
||||
|
||||
@Composable
|
||||
fun rememberDecimalFormat(): DecimalFormat {
|
||||
val locale = LocalConfiguration.current.locale
|
||||
val decimalSymbols = remember { DecimalFormatSymbols.getInstance(locale) }
|
||||
|
||||
return remember {
|
||||
DecimalFormat().apply {
|
||||
decimalFormatSymbols = decimalSymbols
|
||||
isParseBigDecimal = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats input [String] for InputField, to remove wrong symbols, letters etc
|
||||
* Use [decimals] for cut this number symbols after floating point
|
||||
*
|
||||
* Example (with 8 decimals):
|
||||
* input string - ab123.46377372ab53
|
||||
* result string 123.46377372
|
||||
*/
|
||||
fun DecimalFormat.getValidatedNumberWithFixedDecimals(text: String, decimals: Int): String {
|
||||
val thousandsSeparator = decimalFormatSymbols.groupingSeparator
|
||||
val decimalSeparator = decimalFormatSymbols.decimalSeparator
|
||||
|
||||
val lastChar = text.lastOrNull()
|
||||
val trimmedText = if (text.isNotEmpty() && (lastChar == thousandsSeparator || lastChar == POINT_SEPARATOR)) {
|
||||
text.dropLast(1) + decimalSeparator
|
||||
} else {
|
||||
text
|
||||
}
|
||||
|
||||
if (trimmedText.startsWith("0") && trimmedText.length > 1 && trimmedText[1] != decimalSeparator) {
|
||||
return "0"
|
||||
}
|
||||
|
||||
val filteredChars = trimmedText.replace(thousandsSeparator.toString(), "").filterIndexed { index, c ->
|
||||
val isOneOrZeroPoint =
|
||||
c == decimalSeparator && index != 0 && trimmedText.count { it == decimalSeparator } <= 1
|
||||
val isIndexPointIndex =
|
||||
c == decimalSeparator && index != 0 && trimmedText.indexOf(decimalSeparator) == index
|
||||
c.isDigit() || isIndexPointIndex || isOneOrZeroPoint
|
||||
}
|
||||
// If dot is present, take first digits before decimal and first decimals digits after decimal
|
||||
return if (filteredChars.count { it == decimalSeparator } == 1) {
|
||||
val beforeDecimal = filteredChars.substringBefore(decimalSeparator)
|
||||
val afterDecimal = filteredChars.substringAfter(decimalSeparator)
|
||||
beforeDecimal + decimalSeparator + afterDecimal.take(decimals)
|
||||
}
|
||||
// If there is no dot, just take all digits
|
||||
else {
|
||||
filteredChars
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats input [text] with grouping and decimal separators.
|
||||
* Takes into account [decimals] number of digits after floating point.
|
||||
*/
|
||||
fun DecimalFormat.formatWithThousands(text: String, decimals: Int): String {
|
||||
val thousandsSeparator = decimalFormatSymbols.groupingSeparator
|
||||
val decimalSeparator = decimalFormatSymbols.decimalSeparator
|
||||
val localizedText = text.replace("[,.]".toRegex(), decimalSeparator.toString())
|
||||
return if (localizedText.count { it == decimalSeparator } == 1) {
|
||||
val beforeDecimal = localizedText.substringBefore(decimalSeparator)
|
||||
.reversed()
|
||||
.chunked(TEXT_CHUNK_THOUSAND)
|
||||
.joinToString(thousandsSeparator.toString())
|
||||
.reversed()
|
||||
val afterDecimal = localizedText.substringAfter(decimalSeparator)
|
||||
beforeDecimal + decimalSeparator + afterDecimal.take(decimals)
|
||||
}
|
||||
// If there is no dot, just take all digits
|
||||
else {
|
||||
localizedText.reversed()
|
||||
.chunked(TEXT_CHUNK_THOUSAND)
|
||||
.joinToString(thousandsSeparator.toString())
|
||||
.reversed()
|
||||
}
|
||||
}
|
||||
|
||||
fun DecimalFormat.defaultFormat(): String {
|
||||
return "0${decimalFormatSymbols.decimalSeparator}00"
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if text input contains extra decimal separators.
|
||||
* If so, it will return false, otherwise true.
|
||||
*
|
||||
* Note: number can contain only one decimal separator.
|
||||
*/
|
||||
fun DecimalFormat.checkDecimalSeparatorDuplicate(text: String): Boolean {
|
||||
val regex = "[${decimalFormatSymbols.decimalSeparator}]".toRegex()
|
||||
val decimalSeparatorCount = regex.findAll(text).count()
|
||||
return decimalSeparatorCount <= 1 // only one decimal separator
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if text input contains grouping separators.
|
||||
* If so, it will return false, otherwise true.
|
||||
*
|
||||
* Note: grouping separators are used only for VisualTransformations.
|
||||
*/
|
||||
fun DecimalFormat.checkGroupingSeparator(text: String): Boolean {
|
||||
val regex = "[${decimalFormatSymbols.groupingSeparator}]".toRegex()
|
||||
val decimalSeparatorCount = regex.findAll(text).count()
|
||||
return decimalSeparatorCount == 0 // no grouping separator
|
||||
}
|
||||
|
||||
fun String.parseToBigDecimal(decimals: Int): BigDecimal {
|
||||
val decimalFormat = DecimalFormat().apply {
|
||||
decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
|
||||
isParseBigDecimal = true
|
||||
maximumFractionDigits = decimals
|
||||
minimumFractionDigits = decimals
|
||||
}
|
||||
return try {
|
||||
decimalFormat.parse(this) as? BigDecimal ?: BigDecimal.ZERO
|
||||
} catch (e: Exception) {
|
||||
BigDecimal.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
fun BigDecimal.parseBigDecimal(decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN): String {
|
||||
val decimalFormat = DecimalFormat().apply {
|
||||
decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
|
||||
isParseBigDecimal = true
|
||||
isGroupingUsed = false
|
||||
maximumFractionDigits = decimals
|
||||
minimumFractionDigits = 0
|
||||
this.roundingMode = roundingMode
|
||||
}
|
||||
|
||||
return try {
|
||||
decimalFormat.format(this)
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.core.ui.utils
|
|||
|
||||
import java.text.DecimalFormat
|
||||
|
||||
@Deprecated("Deprecated due to unnecessary abstraction. Use methods from DecimalFormatterExt")
|
||||
class InputNumberFormatter(
|
||||
numberFormat: DecimalFormat,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.domain.tokens.model
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class Amount(
|
||||
val currencySymbol: String,
|
||||
val value: BigDecimal? = null,
|
||||
val decimals: Int,
|
||||
val type: AmountType = AmountType.CoinType,
|
||||
)
|
||||
|
||||
sealed class AmountType {
|
||||
object CoinType : AmountType()
|
||||
object ReserveType : AmountType()
|
||||
data class TokenType(val token: CryptoCurrency.Token) : AmountType()
|
||||
data class FiatType(val code: String) : AmountType()
|
||||
}
|
||||
|
||||
/** Converts `BigDecimal` [cryptoCurrency] to [Amount] */
|
||||
fun BigDecimal.convertToAmount(cryptoCurrency: CryptoCurrency) = Amount(
|
||||
currencySymbol = cryptoCurrency.symbol,
|
||||
value = this,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
type = when (cryptoCurrency) {
|
||||
is CryptoCurrency.Coin -> AmountType.CoinType
|
||||
is CryptoCurrency.Token -> AmountType.TokenType(
|
||||
token = cryptoCurrency,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.send.impl.presentation.state.amount
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.isNullOrZero
|
||||
|
||||
internal class SendAmountCurrencyConverter(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<Boolean, SendUiState> {
|
||||
override fun convert(value: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val amountState = state.amountState ?: return state
|
||||
val amountTextField = amountState.amountTextField
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
|
||||
val isValidFiatRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero()
|
||||
return if (amountTextField.isFiatValue == value && !isValidFiatRate) {
|
||||
state
|
||||
} else {
|
||||
return state.copy(
|
||||
amountState = amountState.copy(
|
||||
amountTextField = amountTextField.copy(
|
||||
isFiatValue = value,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue