Android InputMethodService|KeyboardView 自定义输入法和键盘 01

如何自定义 安卓输入法 和 键盘

1.首先有几个关键类

1.InputMethodService 2.Keyboard 3.KeyboardView

1.1 InputMethodService

看下这个类的介绍


图片.png

InputMethodService provides a standard implementation of an InputMethod
作用 提供一个标准键盘实现 balabala.....

输入法生命周期.png

请参考 http://blog.csdn.net/weijinqian0/article/details/76906317

1.2.Keyboard 源码分析

图片.png

xmL 定义键盘的属性 : 键位宽/高/水平间距/垂直间距/按键文字图标/键值.....
键盘的UI样式就在这个类里面定义,准确的说 是在keyboard加载的xml文件中定义

构造方法
传入键盘布局文件id

    /**
     * Creates a keyboard from the given xml key layout file.
     * @param context the application or service context
     * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
     */
    public Keyboard(Context context, int xmlLayoutResId) {
    this(context, xmlLayoutResId, 0);
   }


     /**
 * Creates a keyboard from the given xml key layout file. Weeds out rows
 * that have a keyboard mode defined but don't match the specified mode.
 * @param context the application or service context
 * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
 * @param modeId keyboard mode identifier
 * @param width sets width of keyboard
 * @param height sets height of keyboard
 */
public Keyboard(Context context, @XmlRes int xmlLayoutResId, int modeId, int width,
        int height) {
    mDisplayWidth = width;
    mDisplayHeight = height;

    mDefaultHorizontalGap = 0;
    mDefaultWidth = mDisplayWidth / 10;
    mDefaultVerticalGap = 0;
    mDefaultHeight = mDefaultWidth;
    mKeys = new ArrayList<Key>();
    mModifierKeys = new ArrayList<Key>();
    mKeyboardMode = modeId;
//加载键盘
    loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
}

xml 解析键盘布局

private void loadKeyboard(Context context, XmlResourceParser parser) {
    boolean inKey = false;
    boolean inRow = false;
    boolean leftMostKey = false;
    int row = 0;
    int x = 0;
    int y = 0;
    Key key = null;
    Row currentRow = null;
    Resources res = context.getResources();
    boolean skipRow = false;

    try {
        int event;
        while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
            if (event == XmlResourceParser.START_TAG) {
                String tag = parser.getName();
                if (TAG_ROW.equals(tag)) {
                    inRow = true;
                    x = 0;
                    currentRow = createRowFromXml(res, parser);
                    rows.add(currentRow);
                    skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode;
                    if (skipRow) {
                        skipToEndOfRow(parser);
                        inRow = false;
                    }
               } else if (TAG_KEY.equals(tag)) {
                    inKey = true;
                    key = createKeyFromXml(res, currentRow, x, y, parser);
                    mKeys.add(key);
                    if (key.codes[0] == KEYCODE_SHIFT) {
                        // Find available shift key slot and put this shift key in it
                        for (int i = 0; i < mShiftKeys.length; i++) {
                            if (mShiftKeys[i] == null) {
                                mShiftKeys[i] = key;
                                mShiftKeyIndices[i] = mKeys.size()-1;
                                break;
                            }
                        }
                        mModifierKeys.add(key);
                    } else if (key.codes[0] == KEYCODE_ALT) {
                        mModifierKeys.add(key);
                    }
                    currentRow.mKeys.add(key);
                } else if (TAG_KEYBOARD.equals(tag)) {
                    parseKeyboardAttributes(res, parser);
                }
            } else if (event == XmlResourceParser.END_TAG) {
                if (inKey) {
                    inKey = false;
                    x += key.gap + key.width;
                    if (x > mTotalWidth) {
                        mTotalWidth = x;
                    }
                } else if (inRow) {
                    inRow = false;
                    y += currentRow.verticalGap;
                    y += currentRow.defaultHeight;
                    row++;
                } else {
                    // TODO: error or extend?
                }
            }
        }
    } catch (Exception e) {
        Log.e(TAG, "Parse error:" + e);
        e.printStackTrace();
    }
    mTotalHeight = y - mDefaultVerticalGap;
}

1.3.KeyboardView :自定义键盘一般继承keyboardView 重写其方法

图片.png

作用:渲染按键 侦测按压

<com.example.ableqing.androidkeyboardviewdemo.keyboard.MyKeyboardView

    android:id="@+id/keyboardView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:background="#ECECEC"
 //每一个按键的背景 (全部)
  android:keyBackground="@drawable/btn_keyboard_key"
  //预览的view 的高度
    android:keyPreviewHeight="100dp"
  //按键预览的布局
    android:keyPreviewLayout="@layout/keyboard_preview"
  //按键预览的pop的y轴偏移量
    android:keyPreviewOffset="50dp"
    android:keyTextColor="#4F4F4F"
    android:shadowColor="#FFFFFF"
    android:shadowRadius="0.0"

    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"/>

