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

Kotlin EditText十六进制编辑器覆盖模式跳空格功能异常排查

Hex编辑器风格EditText的字符覆盖与空格跳过问题

需要实现Hex编辑器样式的EditText,核心需求:

  • 字符覆盖模式:输入字符时替换当前光标位置的字符,而非插入
  • 永久双空格:光标遇到连续两个空格时自动跳过,不允许修改这些空格;输入完成后自动跳转到下一个可编辑的数字位置

当前实现存在的bug:

  • 输入时偶尔出现插入字符的异常:比如原文本为01 02 0|3 04 05,输入5后,预期是替换3并跳转到下一个0,但实际会出现两种错误:
    1. 插入成01 02 05|3 04 05
    2. 替换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方法,通过固定文本位置实现覆盖模式,期望减少文本移位问题并降低资源占用

代码优化建议

  1. 修正位置跟踪逻辑:

    • 当前onTextChanged中mStart = start + count的计算有误,插入字符时start是插入位置,mStart应设为start而非start+count,因为插入后光标位置不代表实际修改的起始点
    • 移除不必要的insert操作,覆盖模式下应始终替换而非插入,仅在处理完空格跳转后手动调整光标位置
  2. 完善空格跳过逻辑:

    • 在afterTextChanged处理完字符替换后,自动定位到下一个可编辑位置:跳过所有连续空格,找到下一个数字的位置
    • 增加双空格的强校验,确保所有编辑操作仅针对数字字符,不修改双空格
  3. 确保状态变量正确重置:

    • 在afterTextChanged的格式化逻辑中用try-finally包裹,确保isFormatting = false一定会执行,避免因异常导致变量未重置
  4. 优化光标控制:

    • 在onSelectionChanged中补充光标位于双空格左侧的处理,确保光标始终定位在数字位置
    • 字符替换完成后,手动设置光标到下一个正确位置,不依赖系统自动定位

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 12:55:34