自定义系统键盘

   注意: 写该文章主要帮助自己记忆,贴出来希望可以给有同样问题的人解惑,不喜勿喷,可以提意见哦。

一,键盘更改内容

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方法中为根布局设置理想中的背景图

自定义系统键盘_第1张图片
1510714059(1).png

这样设置了之后,不会得到自己想要的效果,背景图仍然没有切换,最重要的是接下来的一步,在InputMethodService中找到方法updateFullscreenMode(),注释掉图中的两行,这下就达到了想要的效果。


自定义系统键盘_第2张图片
1510713991(1).png

2、更改系统键盘编辑框的风格
将编辑框的文字颜色改为白色,更换编辑框的背景颜色,离各边距2dp,在InputMethodService.java中找到setExtraView(View view)方法,增加红框中的内容。


1510714799(1).png

3、更改系统键盘的位置
在InputMethodService中找到方法showWindowInner(),将键盘的长设为原来的0.56,宽设为原来的一半,改变键盘的位置为右下角,增加下图红框中的内容,虽然键盘大小已经更改,但是内部只显示了一半的键盘字数。


自定义系统键盘_第3张图片
1510715166(1).png

自定义系统键盘_第4张图片
1510715137(1).png

4、显示完整的键盘字数
在packages\inputmethods\PinyinIME\src\com\android\inputmethod\pinyin路径下找到SkbContainer.java
(1)找到onMeasure方法,更改测量的宽。


自定义系统键盘_第5张图片
1510716009(1).png

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


自定义系统键盘_第6张图片
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的时候,不要往下执行,直接返回。解决如下:


自定义系统键盘_第7张图片
image.png

你可能感兴趣的:(自定义系统键盘)