1. 程式人生 > >Android自定義鍵盤詳解、自定義輸入法簡介

Android自定義鍵盤詳解、自定義輸入法簡介

概述

Android中有兩個系統類是用來實現鍵盤的,分別是Keyboard和KeyboardView。

Keyboard有個內部類Key,用於記錄每個鍵的資訊,如code、width、height等。而KeyBorad則持有一個List用於儲存所有Key,並對外提供介面。

KeyBoardView則是負責繪製所有Key,監聽Touch事件,根據Touch處的座標,計算出點選的是哪個Key,然後通過OnKeyboardActionListener通知呼叫處,點選的是哪個Key、code是多少,呼叫處再把該Key對應的string值設定到EidtText中。

大致流程就是這樣,但是有很多細節、坑需要注意,本文的主要內容就是來分析這些細節和坑。

這兩個類的原始碼都很短,只有1000行左右,但是這裡並不準備對所有原始碼進行分析,為什麼呢?因為這兩個類裡面的成員變數、方法幾乎都是private的,完全無法供我們修改、自定義。所以對於自定義鍵盤要求很高、很炫酷的需求,還是得完全重寫特定的“KeyBoradView”,KeyBorad倒是可以直接用。

自定義鍵盤

下面以一個最簡單的例子來講解如何使用,該注意的點就直接在程式碼中以註釋形勢給出。

1、首先在res目錄下建立xml目錄,在xml目錄下建立qwer.xml檔案,用於鍵盤佈局,程式碼如下:

<?xml version="1.0" encoding="UTF-8"?>
<Keyboard
android:keyWidth="10%p" android:keyHeight="45dp" android:horizontalGap="3px" android:verticalGap="3px" xmlns:android="http://schemas.android.com/apk/res/android">
// KeyBoard標籤下的keyWidth="10%p"表示下面的每個鍵寬度佔parent寬度的10%,xxxGap屬性是鍵與鍵之間的水平和垂直間隔 <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> // codes屬性就是該Key對應的ASCII碼(其實也可以不一定得是ASCII碼,反正後面會在監聽事件中返回給你這個codes值,你想怎麼處理都行),keyLabel屬性就是該Key顯示在鍵盤上的string,還有個keyIcon屬性是該Key顯示在鍵盤上的drawable <Row> <Key android:horizontalGap="5%p" android:codes="97" 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:keyEdgeFlags="right" android:keyLabel="l" /> </Row> // Key標籤下的horizontalGap屬性表示距離parent的距離 <Row> <Key android:keyWidth="15p" android:codes="-1" android:keyEdgeFlags="left" android:keyLabel="Shift" /> <Key android:codes="122" android:keyLabel="z" /> <Key android:codes="120" android:keyLabel="x" /> <Key android:codes="99" android:keyLabel="c" /> <Key android:codes="118" android:keyLabel="v" /> <Key android:codes="98" android:keyLabel="b" /> <Key android:codes="110" android:keyLabel="n" /> <Key android:codes="109" android:keyLabel="m" /> <Key android:keyWidth="15p" android:codes="-5" android:keyEdgeFlags="right" android:isRepeatable="true" android:keyLabel="Delete" /> </Row> // Key標籤下的keyWidth屬性會覆蓋parent中賦的值,isRepeatable屬性表示是長按時是否重複輸入 <Row android:rowEdgeFlags="bottom"> <Key android:keyWidth="20%p" android:codes="-2" android:keyLabel="12#" /> <Key android:keyWidth="15%p" android:codes="44" android:keyLabel="," /> <Key android:keyWidth="30%p" android:codes="32" android:isRepeatable="true" android:keyLabel="Space" /> <Key android:keyWidth="15%p" android:codes="46" android:keyLabel="." /> <Key android:keyWidth="20%p" android:codes="-3" android:keyEdgeFlags="right" android:keyLabel="完成" /> </Row> </Keyboard>

2、建立自定義的MyKeyBoradView,主要用於處理邏輯,讓外界使用更加方便。網上很多文章沒有建立自定義View,用的XXXUtil也是一樣的。個人覺得建立自定義的View封裝好這些處理邏輯更好一些。

public class MyKeyBoardView extends LinearLayout {
    private EditText mEditText;

    public MyKeyBoardView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    private void init(Context context) {
        setOrientation(VERTICAL);
        LayoutInflater.from(context).inflate(R.layout.lyt_keyboard, this, true);

        KeyboardView kv = (KeyboardView) findViewById(R.id.kv_lyt_keyboard);
        // 載入上面的qwer.xml鍵盤佈局,new出KeyBoard物件
        Keyboard kb = new Keyboard(context, R.xml.qwer);

        kv.setKeyboard(kb); 
        kv.setEnabled(true);
        // 設定是否顯示預覽,就是某個鍵時,上面彈出小框顯示你按下的鍵,稱為預覽框
        kv.setPreviewEnabled(true); 
        // 設定監聽器
        kv.setOnKeyboardActionListener(mListener);
    }

