一、项目概述
在移动端应用中,软键盘不仅是用户与应用之间最重要的输入通道,也深刻影响着用户体验和输入效率。Android 平台原生自带若干输入法供选择,但往往缺少符合特定业务场景或品牌调性的键盘界面与交互。基于此,我们设计并实现一款完全自定义的 Android 软键盘,包括:
-
自定义布局:支持英文字母(QWERTY)、符号/数字、表情等多种页面;
-
按键预览:长按或点击时弹出放大预览效果;
-
弹出符号页:某些按键可长按或单击弹出更多符号;
-
主题支持:深色/浅色皮肤切换;
-
声音与震动反馈:按键音与震动开关;
-
可配置设置:通过设置界面调整键盘高度、键间距、反馈方式;
-
多语言子类型:支持中文拼音、英文模式等输入子类型;
-
输入事件处理:精准处理删除、回车、空格等功能键;
-
生命周期管理:在应用切换、屏幕旋转、后台回收后仍能正常恢复。
本项目将以 10 000 字以上的篇幅,系统地介绍自定义软键盘的原理、技术细节和完整实现,帮助您快速上手并进行深度扩展。
二、相关知识
2.1 Android 输入法框架(IMF)简介
Android 的输入法框架(Input Method Framework)由三部分组成:
-
输入法服务(Input Method Service)
-
扩展自
InputMethodService
,负责管理软键盘 UI、输入连接、设置页面等;
-
-
键盘视图(KeyboardView)
-
一个
View
类型组件,用于展示按键网格,并转发按键事件;
-
-
输入框(Editor)
-
如
EditText
、TextView
等,作为输入目标,通过InputConnection
接口与输入法交互。
-
系统通过 AndroidManifest.xml
中声明的 <service>
与 <meta-data>
将自定义输入法注册到系统列表,用户可在“设置→语言与输入法”中启用并切换。
2.2 InputMethodService
与软键盘生命周期
-
onCreateInputView()
-
当输入框获得焦点并显示软键盘时调用,用于创建并返回自定义键盘视图。
-
-
onStartInput(EditorInfo, boolean)
-
在即将开始编辑时触发,可读取输入类型(如数字、文本)并切换不同布局。
-
-
onFinishInput()
-
当输入完成或切换到其他输入框时调用,用于清理状态。
-
-
onKey(int primaryCode, int[] keyCodes)
-
核心按键回调,根据
primaryCode
执行删除、提交等操作。
-
此外,onInitializeInterface()
用于初始化一次性资源,onEvaluateInputViewShown()
可根据场景决定是否显示软键盘。
2.3 KeyboardView
与 Keyboard
类
-
Keyboard
-
通过解析 XML 文件构造,包含多个
Row
与Key
对象,每个Key
定义了按键码(codes)、标签(keyLabel)、预览资源、尺寸属性等;
-
-
KeyboardView
-
负责绘制按键网格,通过
setKeyboard(Keyboard)
绑定布局,通过setOnKeyboardActionListener()
监听用户触摸事件,并调用onDraw()
、onBufferDraw()
进行渲染。
-
KeyboardView
支持自定义属性,如按键背景(keyBackground
)、标签文字颜色/大小(keyTextColor
/labelTextSize
)、长按弹出布局(popupLayout
)等。
2.4 键盘布局 XML 结构
键盘布局文件位于 res/xml
目录,示例如下:
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
android:keyWidth="10%p" <!-- 按键宽度:父视图宽度的百分比 -->
android:keyHeight="60dp" <!-- 按键高度 -->
android:horizontalGap="0dp" <!-- 按键间水平间隙 -->
android:verticalGap="0dp"> <!-- 按键间垂直间隙 -->
<Row android:rowEdgeFlags="top">
<Key android:codes="113" android:keyLabel="q"/>
…
<Key android:codes="-5" android:keyLabel="Del" android:keyWidth="20%p"/>
</Row>
…
</Keyboard>
-
codes
:按键码数组,通常仅使用第一个;正值代表 Unicode 字符,负值代表功能键(如Keyboard.KEYCODE_DELETE
= -5)。 -
keyLabel
:按键上显示的文字。 -
popupKeyboard
:当长按该键时弹出的子布局,文件名通过popupLayout
属性指定。
2.5 InputConnection
接口
软键盘与输入框之间通过 InputConnection
交互,常用方法有:
-
commitText(CharSequence text, int newCursorPosition)
:插入文本并移动光标。 -
deleteSurroundingText(int beforeLength, int afterLength)
:删除当前光标前后字符。 -
sendKeyEvent(KeyEvent event)
:发送物理键事件,如回车、方向键等。 -
setComposingText(CharSequence text, int newCursorPosition)
:用于实现拼写校正和候选词。
2.6 自定义主题与皮肤
为了满足不同 UI 风格,可在 styles.xml
中定义软键盘主题:
<style name="KeyboardTheme.Light" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:background">#F5F5F5</item>
<item name="android:keyBackground">@drawable/key_bg_light</item>
<item name="android:keyTextColor">#212121</item>
</style>
<style name="KeyboardTheme.Dark" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:background">#303030</item>
<item name="android:keyBackground">@drawable/key_bg_dark</item>
<item name="android:keyTextColor">#EEEEEE</item>
</style>
在 InputMethodService
中可通过读取设置决定使用哪个主题,调用 setTheme()
或在布局中动态切换样式。
三、实现思路
3.1 工程结构与模块划分
-
manifest/AndroidManifest.xml
-
注册输入法服务、声明权限与元数据
-
-
res/xml/
-
method.xml
:输入法元信息(subtype 等) -
多个键盘布局:
qwerty_keyboard.xml
、symbol_keyboard.xml
、numeric_keyboard.xml
-
-
res/layout/
-
keyboard_view.xml
:KeyboardView
主布局 -
preview.xml
:按键点击时的预览弹出布局 -
popup_symbols.xml
:弹出符号页布局
-
-
drawable/
-
key_bg_normal.xml
、key_bg_pressed.xml
:按键状态背景
-
-
java/com/example/customkeyboard/
-
MyInputMethodService.java
:键盘服务核心逻辑 -
SettingsActivity.java
&KeyboardSettingsFragment.java
:设置界面,用于切换皮肤、调整高度、开启反馈
-
3.2 键盘布局设计
-
英文字母键盘(QWERTY)
-
三行字母行 + 功能键行(切换符号、空格、回车、删除)
-
-
符号/数字键盘
-
多行符号、数字排列
-
长按某些符号弹出更多子符号(通过
popupKeyboard
属性)
-
-
数字专用键盘(如仅数字输入场景)
-
纯数字+删除+确定
-
所有布局均使用百分比宽度(%p
)保证在不同屏幕尺寸上自适应。
3.3 软键盘服务实现
-
在
onCreateInputView()
时加载主布局,并根据当前输入类型(EditorInfo.inputType
)选择要显示的键盘页面; -
实现
KeyboardView.OnKeyboardActionListener
,将触摸事件映射到onKey()
; -
在
onStartInput()
根据EditorInfo
进行布局切换或候选词重置; -
在
onFinishInput()
时关闭候选窗口并清理状态。
3.4 按键事件处理
在 onKey(int primaryCode, int[] keyCodes)
中:
-
删除键(
Keyboard.KEYCODE_DELETE
,-5)-
调用
ic.deleteSurroundingText(1, 0)
删除一个字符
-
-
回车键(
Keyboard.KEYCODE_DONE
,-4)-
调用
ic.sendKeyEvent(new KeyEvent(... KEYCODE_ENTER))
-
-
切换符号/字母(自定义码,如 -10、-11)
-
切换
kv.setKeyboard(symbolKeyboard)
或qwertyKeyboard
-
-
标准字符
-
ic.commitText(String.valueOf((char) primaryCode), 1)
-
支持长按(onPress()
/ onRelease()
)时更改按键样式或播放声音/震动。
3.5 长按、弹幕、符号页切换
-
某些
Key
在 XML 中设置android:popupKeyboard="@xml/popup_symbols"
,KeyboardView
在长按时会自动弹出该布局; -
在
popup_symbols.xml
中定义更多符号行,用户滑动并松手时即可选择子符号; -
也可在
onLongPress()
回调中自定义逻辑,例如弹出一个对话框或自定义PopupWindow
。
3.6 设置界面与动态配置
-
使用
SettingsActivity
托管一个KeyboardSettingsFragment
(继承自PreferenceFragmentCompat
),提供开关项:-
键盘高度(slider)
-
按键声音(switch)
-
振动反馈(switch)
-
皮肤选择(list)
-
-
在用户修改设置后,将值保存到
SharedPreferences
;在MyInputMethodService
中onCreate()
时读取,并应用:-
kv.setKeyboardHeight()
(自定义方法) -
setTheme()
切换样式 -
kv.setPreviewEnabled()
控制预览
-
四、完整代码
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.customkeyboard">
<uses-permission android:name="android.permission.BIND_INPUT_METHOD"/>
<application
android:allowBackup="true"
android:label="自定义键盘"
android:theme="@style/KeyboardTheme.Light">
<service
android:name=".MyInputMethodService"
android:label="@string/keyboard_label"
android:permission="android.permission.BIND_INPUT_METHOD">
<intent-filter>
<action android:name="android.view.InputMethod"/>
</intent-filter>
<meta-data
android:name="android.view.im"
android:resource="@xml/method"/>
</service>
<activity
android:name=".SettingsActivity"
android:label="键盘设置"
android:exported="false"/>
</application>
</manifest>
<!-- res/xml/method.xml -->
<input-method xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="com.example.customkeyboard.SettingsActivity">
<subtype
android:label="@string/subtype_qwerty"
android:imeSubtypeLocale="en_US"
android:imeSubtypeMode="keyboard"/>
<subtype
android:label="@string/subtype_pinyin"
android:imeSubtypeLocale="zh_CN"
android:imeSubtypeMode="keyboard"/>
</input-method>
<!-- res/xml/qwerty_keyboard.xml -->
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
android:keyWidth="10%p" android:keyHeight="60dp"
android:horizontalGap="0dp" android:verticalGap="0dp">
<!-- 第一行 -->
<Row android:rowEdgeFlags="top">
<Key android:codes="113" android:keyLabel="q"/>
<Key android:codes="119" android:keyLabel="w"/>
<!-- …其他字母… -->
<Key android:codes="-5" android:keyLabel="Del"
android:keyWidth="15%p"/>
</Row>
<!-- 第二行 -->
<Row>
<Key android:codes="97" android:keyLabel="a"
android:horizontalGap="5%p"/>
<!-- … -->
<Key android:codes="-10" android:keyLabel="?123"
android:keyWidth="15%p"/>
</Row>
<!-- 第三行 -->
<Row>
<Key android:codes="-1" android:keyLabel="↑"/>
<!-- … -->
<Key android:codes="-4" android:keyLabel="Enter"
android:keyWidth="15%p"/>
</Row>
<!-- 第四行:功能键 -->
<Row android:rowEdgeFlags="bottom">
<Key android:codes="-11" android:keyLabel="中/英"
android:keyWidth="15%p"/>
<Key android:codes="32" android:keyLabel="Space"
android:keyWidth="50%p"/>
<Key android:codes="-5" android:keyLabel="Del"
android:keyWidth="15%p"/>
</Row>
</Keyboard>
<!-- res/xml/symbol_keyboard.xml -->
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
android:keyWidth="10%p" android:keyHeight="60dp">
<Row>
<Key android:codes="33" android:keyLabel="!"/>
<!-- …其他符号… -->
<Key android:codes="-12" android:keyLabel="ABC" android:keyWidth="15%p"/>
</Row>
<!-- 其他行 -->
</Keyboard>
<!-- res/layout/keyboard_view.xml -->
<android.inputmethodservice.KeyboardView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/keyboard_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:keyBackground="@drawable/key_bg_selector"
android:keyPreviewLayout="@layout/preview"
android:keyTextColor="@color/key_text"
android:labelTextSize="18sp"
android:popupKeyboard="@xml/popup_symbols"/>
<!-- res/layout/preview.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:background="@drawable/preview_bg"
android:padding="8dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/preview_text"
android:textSize="24sp"
android:textColor="@color/key_text"/>
</LinearLayout>
<!-- res/layout/popup_symbols.xml -->
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
android:keyWidth="10%p" android:keyHeight="60dp">
<Row>
<Key android:codes="64" android:keyLabel="@"/>
<!-- …更多符号… -->
</Row>
</Keyboard>
<!-- res/drawable/key_bg_selector.xml -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@color/key_pressed"/>
<item android:drawable="@color/key_normal"/>
</selector>
<!-- res/values/colors.xml -->
<resources>
<color name="key_normal">#FFFFFF</color>
<color name="key_pressed">#DDDDDD</color>
<color name="key_text">#000000</color>
</resources>
<!-- res/values/styles.xml -->
<resources>
<style name="KeyboardTheme.Light" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:background">#F5F5F5</item>
</style>
<style name="KeyboardTheme.Dark" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:background">#303030</item>
</style>
</resources>
<!-- src/com/example/customkeyboard/MyInputMethodService.java -->
package com.example.customkeyboard;
import android.inputmethodservice.InputMethodService;
import android.inputmethodservice.Keyboard;
import android.inputmethodservice.KeyboardView;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
public class MyInputMethodService extends InputMethodService
implements KeyboardView.OnKeyboardActionListener {
private KeyboardView kv;
private Keyboard qwertyKeyboard, symbolKeyboard;
private SharedPreferences prefs;
@Override
public void onCreate() {
super.onCreate();
prefs = PreferenceManager.getDefaultSharedPreferences(this);
// 根据设置切换主题
boolean isDark = prefs.getBoolean("pref_theme_dark", false);
setTheme(isDark ? R.style.KeyboardTheme_Dark : R.style.KeyboardTheme_Light);
}
@Override
public View onCreateInputView() {
kv = (KeyboardView) getLayoutInflater()
.inflate(R.layout.keyboard_view, null);
qwertyKeyboard = new Keyboard(this, R.xml.qwerty_keyboard);
symbolKeyboard = new Keyboard(this, R.xml.symbol_keyboard);
kv.setKeyboard(qwertyKeyboard);
kv.setOnKeyboardActionListener(this);
kv.setPreviewEnabled(prefs.getBoolean("pref_key_preview", true));
return kv;
}
@Override
public void onStartInput(EditorInfo attribute, boolean restarting) {
super.onStartInput(attribute, restarting);
// 根据输入类型切换数字键盘
int inputType = attribute.inputType & EditorInfo.TYPE_MASK_CLASS;
if (inputType == EditorInfo.TYPE_CLASS_NUMBER ||
inputType == EditorInfo.TYPE_CLASS_DATETIME) {
kv.setKeyboard(symbolKeyboard);
} else {
kv.setKeyboard(qwertyKeyboard);
}
}
@Override
public void onKey(int primaryCode, int[] keyCodes) {
InputConnection ic = getCurrentInputConnection();
if (ic == null) return;
switch (primaryCode) {
case Keyboard.KEYCODE_DELETE:
ic.deleteSurroundingText(1, 0);
break;
case Keyboard.KEYCODE_DONE:
ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN,
KeyEvent.KEYCODE_ENTER));
break;
case -10: // 切换符号
kv.setKeyboard(symbolKeyboard);
break;
case -12: // 返回字母
kv.setKeyboard(qwertyKeyboard);
break;
default:
char c = (char) primaryCode;
ic.commitText(String.valueOf(c), 1);
}
}
@Override public void onPress(int primaryCode) { }
@Override public void onRelease(int primaryCode) { }
@Override public void onText(CharSequence text) { }
@Override public void swipeLeft() { }
@Override public void swipeRight() { }
@Override public void swipeUp() { }
@Override public void swipeDown() { }
}
<!-- src/com/example/customkeyboard/SettingsActivity.java -->
package com.example.customkeyboard;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceFragmentCompat;
public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle b) {
super.onCreate(b);
getSupportFragmentManager().beginTransaction()
.replace(android.R.id.content, new KeyboardSettingsFragment())
.commit();
}
public static class KeyboardSettingsFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle b, String s) {
setPreferencesFromResource(R.xml.preferences, s);
}
}
}
<!-- res/xml/preferences.xml -->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreferenceCompat
android:key="pref_theme_dark"
android:title="深色主题"
android:defaultValue="false"/>
<SwitchPreferenceCompat
android:key="pref_key_preview"
android:title="按键预览"
android:defaultValue="true"/>
<ListPreference
android:key="pref_key_height"
android:title="键盘高度"
android:entries="@array/height_names"
android:entryValues="@array/height_values"
android:defaultValue="60"/>
</PreferenceScreen>
五、代码解读
5.1 AndroidManifest.xml
注册与权限
-
声明
<service>
,android:permission="BIND_INPUT_METHOD"
确保只有系统能绑定; -
<meta-data>
中的@xml/method
指定输入法在系统“语言与输入法”中显示的名称、子类型等。
5.2 res/xml/method.xml
输入法元信息
-
<subtype>
多个子类型支持多语言切换; -
imeSubtypeLocale
与imeSubtypeMode
决定切换列表展示。
5.3 主/符号键盘布局
-
qwerty_keyboard.xml
定义英文字母主键盘,并使用负值-10
、-12
自定义切页码; -
symbol_keyboard.xml
定义符号键盘,并使用码-12
切回主键盘。
5.4 keyboard_view.xml
键盘视图模板
-
key_bg_selector.xml
结合android:state_pressed
实现按压效果; -
popupKeyboard
属性自动处理长按弹出子布局。
5.5 MyInputMethodService
核心逻辑
-
onCreate()
读取SharedPreferences
切换主题; -
onCreateInputView()
加载KeyboardView
,并根据设置启用按键预览; -
onStartInput()
根据EditorInfo
中的inputType
切换符号或字母布局; -
onKey()
精确处理删除、回车、切页与普通字符输入;
5.6 设置界面动态配置
-
使用 AndroidX 的
PreferenceFragmentCompat
构建设置界面; -
修改后
SharedPreferences
值生效于下次onCreateInputView()
或需在onSharedPreferenceChanged()
中实时应用。
六、项目总结
本项目完整示例了如何在 Android 平台上实现一款功能丰富的自定义软键盘,涵盖了输入法框架、键盘视图、按键布局、按键事件处理、主题切换、符号弹出、设置界面等关键模块。核心收获包括:
-
Input Method Framework(IMF) 的使用与生命周期管理;
-
KeyboardView 与 Keyboard XML 布局的深度定制;
-
InputConnection 在不同场景下对文本的插入、删除与提交;
-
Settings Activity 与 PreferenceFragment 结合,实现键盘功能动态配置;
-
主题和皮肤 切换,为软键盘注入品牌风格;
-
扩展方向:可加入候选词栏、智能输入法算法(拼音、词频)、多语言混合输入、语音输入、手写板支持等,打造专业级输入法