自定义安全键盘——仿民生银行

系统自带键盘和第三方的键盘不管是从性能还是从体验上来说都要胜于我们自己写的,但我们为什么还要去自定义键盘呢?其实就为了安全性,比如用户在输入账户密码,支付密码的时候,防止键盘获取到我们的数据;或者说美工要求Android的键盘需要和IOS的一样,那我们就得自己去写个键盘了。
效果图:
自定义安全键盘——仿民生银行_第1张图片
keyboardView.gif

一:键盘布局

  • XML布局属性
自定义安全键盘——仿民生银行_第2张图片
Keyboard.Key的属性.png
自定义安全键盘——仿民生银行_第3张图片
KeyboardView的属性.png
自定义安全键盘——仿民生银行_第4张图片
Keyboard.Row的属性.png
自定义安全键盘——仿民生银行_第5张图片
ASCII值.png

Key和Row为Keyboard的静态内部类,从上面的图中我们能看到每个属性对应的Description,在这里再简单介绍一下。

Key的xml属性 属性说明
android:codes 表示键Key的标签,字母/符号/数字对应的ASCII值。
android:iconPreview 表示键点击后放大后的View
android:isModifier 按键是否为功能键,例如Alt/Shift/Ctrl键。取值为true或false。
android:isRepeatable 表示长时间按下key键重复执行这个键的操作,如:长按删除键会一直执行删除操作。
android:isSticky 指定按键是否为状态键。如:Shift大小写切换按键,具有两种状态,按下状态和正常状态,取值为true或则false。
android:keyEdgeFlags 指定按键的对齐指令,取值为left或则right。
android:keyIcon 按键图标,如果指定了该值则文本属性无效。
android:keyLabel 代表按键显示的文本内容。
android:keyOutputText 指定按键输出的文本内容,取值为字符串。
android:popupCharacters 表示编辑特殊字符,空格、换行等.。
android:popupKeyboard 表示按键点击预览窗口。
android:horizontalGap 键的水平间隙,当前键的左边的水平间隙。
KeyboardView的xml属性 属性说明
android:background 设置整个键盘的背景色
android:keyPreviewHeight 正在输入时弹出预览框的高度
android:keyPreviewLayout 输入时预览框的布局样式,要求根布局为TextView。
android:keyTextColor 键的字体颜色
android:keyTextSize 键的字体大小
android:labelTextSize 字符串键文本字体大小
android:shadowColor、android:shadowRadius 设置这两个属性可以很好的解决键的字体发虚的问题,设置shadowColor值和键的字体颜色相同。
android:keyBackground 设置键的背景色,可以用drawable中的selector标签设置键的正常状态样式和按下样式
  • 英文键盘



    
        
        
        
        
        
        
        
        
        
        
    

    
        
        
        
        
        
        
        
        
        
    

    
        
        
        
        
        
        
        
        
        
    

    
        
        
        
    


  • 数字键盘



    
        
        
        
    

    
        
        
        
    

    
        
        
        
    

    
        
        
        
    

 

在键盘布局的时候,要保证键所占的宽度(包括间隙)所占的比例和为100%,不然整个键盘的右边会过宽或过窄。如:数字键盘的第一行:2+30+3+30+3+30+2=100,第一个2和最后一个2是键盘离屏幕的距离。

二:代码实现

  • 自定义键的按下效果
    我们有时候可能需要为删除键设置这样的按下效果,为退格键设置那样的效果;设置键的按下效果可通过KeyboardView的android:keyBackground属性进行设置,但是所有键的按下效果都是一样的,不能满足我们的需求;但是我们可以继承系统KeyboardView,在onDraw方法中通过键的code(ASCII值)为不同的键绘制不同的背景、图标、文本。
public class SKeyboardView extends KeyboardView {
    private Context context;
    private Rect rect;
    private Paint paint;

    private int keyboardType = -1;

