
每种平台都有自己的输入法框架. GNU/Linux 桌面环境有多种输入法框架, 比如 ibus, fcitx 等. 但是 Android 操作系统只有一种, 是统一提供的输入法框架.
相关链接:
+ 《ibus 源代码阅读 (1)》 https://www.bilibili.com/read/cv31187008/
+ <https://developer.android.google.cn/develop/ui/views/touch-and-input/creating-input-method>
+ 1 Android 输入法框架
+ 2 实现一个简单的 Android 输入法
+ 3 测试
+ 4 总结与展望
+ 附录 1 相关代码

这个图看起来和 ibus 输入法框架差不多, 都有具体的输入法 (engine), 接受输入的应用, 以及系统服务 (输入法框架).
在 Android 系统中, 输入法, 以及接受输入的应用, 都以应用 (apk) 的形式存在. 可以很方便的安装新的输入法, 就和安装普通的应用一样.
要想详细的了解 Android 系统的输入法接口, 最好的方法还是自己做一个输入法.
+ (1) 打开 Android Studio, 随意创建一个新的空白应用.
+ (2) 编写一个新的类, 继承 InputMethodService <https://developer.android.google.cn/reference/android/inputmethodservice/InputMethodService>
比如创建文件 app/src/main/java/io/github/fm_elpac/pmim_apk/im/PmimService.kt (有省略):
package io.github.fm_elpac.pmim_apk.im
import android.inputmethodservice.InputMethodService
import android.webkit.WebView
import android.webkit.JavascriptInterface
class PmimService : InputMethodService() {
// 生命周期函数
override fun onCreate() {
super.onCreate()
// 用于调试 (服务生命周期), 下同
println(&quot;PmimService.onCreate()&quot;)
}
override fun onCreateInputView(): View {
println(&quot;PmimService.onCreateInputView()&quot;)
// 创建 WebView
var w = WebView(this)
w.getSettings().setJavaScriptEnabled(true)
class 接口 {
@JavascriptInterface
fun commit(t: String) {
im_commitText(t)
}
}
w.addJavascriptInterface(接口(), &quot;pmim&quot;)
w.loadUrl(&quot;file:///android_asset/ui/index.html&quot;)
return setH(w)
}
// 预留接口: 输入文本
fun im_commitText(text: String) {
currentInputConnection.commitText(text, 1)
}
fun sendKeyEvent(event: KeyEvent) {
currentInputConnection.sendKeyEvent(event)
} 这个类就相当于自己实现的一个输入法了.
其中重要函数 onCreateInputView() 创建软键盘, 就是显示在屏幕底部的触摸输入区域.
+ (3) 添加输入法相关信息.
文件 app/src/main/res/xml/im.xml:
&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;input-method
xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
android:settingsActivity=&quot;io.github.fm_elpac.pmim_apk.MainActivity&quot;
android:icon=&quot;@mipmap/ic_launcher&quot;&gt;
&lt;subtype
android:label=&quot;@string/im_label&quot;
android:name=&quot;@string/im_name&quot;
android:imeSubtypeLocale=&quot;zh_CN&quot;
android:imeSubtypeMode=&quot;keyboard&quot;
/&gt;
&lt;subtype
android:label=&quot;@string/im_label_en&quot;
android:name=&quot;@string/im_name&quot;
android:imeSubtypeLocale=&quot;en_US&quot;
android:imeSubtypeMode=&quot;keyboard&quot;
/&gt;
&lt;/input-method&gt; 文件 app/src/main/res/values/strings.xml:
&lt;resources&gt;
&lt;string name=&quot;app_name&quot;&gt;胖喵拼音&lt;/string&gt;
&lt;string name=&quot;im_name&quot;&gt;胖喵拼音&lt;/string&gt;
&lt;string name=&quot;im_label&quot;&gt;中文 (中国)&lt;/string&gt;
&lt;string name=&quot;im_label_en&quot;&gt;Chinese (zh_CN)&lt;/string&gt;
&lt;/resources&gt; im.xml 里面是输入法的信息, 操作系统 (设置输入法) 需要使用.
+ (4) 清单文件 app/src/main/AndroidManifest.xml (有省略):
&lt;!-- Android 输入法服务 --&gt;
&lt;service
android:name=&quot;.im.PmimService&quot;
android:exported=&quot;true&quot;
android:label=&quot;@string/im_name&quot;
android:permission=&quot;android.permission.BIND_INPUT_METHOD&quot;&gt;
&lt;intent-filter&gt;
&lt;action android:name=&quot;android.view.InputMethod&quot; /&gt;
&lt;/intent-filter&gt;
&lt;!-- 必须有此元数据, 输入法才能在系统设置中出现 --&gt;
&lt;meta-data android:name=&quot;android.view.im&quot; android:resource=&quot;@xml/im&quot; /&gt;
&lt;/service&gt; 前面编写的类 PmimService 是传说中的 Android 四大组件 之一 (服务), 所以必须在清单文件中声明.
+ (5) 最后实现用户界面 (底部的软键盘).
文件 app/src/main/assets/ui/index.html:
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;meta charset=&quot;utf-8&quot; /&gt;
&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
&lt;title&gt;测试输入法键盘&lt;/title&gt;
&lt;style&gt;
body {
background-color: #FFF3E0;
}
img {
width: 150px;
height: 150px;
}
.b {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
border-top: solid 8px #FF9800;
display: flex;
align-items: center;
justify-content: space-around;
}
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class=&quot;b&quot;&gt;
&lt;img id=&quot;m&quot; src=&quot;./m.jpg&quot; /&gt;
&lt;img id=&quot;q&quot; src=&quot;./q.png&quot; /&gt;
&lt;/div&gt;
&lt;script&gt;
function 输入(t) {
console.log(t);
pmim.commit(t);
}
function 初始化() {
const m = document.getElementById(&quot;m&quot;);
const q = document.getElementById(&quot;q&quot;);
m.addEventListener(&quot;click&quot;, () =&gt; 输入(&quot;喵&quot;));
q.addEventListener(&quot;click&quot;, () =&gt; 输入(&quot;穷&quot;));
}
初始化();
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt; 又到了喜闻乐见的测试环节.
+ (1) 编译 apk (相关重要文件的完整代码请见 附录 1):
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./gradlew assembleDebug 编译生成的 apk 文件位于: app/build/outputs/apk/debug/app-debug.apk
+ (2) 安装 apk.
使用 USB 数据线连接手机和 PC, 然后:
&gt; adb devices
List of devices attached
268bca3e device
&gt; adb install app-debug.apk
Performing Streamed Install
Success + (3) 在手机的系统设置里, 启用新的输入法:

+ (4) 找一个能输入的地方, 切换输入法:

+ (5) 然后就可以愉快的输入啦 ~

嗯, 点击这俩图标分别可以输入一个汉字.
我们来分析一下, 点击图标的时候发生了什么.
+ (1) 用户界面 (网页) js 代码调用 pmim.commit()
+ (2) 其中 pmim 是 kotlin 代码调用 addJavascriptInterface() 添加的接口.
+ (3) 最后 kotlin 代码调用了 Android 输入法框架的接口 currentInputConnection.commitText(), 最终实现了文字的输入 (撒花 ~~)
各个平台的输入法框架的整体工作原理都差不多, 输入法框架在中间做管理, 一边是输入法, 一边是接受输入的应用.
和 ibus 相比, Android 输入法框架使用起来要简单容易很多, Android 官方文档写的也很清楚, 好评 !
今天实现了输入俩字, 距离实现完整的输入法还会远嘛 ?
彩蛋: 本文使用刚开发的 ibus 输入法编写. 编写本文的过程中顺便又修复了一个 BUG (输入 嗯).

+ app/src/main/java/io/github/fm_elpac/pmim_apk/im/PmimService.kt
package io.github.fm_elpac.pmim_apk.im
import android.inputmethodservice.InputMethodService
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.LinearLayout
import android.webkit.WebView
import android.webkit.JavascriptInterface
import android.view.ViewGroup.LayoutParams
// Android 输入法服务, 仅关注面向 Android 系统的接口部分
class PmimService : InputMethodService() {
// 生命周期函数
override fun onCreate() {
super.onCreate()
// 用于调试 (服务生命周期), 下同
println(&quot;PmimService.onCreate()&quot;)
}
// 设置软键盘高度
private fun setH(view: View): View {
val h = 350f;
// dp -&gt; px
val d = resources.displayMetrics.density
val px = h * d + 0.5f
println(&quot; dp = &quot; + h + &quot; d = &quot; + d + &quot; px = &quot; + px)
view.setLayoutParams(LayoutParams(-1, px.toInt()))
val l = LinearLayout(this)
l.addView(view)
return l
}
override fun onCreateInputView(): View {
println(&quot;PmimService.onCreateInputView()&quot;)
// 创建 WebView
var w = WebView(this)
w.getSettings().setJavaScriptEnabled(true);
class 接口 {
@JavascriptInterface
fun commit(t: String) {
im_commitText(t)
}
}
w.addJavascriptInterface(接口(), &quot;pmim&quot;)
w.loadUrl(&quot;file:///android_asset/ui/index.html&quot;)
return setH(w)
}
override fun onBindInput() {
super.onBindInput()
println(&quot;PmimService.onBindInput()&quot;)
}
override fun onUnbindInput() {
super.onUnbindInput()
println(&quot;PmimService.onUnbindInput()&quot;)
}
// 软键盘显示
override fun onStartInputView(info: EditorInfo, restarting: Boolean) {
println(&quot;PmimService.onStartInputView()&quot;)
}
// 软键盘隐藏
override fun onFinishInput() {
println(&quot;PmimService.onFinishInput()&quot;)
}
override fun onDestroy() {
super.onDestroy()
println(&quot;PmimService.onDestroy()&quot;)
}
// 预留接口: 关闭软键盘
fun im_hideKb() {
// run on ui thread
hideWindow()
}
// 预留接口: 输入文本
fun im_commitText(text: String) {
currentInputConnection.commitText(text, 1)
}
fun sendKeyEvent(event: KeyEvent) {
currentInputConnection.sendKeyEvent(event)
}
// 预留接口: 发送编辑器默认动作 (比如: 搜索)
fun im_sendDefaultEditorAction(fromEnterKey: Boolean) {
sendDefaultEditorAction(fromEnterKey)
}
// 预留接口: 发送字符
fun im_sendKeyChar(code: Char) {
sendKeyChar(code)
}
// 预留接口: 获取选择的文本 (复制)
fun im_getSelectedText(): String? {
return currentInputConnection.getSelectedText(0)?.toString()
}
// 预留接口: 设置选择的文本 (比如: 全选)
fun im_setSelection(start: Int, end: Int) {
currentInputConnection.setSelection(start, end)
}
} + app/src/main/res/xml/im.xml: 正文中已贴出完整代码.
+ app/src/main/res/values/strings.xml: 正文中已贴出完整代码.
+ app/src/main/AndroidManifest.xml
&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;manifest
xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
xmlns:tools=&quot;http://schemas.android.com/tools&quot;&gt;
&lt;uses-permission android:name=&quot;android.permission.READ_EXTERNAL_STORAGE&quot; /&gt;
&lt;uses-permission android:name=&quot;android.permission.WRITE_EXTERNAL_STORAGE&quot; /&gt;
&lt;uses-permission android:name=&quot;android.permission.VIBRATE&quot; /&gt;
&lt;uses-permission android:name=&quot;android.permission.FOREGROUND_SERVICE&quot; /&gt;
&lt;uses-permission android:name=&quot;android.permission.INTERNET&quot; /&gt;
&lt;application
android:allowBackup=&quot;true&quot;
android:dataExtractionRules=&quot;@xml/data_extraction_rules&quot;
android:fullBackupContent=&quot;@xml/backup_rules&quot;
android:icon=&quot;@mipmap/ic_launcher&quot;
android:label=&quot;@string/app_name&quot;
android:roundIcon=&quot;@mipmap/ic_launcher_round&quot;
android:supportsRtl=&quot;true&quot;
android:theme=&quot;@style/Theme.MyApp&quot;
tools:targetApi=&quot;31&quot;&gt;
&lt;activity
android:name=&quot;.MainActivity&quot;
android:exported=&quot;true&quot;
android:label=&quot;@string/app_name&quot;
android:theme=&quot;@style/Theme.MyApp&quot;&gt;
&lt;intent-filter&gt;
&lt;action android:name=&quot;android.intent.action.MAIN&quot; /&gt;
&lt;category android:name=&quot;android.intent.category.LAUNCHER&quot; /&gt;
&lt;/intent-filter&gt;
&lt;/activity&gt;
&lt;!-- Android 输入法服务 --&gt;
&lt;service
android:name=&quot;.im.PmimService&quot;
android:exported=&quot;true&quot;
android:label=&quot;@string/im_name&quot;
android:permission=&quot;android.permission.BIND_INPUT_METHOD&quot;&gt;
&lt;intent-filter&gt;
&lt;action android:name=&quot;android.view.InputMethod&quot; /&gt;
&lt;/intent-filter&gt;
&lt;!-- 必须有此元数据, 输入法才能在系统设置中出现 --&gt;
&lt;meta-data android:name=&quot;android.view.im&quot; android:resource=&quot;@xml/im&quot; /&gt;
&lt;/service&gt;
&lt;/application&gt;
&lt;/manifest&gt; + app/src/main/assets/ui/index.html: 正文中已贴出完整代码.
本文使用 CC-BY-SA 4.0 许可发布.