1. 程式人生 > >修改安卓系統鍵盤

修改安卓系統鍵盤

轉載:https://www.jianshu.com/p/e7a28189c7b7?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

一,鍵盤更改內容

1、更改系統鍵盤背景色
2、更改系統鍵盤的大小
3、更改系統鍵盤的位置
4、更改系統鍵盤提示字元的大小
5、從手指點選鍵盤到鍵盤發出聲音的流程。
6、遇到的坑

二、詳細實現。

1、更改系統鍵盤背景圖片.
(1)找到packages/inputmethods/PinyinIME/res/layout/skb_container.xml,skb_container.xml檔案,將背景圖去掉,增加左右padding值。


1510716521(1).png

(2)在frameworks\base\core\java\android\inputmethodservice路徑下,找到InputMethodService.java,首先在initView方法中為根佈局設定理想中的背景圖

1510714059(1).png

這樣設定了之後,不會得到自己想要的效果,背景圖仍然沒有切換,最重要的是接下來的一步,在InputMethodService中找到方法updateFullscreenMode(),註釋掉圖中的兩行,這下就達到了想要的效果。


1510713991(1).png

2、更改系統鍵盤編輯框的風格
將編輯框的文字顏色改為白色,更換編輯框的背景顏色,離各邊距2dp,在InputMethodService.java中找到setExtraView(View view)方法,增加紅框中的內容。


1510714799(1).png

3、更改系統鍵盤的位置
在InputMethodService中找到方法showWindowInner(),將鍵盤的長設為原來的0.56,寬設為原來的一半,改變鍵盤的位置為右下角,增加下圖紅框中的內容,雖然鍵盤大小已經更改,但是內部只顯示了一半的鍵盤字數。


1510715166(1).png
1510715137(1).png

4、顯示完整的鍵盤字數
在packages\inputmethods\PinyinIME\src\com\android\inputmethod\pinyin路徑下找到SkbContainer.java
(1)找到onMeasure方法,更改測量的寬。


1510716009(1).png

(2)找到方法updateSkbLayout,在紅框的地方更改佈局的寬高。


1510715797(1).png

經過3,4系統鍵盤的風格已經更改完成。

4、更改系統鍵盤提示字元的大小
將提示字元大小改成原來的兩倍


1510717395(1).png

5、從手指點選鍵盤到鍵盤發出聲音的流程。

瞭解Android系統事件分發機制,應該不難理解,當手指點選螢幕的時候會經過一番處理最後會呼叫OnTouchEvent方法,鍵盤也不例外,當點選鍵盤某個按鍵的時候,會呼叫packages/inputmethods/PinyinIME/com/android/inputmethod/pinyin/SkbContainer.java類中的onTouchEvent(...)方法,在該方法中會呼叫關鍵方法 mSkv.onKeyPress(...),其中mSkv是packages/inputmethods/PinyinIME/com/android/inputmethod/pinyin/SoftKeyboardView.java類的一個物件。onTouchEvent(...)方法所對應的程式碼如下所示:

 @Override
public boolean onTouchEvent(MotionEvent event) {
    super.onTouchEvent(event);
    ...
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        ...
       //收到按下事件,經過一番處理會呼叫mSkv.onKeyPress(...)方法。
        if (null != mSkv) {
            mSoftKeyDown = mSkv.onKeyPress(x - mSkvPosInContainer[0], y
                    - mSkvPosInContainer[1], mLongPressTimer, false);
        }
        break;

    case MotionEvent.ACTION_MOVE:
        if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) {
            break;
        }
        if (mDiscardEvent) {
            resetKeyPress(0);
            break;
        }

        if (mPopupSkbShow && mPopupSkbNoResponse) {
            break;
        }

        SoftKeyboardView skv = inKeyboardView(x, y, mSkvPosInContainer);
        if (null != skv) {
            if (skv != mSkv) {
                mSkv = skv;
               //收到移動事件,經過一番處理會呼叫mSkv.onKeyPress(...)方法。
                mSoftKeyDown = mSkv.onKeyPress(x - mSkvPosInContainer[0], y
                        - mSkvPosInContainer[1], mLongPressTimer, true);
            } else if (null != skv) {
                if (null != mSkv) {
                    mSoftKeyDown = mSkv.onKeyMove(
                            x - mSkvPosInContainer[0], y
                                    - mSkvPosInContainer[1]);
                    if (null == mSoftKeyDown) {
                        mDiscardEvent = true;
                    }
                }
            }
        }
        break;
       ...
     }