    public SKeyboardView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        initSKeyboardView();
    }

    public SKeyboardView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        initSKeyboardView();
    }

    /**
     * 初始化画笔等
     */
    private void initSKeyboardView() {
        rect = new Rect();

        paint = new Paint();
        paint.setTextSize(70);
        paint.setAntiAlias(true);
        paint.setColor(Color.BLACK);
        paint.setTextAlign(Paint.Align.CENTER);
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (getKeyboard() == null) {
            return;
        }
        List keys = getKeyboard().getKeys();

        if (keyboardType == 0) {// 数字键盘
            drawKeyboardNumber(keys, canvas);
        } else if (keyboardType == 1) {// 英文键盘
            drawKeyboardEnglish(keys, canvas);
        }
    }

    /**
     * 绘制数字键盘
     *
     * @param keys
     * @param canvas
     */
    private void drawKeyboardNumber(List keys, Canvas canvas) {
        for (Keyboard.Key key : keys) {
            if (key.codes[0] == -5) {//删除键
                drawKeyBackground(R.drawable.img_edit_clear, canvas, key);
            }
        }
    }

    /**
     * 绘制英文键盘
     *
     * @param keys
     * @param canvas
     */
    private void drawKeyboardEnglish(List keys, Canvas canvas) {
        for (Keyboard.Key key : keys) {
            if (key.codes[0] == -5) {//删除键
                drawKeyBackground(R.drawable.img_edit_clear, canvas, key);
            }
            if (key.codes[0] == -1) {//大小写切换
                drawKeyBackground(R.drawable.img_edit_clear, canvas, key);
            }
            if (key.codes[0] == 32) {//space

            }
            if (key.codes[0] == -4) {//完成

            }
        }
    }

    /**
     * 设置当前键盘标识 0:数字键盘;1:英文键盘
     *
     * @param keyboardType
     */
    public void setCurrentKeyboard(int keyboardType) {
        this.keyboardType = keyboardType;
        invalidate();
    }

    /**
     * 绘制键盘key的背景
     *
     * @param drawableId 将要绘制上去的图标
     * @param canvas
     * @param key        需要绘制的键
     */
    private void drawKeyBackground(int drawableId, Canvas canvas, Keyboard.Key key) {
        Drawable npd = ResUtil.getDrawable(drawableId);
        int[] drawableState = key.getCurrentDrawableState();
        if (key.codes[0] != 0) {
            npd.setState(drawableState);
        }
        npd.setBounds(key.x, key.y, key.x + key.width, key.y + key.height);
        npd.draw(canvas);
    }

    /**
     * 绘制字体
     *
     * @param canvas
     * @param key
     */
    private void drawKeyText(Canvas canvas, Keyboard.Key key) {
        if (keyboardType == 0) {
            if (key.label != null) {
                paint.getTextBounds(key.label.toString(), 0, key.label.toString().length(), rect);
                canvas.drawText(key.label.toString(), key.x + (key.width / 2), (key.y + key.height / 2) + rect.height() / 2, paint);
            }
        } else if (keyboardType == 1) {
            if (key.label != null) {
                paint.getTextBounds(key.label.toString(), 0, key.label.toString().length(), rect);
                canvas.drawText(key.label.toString(), key.x + (key.width / 2), (key.y + key.height / 2) + rect.height() / 2, paint);
            }
        }
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
    }
}
  • 输入的控制逻辑处理
    由于在应用中只有在输入安全性要求较高的地方才会用到我们自定义的键盘,所以我就继承EditText,在子类中写相关控制逻辑,避免在使用的时候写更多代码。
public class EditView extends EditText implements SKeyboardView.OnKeyboardActionListener {
    private Context context;

    private Keyboard keyboardNumber;
    private Keyboard keyboardEnglish;
    private ViewGroup viewGroup;
    private SKeyboardView keyboardView;

    //标识数字键盘和英文键盘的切换
    private boolean isShift = true;
    //标识英文键盘大小写切换
    private boolean isCapital = false;

    //点击【完成】、键盘隐藏、键盘显示时的回调
    private OnKeyboardListener onKeyboardListener;

    public EditView(Context context) {
        this(context, null);
    }

