Android实现自定义键盘(附带源码)

一、项目概述

在移动端应用中,软键盘不仅是用户与应用之间最重要的输入通道,也深刻影响着用户体验和输入效率。Android 平台原生自带若干输入法供选择,但往往缺少符合特定业务场景或品牌调性的键盘界面与交互。基于此,我们设计并实现一款完全自定义的 Android 软键盘,包括:

  • 自定义布局:支持英文字母(QWERTY)、符号/数字、表情等多种页面;

  • 按键预览:长按或点击时弹出放大预览效果;

  • 弹出符号页:某些按键可长按或单击弹出更多符号;

  • 主题支持:深色/浅色皮肤切换;

  • 声音与震动反馈:按键音与震动开关;

  • 可配置设置:通过设置界面调整键盘高度、键间距、反馈方式;

  • 多语言子类型:支持中文拼音、英文模式等输入子类型;

  • 输入事件处理:精准处理删除、回车、空格等功能键;

  • 生命周期管理:在应用切换、屏幕旋转、后台回收后仍能正常恢复。

本项目将以 10 000 字以上的篇幅,系统地介绍自定义软键盘的原理、技术细节和完整实现,帮助您快速上手并进行深度扩展。


二、相关知识

2.1 Android 输入法框架(IMF)简介

Android 的输入法框架(Input Method Framework)由三部分组成:

  1. 输入法服务(Input Method Service)

    • 扩展自 InputMethodService,负责管理软键盘 UI、输入连接、设置页面等;

  2. 键盘视图(KeyboardView)

    • 一个 View 类型组件,用于展示按键网格,并转发按键事件;

  3. 输入框(Editor)

    • EditTextTextView 等,作为输入目标,通过 InputConnection 接口与输入法交互。

系统通过 AndroidManifest.xml 中声明的 <service><meta-data> 将自定义输入法注册到系统列表,用户可在“设置→语言与输入法”中启用并切换。

2.2 InputMethodService 与软键盘生命周期

  • onCreateInputView()

    • 当输入框获得焦点并显示软键盘时调用,用于创建并返回自定义键盘视图。

  • onStartInput(EditorInfo, boolean)

    • 在即将开始编辑时触发,可读取输入类型(如数字、文本)并切换不同布局。

  • onFinishInput()

    • 当输入完成或切换到其他输入框时调用,用于清理状态。

  • onKey(int primaryCode, int[] keyCodes)

    • 核心按键回调,根据 primaryCode 执行删除、提交等操作。

此外,onInitializeInterface() 用于初始化一次性资源,onEvaluateInputViewShown() 可根据场景决定是否显示软键盘。

2.3 KeyboardViewKeyboard

  • Keyboard

    • 通过解析 XML 文件构造,包含多个 RowKey 对象,每个 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.xmlsymbol_keyboard.xmlnumeric_keyboard.xml

  • res/layout/

    • keyboard_view.xmlKeyboardView 主布局

    • preview.xml:按键点击时的预览弹出布局

    • popup_symbols.xml:弹出符号页布局

  • drawable/

    • key_bg_normal.xmlkey_bg_pressed.xml:按键状态背景

  • java/com/example/customkeyboard/

    • MyInputMethodService.java:键盘服务核心逻辑

    • SettingsActivity.java & KeyboardSettingsFragment.java:设置界面,用于切换皮肤、调整高度、开启反馈

3.2 键盘布局设计

  1. 英文字母键盘(QWERTY)

    • 三行字母行 + 功能键行(切换符号、空格、回车、删除)

  2. 符号/数字键盘

    • 多行符号、数字排列

    • 长按某些符号弹出更多子符号(通过 popupKeyboard 属性)

  3. 数字专用键盘(如仅数字输入场景)

    • 纯数字+删除+确定

所有布局均使用百分比宽度(%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;在 MyInputMethodServiceonCreate() 时读取,并应用:

    • 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> 多个子类型支持多语言切换;

  • imeSubtypeLocaleimeSubtypeMode 决定切换列表展示。

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 平台上实现一款功能丰富的自定义软键盘,涵盖了输入法框架、键盘视图、按键布局、按键事件处理、主题切换、符号弹出、设置界面等关键模块。核心收获包括:

  1. Input Method Framework(IMF) 的使用与生命周期管理;

  2. KeyboardViewKeyboard XML 布局的深度定制;

  3. InputConnection 在不同场景下对文本的插入、删除与提交;

  4. Settings ActivityPreferenceFragment 结合,实现键盘功能动态配置;

  5. 主题和皮肤 切换,为软键盘注入品牌风格;

  6. 扩展方向:可加入候选词栏、智能输入法算法(拼音、词频)、多语言混合输入、语音输入、手写板支持等,打造专业级输入法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值