2. 具体实现

2.1 自定义键盘继承KeyboardView 当然也可以不定义 直接使用keyboardView

2.2 创建布局文件

 <?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.LinearLayoutCompat
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom"
android:orientation="vertical">

   <!--keyboard_preview 显示区占位-->
<View
    android:layout_width="match_parent"
    android:layout_height="100dp"/>

 <com.example.ableqing.androidkeyboardviewdemo.keyboard.MyKeyboardView

    android:id="@+id/keyboardView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:background="#ECECEC"
    android:focusable="true"
    android:focusableInTouchMode="true"
    android:keyBackground="@drawable/btn_keyboard_key"
    android:keyPreviewHeight="100dp"
    android:keyPreviewLayout="@layout/keyboard_preview"
    android:keyPreviewOffset="50dp"
    android:keyTextColor="#4F4F4F"
    android:shadowColor="#FFFFFF"
    android:shadowRadius="0.0"

    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"/>


</android.support.v7.widget.LinearLayoutCompat>

2.3 新建服务继承InputMethodService

 public class MyInputMethodService extends InputMethodService {
 @Override
 public View onCreateInputView() {

    View view = getLayoutInflater().
            //键盘的布局文件
            inflate(R.layout.keyboard_global, null);

    return view;
  }
 }

清单文件

    <service
        android:name="com.example.ableqing.androidkeyboardviewdemo.MyInputMethodService"
        android:label="@string/keyboard_name"
        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>

2.4 xml 定义键盘的resource 文件 和 不同键盘布局文件


图片.png

字母键盘的布局

<?xml version="1.0" encoding="UTF-8"?>
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
      android:horizontalGap="0.9%p"
      android:keyHeight="52dp"
      android:keyWidth="9%p"
      android:verticalGap="0px">


<Row>
    <Key
        android:codes="113"
        android:keyEdgeFlags="left"
        android:keyLabel="q"/>
    <Key
        android:codes="119"
        android:keyLabel="w"/>
    <Key
        android:codes="101"
        android:keyLabel="e"/>
    <Key
        android:codes="114"
        android:keyLabel="r"/>
    <Key
        android:codes="116"
        android:keyLabel="t"/>
    <Key
        android:codes="121"
        android:keyLabel="y"/>
    <Key
        android:codes="117"
        android:keyLabel="u"/>
    <Key
        android:codes="105"
        android:keyLabel="i"/>
    <Key
        android:codes="111"
        android:keyLabel="o"/>
    <Key
        android:codes="112"
        android:keyEdgeFlags="right"
        android:keyLabel="p"/>
</Row>
<Row>
    <Key
        android:codes="97"
        android:horizontalGap="6%p"
        android:keyEdgeFlags="left"
        android:keyLabel="a"/>
    <Key
        android:codes="115"
        android:keyLabel="s"/>
    <Key
        android:codes="100"
        android:keyLabel="d"/>
    <Key
        android:codes="102"
        android:keyLabel="f"/>
    <Key
        android:codes="103"
        android:keyLabel="g"/>
    <Key
        android:codes="104"
        android:keyLabel="h"/>
    <Key
        android:codes="106"
        android:keyLabel="j"/>
    <Key
        android:codes="107"
        android:keyLabel="k"/>
    <Key
        android:codes="108"

        android:keyLabel="l"/>


</Row>
<Row>
    <Key
        android:codes="-1"
        android:isModifier="true"
        android:isSticky="true"
        android:keyEdgeFlags="left"
        android:keyWidth="12.6%p"/>
    <Key
        android:codes="122"
        android:horizontalGap="2.3%p"
        android:keyLabel="z"
        android:keyWidth="9%p"/>
    <Key
        android:codes="120"
        android:keyLabel="x"
        android:keyWidth="9%p"/>
    <Key
        android:codes="99"
        android:keyLabel="c"
        android:keyWidth="9%p"/>
    <Key
        android:codes="118"
        android:keyLabel="v"
        android:keyWidth="9%p"/>
    <Key
        android:codes="98"
        android:keyLabel="b"
        android:keyWidth="9%p"/>
    <Key
        android:codes="110"
        android:keyLabel="n"
        android:keyWidth="9%p"/>
    <Key
        android:codes="109"
        android:keyLabel="m"
        android:keyWidth="9%p"/>

    <Key
        android:codes="-5"
        android:horizontalGap="2.3%p"
        android:isRepeatable="true"
        android:keyEdgeFlags="right"
        android:keyIcon="@mipmap/key_back"
        android:keyWidth="12.6%p"/>


</Row>


