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

Android EditText:重写onDraw后对齐文本绘制与实际交互位置

解决EditText自定义绘制后交互位置不匹配及递归问题

核心问题

自定义EditText的onDraw方法把文本按两位一组重新布局绘制后,点击、光标选择的交互区域还是对应原生文本的布局,和自定义绘制位置不匹配;而且重写getSelectionStart/getSelectionEnd时,直接调用selectionStart会触发无限递归。

解决步骤

一、修复无限递归问题

重写选择方法时,不能直接用selectionStart(这会调用你重写后的方法,陷入递归),得用super.getSelectionStart()获取系统原始的选择位置:

override fun getSelectionStart(): Int {
    val originalStart = super.getSelectionStart()
    val layout = layout ?: return originalStart
    val line = layout.getLineForOffset(originalStart)
    val x = layout.getPrimaryHorizontal(originalStart)
    return (x / (characterWidth + characterWidth / 2)).toInt() * 2
}

override fun getSelectionEnd(): Int {
    val originalEnd = super.getSelectionEnd()
    val layout = layout ?: return originalEnd
    val line = layout.getLineForOffset(originalEnd)
    val x = layout.getPrimaryHorizontal(originalEnd)
    return (x / (characterWidth + characterWidth / 2)).toInt() * 2
}

二、修正交互位置匹配

光改选择方法不够,还得处理点击事件的坐标映射,让点击位置对应自定义绘制的文本组:

  1. 重写onTouchEvent处理点击
    把屏幕点击坐标转换成自定义布局下的文本偏移量,直接设置光标位置:
override fun onTouchEvent(event: MotionEvent): Boolean {
    if (event.action == MotionEvent.ACTION_UP) {
        val x = event.x
        // 计算点击位置对应的两位组索引
        val groupIndex = (x / (characterWidth + characterWidth / 2)).toInt()
        val targetOffset = groupIndex * 2
        // 确保偏移量合法,不会超出文本长度
        val clampedOffset = targetOffset.coerceIn(0, text.length)
        setSelection(clampedOffset)
        return true
    }
    return super.onTouchEvent(event)
}
  1. 禁用原生文本绘制
    因为已经完全自定义了绘制逻辑,关掉原生的文本绘制避免重叠:
init {
    // ... 原有初始化代码
    setWillNotDraw(false)
}
  1. 处理多行文本(可选)
    如果文本可能换行,得在onDraw里处理y坐标递增,同时触摸事件里计算对应行:
override fun onDraw(canvas: Canvas?) {
    val text = text.toString()
    var x = 0f
    var y = characterHeight // 初始y设为字体高度,避免顶部截断
    val maxWidth = width.toFloat()
    for (i in 0 until text.length step 2) {
        val endIndex = min(i + 2, text.length)
        val pair = text.substring(i, endIndex)
        // 超出当前行宽度就换行
        if (x + characterWidth > maxWidth) {
            x = 0f
            y += characterHeight + characterHeight / 2 // 加上行间距
        }
        canvas?.drawText(pair, x, y, paint)
        x += characterWidth + characterWidth / 2
    }
}

三、完整修正代码

class HexEditorEditText(context: Context, attrs: AttributeSet?) : EditText(context, attrs) {
    private val characterWidth: Float
    private val characterHeight: Float

    init {
        val textPaint = paint
        characterWidth = textPaint.measureText("00")
        characterHeight = textPaint.fontSpacing
        // 禁用原生文本绘制,避免重叠
        setWillNotDraw(false)
    }

    override fun onDraw(canvas: Canvas?) {
        val text = text.toString()
        var x = 0f
        var y = characterHeight // 初始y坐标,避免文本顶部被截断
        val maxWidth = width.toFloat()
        for (i in 0 until text.length step 2) {
            // 处理文本长度不足两位的情况
            val endIndex = min(i + 2, text.length)
            val pair = text.substring(i, endIndex)
            // 换行判断
            if (x + characterWidth > maxWidth) {
                x = 0f
                y += characterHeight + characterHeight / 2
            }
            canvas?.drawText(pair, x, y, paint)
            x += characterWidth + characterWidth / 2
        }
    }

    override fun getSelectionStart(): Int {
        // 用super获取原始值,避免递归
        val originalStart = super.getSelectionStart()
        val layout = layout ?: return originalStart
        val line = layout.getLineForOffset(originalStart)
        val x = layout.getPrimaryHorizontal(originalStart)
        return (x / (characterWidth + characterWidth / 2)).toInt() * 2
    }

    override fun getSelectionEnd(): Int {
        val originalEnd = super.getSelectionEnd()
        val layout = layout ?: return originalEnd
        val line = layout.getLineForOffset(originalEnd)
        val x = layout.getPrimaryHorizontal(originalEnd)
        return (x / (characterWidth + characterWidth / 2)).toInt() * 2
    }

    override fun setSelection(start: Int, stop: Int) {
        // 确保选择位置是偶数位,符合两位一组的逻辑
        val adjustedStart = if (start % 2 != 0) start - 1 else start
        val adjustedStop = if (stop % 2 != 0) stop - 1 else stop
        super.setSelection(adjustedStart.coerceAtLeast(0), adjustedStop.coerceAtMost(text.length))
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (event.action == MotionEvent.ACTION_UP) {
            val x = event.x
            val y = event.y
            // 计算点击的行
            val lineIndex = (y / (characterHeight + characterHeight / 2)).toInt()
            // 计算该行能容纳的组数量
            val groupsPerLine = (width.toFloat() / (characterWidth + characterWidth / 2)).toInt()
            // 该行起始偏移量
            val lineStartOffset = lineIndex * groupsPerLine * 2
            // 该行内的组索引
            val groupIndex = (x / (characterWidth + characterWidth / 2)).toInt()
            val targetOffset = lineStartOffset + groupIndex * 2
            // 确保偏移量在合法范围内
            val clampedOffset = targetOffset.coerceIn(0, text.length)
            setSelection(clampedOffset)
            return true
        }
        return super.onTouchEvent(event)
    }
}

内容的提问来源于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 16:46:14