    // 設定接受字元的EditText
    public void setStrReceiver(EditText et) {
        mEditText = et;
    }

    private KeyboardView.OnKeyboardActionListener mListener = new KeyboardView.OnKeyboardActionListener() {
        @Override
        public void swipeUp() {
        }

        @Override
        public void swipeRight() {
        }

        @Override
        public void swipeLeft() {
        }

        @Override
        public void swipeDown() {
        }

        @Override
        public void onText(CharSequence text) {
        }

        @Override
        public void onRelease(int primaryCode) {
            // 手指離開該鍵(擡起手指或手指移動到其它鍵)時回撥
        }

        @Override
        public void onPress(int primaryCode) {
            // 當某個鍵被按下時回撥,只有press_down時會觸發,長按不會多次觸發
        }

        @Override
        public void onKey(int primaryCode, int[] keyCodes) {
            // 鍵被按下時回撥,在onPress後面。如果isRepeat屬性設定為true,長按時會連續回撥

            Editable editable = mEditText.getText();
            int selectionPosition = mEditText.getSelectionStart();

            if (primaryCode == Keyboard.KEYCODE_DELETE) {
                // 如果按下的是delete鍵,就刪除EditText中的str
                if (editable != null && editable.length() > 0) {
                    if (selectionPosition > 0) {
                        editable.delete(selectionPosition - 1, selectionPosition);
                    }
                }
            } else {
                // 把該鍵對應的string值設定到EditText中
                editable.insert(selectionPosition, Character.toString((char) primaryCode));
            }
            // 其實還有很多code的判斷,比如“完成”鍵、“Shift”鍵等等,這裡就不一一列出了
            // 對於Shift鍵被按下,需要做兩件事,一件是把鍵盤顯示字元全部切換為大寫,呼叫setShifted()方法就可以了;另一件是把Shift狀態下接收到的正常字元(Shift、完成、Delete等除外)的code值-32再轉換成相應str,插入到EidtText中
        }
    };
}

3、自定義的MyKeyBoardView對應的layout檔案,以及預覽框對應的layout檔案:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <android.inputmethodservice.KeyboardView
        android:id="@+id/kv_lyt_keyboard"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#999" 
        android:focusable="true"
        android:focusableInTouchMode="true"
        android:keyBackground="@color/colorWhite"
        android:keyTextColor="#333"
        android:shadowColor="#00000000" 
        android:keyPreviewHeight="100dp"
        android:keyPreviewLayout="@layout/lyt_preview"/>

    // background是整個鍵盤View的背景,設定了鍵的xxxgap後,中間的縫隙就會是這個背景色
    // keyBackground是每個鍵的背景
    // keyTextColor是每個鍵中字元的textColor
    // shadowColor是每個鍵中字元的shadow,請設定為全透明!!!
    // keyPreviewHeight是預覽框的高度!!!
    // keyPreviewLayout是預覽框對應的佈局,跟佈局必須是TextView!!!
</merge>

lyt_preview檔案程式碼:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/bg_preview"
    android:gravity="center"
    android:paddingLeft="100dp"
    android:paddingRight="100dp"
    android:textColor="#000"
    android:textSize="30sp"
    android:textStyle="bold"/>

4、然後就是最後一個步驟,Activity中呼叫了。Activity的佈局檔案就不貼出了,就是最上面一個EditText,最下面一個MyKeyBoardView。

ublic class MainActivity extends Activity {
    private MyKeyBoardView mKeyView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        EditText editText = (EditText) findViewById(R.id.et_main);
        mKeyView = (MyKeyBoardView) findViewById(R.id.kv_main);
        mKeyView.setStrReceiver(editText);

        editText.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
                imm.hideSoftInputFromWindow(v.getWindowToken(), 0);

                mKeyView.setVisibility(View.VISIBLE);
                return true;
            }
        });
    }
}

遇到的問題

1、預覽框的顯示效果
為了不讓文章太長,我就不列出各種可能的坑了,直接列出最正確的做法,這裡就得分析下原始碼了。下面所有原始碼都是android7.0的原始碼,其他版本可能會有細微不同。

看KeyBoardView中的showKey()方法:

private void showKey(final int keyIndex) {
        final PopupWindow previewPopup = mPreviewPopup;
        final Key[] keys = mKeys;
        if (keyIndex < 0 || keyIndex >= mKeys.length) return;
        Key key = keys[keyIndex];
        if (key.icon != null) {
            mPreviewText.setCompoundDrawables(null, null, null, 
                    key.iconPreview != null ? key.iconPreview : key.icon);
            mPreviewText.setText(null);
        } else {
            mPreviewText.setCompoundDrawables(null, null, null, null);
            mPreviewText.setText(getPreviewText(key));
            if (key.label.length() > 1 && key.codes.length < 2) {
                mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize);
                mPreviewText.setTypeface(Typeface.DEFAULT_BOLD);
            } else {
                mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge);
                mPreviewText.setTypeface(Typeface.DEFAULT);
            }
        }
        mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 
                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));

        // 計算預覽框的寬度,由TextView和Key寬度中較大的決定
        int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width 
                + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight());

        // 確定預覽框的高度。這裡的mPreviewHeight是在構造方法中獲取xml屬性中的值,對應的屬性就是keyPreviewHeight
        // 是不是很坑爹,寬度在TextView中設定,高度在KeyBoardView的xml屬性中設定。。。
        final int popupHeight = mPreviewHeight;

        LayoutParams lp = mPreviewText.getLayoutParams();
        if (lp != null) {
            lp.width = popupWidth;
            lp.height = popupHeight;
        }

        // mPreviewCentered預設為false,7.0版本原始碼中沒有地方會改變其值,所有一定會進入下面的判斷
        if (!mPreviewCentered) {
            // 以Key的x座標為基準,計算預覽框的x座標
            // 這裡注意了,如果預覽TextView直接設定寬度,由於預覽框和Key的左邊是對齊的,如果預覽框和Key的寬度不一樣,顯示就很醜;
            // 所以正確的設定預覽框寬度的方法是通過設定padding值的大小來控制預覽框的寬度
            mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + mPaddingLeft;

            // 以Key的y座標為基準,計算預覽框的y座標
            // 這裡又要注意了,mPreviewOffset是從xml屬性中取的值,如果不設定該屬性就會取預設值,不同android版本的預設值是不一樣的。。。所以一定要設定一下這個屬性
            mPopupPreviewY = key.y - popupHeight + mPreviewOffset;
        } else {
            // TODO: Fix this if centering is brought back
            mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2;
            mPopupPreviewY = - mPreviewText.getMeasuredHeight();
        }
        mHandler.removeMessages(MSG_REMOVE_PREVIEW);
        getLocationInWindow(mCoordinates);

        mCoordinates[0] += mMiniKeyboardOffsetX; // Offset may be zero
        mCoordinates[1] += mMiniKeyboardOffsetY; // Offset may be zero

        // Set the preview background state
        mPreviewText.getBackground().setState(
                key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
        mPopupPreviewX += mCoordinates[0];
        mPopupPreviewY += mCoordinates[1];

        // If the popup cannot be shown above the key, put it on the side
        getLocationOnScreen(mCoordinates);
        if (mPopupPreviewY + mCoordinates[1] < 0) {
            // If the key you're pressing is on the left side of the keyboard, show the popup on
            // the right, offset by enough to see at least one key to the left/right.
            if (key.x + key.width <= getWidth() / 2) {
                mPopupPreviewX += (int) (key.width * 2.5);
            } else {
                mPopupPreviewX -= (int) (key.width * 2.5);
            }
            mPopupPreviewY += popupHeight;
        }

        if (previewPopup.isShowing()) {
            previewPopup.update(mPopupPreviewX, mPopupPreviewY,
                    popupWidth, popupHeight);
        } else {
            previewPopup.setWidth(popupWidth);
            previewPopup.setHeight(popupHeight);
            previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY, 
                    mPopupPreviewX, mPopupPreviewY);
        }
        mPreviewText.setVisibility(VISIBLE);
    }


2、鍵盤字型顯示模糊
KeyBoardView會遍歷所有key,然後繪製其鍵盤字元,下面看下繪製邏輯:

if (label != null) {
    // For characters, use large font. For labels like "Done", use small font.
    if (label.length() > 1 && key.codes.length < 2) {
        paint.setTextSize(mLabelTextSize);
        paint.setTypeface(Typeface.DEFAULT_BOLD);
    } else {
        paint.setTextSize(mKeyTextSize);
        paint.setTypeface(Typeface.DEFAULT);
    }
    // Draw a drop shadow for the text
    // 看到沒,會在字元上面繪製一層shadow。。。所以在xml屬性中一定要將該屬性設定為全透明
    paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor);
    // Draw the text
    canvas.drawText(label,(key.width - padding.left - padding.right) / 2+ padding.left,(key.height - padding.top - padding.bottom) / 2+ (paint.getTextSize() - paint.descent()) / 2 + padding.top,paint);
    // Turn off drop shadow
    paint.setShadowLayer(0, 0, 0, 0);
} 

自定義輸入法

輸入法以後再寫吧。。。。