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 }
二、修正交互位置匹配
光改选择方法不够,还得处理点击事件的坐标映射,让点击位置对应自定义绘制的文本组:
- 重写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) }
- 禁用原生文本绘制
因为已经完全自定义了绘制逻辑,关掉原生的文本绘制避免重叠:
init { // ... 原有初始化代码 setWillNotDraw(false) }
- 处理多行文本(可选)
如果文本可能换行,得在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
相关产品推荐
相关产品推荐

