自定义安全键盘——仿民生银行
VicJKX
8年前
<p>系统自带键盘和第三方的键盘不管是从性能还是从体验上来说都要胜于我们自己写的,但我们为什么还要去自定义键盘呢?其实就为了安全性,比如用户在输入账户密码,支付密码的时候,防止键盘获取到我们的数据;或者说美工要求Android的键盘需要和IOS的一样,那我们就得自己去写个键盘了。</p> <p>效果图:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/6764eb56290e2f1c92248acf496b8e63.gif"></p> <p style="text-align:center">keyboardView.gif</p> <p><strong>一:键盘布局</strong></p> <ul> <li>XML布局属性</li> </ul> <p style="text-align:center"><img src="https://simg.open-open.com/show/1f2cbc8f57deb6c8041043aaf223dc77.png"></p> <p>Keyboard.Key的属性.png</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/701f599154998078e7c712ead8d24e1a.png"></p> <p>KeyboardView的属性.png</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/7781c77d94c1d8f08a5a38b5601ee922.png"></p> <p>Keyboard.Row的属性.png</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/f92c040bd6586ad558e197d5d4fed89d.png"></p> <p>ASCII值.png</p> <p>Key和Row为Keyboard的静态内部类,从上面的图中我们能看到每个属性对应的Description,在这里再简单介绍一下。</p> <table> <thead> <tr> <th>Key的xml属性</th> <th>属性说明</th> </tr> </thead> <tbody> <tr> <td>android:codes</td> <td>表示键Key的标签, <strong>字母/符号/数字对应的ASCII值。</strong></td> </tr> <tr> <td>android:iconPreview</td> <td>表示键点击后放大后的View</td> </tr> <tr> <td>android:isModifier</td> <td>按键是否为功能键,例如Alt/Shift/Ctrl键。取值为true或false。</td> </tr> <tr> <td>android:isRepeatable</td> <td>表示长时间按下key键重复执行这个键的操作,如:长按删除键会一直执行删除操作。</td> </tr> <tr> <td>android:isSticky</td> <td>指定按键是否为状态键。如:Shift大小写切换按键,具有两种状态,按下状态和正常状态,取值为true或则false。</td> </tr> <tr> <td>android:keyEdgeFlags</td> <td>指定按键的对齐指令,取值为left或则right。</td> </tr> <tr> <td>android:keyIcon</td> <td>按键图标,如果指定了该值则文本属性无效。</td> </tr> <tr> <td>android:keyLabel</td> <td>代表按键显示的文本内容。</td> </tr> <tr> <td>android:keyOutputText</td> <td>指定按键输出的文本内容,取值为字符串。</td> </tr> <tr> <td>android:popupCharacters</td> <td>表示编辑特殊字符,空格、换行等.。</td> </tr> <tr> <td>android:popupKeyboard</td> <td>表示按键点击预览窗口。</td> </tr> <tr> <td>android:horizontalGap</td> <td>键的水平间隙, <strong>当前键的左边的水平间隙。</strong></td> </tr> </tbody> </table> <table> <thead> <tr> <th>KeyboardView的xml属性</th> <th>属性说明</th> </tr> </thead> <tbody> <tr> <td>android:background</td> <td>设置整个键盘的背景色</td> </tr> <tr> <td>android:keyPreviewHeight</td> <td>正在输入时弹出预览框的高度</td> </tr> <tr> <td>android:keyPreviewLayout</td> <td>输入时预览框的布局样式, <strong>要求根布局为TextView。</strong></td> </tr> <tr> <td>android:keyTextColor</td> <td>键的字体颜色</td> </tr> <tr> <td>android:keyTextSize</td> <td>键的字体大小</td> </tr> <tr> <td>android:labelTextSize</td> <td>字符串键文本字体大小</td> </tr> <tr> <td>android:shadowColor、android:shadowRadius</td> <td><strong>设置这两个属性可以很好的解决键的字体发虚的问题</strong> ,设置shadowColor值和键的字体颜色相同。</td> </tr> <tr> <td>android:keyBackground</td> <td>设置键的背景色,可以用drawable中的selector标签设置键的正常状态样式和按下样式</td> </tr> </tbody> </table> <ul> <li>英文键盘</li> </ul> <pre> <code class="language-java"><?xml version="1.0" encoding="UTF-8"?> <Keyboard xmlns:android="http://schemas.android.com/apk/res/android" android:horizontalGap="0.0px" android:keyHeight="7.5%p" android:keyWidth="8%p" android:verticalGap="0.0px"> <Row android:verticalGap="2%p"> <Key android:codes="113" android:horizontalGap="1%p" android:keyEdgeFlags="left" android:keyLabel="q" android:keyWidth="8%p"/> <Key android:codes="119" android:horizontalGap="2%p" android:keyLabel="w" android:keyWidth="8%p"/> <Key android:codes="101" android:horizontalGap="2%p" android:keyLabel="e" android:keyWidth="8%p"/> <Key android:codes="114" android:horizontalGap="2%p" android:keyLabel="r" android:keyWidth="8%p"/> <Key android:codes="116" android:horizontalGap="2%p" android:keyLabel="t" android:keyWidth="8%p"/> <Key android:codes="121" android:horizontalGap="2%p" android:keyLabel="y" android:keyWidth="8%p"/> <Key android:codes="117" android:horizontalGap="2%p" android:keyLabel="u" android:keyWidth="8%p"/> <Key android:codes="105" android:horizontalGap="2%p" android:keyLabel="i" android:keyWidth="8%p"/> <Key android:codes="111" android:horizontalGap="2%p" android:keyLabel="o" android:keyWidth="8%p"/> <Key android:codes="112" android:horizontalGap="2%p" android:keyEdgeFlags="right" android:keyLabel="p" android:keyWidth="8%p"/> </Row> <Row android:verticalGap="2%p"> <Key android:codes="97" android:horizontalGap="6%p" android:keyEdgeFlags="left" android:keyLabel="a" android:keyWidth="8%p"/> <Key android:codes="115" android:horizontalGap="2%p" android:keyLabel="s" android:keyWidth="8%p"/> <Key android:codes="100" android:horizontalGap="2%p" android:keyLabel="d" android:keyWidth="8%p"/> <Key android:codes="102" android:horizontalGap="2%p" android:keyLabel="f" android:keyWidth="8%p"/> <Key android:codes="103" android:horizontalGap="2%p" android:keyLabel="g" android:keyWidth="8%p"/> <Key android:codes="104" android:horizontalGap="2%p" android:keyLabel="h" android:keyWidth="8%p"/> <Key android:codes="106" android:horizontalGap="2%p" android:keyLabel="j" android:keyWidth="8%p"/> <Key android:codes="107" android:horizontalGap="2%p" android:keyLabel="k" android:keyWidth="8%p"/> <Key android:codes="108" android:horizontalGap="2%p" android:keyEdgeFlags="right" android:keyLabel="l" android:keyWidth="8%p"/> </Row> <Row android:verticalGap="2%p"> <Key android:codes="-1" android:isModifier="true" android:isSticky="true" android:horizontalGap="1%p" android:keyEdgeFlags="left" android:keyIcon="@android:drawable/ic_menu_manage" android:keyWidth="12%p"/> <Key android:codes="122" android:horizontalGap="3%p" android:keyLabel="z" android:keyWidth="8%p"/> <Key android:codes="120" android:horizontalGap="2%p" android:keyLabel="x" android:keyWidth="8%p"/> <Key android:codes="99" android:horizontalGap="2%p" android:keyLabel="c" android:keyWidth="8%p"/> <Key android:codes="118" android:horizontalGap="2%p" android:keyLabel="v" android:keyWidth="8%p"/> <Key android:codes="98" android:horizontalGap="2%p" android:keyLabel="b" android:keyWidth="8%p"/> <Key android:codes="110" android:horizontalGap="2%p" android:keyLabel="n" android:keyWidth="8%p"/> <Key android:codes="109" android:horizontalGap="2%p" android:keyLabel="m" android:keyWidth="8%p"/> <Key android:codes="-5" android:horizontalGap="3%p" android:isRepeatable="true" android:keyEdgeFlags="right" android:keyIcon="@drawable/img_edit_clear" android:keyWidth="12%p"/> </Row> <Row> <Key android:codes="-2" android:isModifier="true" android:isSticky="true" android:horizontalGap="1%p" android:keyEdgeFlags="left" android:keyLabel="123" android:keyWidth="20%p"/> <Key android:codes="32" android:horizontalGap="5%p" android:isRepeatable="true" android:keyLabel="space" android:keyWidth="48%p"/> <Key android:codes="-4" android:horizontalGap="5%p" android:keyEdgeFlags="right" android:keyLabel="完成" android:keyWidth="20%p"/> </Row> </Keyboard></code></pre> <ul> <li>数字键盘</li> </ul> <pre> <code class="language-java"><?xml version="1.0" encoding="UTF-8"?> <Keyboard xmlns:android="http://schemas.android.com/apk/res/android" android:horizontalGap="0px" android:keyHeight="7.5%p" android:keyWidth="30%p" android:verticalGap="0px"> <Row android:verticalGap="2%p"> <Key android:codes="49" android:horizontalGap="2%p" android:keyEdgeFlags="left" android:keyLabel="1" android:keyWidth="30%p"/> <Key android:codes="50" android:horizontalGap="3%p" android:keyLabel="2" android:keyWidth="30%p"/> <Key android:codes="51" android:horizontalGap="3%p" android:keyEdgeFlags="right" android:keyLabel="3" android:keyWidth="30%p"/> </Row> <Row android:verticalGap="2%p"> <Key android:codes="52" android:horizontalGap="2%p" android:keyLabel="4" android:keyWidth="30%p"/> <Key android:codes="53" android:horizontalGap="3%p" android:keyLabel="5" android:keyWidth="30%p"/> <Key android:codes="54" android:horizontalGap="3%p" android:keyEdgeFlags="right" android:keyLabel="6" android:keyWidth="30%p"/> </Row> <Row android:verticalGap="2%p"> <Key android:codes="55" android:horizontalGap="2%p" android:keyLabel="7" android:keyWidth="30%p"/> <Key android:codes="56" android:horizontalGap="3%p" android:keyLabel="8" android:keyWidth="30%p"/> <Key android:codes="57" android:horizontalGap="3%p" android:keyEdgeFlags="right" android:keyLabel="9" android:keyWidth="30%p"/> </Row> <Row> <Key android:codes="-2" android:horizontalGap="2%p" android:isModifier="true" android:isRepeatable="false" android:isSticky="true" android:keyEdgeFlags="left" android:keyLabel="abc" android:keyWidth="30%p"/> <Key android:codes="48" android:horizontalGap="3%p" android:keyLabel="0" android:keyWidth="30%p"/> <Key android:codes="-5" android:horizontalGap="3%p" android:isRepeatable="true" android:keyEdgeFlags="right" android:keyIcon="@drawable/img_edit_clear" android:keyWidth="30%p"/> </Row> </Keyboard></code></pre> <p>在键盘布局的时候,要保证键所占的宽度(包括间隙)所占的比例和为100%,不然整个键盘的右边会过宽或过窄。如:数字键盘的第一行:2+30+3+30+3+30+2=100,第一个2和最后一个2是键盘离屏幕的距离。</p> <p><strong>二:代码实现</strong></p> <ul> <li> <p>自定义键的按下效果</p> <p>我们有时候可能需要为删除键设置这样的按下效果,为退格键设置那样的效果;设置键的按下效果可通过KeyboardView的android:keyBackground属性进行设置,但是所有键的按下效果都是一样的,不能满足我们的需求;但是我们可以继承系统KeyboardView,在onDraw方法中通过键的code(ASCII值)为不同的键绘制不同的背景、图标、文本。</p> </li> </ul> <pre> <code class="language-java">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<Keyboard.Key> keys = getKeyboard().getKeys(); if (keyboardType == 0) {// 数字键盘 drawKeyboardNumber(keys, canvas); } else if (keyboardType == 1) {// 英文键盘 drawKeyboardEnglish(keys, canvas); } } /** * 绘制数字键盘 * * @param keys * @param canvas */ private void drawKeyboardNumber(List<Keyboard.Key> 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<Keyboard.Key> 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(); } }</code></pre> <ul> <li> <p>输入的控制逻辑处理</p> <p>由于在应用中只有在输入安全性要求较高的地方才会用到我们自定义的键盘,所以我就继承EditText,在子类中写相关控制逻辑,避免在使用的时候写更多代码。</p> </li> </ul> <pre> <code class="language-java">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<Keyboard.Key> 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<Integer> 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; } }</code></pre> <p>相同英文字母大小写的ASCII值相差32;在键盘切换的时候,如果当前为小写键盘样式,要切换为大写,则把对应字母的ASCII值减去32,并且键上显示的字母设置为大写字母。</p> <p><strong>三:开发中遇到的问题</strong></p> <ul> <li>输入预览<br> 设置预览框大小的时候,设置android:layout_width、android:layout_height属性是无效的,因为预览框是弹出的PopupWindow,所以我们只有设置TextView的android:paddingLeft、android:paddingRight属性。在此也可以设置预览框的背景色,字体颜色等等。</li> <li>设置不同键的按下状态的样式<br> 想要给不同的键设置不同的按下效果的时候,只能设置统一的样式,不能满足我们的需求,这时继承系统KeyboardView,并在onDraw中绘制不同键的背景可以解决问题。</li> <li>把自定义键盘放入PopupWindow中,开启输入预览时崩溃<br> ERROR/AndroidRuntime(888): android.view.WindowManager$BadTokenException: Unable to add window -- token android.view.ViewRoot$W@44ef1b68 is not valid; is your activity running?<br> PopupWindow中再弹出PopupWindow,后面的PopupWindow没有载体了,已经不在视图窗口中了。</li> <li>软键盘遮挡编辑框<br> 不同场景解决方案方案,在此不作说明。</li> <li>光标消失?<br> 在使用的过程中有时候,光标会消失,若有好的解决方案可以告知下,Thanks!</li> </ul> <p><strong>四:具体使用</strong></p> <pre> <code class="language-java">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) { } });</code></pre> <p>llKeyboard为包裹键盘的父布局,llGuan为包裹输入框的父布局。</p> <p> </p> <p> </p>