Kotlin EditText十六进制编辑器覆盖模式跳空格功能异常排查
Hex编辑器风格EditText的字符覆盖与空格跳过问题
需要实现Hex编辑器样式的EditText,核心需求:
- 字符覆盖模式:输入字符时替换当前光标位置的字符,而非插入
- 永久双空格:光标遇到连续两个空格时自动跳过,不允许修改这些空格;输入完成后自动跳转到下一个可编辑的数字位置
当前实现存在的bug:
- 输入时偶尔出现插入字符的异常:比如原文本为
01 02 0|3 04 05,输入5后,预期是替换3并跳转到下一个0,但实际会出现两种错误:- 插入成
01 02 05|3 04 05 - 替换
3后删除一个空格,变成01 02 05 |04 05
- 插入成
- 光标靠近空格时,仍会出现替换空格而非跳过双空格的问题
- 移动光标后能短暂正常工作,但很快又出现插入字符的情况,推测是
TextWatcher的formatting变量未正确重置或存在逻辑遗漏
当前实现代码
class CustomEditText : AppCompatEditText { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) private var isFormatting = false private var mStart = -1 // Start position of the Text being modified private var mEnd = -1 // End position of the Text being modified private val watcher = object : TextWatcher { override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { mStart = -1 // Keep track of the start and end position of the text change if (before == 0) { // The start and end variables have not been set yet, and there are no characters being deleted. // Set the start position to the current cursor position. mStart = start + count // Set the end position to either the end of the string or the current cursor position + count. mEnd = min(mStart + count, s!!.length) } } override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { // No action needed before text change } override fun afterTextChanged(s: Editable?) { // Avoid infinite recursion if the TextWatcher is modifying the text if (!isFormatting) { if (mStart >= 0) { // Set `isFormatting` to true to indicate that we're currently formatting the text. isFormatting = true // Check if the current position is a digit if (Character.isDigit(s!![mStart - 1])) { // Check if the next position is a space or the end of the string if (mStart < s.length && s[mStart] == ' ' || mStart == s.length) { // If the next position is a space or the end of the string, insert the digit at the next position s.insert(mStart, s[mStart - 1].toString()) mStart++ } else { // Overwrite the text at the current position s.replace(mStart, mEnd, "") } } else if (s[mStart - 1] == ' ') { // Check if the next position is a digit if (mStart + 1 < s.length && Character.isDigit(s[mStart + 1])) { // Jump over the spaces and overwrite the first character in the next set mStart = s.indexOf(" ", mStart) + 2 s.replace(mStart, mStart + 1, "") } else { // Overwrite the text at the current position s.replace(mStart, mEnd, "") } } else { // Overwrite the text at the current position s.replace(mStart, mEnd, "") } isFormatting = false } } } } init { // Initiate and add the text change listener "watcher" addTextChangedListener(watcher) } override fun onSelectionChanged(selStart: Int, selEnd: Int) { // Get the current text in the EditText val text = text if (text != null) { val len = text.length // If the selection start and end are equal, meaning the cursor is at a certain position if (selStart == selEnd) { // Check if the cursor is positioned at a space between two hexadecimal digits // And if the character after the space is also a space if (selStart != 0 && selStart < len && text[selStart - 1] == ' ' && text[selStart + 1] == ' ') { // Move the cursor one position to the right to position it at the start of the next hexadecimal digit setSelection(selStart + 1) return } // Check if the cursor is positioned at a space and the character after the space is not a space if (selStart < len && text[selStart] == ' ' && (selStart + 1 >= len || text[selStart + 1] != ' ')) { // Move the cursor one position to the right to position it at the start of the next hexadecimal digit setSelection(selStart + 1) } } } // Call the superclass implementation of onSelectionChanged super.onSelectionChanged(selStart, selEnd) } }
其他尝试与后续方向
- 曾尝试用自定义Drawable实现井字格布局,让数字固定在格子中,但无论哪种方案都需要处理退格和箭头键移动逻辑,暂时搁置
- 考虑到Drawable方案可能更耗资源,优先尝试修复当前TextWatcher方案,但目前bug频发,已开始尝试重写EditText的
onDraw方法,通过固定文本位置实现覆盖模式,期望减少文本移位问题并降低资源占用
代码优化建议
修正位置跟踪逻辑:
- 当前
onTextChanged中mStart = start + count的计算有误,插入字符时start是插入位置,mStart应设为start而非start+count,因为插入后光标位置不代表实际修改的起始点 - 移除不必要的
insert操作,覆盖模式下应始终替换而非插入,仅在处理完空格跳转后手动调整光标位置
- 当前
完善空格跳过逻辑:
- 在
afterTextChanged处理完字符替换后,自动定位到下一个可编辑位置:跳过所有连续空格,找到下一个数字的位置 - 增加双空格的强校验,确保所有编辑操作仅针对数字字符,不修改双空格
- 在
确保状态变量正确重置:
- 在
afterTextChanged的格式化逻辑中用try-finally包裹,确保isFormatting = false一定会执行,避免因异常导致变量未重置
- 在
优化光标控制:
- 在
onSelectionChanged中补充光标位于双空格左侧的处理,确保光标始终定位在数字位置 - 字符替换完成后,手动设置光标到下一个正确位置,不依赖系统自动定位
- 在
内容的提问来源于stack exchange,提问作者WBarber
相关产品推荐
相关产品推荐

