android应用框架Andorid的好东西Android效果/自定义

Android 自定义键盘实现

2016-11-02  本文已影响5582人  kangqiao182

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

效果如下:


股票交易键盘.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进行扩展.

扩展思路:
通过扩展的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

上一篇下一篇

猜你喜欢

热点阅读