自定義系統(tǒng)鍵盤

   注意: 寫該文章主要幫助自己記憶,貼出來希望可以給有同樣問題的人解惑,不喜勿噴,可以提意見哦。

一,鍵盤更改內(nèi)容

1、更改系統(tǒng)鍵盤背景色
2、更改系統(tǒng)鍵盤的大小
3、更改系統(tǒng)鍵盤的位置
4、更改系統(tǒng)鍵盤提示字符的大小
5、從手指點擊鍵盤到鍵盤發(fā)出聲音的流程。
6、遇到的坑

二、詳細(xì)實現(xiàn)。

1、更改系統(tǒng)鍵盤背景圖片.
(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方法中為根布局設(shè)置理想中的背景圖

1510714059(1).png

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


1510713991(1).png

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


1510714799(1).png

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


1510715166(1).png

1510715137(1).png

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


1510716009(1).png

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


1510715797(1).png

經(jīng)過3,4系統(tǒng)鍵盤的風(fēng)格已經(jīng)更改完成。

4、更改系統(tǒng)鍵盤提示字符的大小
將提示字符大小改成原來的兩倍


1510717395(1).png

5、從手指點擊鍵盤到鍵盤發(fā)出聲音的流程。

了解Android系統(tǒng)事件分發(fā)機(jī)制,應(yīng)該不難理解,當(dāng)手指點擊屏幕的時候會經(jīng)過一番處理最后會調(diào)用OnTouchEvent方法,鍵盤也不例外,當(dāng)點擊鍵盤某個按鍵的時候,會調(diào)用packages/inputmethods/PinyinIME/com/android/inputmethod/pinyin/SkbContainer.java類中的onTouchEvent(...)方法,在該方法中會調(diào)用關(guān)鍵方法 mSkv.onKeyPress(...),其中mSkv是packages/inputmethods/PinyinIME/com/android/inputmethod/pinyin/SoftKeyboardView.java類的一個對象。onTouchEvent(...)方法所對應(yīng)的代碼如下所示:

 @Override
public boolean onTouchEvent(MotionEvent event) {
    super.onTouchEvent(event);
    ...
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        ...
       //收到按下事件,經(jīng)過一番處理會調(diào)用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;
               //收到移動事件,經(jīng)過一番處理會調(diào)用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(...)方法,該方法主要是根據(jù)用戶觸摸的坐標(biāo)計算出當(dāng)前點擊的是鍵盤的哪個按鍵,判斷是否需要發(fā)出按鍵聲音。

 public SoftKey onKeyPress(int x, int y,
        SkbContainer.LongPressTimer longPressTimer, boolean movePress) {
   //mSoftKeyDown 用于記錄當(dāng)前按下的按鍵Softkey
    mKeyPressed = false;
    boolean moveWithinPreviousKey = false;//判斷是否在上一個SoftKey上移動
     
    if (movePress) {
        //獲取當(dāng)前點擊的SoftKey
        SoftKey newKey = mSoftKeyboard.mapToKey(x, y);
         //如果直接的SoftKey和新的SoftKey相同則直接設(shè)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類中,該方法調(diào)用了聲音管理器SoundManager中的playKeyDown方法。

  private void tryPlayKeyDown() {
    //判斷系統(tǒng)鍵盤音是否已經(jīng)開啟,只有開啟了才有聲音
    if (Settings.getKeySound()) {
        //調(diào)用播放接口
        mSoundManager.playKeyDown();
   }
}

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

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

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

  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是一個接口,其主要實現(xiàn)類是AudioService.java,其中playSoundEffectVolume(...)方法進(jìn)行了正在的音頻調(diào)用與播放,如下所示:

   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 {
               到這里大家應(yīng)用就很熟悉了
                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();
                    //設(shè)置播放音頻的音量
                    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、不停的點擊按鍵值,快速按完成,出現(xiàn)鍵盤彈出,界面混亂的現(xiàn)象,正常情況應(yīng)該鍵盤隱藏。
(1)原因:當(dāng)收到完成的指令時,鍵盤應(yīng)該隱藏,由于鍵盤隱藏的過程中有一個200ms的動畫,如果在200ms之內(nèi)沒有完成隱藏指令,又收到了按鍵值,再隱藏的過程中就會把鍵盤再一次顯示,所以出現(xiàn)了難以預(yù)料的問題。
(2)解決方式:
設(shè)置一個標(biāo)簽,用于判斷鍵盤是否正在執(zhí)行隱藏操作。在frameworks\base\core\java\android\inputmethodservice\InputMethodService.java類hideWindow()方法中,實現(xiàn)如下代碼。


image.png

由于packages\inputmethods\PinyinIME類繼承InputMethodService,所以可以直接調(diào)用InputMethodService的公共方法,在PinyinIME.java類的onKeyDown和onKeyDownUp方法中判斷,當(dāng)aimationStarting()等于true的時候,不要往下執(zhí)行,直接返回。解決如下:


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

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,366評論 25 708
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,724評論 19 139
  • 【小太陽《零極限》 D2】 跟你身體對話,跟它說:“我愛你現(xiàn)在的樣子。謝謝你一直與我同在。如果我曾經(jīng)對你有任何怠...
    小太陽小姐閱讀 104評論 0 0
  • 何其冷冰冰 也不見月色 細(xì)雨霏霏潛在幽幽夜里 朦朧里那一絲燭光搖曳 有家的人怎么還不回家 輾轉(zhuǎn)...
    瞞青兮閱讀 201評論 4 7
  • 塵封是一念銘記,只是釋放蘊藏的驚喜需要塵緣的溫融還以本色。塵封是愿力的蟄伏,時機(jī)未到無法彰顯洪荒的無畏。塵封的人和...
    鼎森書院胡老師閱讀 282評論 0 1

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