    public EditView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public EditView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        initEditView();
    }

    /**
     * 初始化自定义键盘
     */
    private void initEditView() {
        keyboardNumber = new Keyboard(context, R.xml.keyboard_number);
        keyboardEnglish = new Keyboard(context, R.xml.keyboard_english);
    }

    /**
     * 设置键盘
     *
     * @param viewGroup
     * @param keyboardView
     * @param isNumber     true:表示默认数字键盘,false:表示默认英文键盘
     */
    public void setEditView(ViewGroup viewGroup, SKeyboardView keyboardView, boolean isNumber) {
        this.viewGroup = viewGroup;
        this.keyboardView = keyboardView;
        this.isShift = isNumber;

        if (isNumber) {
            keyboardView.setKeyboard(keyboardNumber);
            keyboardView.setCurrentKeyboard(0);
        } else {
            keyboardView.setKeyboard(keyboardEnglish);
            keyboardView.setCurrentKeyboard(1);
        }
        keyboardView.setEnabled(true);
        keyboardView.setPreviewEnabled(!isNumber);
        keyboardView.setOnKeyboardActionListener(this);
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        SystemUtil.closeKeyboard(this);
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        SystemUtil.closeKeyboard(this);
        keyboardView = null;
        viewGroup = null;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        requestFocus();
        requestFocusFromTouch();
        SystemUtil.closeKeyboard(this);
        if (event.getAction() == MotionEvent.ACTION_UP) {
            if (!isShow()) {
                show();
            }
        }
        return true;
    }

    @Override
    public void onPress(int primaryCode) {
        if (onKeyboardListener != null) {
            onKeyboardListener.onPress(primaryCode);
        }
        if (isShift) {
            return;
        }
        setPreview(primaryCode);
    }

    @Override
    public void onRelease(int primaryCode) {
        switch (primaryCode) {
            case Keyboard.KEYCODE_DONE:// 完成-4
                hide(true);
                break;

            default:
                break;
        }
    }

    @Override
    public void onKey(int primaryCode, int[] ints) {
        Editable editable = getText();
        int start = getSelectionStart();

        switch (primaryCode) {
            case Keyboard.KEYCODE_MODE_CHANGE:// 英文键盘与数字键盘切换-2
                shiftKeyboard();
                break;
            case Keyboard.KEYCODE_DELETE:// 回退-5
                if (editable != null && editable.length() > 0 && start > 0) {
                    editable.delete(start - 1, start);
                }
                break;
            case Keyboard.KEYCODE_SHIFT:// 英文大小写切换-1
                shiftEnglish();
                keyboardView.setKeyboard(keyboardEnglish);
                break;
            case Keyboard.KEYCODE_DONE:// 完成-4
                break;

            default:
                editable.insert(start, Character.toString((char) primaryCode));
                break;
        }
    }

    /**
     * 切换键盘
     */
    private void shiftKeyboard() {
        if (isShift) {
            keyboardView.setKeyboard(keyboardEnglish);
            keyboardView.setCurrentKeyboard(1);
        } else {
            keyboardView.setKeyboard(keyboardNumber);
            keyboardView.setCurrentKeyboard(0);
        }
        isShift = !isShift;
    }

    /**
     * 英文键盘大小写切换
     */
    private void shiftEnglish() {
        List keyList = keyboardEnglish.getKeys();
        for (Keyboard.Key key : keyList) {
            if (key.label != null && isKey(key.label.toString())) {
                if (isCapital) {
                    key.label = key.label.toString().toLowerCase();
                    key.codes[0] = key.codes[0] + 32;
                } else {
                    key.label = key.label.toString().toUpperCase();
                    key.codes[0] = key.codes[0] - 32;
                }
            }
        }
        isCapital = !isCapital;
    }

    /**
     * 判断是否需要预览Key
     *
     * @param primaryCode keyCode
     */
    private void setPreview(int primaryCode) {
        List list = Arrays.asList(Keyboard.KEYCODE_MODE_CHANGE, Keyboard.KEYCODE_DELETE, Keyboard.KEYCODE_SHIFT, Keyboard.KEYCODE_DONE, 32);
        if (list.contains(primaryCode)) {
            keyboardView.setPreviewEnabled(false);
        } else {
            keyboardView.setPreviewEnabled(true);
        }
    }

    /**
     * 判断此key是否正确,且存在
     *
     * @param key
     * @return
     */
    private boolean isKey(String key) {
        String lowercase = "abcdefghijklmnopqrstuvwxyz";
        if (lowercase.indexOf(key.toLowerCase()) > -1) {
            return true;
        }
        return false;
    }

    /**
     * 设置键盘隐藏
     *
     * @param isCompleted true:表示点击了【完成】
     */
    public void hide(boolean isCompleted) {
        int visibility = keyboardView.getVisibility();
        if (visibility == View.VISIBLE) {
            keyboardView.setVisibility(View.INVISIBLE);
            if (viewGroup != null) {
                viewGroup.setVisibility(View.GONE);
            }
        }
        if (onKeyboardListener != null) {
            onKeyboardListener.onHide(isCompleted);
        }
    }

    /**
     * 设置键盘对话框显示,并且屏幕上移
     */
    public void show() {
        //设置键盘显示
        int visibility = keyboardView.getVisibility();
        if (visibility == View.GONE || visibility == View.INVISIBLE) {
            keyboardView.setVisibility(View.VISIBLE);
            if (viewGroup != null) {
                viewGroup.setVisibility(View.VISIBLE);
            }
        }
        if (onKeyboardListener != null) {
            onKeyboardListener.onShow();
        }
    }

    /**
     * 键盘状态
     *
     * @return true:表示键盘开启 false:表示键盘隐藏
     */
    public boolean isShow() {
        return keyboardView.getVisibility() == View.VISIBLE;
    }

    @Override
    public void onText(CharSequence charSequence) {

    }

    @Override
    public void swipeLeft() {

    }

    @Override
    public void swipeRight() {

    }

    @Override
    public void swipeDown() {

    }

    @Override
    public void swipeUp() {

    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            hide(false);
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }

    public interface OnKeyboardListener {
        /**
         * 键盘隐藏了
         *
         * @param isCompleted true:表示点击了【完成】
         */
        void onHide(boolean isCompleted);

        /**
         * 键盘弹出了
         */
        void onShow();

        /**
         * 按下
         *
         * @param primaryCode
         */
        void onPress(int primaryCode);
    }

    /**
     * 对外开放的方法
     *
     * @param onKeyboardListener
     */
    public void setOnKeyboardListener(OnKeyboardListener onKeyboardListener) {
        this.onKeyboardListener = onKeyboardListener;
    }
}

