如何实现可从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
相关产品推荐
相关产品推荐