追蹤到packages/inputmethods/PinyinIME/com/android/inputmethod/pinyin/SoftKeyboardView.java類中的onKeyPress(...)方法,該方法主要是根據使用者觸控的座標計算出當前點選的是鍵盤的哪個按鍵,判斷是否需要發出按鍵聲音。

 public SoftKey onKeyPress(int x, int y,
        SkbContainer.LongPressTimer longPressTimer, boolean movePress) {
   //mSoftKeyDown 用於記錄當前按下的按鍵Softkey
    mKeyPressed = false;
    boolean moveWithinPreviousKey = false;//判斷是否在上一個SoftKey上移動
     
    if (movePress) {
        //獲取當前點選的SoftKey
        SoftKey newKey = mSoftKeyboard.mapToKey(x, y);
         //如果直接的SoftKey和新的SoftKey相同則直接設moveWithinPreviousKey =true,並返回
        if (newKey == mSoftKeyDown) moveWithinPreviousKey = true;
        mSoftKeyDown = newKey;
    } else {
        mSoftKeyDown = mSoftKeyboard.mapToKey(x, y);
    }
    //如果使用者在鍵盤上移動,按鍵聲音只響一下,之後的不會再想,直接返回。
    if (moveWithinPreviousKey || null == mSoftKeyDown) return mSoftKeyDown;
    mKeyPressed = true;

    if (!movePress) {
        //播放按鍵聲音的介面
        tryPlayKeyDown();
       //表示鍵盤可以震動的介面
        tryVibrate();
    }

    mLongPressTimer = longPressTimer;
    ...
  }

tryPlayKeyDown()方法位於packages/inputmethods/PinyinIME/com/android/inputmethod/pinyin/SoftKeyboardView.java類中,該方法呼叫了聲音管理器SoundManager中的playKeyDown方法。

  private void tryPlayKeyDown() {
    //判斷系統鍵盤音是否已經開啟,只有開啟了才有聲音
    if (Settings.getKeySound()) {
        //呼叫播放介面
        mSoundManager.playKeyDown();
   }
}

SoundManager.java位於目錄packages/inputmethods/PinyinIME/com/android/inputmethod/pinyin/下,SoundManager.java所對應的playKeyDown()方法如下:

  public void playKeyDown() {
    //該方法主要初始化AudioManager,獲取mSilentMode的狀態。
    updateRingerMode();
    //當沒有靜音的情況下可以播放聲音
    if (!mSilentMode) {
       //在這裡可以指定按鍵的聲音
        int sound = AudioManager.FX_KEY_CLICK;
        //回撥AudioManager的playSoundEffect方法。
        mAudioManager.playSoundEffect(sound, FX_VOLUME);
    }
}

