You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Compose中TextField货币掩码问题排查与优化方案咨询

TextField货币掩码无法完全删除字符的问题排查与优化方案

问题说明

需要为TextField实现格式为$ NNNNNNNNNNNNNNNNNNNNN的掩码(前缀$ ,后跟最多20位数字),但当前代码存在无法完全删除输入字符的问题,需排查代码问题并提供可靠实现。

原代码

掩码转换类

class CurrencyMaskTransformation : VisualTransformation {
    override fun filter(text: AnnotatedString): TransformedText = maskFilter(text)

    private fun maskFilter(text: AnnotatedString): TransformedText {
        // $ NNNNNNNNNNNNNNNNNNNNN
        var out = ""
        for (i in text.text.indices) {
            if (i == 0) out += "$ "
            out += text.text[i]
        }

        val numberOffsetTranslator = object : OffsetMapping {
            override fun originalToTransformed(offset: Int): Int {
                if (offset <= 0) return offset
                if (offset <= 2) return offset + 1
                return text.text.length + 2
            }

            override fun transformedToOriginal(offset: Int): Int {
                if (offset <= 0) return offset
                if (offset <= 2) return offset - 1
                return text.text.length
            }
        }

        return TransformedText(AnnotatedString(out), numberOffsetTranslator)
    }
}

TextField使用代码

val inputValue = remember { mutableStateOf(TextFieldValue("100")) }

TextField(
    modifier = Modifier
        .wrapContentWidth()
        .height(64.dp),
    value = inputValue.value,
    onValueChange = {
        inputValue.value = it
    },
    visualTransformation = CurrencyMaskTransformation()
)

问题排查

  1. 掩码拼接逻辑错误:原代码仅在输入第一个字符时添加$ 前缀,输入为空时无前缀显示;且未过滤非数字字符,易导致格式混乱。
  2. OffsetMapping逻辑严重错误:
    • originalToTransformed中,当offset > 2时直接返回text.text.length + 2,导致所有原偏移量大于2的位置都映射到掩码文本末尾,光标位置完全错位。
    • transformedToOriginal中,当offset > 2时直接返回text.text.length,删除操作时无法正确映射到原文本对应位置,导致无法逐字符删除,最终出现删不干净的情况。

优化实现方案

步骤1:修正掩码转换类

实现正确的掩码拼接(始终显示$ 前缀,仅保留数字字符,限制最大20位),并编写正确的OffsetMapping:

class CurrencyMaskTransformation : VisualTransformation {
    // 前缀"$ "的长度
    private val prefixLength = 2
    // 最大允许输入的数字位数
    private val maxDigitCount = 20

    override fun filter(text: AnnotatedString): TransformedText {
        // 过滤非数字字符并限制长度
        val cleanedText = text.text.filter { it.isDigit() }.take(maxDigitCount)
        // 拼接标准掩码格式
        val maskedText = "$ $cleanedText"

        val offsetMapping = object : OffsetMapping {
            override fun originalToTransformed(offset: Int): Int {
                // 原文本的每个字符都对应掩码文本前缀后的位置,偏移量加前缀长度
                return offset + prefixLength
            }

            override fun transformedToOriginal(offset: Int): Int {
                // 掩码前缀区域对应原文本起始位置,前缀后位置减去前缀长度得到原文本偏移量,同时限制不超过原文本长度
                return maxOf(0, offset - prefixLength).coerceAtMost(cleanedText.length)
            }
        }

        return TransformedText(AnnotatedString(maskedText), offsetMapping)
    }
}

步骤2:优化TextField输入处理

在onValueChange中提前过滤非数字字符,避免无效输入,同时修正光标位置:

val inputValue = remember { mutableStateOf(TextFieldValue("100")) }

TextField(
    modifier = Modifier
        .wrapContentWidth()
        .height(64.dp),
    value = inputValue.value,
    onValueChange = { newValue ->
        // 过滤非数字字符并限制最大长度
        val filteredText = newValue.text.filter { it.isDigit() }.take(20)
        // 修正光标位置,避免过滤后光标跳变
        val newSelection = newValue.selection.copy(
            start = filteredText.length.coerceAtMost(newValue.selection.start),
            end = filteredText.length.coerceAtMost(newValue.selection.end)
        )
        inputValue.value = TextFieldValue(filteredText, newSelection)
    },
    visualTransformation = CurrencyMaskTransformation(),
    // 设置数字键盘,提升输入体验
    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)

方案说明

  • 掩码逻辑:始终保留$ 前缀,自动过滤非数字输入,严格限制20位数字长度,完全符合需求格式。
  • OffsetMapping:精准映射原文本与掩码文本的光标位置,彻底解决删除异常问题。
  • 输入过滤:提前拦截无效输入,配合数字键盘确保输入有效性,同时修正光标位置避免用户体验断层。

内容的提问来源于stack exchange,提问作者Master Disaster

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 13:30:52