我正在尝试为货币的 TextField 元素提供某种格式。我不想在开头接受 $ 或 € 符号。我只想接受以下值范围。
MIN VALUE: X
MAX VALUE: XXX.XX
我想接受。用于将整数与小数分开。
例如最小一个整数和最大三个整数(0 — 999)
小数部分不接受最大两位数(0.99 — 0.1 — 1)
对于验证,我只想在它带有十进制数字时有一个前导零,但如果我输入一个整数,则应该删除前导零。例如:
无法接受这些值:
00.2
0123.30
如何在 Android 上使用 Jetpack Compose 执行此操作?
首先,文本字段的可组合项是 androidx.compose.material.TextField。 鉴于仅允许使用数字,请选择数字键盘类型:
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
),
您可以在 onValueChange 函数闭包中限制错误输入:
var textState by remember { mutableStateOf("") }
.....
onValueChange = { newText: String ->
if (newText.isDigitsOnly() &&
conditionMinValue(newText) &&
conditionMaxValue(newText) && etc...) {
textState = newText
}
},
尽管如此,不建议在可视部分的 onValueChanged 回调中更改文本,因为文本状态与进程 IME(软件键盘)共享,然后您必须使用键盘处理错误。
视觉部分需要实现自己的视觉转换类
visualTransformation = NumberCommaTransformation(),
您可以阅读:https://blog.shreyaspatil.dev/filtering-and-modifying-text-input-in-jetpack-compose-way
再见。
更新
我删除了第一个更新并留下了最后一个代码
更新第二次
我的代码是:
package com.intecanar.candida.ui.newbrand.product.components
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import java.text.DecimalFormat
import kotlin.math.max
fun priceValueToStringFloat(price: Float): String {
//from float to "floatString"
return price.toString()
.replace(".", "")
.trimStart('0')
}
fun myAmountInputToFloat(amount: String): Float {
val price = 0.0f
if (amount.isEmpty()) {
return price
}
val symbols = DecimalFormat().decimalFormatSymbols
val decimalSeparator = symbols.decimalSeparator
val integerPart: String = if (amount.length > 2) {
amount.subSequence(0, amount.length - 2).toString()
} else {
"0"
}
var fractionPart: String = if (amount.length >= 2) {
amount.subSequence(amount.length - 2, amount.length).toString()
} else {
amount
}
// Add zeros if the fraction part length is not 2
if (fractionPart.length < 2) {
fractionPart = fractionPart.padStart(2, '0')
}
return (
integerPart + decimalSeparator + fractionPart
).toFloat()
}
class NumberCommaTransformation(val showCurrency: Boolean = false) : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
val (integerPart, newAnnotatedText) = formatText(text, showCurrency)
val offsetMapping = ThousandSeparatorOffsetMapping(
originalIntegerLength = integerPart.length,
showCurrency = showCurrency,
originalTextLength = text.length
)
return TransformedText(
text = newAnnotatedText,
offsetMapping = offsetMapping
)
}
private fun formatText(
text: AnnotatedString,
showCurrency: Boolean
): Pair<CharSequence, AnnotatedString> {
val symbols = DecimalFormat().decimalFormatSymbols
val thousandsSeparator = symbols.groupingSeparator
val decimalSeparator = symbols.decimalSeparator
val currencySymbol = symbols.currencySymbol
val originalText: String = text.text
val integerPart = if (originalText.length > 2) {
originalText.subSequence(0, originalText.length - 2)
} else {
"0"
}
var fractionPart = if (originalText.length >= 2) {
originalText.subSequence(originalText.length - 2, originalText.length)
} else {
originalText
}
// Add zeros if the fraction part length is not 2
if (fractionPart.length < 2) {
fractionPart = fractionPart.padStart(2, '0')
}
val thousandsReplacementPattern = Regex("\\B(?=(?:\\d{3})+(?!\\d))")
val formattedIntWithThousandsSeparator =
integerPart.replace(
thousandsReplacementPattern,
thousandsSeparator.toString()
)
val formattedText = if (showCurrency) {
currencySymbol + " " +
formattedIntWithThousandsSeparator + decimalSeparator + fractionPart
} else {
formattedIntWithThousandsSeparator + decimalSeparator + fractionPart
}
val newAnnotatedText = AnnotatedString(
formattedText,
text.spanStyles,
text.paragraphStyles
)
return Pair(integerPart, newAnnotatedText)
}
}
class ThousandSeparatorOffsetMapping(
val originalIntegerLength: Int, val showCurrency: Boolean, val originalTextLength: Int
) : OffsetMapping {
private val currencyOffset: Int = if (showCurrency) 2 else 0 //currencySymbol + " "
private val defaultValueOffset: Int = 4 // "0.00"
private val decimalSeparatorOffset: Int = 1 // "."
//counting the extra characters shows after transformation
/**
* It is important to understand that we prefer for our cursor to remain stationary at
* the end of the sum. So, if the input is empty, or we just inserted two digits,
* our output string will always have minimum 4 characters (“0.00”),
* that’s why we fixed originalToTransformed offset 0, 1, 2 -> 4.
* */
//https://medium.com/google-developer-experts/
// hands-on-jetpack-compose-visualtransformation-to-create-a-phone-number-formatter-99b0347fc4f6
//https://medium.com/@banmarkovic/
// how-to-create-currency-amount-input-in-android-jetpack-compose-1bd11ba3b629
//https://developer.android.com/reference/kotlin/
// androidx/compose/ui/text/input/VisualTransformation
override fun originalToTransformed(offset: Int): Int =
when (offset) {
0, 1, 2 -> currencyOffset + defaultValueOffset
else -> currencyOffset + offset + decimalSeparatorOffset + calculateThousandsSeparatorCount(
originalIntegerLength
)
}
/**
* it must return a value between 0 and the original length
* or it will return an exception
* //es el offset para que siempre este al final
* */
override fun transformedToOriginal(offset: Int): Int {
var calculated = originalIntegerLength +
calculateThousandsSeparatorCount(originalIntegerLength) + 2
if (originalTextLength < calculated) {
calculated = originalTextLength
}
if (calculated < 0) {
calculated = 0
}
return calculated
}
private fun calculateThousandsSeparatorCount(
intDigitCount: Int
) = max((intDigitCount - 1) / 3, 0)
}
输入:
package com.intecanar.candida.ui.newbrand.product.components
import android.content.res.Configuration
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.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
import androidx.core.text.isDigitsOnly
import com.intecanar.candida.R
import com.intecanar.candida.ui.theme.CandidaTheme
import com.intecanar.candida.ui.theme.Price
@Composable
fun MyAmountInput(
modifier: Modifier = Modifier,
initialPrice: Float = 0.0f,
priceState: MutableState<String> = rememberSaveable {
mutableStateOf( priceValueToStringFloat(initialPrice) )
},
caption: String, maxInput: Int,
captionFontSize: TextUnit = 20.sp,
innerTextFontSize: TextUnit = 25.sp,
maxLengthFontSize: TextUnit = 15.sp,
notifyNewValue: (Float) -> Unit,
) {
Column(modifier = modifier) {
val colors = MaterialTheme.colors
val numberCommaTransformation = remember {
NumberCommaTransformation(showCurrency = true)
}
val visualTransformation by remember(priceState.value) {
mutableStateOf(
if (priceState.value.isBlank()) {
VisualTransformation.None
} else {
numberCommaTransformation
}
)
}
val primaryColor = colors.primary
val alphaColor = 0.12f
val primaryOpaque = colors.primary.copy(alpha = alphaColor)
Text(
text = caption,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 4.dp),
textAlign = TextAlign.Start,
color = primaryColor,
fontSize = captionFontSize,
fontWeight = FontWeight.W600,
)
TextField(
modifier = Modifier.fillMaxWidth().testTag("TextField $caption"),
value = priceState.value,
colors = TextFieldDefaults.textFieldColors(
backgroundColor = primaryOpaque,
cursorColor = primaryColor,
disabledLabelColor = primaryOpaque,
//hide the indicator
focusedIndicatorColor = Color.Transparent,
//bottom line color
unfocusedIndicatorColor = Color.Transparent
),
onValueChange = { newPrice: String ->
if (newPrice.length <= maxInput) {
if (newPrice.isDigitsOnly()) {
if (newPrice.startsWith("0")) {
priceState.value = ""
} else {
priceState.value = newPrice
}
notifyNewValue(
myAmountInputToFloat(priceState.value)
)
} else {
//Log.i("AMOUNT", "2BBBBB")
}
}
},
shape = RoundedCornerShape(8.dp),
singleLine = true,
trailingIcon = {
if (priceState.value.isNotEmpty()) {
IconButton(onClick = {
priceState.value = ""
notifyNewValue(0.0f)
}, modifier = Modifier.testTag("X IconButton $caption")) {
Icon(
imageVector = Icons.Outlined.Close,
contentDescription = stringResource(id = R.string.cd_close_text)
)
}
}
},
textStyle = TextStyle(
fontFamily = Price,
fontSize = innerTextFontSize,
fontWeight = FontWeight.W400,
color = primaryColor
),
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.NumberPassword,
),
//Only transform the visual part do not affect the textState
visualTransformation = visualTransformation,
)
Text(
text = "${priceState.value.length} / $maxInput",
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp).testTag("Remaining Text $caption"),
textAlign = TextAlign.End,
fontWeight = FontWeight.W600,
color = primaryColor,
fontFamily = Price,
fontSize = maxLengthFontSize,
)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun MyAmountInputPreview() {
CandidaTheme {
Column(
Modifier
.background(color = MaterialTheme.colors.background) //OK
) {
MyAmountInput(
caption = "Amount",
initialPrice = 45484.13f, maxInput = 110,
notifyNewValue = { }
)
}
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun MyAmountInputEmptyPreview() {
CandidaTheme {
Column(
Modifier
.background(color = MaterialTheme.colors.background) //OK
) {
MyAmountInput(
caption = "Amount",
initialPrice = 0.0f, maxInput = 110,
notifyNewValue = { }
)
}
}
}