Jetpack Compose动态掩码格式化手机号:OffsetMapping偏移恒为0问题排查
问题诊断与修复方案
核心问题
- VisualTransformation致命错误:你在
filter方法里返回的是原输入文本source,而非格式化后的formattedPhone!这等于让Compose放弃渲染掩码格式,直接显示原始输入,偏移映射完全失去作用,光标偏移自然全为0。 - OffsetMapping逻辑漏洞:
- 当前构建偏移列表的逻辑混乱,且未处理偏移超出列表长度的边界情况,极易引发越界问题;
- 动态切换掩码时,旧偏移映射未与新掩码对齐,导致光标位置错乱。
- 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
相关产品推荐
相关产品推荐

