如何自定義 安卓輸入法 和 鍵盤
1.首先有幾個關(guān)鍵類
1.InputMethodService 2.Keyboard 3.KeyboardView
1.1 InputMethodService
看下這個類的介紹

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

請參考 http://blog.csdn.net/weijinqian0/article/details/76906317
1.2.Keyboard 源碼分析

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 重寫其方法

作用:渲染按鍵 偵測按壓
<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 文件 和 不同鍵盤布局文件

字母鍵盤的布局
<?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

跳轉(zhuǎn)輸入法設(shè)置界面:
Intent intent = new Intent();
intent.setAction( Settings.ACTION_INPUT_METHOD_SETTINGS);
SoftDemoActivity.this.startActivity(intent);

彈出輸入法選擇框
((InputMethodManager) service.getSystemService(Context.INPUT_METHOD_SERVICE)).showInputMethodPicker();


3問題處理
3.1 第一排按鍵預(yù)覽位置錯誤

對比下訊飛輸入法

可以看出訊飛輸入法上面還有一個透明布局
按鍵的預(yù)覽本質(zhì)是一個popupwindow

在keyboardViwe 源碼中可以看到按鍵預(yù)覽的實(shí)現(xiàn)方式,按鍵預(yù)覽的pop的位置不會超過給 InputMethodService 的布局的范圍
更證: 其實(shí)onCreateCandidatesView 中設(shè)置candidatesView也可以
override fun onCreateCandidatesView(): View {
// return super.onCreateCandidatesView()
return 自定義的view
}
問題解決: 只要給InputMethodService 設(shè)置的布局的高度大于布局中的keyboardView的高度,那么就可以顯示正常

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);
}