<Row android:rowEdgeFlags="bottom">
    <Key
        android:codes="-101"
        android:keyEdgeFlags="left"
        android:keyLabel="123"
        android:keyWidth="12.6%p"/>
    <Key
        android:codes="-105"
        android:keyIcon="@mipmap/key_switch"
        android:keyWidth="12.6%p"/>

    <Key
        android:codes="-100"
        android:keyIcon="@mipmap/key_setting"
        android:keyWidth="11%p"/>
    <Key
        android:codes="32"
        android:isRepeatable="true"
        android:keyLabel="space"
        android:keyWidth="34.2%p"/>

    <Key


        android:codes="-4"
        android:keyEdgeFlags="right"
        android:keyLabel="return"
        android:keyWidth="24%p"/>
</Row>


</Keyboard>

xml 中定义布局的 属性

keyLabel 按键显示的内容
keyIcon 按键显示的图标内容
keyWidth 按键的宽度
keyHeight 按键的高度
horizontalGap 代表按键前的间隙水平方向上的
isSticky 按键是否是sticky的,就像shift 键 具有两种状态
isModifier 按键是不是功能键
keyOutputText 指定按键输出的内容是字符串
isRepeatable 按键是可重复的,如果长按键可以触发重复按键事件则为true,else为false
keyEdgeFlags 指定按键的对齐指令,取值为left或者right

设置和选择输入法
跳转输入法设置界面:

Intent intent = new Intent();
intent.setAction( Settings.ACTION_INPUT_METHOD_SETTINGS);
SoftDemoActivity.this.startActivity(intent);

设置输入法.png

弹出输入法选择框

((InputMethodManager) service.getSystemService(Context.INPUT_METHOD_SERVICE)).showInputMethodPicker();


选择输入法.png
Kapture 2018-02-02 at 15.08.06.gif

3问题处理

3.1 第一排按键预览位置错误

图片.png

对比下讯飞输入法


图片.png

可以看出讯飞输入法上面还有一个透明布局

按键的预览本质是一个popupwindow


图片.png

在keyboardViwe 源码中可以看到按键预览的实现方式,按键预览的pop的位置不会超过给 InputMethodService 的布局的范围
更证: 其实onCreateCandidatesView 中设置candidatesView也可以
override fun onCreateCandidatesView(): View {
// return super.onCreateCandidatesView()
return 自定义的view
}

问题解决: 只要给InputMethodService 设置的布局的高度大于布局中的keyboardView的高度,那么就可以显示正常


图片.png

3.2 给不同按键设置不同背景

keyboardView 可以给按键设置背景 但是是给所有的按键设置

这个地方不是在xml 资源文件中设置按键的keyIcon 而是重写keyboardView的Ondraw 根据不同keycode重新绘制

3.3 禁止部分按键的按键预览

1.在 keyboardView的监听 OnKeyboardActionListener 回调的onPress()中处理

  @Override
    public void onPress(int primaryCode) {

        //设置某些按键不显示预览的view
        LogUtil.d(TAG, "-->Keyboard: onPress at >>" + primaryCode);
        KeyboardUtils.vibrate(20);
        if (primaryCode == Keyboard.KEYCODE_SHIFT || primaryCode == Keyboard.KEYCODE_DELETE  //
                || primaryCode == Keyboard.KEYCODE_DONE || primaryCode == VipKeyboardCode.CODE_SPACE //
                || primaryCode == VipKeyboardCode.CODE_TYPE_QWERTY || primaryCode == VipKeyboardCode.CODE_TYPE_NUM //
                || primaryCode == VipKeyboardCode.CODE_TYPE_SYMBOL || primaryCode == VipKeyboardCode.CODE_OPEN_VIP //
                || primaryCode == VipKeyboardCode.CODE_TYPE_SWITCH_INPUT || primaryCode == Keyboard.KEYCODE_MODE_CHANGE) {
            setPreviewEnabled(false);
        } else {
            setPreviewEnabled(true);
        }
    }

2.上面处理了点击是没问题了 但是在键盘上滑动时,滑动到每个按键还是会显示preView

所以可以重写KeyboardView的OnTouchEvent方法处理

/**
 * 处理滑动时 回车等键位的 按键预览 出现问题
 */
@Override
public boolean onTouchEvent(MotionEvent me) {


    float x = me.getX();
    float y = me.getY();
    switch (me.getAction()) {

        case MotionEvent.ACTION_DOWN:
            mDownX = x;
            mDownY = y;
            break;

        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            //取消预览
            setPreviewEnabled(false);
            setPopupOffset(0, ScreenUtil.dp2px(0));
            break;

        case MotionEvent.ACTION_MOVE:
            setPreviewEnabled(false);
            //滑动距离小于10dp时不隐藏键盘预览 大于10dp时隐藏键盘按键预览
            if (Math.abs(x - mDownX) > ScreenUtil.dp2px(10) || Math.abs(y - mDownY) > ScreenUtil
                    .dp2px(10)) {
                //取消预览
                setPopupOffset(0, ScreenUtil.dp2px(0));
            }
            break;
    }


    return super.onTouchEvent(me);

}

demo地址 https://github.com/qingyc/AndroidKeyboardViewDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容