Jetpack 在 TextField 元素上撰写货币格式

问题描述 投票:0回答:1

我正在尝试为货币的 TextField 元素提供某种格式。我不想在开头接受 $ 或 € 符号。我只想接受以下值范围。

MIN VALUE: X
MAX VALUE: XXX.XX

我想接受。用于将整数与小数分开。

例如最小一个整数和最大三个整数(0 — 999)

小数部分不接受最大两位数(0.99 — 0.1 — 1)

对于验证,我只想在它带有十进制数字时有一个前导零,但如果我输入一个整数,则应该删除前导零。例如:

无法接受这些值:

00.2
0123.30

如何在 Android 上使用 Jetpack Compose 执行此操作?

android format currency
1个回答
1
投票

首先,文本字段的可组合项是 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 = { }
            )
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.