相同英文字母大小写的ASCII值相差32;在键盘切换的时候,如果当前为小写键盘样式,要切换为大写,则把对应字母的ASCII值减去32,并且键上显示的字母设置为大写字母。

三:开发中遇到的问题

  • 输入预览
    设置预览框大小的时候,设置android:layout_width、android:layout_height属性是无效的,因为预览框是弹出的PopupWindow,所以我们只有设置TextView的android:paddingLeft、android:paddingRight属性。在此也可以设置预览框的背景色,字体颜色等等。
  • 设置不同键的按下状态的样式
    想要给不同的键设置不同的按下效果的时候,只能设置统一的样式,不能满足我们的需求,这时继承系统KeyboardView,并在onDraw中绘制不同键的背景可以解决问题。
  • 把自定义键盘放入PopupWindow中,开启输入预览时崩溃
    ERROR/AndroidRuntime(888): android.view.WindowManager$BadTokenException: Unable to add window -- token android.view.ViewRoot$W@44ef1b68 is not valid; is your activity running?
    PopupWindow中再弹出PopupWindow,后面的PopupWindow没有载体了,已经不在视图窗口中了。
  • 软键盘遮挡编辑框
    不同场景解决方案方案,在此不作说明。
  • 光标消失?
    在使用的过程中有时候,光标会消失,若有好的解决方案可以告知下,Thanks!

四:具体使用

editView.setEditView(llKeyboard, keyboardView, true);

editView.setOnKeyboardListener(new EditView.OnKeyboardListener() {
    @Override
    public void onHide(boolean isCompleted) {
        if (height > 0) {
            llGuan.scrollBy(0, -(height + DensityUtil.dp2px(MainActivity.this, 16)));
        }

        if (isCompleted) {
            Log.i("", "你点击了完成按钮");
        }
    }

    @Override
    public void onShow() {
        llGuan.post(new Runnable() {
            @Override
            public void run() {
                //pos[0]: X,pos[1]: Y
                int[] pos = new int[2];
                //获取编辑框在整个屏幕中的坐标
                editView.getLocationOnScreen(pos);
                //编辑框的Bottom坐标和键盘Top坐标的差
                height = (pos[1] + editView.getHeight())
                        - (ScreenUtil.getScreenHeight(MainActivity.this) - keyboardView.getHeight());
                if (height > 0) {
                    //编辑框和键盘之间预留出16dp的距离
                    llGuan.scrollBy(0, height + DensityUtil.dp2px(MainActivity.this, 16));
                }
            }
        });
    }

    @Override
    public void onPress(int primaryCode) {

    }
});

llKeyboard为包裹键盘的父布局,llGuan为包裹输入框的父布局。

源码:https://github.com/GitPhoenix/KeyboardView

你可能感兴趣的:(自定义安全键盘——仿民生银行)