Android 自定义键盘实现

作者: kangqiao182 | 来源:发表于2016-11-02 14:24 被阅读5582次

    最近项目中在做一个股票交易需求升级, 产品对于输入方式有一些特殊的要求, 具体就是对于输入键盘加了诸多限制. 这就必须需要自定义键盘来完成需求.

    效果如下:


    股票交易键盘.png

    具体需求:

    • 当焦点在股票价格编辑框上时, 键盘弹出时不能遮盖住卖出数量.
      即键盘弹出是以两个输入框底部为基线的.
    • 键盘弹击要有一个向上推出的动画效果.
    • 两个输入框弹出不同的键盘界面,
      股票价格输入框 弹出数字键盘
      股票数量输入框 弹出数量键盘(如上图)

    最终的实现效果:


    最终效果.gif

    简书上找了些自定义键盘的例子, 基本都不能满足我的需求, 但是给了我一个很好的切入点. 在此非常感谢!
    参考其实现, 我做了些封装. 做了一个自定义键盘的工具类,

    设计原则:与外界充分解耦,通过自定议键盘管理者, 绑定对应输入框和键盘,键盘的实现者仅需要关注特殊按键的响应处理.

    设计原理:通过传入activity获得其DecorView,添加键盘布局。将键盘布局set到屏幕底部,当输入框获得焦点时,如果设置了基线view, 则判断基线view所在位置, 否则默认以输入框为基线View,若键盘弹出会遮挡基线View,则屏幕整体向上滑动一定的距离:
    屏幕移动高度为:
    移动距离 = 基线View到屏幕顶部距离 + 自定义键盘高度 - 整个屏幕高度
    if 移动距离 > 0 则说明当键盘加入到根布局后, 屏幕无法完成加载, 需要屏幕向上滚动一定的偏移量.
    if 移动距离 <= 0 则说明键盘弹出后还没有达到基线设置位置, 不需要滚动整个屏幕.

    计算屏幕需要移动的偏移量:

        /**
         * 计算屏幕向上移动距离
         * @param view 响应输入焦点的控件
         * @return 移动偏移量
         */
        private int getMoveHeight(View view) {
            Rect rect = new Rect();
            mRootView.getWindowVisibleDisplayFrame(rect); //获取当前显示区域的宽高
    
            int[] vLocation = new int[2];
            view.getLocationOnScreen(vLocation); //计算输入框在屏幕中的位置
            int keyboardTop = vLocation[1] + view.getHeight() + view.getPaddingBottom() + view.getPaddingTop();
            if (keyboardTop - mKeyboardHeight < 0) { //如果输入框到屏幕顶部已经不能放下键盘的高度, 则不需要移动了.
                return 0;
            }
            if (null != mShowUnderView) { //如果有基线View. 则计算基线View到屏幕的距离
                int[] underVLocation = new int[2];
                mShowUnderView.getLocationOnScreen(underVLocation);
                keyboardTop = underVLocation[1] + mShowUnderView.getHeight() + mShowUnderView.getPaddingBottom() + mShowUnderView.getPaddingTop();
            }
            //输入框或基线View的到屏幕的距离 + 键盘高度 如果 超出了屏幕的承载范围, 就需要移动.
            int moveHeight = keyboardTop + mKeyboardHeight - rect.bottom;
            return moveHeight > 0 ? moveHeight : 0;
        }
    

    显示自定义的键盘:

        public void showSoftKeyboard(EditText view) {
            BaseKeyboard keyboard = getKeyboard(view); //获取输入框所绑定的键盘BaseKeyboard
            if (null == keyboard) {
                Log.e(TAG, "The EditText not bind BaseKeyboard!");
                return;
            }
            keyboard.setCurEditText(view);
            keyboard.setNextFocusView(etFocusScavenger); //为键盘设置下一个焦点响应控件.
            refreshKeyboard(keyboard); //设置键盘keyboard到KeyboardView中.
    
            //将键盘布局加入到根布局中.
            mRootView.addView(mKeyboardViewContainer, mKeyboardViewLayoutParams);
            //设置加载动画.
            mKeyboardViewContainer.setAnimation(AnimationUtils.loadAnimation(mActivity, R.anim.down_to_up));
    
            int moveHeight = getMoveHeight(view);
            if (moveHeight > 0) {
                mRootView.getChildAt(0).scrollBy(0, moveHeight); //移动屏幕
            } else {
                moveHeight = 0;
            }
    
            view.setTag(R.id.keyboard_view_move_height, moveHeight);
        }
    

    隐藏自定义的键盘

        public void hideSoftKeyboard(EditText view) {
            int moveHeight = 0;
            Object tag = view.getTag(R.id.keyboard_view_move_height);
            if (null != tag) moveHeight = (int) tag;
            if (moveHeight > 0) { //复原屏幕
                mRootView.getChildAt(0).scrollBy(0, -1 * moveHeight);
                view.setTag(R.id.keyboard_view_move_height, 0);
            }
    
            mRootView.removeView(mKeyboardViewContainer); //将键盘从根布局中移除.
    
            mKeyboardViewContainer.setAnimation(AnimationUtils.loadAnimation(mActivity, R.anim.up_to_hide));
        }
    

    为了适应不同的键盘布局, 有必要定义一个Keyboard的基类, 所有的自定义键盘都继承于它. 并且它响应KeyboardView.OnKeyboardActionListener的所有接口.

    public abstract class CustomBaseKeyboard extends Keyboard implements KeyboardView.OnKeyboardActionListener{
    
        protected EditText etCurrent;
        protected View nextFocusView;
        protected CustomKeyStyle customKeyStyle;
    
        public CustomBaseKeyboard(Context context, int xmlLayoutResId) {
            super(context, xmlLayoutResId);
        }
    
        public CustomBaseKeyboard(Context context, int xmlLayoutResId, int modeId, int width, int height) {
            super(context, xmlLayoutResId, modeId, width, height);
        }
    
        public CustomBaseKeyboard(Context context, int xmlLayoutResId, int modeId) {
            super(context, xmlLayoutResId, modeId);
        }
    
        public CustomBaseKeyboard(Context context, int layoutTemplateResId, CharSequence characters, int columns, int horizontalPadding) {
            super(context, layoutTemplateResId, characters, columns, horizontalPadding);
        }
    
        protected int getKeyCode(int resId) {
            if (null != etCurrent) {
                return etCurrent.getContext().getResources().getInteger(resId);
            } else {
                return Integer.MIN_VALUE;
            }
        }
    
        public void setCurEditText(EditText etCurrent) {
            this.etCurrent = etCurrent;
        }
    
        public EditText getCurEditText() {
            return etCurrent;
        }
    
        public void setNextFocusView(View view) {
            this.nextFocusView = view;
        }
    
        public CustomKeyStyle getCustomKeyStyle() {
            return customKeyStyle;
        }
    
        public void setCustomKeyStyle(CustomKeyStyle customKeyStyle) {
            this.customKeyStyle = customKeyStyle;
        }
    
        @Override
        public void onPress(int primaryCode) {
    
        }
    
        @Override
        public void onRelease(int primaryCode) {
    
        }
    
        @Override
        public void onKey(int primaryCode, int[] keyCodes) {
            if (null != etCurrent && etCurrent.hasFocus() && !handleSpecialKey(etCurrent, primaryCode)) {
                Editable editable = etCurrent.getText();
                int start = etCurrent.getSelectionStart();
    
                if (primaryCode == Keyboard.KEYCODE_DELETE) { //回退
                    if (!TextUtils.isEmpty(editable)) {
                        if (start > 0) {
                            editable.delete(start - 1, start);
                        }
                    }
                } else if (primaryCode == getKeyCode(R.integer.keycode_empty_text)) { //清空
                    editable.clear();
                } else if (primaryCode == getKeyCode(R.integer.keycode_hide_keyboard)) { //隐藏
                    hideKeyboard();
                } else if (primaryCode == 46) { //小数点
                    if (!editable.toString().contains(".")) {
                        editable.insert(start, Character.toString((char) primaryCode));
                    }
                } else { //其他默认
                    editable.insert(start, Character.toString((char) primaryCode));
                }
            }
            //getKeyboardView().postInvalidate();
        }
    
        public void hideKeyboard() {
            //hideSoftKeyboard(etCurrent);
            if (null != nextFocusView) nextFocusView.requestFocus();
        }
    
        /**
         * 处理自定义键盘的特殊定制键
         * 注: 所有的操作要针对etCurrent来操作
         *
         * @param etCurrent   当前操作的EditText
         * @param primaryCode 选择的Key
         * @return true: 已经处理过, false: 没有被处理
         */
        public abstract boolean handleSpecialKey(EditText etCurrent, int primaryCode);
    ...... //其它的默认空实现
    
    }
    

    当自定义键盘时, 仅需要去实现handleSpecialKey接口, 处理键盘中自定义键
    在BaseKeyboard中已经默认实现了基础的输入字符, 和 回退, 清空, 隐藏.
    当然在构造时也必须传入Keyboard所必需的参数 context 和 键盘布局xml
    如下:

            customKeyboardManager = new CustomKeyboardManager(mActivity);
    
            CustomKeyboardManager.BaseKeyboard priceKeyboard = new CustomKeyboardManager.BaseKeyboard(getContext(), R.xml.stock_price_num_keyboard) {
                @Override
                public boolean handleSpecialKey(EditText etCurrent, int primaryCode) {
                    if (primaryCode == getKeyCode( R.integer.keycode_cur_price)) {
                        etCurrent.setText("9.99");
                        return true;
                    }
                    return false;
                }
            };
            //为etInputPrice1和etInputPrice2都定制priceKeyboard键盘.
            customKeyboardManager.attachTo(etInputPrice1, priceKeyboard);
            customKeyboardManager.attachTo(etInputPrice2, priceKeyboard);
            
            customKeyboardManager.attachTo(etInputNum, new CustomKeyboardManager.BaseKeyboard(getContext(), R.xml.stock_trade_num_keyboard) {
                @Override
                public boolean handleSpecialKey(EditText etCurrent, int primaryCode) {
                    Editable editable = etCurrent.getText();
                    int start = etCurrent.getSelectionEnd();
                    if (primaryCode == getKeyCode( R.integer.keycode_stocknum_000)) {
                        editable.insert(start, "000");
                        return true;
                    } else if (primaryCode == getKeyCode(R.integer.keycode_stocknum_all)){ //全仓
                        setStockNumAll(etCurrent);
                        return true;
                    }
                    return false;
                }
            });
            customKeyboardManager.setShowUnderView(underView); //设置键盘弹出所达到的基线View
    

    另外在attachTo(editText, baseKeyboard)时, 会设置editText隐藏系统键盘. 设置其绑定的keyboard, 设置FocusChangeListener事件监听.
    下面是键盘布局:

    <?xml version="1.0" encoding="UTF-8"?><!-- 数字键盘 -->
    <Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
        android:horizontalGap="2dp"
        android:keyHeight="62dp"
        android:keyWidth="20%p"
        android:verticalGap="2dp">
        <Row>
            <Key
                android:codes="@integer/keycode_stocknum_all"
                android:keyEdgeFlags="left"
                android:keyLabel="全仓"/>
    
            <Key
                android:codes="49"
                android:keyLabel="1" />
    
            <Key
                android:codes="50"
                android:keyLabel="2" />
    
            <Key
                android:codes="51"
                android:keyLabel="3" />
    
            <Key
                android:codes="-5"
                android:keyLabel="回退"
                android:iconPreview="@drawable/bg_custom_key_light_gray"/>
        </Row>
    
        <Row>
            <Key
                android:codes="@integer/keycode_stocknum_half"
                android:keyEdgeFlags="left"
                android:keyLabel="半仓"/>
    
            <Key
                android:codes="52"
                android:keyLabel="4" />
    
            <Key
                android:codes="53"
                android:keyLabel="5" />
    
            <Key
                android:codes="54"
                android:keyLabel="6" />
    
            <Key
                android:codes="@integer/keycode_empty_text"
                android:keyLabel="清空"
                android:iconPreview="@drawable/bg_custom_key_light_gray"/>
        </Row>
    
        <Row>
            <Key
                android:codes="@integer/keycode_stocknum_1_3"
                android:keyEdgeFlags="left"
                android:keyLabel="1/3仓"/>
    
            <Key
                android:codes="55"
                android:keyLabel="7" />
    
            <Key
                android:codes="56"
                android:keyLabel="8" />
    
            <Key
                android:codes="57"
                android:keyLabel="9" />
    
            <Key
                android:codes="@integer/keycode_hide_keyboard"
                android:keyLabel="隐藏"
                android:iconPreview="@drawable/bg_custom_key_light_gray"/>
        </Row>
    
        <Row>
            <Key
                android:codes="@integer/keycode_stocknum_1_4"
                android:keyEdgeFlags="left"
                android:keyLabel="1/4仓"
                android:keyWidth="20%p"/>
    
            <Key
                android:codes="@integer/keycode_stocknum_000"
                android:isRepeatable="true"
                android:keyLabel="000"
                android:keyWidth="20%p"/>
    
            <Key
                android:codes="48"
                android:keyLabel="0"
                android:keyWidth="20%p"/>
    
            <Key
                android:codes="@integer/keycode_stock_sell"
                android:keyLabel="卖出"
                android:iconPreview="@drawable/bg_custom_key_blue"
                android:keyWidth="40%p"/>
        </Row>
    </Keyboard>
    

    对于我们特殊定制的key的code为了唯一性的原则, 这里将其统一定义在res/values/custom_keyboard.xml中

        <!--股票数量键盘-->
        <integer name="keycode_stocknum_000">-10200</integer>
        <integer name="keycode_stocknum_all">-10201</integer>
        <integer name="keycode_stocknum_half">-10202</integer>
        <integer name="keycode_stocknum_1_3">-10203</integer>
        <integer name="keycode_stocknum_1_4">-10204</integer>
        <integer name="keycode_stock_sell">-10205</integer>
    

    可是至此, 仍有一个问题没法解决, 那就是对于每个Key的样式的定制. 看遍源码中, 也没有找到关于这些设置, 有的只是针对KeyboardView的设置. 但是这些设置会统一应用到所有按键上, 还是无法实现对每个按键的独立定制样式.

    //源码中对xml布局中key的解析如下: 
            public Key(Resources res, Row parent, int x, int y, XmlResourceParser parser) {
                this(parent);
                ...........
                width = getDimensionOrFraction(a, 
                        com.android.internal.R.styleable.Keyboard_keyWidth,
                        keyboard.mDisplayWidth, parent.defaultWidth);
                height = getDimensionOrFraction(a, 
                        com.android.internal.R.styleable.Keyboard_keyHeight,
                        keyboard.mDisplayHeight, parent.defaultHeight);
                gap = getDimensionOrFraction(a, 
                        com.android.internal.R.styleable.Keyboard_horizontalGap,
                        keyboard.mDisplayWidth, parent.defaultHorizontalGap);
                ........
    

    源码参考:
    http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/inputmethodservice/Keyboard.java#331

    难道以上都白做了么?
    ...
    ...
    ...

    经过一番细读源码, 决定对KeyboardView进行扩展.

    • 首先Keyboard描述了键盘的布局(通过给定的xml),并解析它,
      CustomBaseKeyboard及其实现,扩展了其对按键的处理与EditText的联系.
    • KeyboardView 是承载不同的keyboard并绘制keyboard, 就像是键盘布局的绘制板, 并与系统交互.

    扩展思路:
    通过扩展的KeyboardView, 对其绘制过程做定制操作, 就可以实现对每个按键样式的定制了

    而KeyboardView的绘制过程并没有给我们任何机会去对其扩展定制.
    源码参考
    http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/inputmethodservice/KeyboardView.java#634
    为此只能通过对KeyboardView的重新绘制才能实现.
    具体就是重写onDraw方法, 在onDraw方法中通过接口调用实现定制.
    并用反射的方法解决需要依赖的KeyboardView中的属性.
    代码片段如下:

    public class CustomKeyboardView extends KeyboardView {
        private static final String TAG = "CustomKeyboardView";
        private Drawable rKeyBackground;
        private int rLabelTextSize;
        private int rKeyTextSize;
        private int rKeyTextColor;
        private float rShadowRadius;
        private int rShadowColor;
    
        private Rect rClipRegion;
        private Keyboard.Key rInvalidatedKey;
        ...........
        private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            rKeyBackground = (Drawable) ReflectionUtils.getFieldValue(this, "mKeyBackground");
            rLabelTextSize = (int) ReflectionUtils.getFieldValue(this, "mLabelTextSize");
            rKeyTextSize = (int) ReflectionUtils.getFieldValue(this, "mKeyTextSize");
            rKeyTextColor = (int) ReflectionUtils.getFieldValue(this, "mKeyTextColor");
            rShadowColor = (int) ReflectionUtils.getFieldValue(this, "mShadowColor");
            rShadowRadius = (float) ReflectionUtils.getFieldValue(this, "mShadowRadius");
        }
    
        @Override
        public void onDraw(Canvas canvas) {
            //说明CustomKeyboardView只针对CustomBaseKeyboard键盘进行重绘,
            // 且CustomBaseKeyboard必需有设置CustomKeyStyle的回调接口实现, 才进行重绘, 这才有意义
            if(null == getKeyboard() || !(getKeyboard() instanceof CustomBaseKeyboard) || null == ((CustomBaseKeyboard)getKeyboard()).getCustomKeyStyle()){
                Log.e(TAG, "");
                super.onDraw(canvas);
                return;
            }
            rClipRegion = (Rect) ReflectionUtils.getFieldValue(this, "mClipRegion");
            rInvalidatedKey = (Keyboard.Key) ReflectionUtils.getFieldValue(this, "mInvalidatedKey");
            super.onDraw(canvas);
            onRefreshKey(canvas);
        }
    
        /**
         * onRefreshKey是对父类的private void onBufferDraw()进行的重写. 只是在对key的绘制过程中进行了重新设置.
         * @param canvas
         */
        private void onRefreshKey(Canvas canvas) {
            ........
    
            //拿到当前键盘被弹起的输入源 和 键盘为每个key的定制实现customKeyStyle
            EditText etCur = ((CustomBaseKeyboard)getKeyboard()).getCurEditText();
            CustomBaseKeyboard.CustomKeyStyle customKeyStyle = ((CustomBaseKeyboard)getKeyboard()).getCustomKeyStyle();
    
            List<Keyboard.Key> keys = getKeyboard().getKeys();
            final int keyCount = keys.size();
            //canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
            for (int i = 0; i < keyCount; i++) {
                final Keyboard.Key key = keys.get(i);
    
                //获取为Key自定义的背景, 若没有定制, 使用KeyboardView的默认属性keyBackground设置
                keyBackground = customKeyStyle.getKeyBackground(key, etCur);
                if(null == keyBackground){ keyBackground = rKeyBackground; }
                ......
                //获取为Key自定义的Label, 若没有定制, 使用xml布局中指定的
                CharSequence keyLabel = customKeyStyle.getKeyLabel(key, etCur);
                 .....
                canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop);
                keyBackground.draw(canvas);
    
                if (label != null) {
                    //获取为Key的Label的字体大小, 若没有定制, 使用KeyboardView的默认属性keyTextSize设置
                    Float customKeyTextSize = customKeyStyle.getKeyTextSize(key, etCur);
                    // For characters, use large font. For labels like "Done", use small font.
                    if(null != customKeyTextSize){
                        paint.setTextSize(customKeyTextSize);
                        paint.setTypeface(Typeface.DEFAULT_BOLD);
                    } else {
                       ....
                    }
    
                    //获取为Key的Label的字体颜色, 若没有定制, 使用KeyboardView的默认属性keyTextColor设置
                    Integer customKeyTextColor = customKeyStyle.getKeyTextColor(key, etCur);
                    if(null != customKeyTextColor) {
                        paint.setColor(customKeyTextColor);
                    } else {
                        paint.setColor(rKeyTextColor);
                    }
       
    

    具体的定制样式接口在CustomBaseKeyboard中定义:

      public interface CustomKeyStyle {
            Drawable getKeyBackground(Key key, EditText etCur);
    
            Float getKeyTextSize(Key key, EditText etCur);
    
            Integer getKeyTextColor(Key key, EditText etCur);
    
            CharSequence getKeyLabel(Key key, EditText etCur);
        }
    

    为了保证我们自定义的键盘都能够在使用了CustomKeyboardView时, 都能进行重绘, 在CustomKeyboardManager的attachTo中还要主动为其设置一个默认的实现.

        public void attachTo(EditText editText, CustomBaseKeyboard keyboard) {
            hideSystemSoftKeyboard(editText);
            editText.setTag(R.id.edittext_bind_keyboard, keyboard);
            if(null == keyboard.getCustomKeyStyle()) keyboard.setCustomKeyStyle(defaultCustomKeyStyle);
            editText.setOnFocusChangeListener(this);
        }
    

    在使用的时候就需要加入对keyboard的样式设置

            numKeyboard.setCustomKeyStyle(new CustomBaseKeyboard.SimpleCustomKeyStyle(){
                @Override
                public Drawable getKeyBackground(Keyboard.Key key, EditText etCur) {
                    if(getKeyCode(etCur.getContext(), R.integer.keycode_stock_sell) == key.codes[0]) {
                        if (R.id.et_input_num_sell == etCur.getId()) {
                            return getDrawable(etCur.getContext(), R.drawable.bg_custom_key_blue);
                        } else if (R.id.et_input_num_buy == etCur.getId()) {
                            return getDrawable(etCur.getContext(), R.drawable.bg_custom_key_red);
                        }
                    }
                    return super.getKeyBackground(key, etCur);
                }
    
                @Override
                public CharSequence getKeyLabel(Keyboard.Key key, EditText etCur) {
                    if(getKeyCode(etCur.getContext(), R.integer.keycode_stock_sell) == key.codes[0]) {
                        if (R.id.et_input_num_sell == etCur.getId()) {
                            return "卖出";
                        } else if (R.id.et_input_num_buy == etCur.getId()) {
                            return "买入";
                        }
                    }
                    return super.getKeyLabel(key, etCur);
                }
            });
    

    文中代码多有省略, 时间仓促且本人能力有限, 仅是对当前项目中的实现做的�定制, 不一定能适用所有的项目, 只是提供了一种参考实现, 相信一定有更好的解决方案, 还请留下你的思路方案, 共同进步, 如有缺陷还请留言, 共同解决成长! _

    参考:
    http://www.jianshu.com/p/8fb70cadca27
    http://www.jianshu.com/p/aedf6f456560
    http://931360439-qq-com.iteye.com/blog/938886
    具体请参考我的Github
    https://github.com/kangqiao182/CustomKeyboard

    相关文章

      网友评论

      • 羽纱:你好,请教你一个问题,在你源码中看到了hideSystemSoftKeyboard,主动把系统键盘给隐藏了,那使用KeyBoardView的意义何在,它与随便写个view添加到屏幕底部作为键盘有什么区别?

      本文标题:Android 自定义键盘实现

      本文链接:https://www.haomeiwen.com/subject/lvsmuttx.html