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

Jetpack Compose动态掩码格式化手机号:OffsetMapping偏移恒为0问题排查

问题诊断与修复方案

核心问题

  1. VisualTransformation致命错误:你在filter方法里返回的是原输入文本source,而非格式化后的formattedPhone!这等于让Compose放弃渲染掩码格式,直接显示原始输入,偏移映射完全失去作用,光标偏移自然全为0。
  2. OffsetMapping逻辑漏洞:
    • 当前构建偏移列表的逻辑混乱,且未处理偏移超出列表长度的边界情况,极易引发越界问题;
    • 动态切换掩码时,旧偏移映射未与新掩码对齐,导致光标位置错乱。
  3. ViewModel状态更新不合理:异步请求返回后直接覆盖phone值,若用户在请求期间继续输入,会直接冲掉用户的新输入内容。

一步步修复

1. 修正VisualTransformation的文本显示逻辑

filter方法必须返回格式化后的文本,掩码才能正常生效:

override fun filter(text: AnnotatedString): TransformedText {
    val source = text.text
    val formattedPhone = source.formatByMask(mask = mask)

    return TransformedText(
        text = AnnotatedString(formattedPhone), // 替换为格式化后的文本
        offsetMapping = offsetFilter(originalText = source, formattedText = formattedPhone),
    )
}

2. 重写OffsetMapping逻辑

替换原有混乱的偏移映射逻辑,确保原文本与格式化文本的偏移正确对应:

private fun offsetFilter(originalText: String, formattedText: String): OffsetMapping {
    return object : OffsetMapping {
        override fun originalToTransformed(offset: Int): Int {
            // 构建原文本每个字符在格式化文本中的位置映射
            val transformedPositions = mutableListOf<Int>()
            var originalIdx = 0
            var formattedIdx = 0

            while (originalIdx < originalText.length && formattedIdx < formattedText.length) {
                if (!isFormattingCharacter(formattedText[formattedIdx])) {
                    transformedPositions.add(formattedIdx)
                    originalIdx++
                }
                formattedIdx++
            }

            // 处理光标在开头、末尾等边界情况
            return when (offset) {
                0 -> 0
                originalText.length -> formattedText.length
                else -> transformedPositions.getOrElse(offset - 1) { formattedText.length } + 1
            }
        }

        override fun transformedToOriginal(offset: Int): Int {
            // 统计格式化文本偏移前的非格式字符数量,对应原文本偏移
            return formattedText.take(offset).count { !isFormattingCharacter(it) }
        }
    }
}

3. 优化ViewModel状态更新逻辑

先同步更新用户输入状态,再异步请求掩码,避免界面无反馈,同时防止覆盖用户新输入:

fun onValueChange(newValue: String) {
    // 先同步更新输入状态,用户输入立刻有反馈
    _state.update { it.copy(phone = newValue) }
    
    viewModelScope.launch {
        val phoneDetails = getPhoneDetailsByInput(newValue)
        _state.update { currentState ->
            // 仅当当前输入未被用户修改时,才更新掩码
            if (currentState.phone == newValue) {
                currentState.copy(mask = phoneDetails.mask)
            } else {
                currentState
            }
        }
    }
}

4. 处理掩码加载延迟的过渡

后端返回掩码存在延迟,给初始状态设置默认掩码兜底,避免光标异常:

InputTextField(
    modifier = modifier,
    input = state.phone,
    visualTransformation = PhoneVisualTransformation(
        mask = state.mask.takeIf { it.isNotEmpty() } ?: "###-###-####" // 默认掩码兜底
    ),
    onValueChange = {
        viewModel.onValueChange(it)
    },
)

测试注意点

  • 输入/删除字符时,光标是否跟随正常?
  • 动态切换掩码(如国内号转国际号)时,光标位置是否平滑过渡?
  • 请求掩码期间继续输入,内容是否会被意外覆盖?

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 05:55:34