Android InputMethodService|KeyboardView 自定義輸入法和鍵盤 01

如何自定義 安卓輸入法 和 鍵盤

1.首先有幾個關(guān)鍵類

1.InputMethodService 2.Keyboard 3.KeyboardView

1.1 InputMethodService

看下這個類的介紹


圖片.png

InputMethodService provides a standard implementation of an InputMethod
作用 提供一個標(biāo)準(zhǔn)鍵盤實(shí)現(xiàn) balabala.....

輸入法生命周期.png

請參考 http://blog.csdn.net/weijinqian0/article/details/76906317

1.2.Keyboard 源碼分析

圖片.png

xmL 定義鍵盤的屬性 : 鍵位寬/高/水平間距/垂直間距/按鍵文字圖標(biāo)/鍵值.....
鍵盤的UI樣式就在這個類里面定義,準(zhǔn)確的說 是在keyboard加載的xml文件中定義

構(gòu)造方法
傳入鍵盤布局文件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"
  //預(yù)覽的view 的高度
    android:keyPreviewHeight="100dp"
  //按鍵預(yù)覽的布局
    android:keyPreviewLayout="@layout/keyboard_preview"
  //按鍵預(yù)覽的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. 具體實(shí)現(xiàn)

2.1 自定義鍵盤繼承KeyboardView 當(dāng)然也可以不定義 直接使用keyboardView

2.2 創(chuàng)建布局文件

 <?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 顯示區(qū)占位-->
<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 新建服務(wù)繼承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 按鍵顯示的內(nèi)容
keyIcon 按鍵顯示的圖標(biāo)內(nèi)容
keyWidth 按鍵的寬度
keyHeight 按鍵的高度
horizontalGap 代表按鍵前的間隙水平方向上的
isSticky 按鍵是否是sticky的,就像shift 鍵 具有兩種狀態(tài)
isModifier 按鍵是不是功能鍵
keyOutputText 指定按鍵輸出的內(nèi)容是字符串
isRepeatable 按鍵是可重復(fù)的,如果長按鍵可以觸發(fā)重復(fù)按鍵事件則為true,else為false
keyEdgeFlags 指定按鍵的對齊指令,取值為left或者right

設(shè)置和選擇輸入法
跳轉(zhuǎn)輸入法設(shè)置界面:

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

設(shè)置輸入法.png

彈出輸入法選擇框

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


選擇輸入法.png
Kapture 2018-02-02 at 15.08.06.gif

3問題處理

3.1 第一排按鍵預(yù)覽位置錯誤

圖片.png

對比下訊飛輸入法


圖片.png

可以看出訊飛輸入法上面還有一個透明布局

按鍵的預(yù)覽本質(zhì)是一個popupwindow


圖片.png

在keyboardViwe 源碼中可以看到按鍵預(yù)覽的實(shí)現(xiàn)方式,按鍵預(yù)覽的pop的位置不會超過給 InputMethodService 的布局的范圍
更證: 其實(shí)onCreateCandidatesView 中設(shè)置candidatesView也可以
override fun onCreateCandidatesView(): View {
// return super.onCreateCandidatesView()
return 自定義的view
}

問題解決: 只要給InputMethodService 設(shè)置的布局的高度大于布局中的keyboardView的高度,那么就可以顯示正常


圖片.png

3.2 給不同按鍵設(shè)置不同背景

keyboardView 可以給按鍵設(shè)置背景 但是是給所有的按鍵設(shè)置

這個地方不是在xml 資源文件中設(shè)置按鍵的keyIcon 而是重寫keyboardView的Ondraw 根據(jù)不同keycode重新繪制

3.3 禁止部分按鍵的按鍵預(yù)覽

1.在 keyboardView的監(jiān)聽 OnKeyboardActionListener 回調(diào)的onPress()中處理

  @Override
    public void onPress(int primaryCode) {

        //設(shè)置某些按鍵不顯示預(yù)覽的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.上面處理了點(diǎn)擊是沒問題了 但是在鍵盤上滑動時,滑動到每個按鍵還是會顯示preView

所以可以重寫KeyboardView的OnTouchEvent方法處理

/**
 * 處理滑動時 回車等鍵位的 按鍵預(yù)覽 出現(xiàn)問題
 */
@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:
            //取消預(yù)覽
            setPreviewEnabled(false);
            setPopupOffset(0, ScreenUtil.dp2px(0));
            break;

        case MotionEvent.ACTION_MOVE:
            setPreviewEnabled(false);
            //滑動距離小于10dp時不隱藏鍵盤預(yù)覽 大于10dp時隱藏鍵盤按鍵預(yù)覽
            if (Math.abs(x - mDownX) > ScreenUtil.dp2px(10) || Math.abs(y - mDownY) > ScreenUtil
                    .dp2px(10)) {
                //取消預(yù)覽
                setPopupOffset(0, ScreenUtil.dp2px(0));
            }
            break;
    }


    return super.onTouchEvent(me);

}

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容