AudioManager.java位於\frameworks\base\media\java\android\media目錄下,該類中幾個比較中要的常量用於標識不同的聲音,如下所示:
* {@link #FX_KEY_CLICK}, 系統按鍵音
* {@link #FX_FOCUS_NAVIGATION_UP},
* {@link #FX_FOCUS_NAVIGATION_DOWN},
* {@link #FX_FOCUS_NAVIGATION_LEFT},
* {@link #FX_FOCUS_NAVIGATION_RIGHT},
* {@link #FX_KEYPRESS_STANDARD}, 標準鍵盤音
* {@link #FX_KEYPRESS_SPACEBAR}, 鍵盤點選空格音
* {@link #FX_KEYPRESS_DELETE},鍵盤點選刪除音
* {@link #FX_KEYPRESS_RETURN},鍵盤點選返回音
playSoundEffect(...)方法主要呼叫的AudioService的playSoundEffectVolume(...)方法。effectType主要標識哪一種聲音,如果不存在以上的聲音,直接返回,不在播放聲音。

  public void  playSoundEffect(int effectType, float volume) {
    if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) {
        return;
    }

    IAudioService service = getService();
    try {
        service.playSoundEffectVolume(effectType, volume);
    } catch (RemoteException e) {
        Log.e(TAG, "Dead object in playSoundEffect"+e);
    }
}

IAudioService是一個介面,其主要實現類是AudioService.java,其中playSoundEffectVolume(...)方法進行了正在的音訊呼叫與播放,如下所示:

   private void playSoundEffect(int effectType, int volume) {
        synchronized (mSoundEffectsLock) {
            if (mSoundPool == null) {
                return;
            }
            float volFloat;
            // use default if volume is not specified by caller
            if (volume < 0) {
                volFloat = (float)Math.pow(10, (float)sSoundEffectVolumeDb/20);
            } else {
                volFloat = (float) volume / 1000.0f;
            }

            if (SOUND_EFFECT_FILES_MAP[effectType][1] > 0) {
                mSoundPool.play(SOUND_EFFECT_FILES_MAP[effectType][1], volFloat, volFloat, 0, 0, 1.0f);
            } else {
               到這裡大家應用就很熟悉了
                MediaPlayer mediaPlayer = new MediaPlayer();
                try {
                    //音訊格式的路徑
                    String filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH + SOUND_EFFECT_FILES[SOUND_EFFECT_FILES_MAP[effectType][0]];
                    mediaPlayer.setDataSource(filePath);
                   mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM);
                    mediaPlayer.prepare();
                    //設定播放音訊的音量
                    mediaPlayer.setVolume(volFloat, volFloat);
                    mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
                        public void onCompletion(MediaPlayer mp) {
                           //播放完成,情況快取
                            cleanupPlayer(mp);
                        }
                    });
                    mediaPlayer.setOnErrorListener(new OnErrorListener() {
                        public boolean onError(MediaPlayer mp, int what, int extra) {
                            cleanupPlayer(mp);
                            return true;
                        }
                    });
                   //開始播放
                    mediaPlayer.start();
                } catch (IOException ex) {
                    Log.w(TAG, "MediaPlayer IOException: "+ex);
                } catch (IllegalArgumentException ex) {
                    Log.w(TAG, "MediaPlayer IllegalArgumentException: "+ex);
                } catch (IllegalStateException ex) {
                    Log.w(TAG, "MediaPlayer IllegalStateException: "+ex);
                }
            }
        }
    }

6、不停的點選按鍵值,快速按完成,出現鍵盤彈出,介面混亂的現象,正常情況應該鍵盤隱藏。
(1)原因:當收到完成的指令時,鍵盤應該隱藏,由於鍵盤隱藏的過程中有一個200ms的動畫,如果在200ms之內沒有完成隱藏指令,又收到了按鍵值,再隱藏的過程中就會把鍵盤再一次顯示,所以出現了難以預料的問題。
(2)解決方式:
設定一個標籤,用於判斷鍵盤是否正在執行隱藏操作。在frameworks\base\core\java\android\inputmethodservice\InputMethodService.java類hideWindow()方法中,實現如下程式碼。


image.png

由於packages\inputmethods\PinyinIME類繼承InputMethodService,所以可以直接呼叫InputMethodService的公共方法,在PinyinIME.java類的onKeyDown和onKeyDownUp方法中判斷,當aimationStarting()等於true的時候,不要往下執行,直接返回。解決如下: