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

如何实现可从XML文件加载配置的Android自定义KeyboardView

自定义KeyboardView XML配置加载实现方案

直接参照原生Keyboard.java的核心逻辑拆分实现即可,不需要通读全量源码,核心流程分3部分:

1. 明确XML结构与属性优先级

你放在res/xml目录下的键盘配置文件遵循三层节点结构,属性优先级从高到低为:Key节点属性 > Row节点属性 > Keyboard根节点全局属性

  • 根节点<Keyboard>:定义全局默认参数,包括默认按键宽度、默认按键高度、按键水平间距、按键垂直间距。注意原生规则里后缀为%p的尺寸值代表占键盘总宽度的百分比,解析时需要单独处理,同时兼容dp、px单位
  • 二级节点<Row>:代表键盘的一行按键,可单独配置当前行的按键宽度、高度、水平间距,未配置时自动继承根节点的全局默认值
  • 三级节点<Key>:代表单个按键,核心属性包括按键码android:codes、按键显示文本android:keyLabel,可扩展支持按键图标、长按连续触发、按键边距标记等自定义属性

原生支持的基础XML结构示例:

<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
    android:keyWidth="20%p"
    android:keyHeight="40dp"
    android:horizontalGap="5dp"
    android:verticalGap="5dp">
    <Row>
        <Key android:codes="0x0030" android:keyLabel="0" />
        <Key android:codes="0x0031" android:keyLabel="1" />
        <Key android:codes="0x0032" android:keyLabel="2" /> 
    </Row>
</Keyboard>

2. 实现XML配置解析逻辑

直接用系统自带的XmlResourceParser做解析即可,不需要引入第三方解析库,核心解析代码参考:

// 先定义三层结构对应的数据模型
data class KeyboardMeta(
    var totalWidth: Int = 0,
    var defaultKeyWidth: Int = 0,
    var defaultKeyHeight: Int = 0,
    var defaultHorizontalGap: Int = 0,
    var defaultVerticalGap: Int = 0,
    val rows: MutableList<KeyboardRow> = mutableListOf()
)
data class KeyboardRow(
    var keyWidth: Int = 0,
    var keyHeight: Int = 0,
    var horizontalGap: Int = 0,
    val keys: MutableList<KeyboardKey> = mutableListOf()
)
data class KeyboardKey(
    var code: Int = 0,
    var label: String = "",
    var x: Int = 0,
    var y: Int = 0,
    var width: Int = 0,
    var height: Int = 0
)

private const val ANDROID_NS = "http://schemas.android.com/apk/res/android"

fun loadKeyboardFromXml(context: Context, @XmlRes xmlResId: Int, keyboardViewWidth: Int): KeyboardMeta {
    val keyboardMeta = KeyboardMeta(totalWidth = keyboardViewWidth)
    val parser = context.resources.getXml(xmlResId)
    var currentRow: KeyboardRow? = null
    var currentYOffset = 0

    while (parser.next() != XmlPullParser.END_DOCUMENT) {
        when (parser.eventType) {
            XmlPullParser.START_TAG -> {
                when (parser.name) {
                    "Keyboard" -> {
                        // 解析全局默认属性,统一转换为px单位
                        keyboardMeta.defaultKeyWidth = parseSize(
                            parser.getAttributeValue(ANDROID_NS, "keyWidth"),
                            keyboardMeta.totalWidth
                        )
                        keyboardMeta.defaultKeyHeight = parseSize(
                            parser.getAttributeValue(ANDROID_NS, "keyHeight"),
                            keyboardMeta.totalWidth
                        )
                        keyboardMeta.defaultHorizontalGap = parseSize(
                            parser.getAttributeValue(ANDROID_NS, "horizontalGap"),
                            keyboardMeta.totalWidth
                        )
                        keyboardMeta.defaultVerticalGap = parseSize(
                            parser.getAttributeValue(ANDROID_NS, "verticalGap"),
                            keyboardMeta.totalWidth
                        )
                    }
                    "Row" -> {
                        currentRow = KeyboardRow()
                        // 行级属性未配置则继承全局默认
                        currentRow.keyWidth = parser.getAttributeValue(ANDROID_NS, "keyWidth")?.let {
                            parseSize(it, keyboardMeta.totalWidth)
                        } ?: keyboardMeta.defaultKeyWidth
                        currentRow.keyHeight = parser.getAttributeValue(ANDROID_NS, "keyHeight")?.let {
                            parseSize(it, keyboardMeta.totalWidth)
                        } ?: keyboardMeta.defaultKeyHeight
                        currentRow.horizontalGap = parser.getAttributeValue(ANDROID_NS, "horizontalGap")?.let {
                            parseSize(it, keyboardMeta.totalWidth)
                        } ?: keyboardMeta.defaultHorizontalGap
                    }
                    "Key" -> {
                        val key = KeyboardKey()
                        key.code = parser.getAttributeIntValue(ANDROID_NS, "codes", 0)
                        key.label = parser.getAttributeValue(ANDROID_NS, "keyLabel") ?: ""
                        key.width = currentRow?.keyWidth ?: keyboardMeta.defaultKeyWidth
                        key.height = currentRow?.keyHeight ?: keyboardMeta.defaultKeyHeight
                        currentRow?.keys?.add(key)
                    }
                }
            }
            XmlPullParser.END_TAG -> {
                if (parser.name == "Row" && currentRow != null) {
                    // 计算当前行所有按键的X坐标,自动拉伸最后一个按键补满整行宽度
                    layoutRowKeys(currentRow, currentYOffset, keyboardMeta.totalWidth)
                    keyboardMeta.rows.add(currentRow)
                    currentYOffset += currentRow.keyHeight + keyboardMeta.defaultVerticalGap
                    currentRow = null
                }
            }
        }
    }
    return keyboardMeta
}

其中parseSize方法自行实现尺寸转换逻辑即可:识别%p后缀做百分比计算、识别dp后缀做系统dp转px、纯数字默认按px处理。layoutRowKeys方法遍历当前行的按键,逐个累加X偏移量设置每个key的x坐标,最后一个按键的宽度设置为剩余行宽,避免出现右侧空隙。

3. 渲染与交互实现

解析完成拿到完整的KeyboardMeta数据后,在自定义KeyboardView中:

  • 重写onSizeChanged方法,拿到View实际宽度后调用上述loadKeyboardFromXml方法加载配置,避免尺寸计算错误
  • 重写onDraw方法,遍历所有行和按键,依次绘制按键背景、按键文本/图标,按压状态下单独绘制高亮效果
  • 重写onTouchEvent方法,根据触摸坐标匹配对应位置的Key,触发按键点击、长按回调

几个原生实现里的实用细节可以直接复用:

  • 不要给每个按键单独加View,全部在onDraw里用Canvas绘制,滑动输入、多按键连按的性能会好很多
  • 支持codes属性配置多个值,实现组合按键输入逻辑
  • 按键宽度支持0%p配置,代表自动占满行内剩余空间,方便做空格、回车这类宽按键

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 01:01:42