PinView 的一些知识点

前言

最近因为某个项目需要添加“验证码”功能,设计上区别于原生 EditText,需要将四个数字分离(类似于微信、支付宝支付密码输入框)。在 Github 上查找了一遍,发现大多数现有的控件都比较 dirty —— 使用多个 EditText 达到字符分离的效果。

唯一一个比较有意思的项目是 PasswordInput,该控件直接继承于 EditText,通过重写 onDraw 来实现每个文本对应一个方框,并通过重写 onTextChanged 方法监听文本变化以绘制对应的圆点。

然而,某些情况下我们需要直接显示输入的验证码(PIN 码、短信验证码等),但 PasswordInput 并没有提供相应的方法达到这种效果。于是抽空根据 PasswordInput 的思路实现了 PinView 并开源于 Github。

此文主要为记录编码过程中的一些思路以及所遇到的一些问题与对应的解决方案。

功能

PinView 主要特点是:

  • 根据 inputType 绘制不同的内容,为 password 时绘制原点,其他的直接绘制文本
  • 分离的输入框
  • 可变的方框颜色
  • 输入文字时带动画效果

最终效果图

思路

主要根据 PasswordInput 的思路,继承于 AppCompatEditText。由于 EditText 本身在文字变化时会触发 onDraw 方法,而文本长度也可以通过 getText().length() 获得,所以无需重写 onTextChanged 方法。

issue 1

设置了方框的总数后,我们需要根据方框数量限制文字最大长度,然而 EditText 并没对应的方法让开发者动态修改文字最大长度。

解决方案

翻了一下 TextView 的源码,主要是使用 InputFilter.LengthFilter(maxLength) 达到目的:

1
2
3
4
5
6
7
8
9
private static final InputFilter[] NO_FILTERS = new InputFilter[0];
private void setMaxLength(int maxLength) {
if (maxLength >= 0) {
setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxLength)});
} else {
setFilters(NO_FILTERS);
}
}

issue 2

inputType 为 password 时,我们需要绘制圆形。同样的,EditText 也没相应的方法可以帮助我们判断 inputtype。

解决方案

在 TextView 源码中找到了一个完整的判断方法:

1
2
3
4
5
6
7
8
9
10
private static boolean isPasswordInputType(int inputType) {
final int variation =
inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
return variation
== (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)
|| variation
== (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD)
|| variation
== (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
}

issue 3

绘制文字时,由于每个字符是独立绘制的,所以 baseline 并不一样。这就造成了文字绘制出来一部分正常,而另一部分可能偏高或者偏低。

解决方案

这里选择了一种折中的办法,所有的字符都直接绘制于正中位置(即使是上标下标都会居中显示)。

1
2
3
4
5
6
7
8
9
private void drawText(Canvas canvas, int i) {
Paint paint = getPaintByIndex(i);
paint.getTextBounds(getText().toString(), i, i + 1, mTextRect);
float cx = mBoxCenterPoint.x;
float cy = mBoxCenterPoint.y;
float x = cx - Math.abs(mTextRect.width()) / 2 - mTextRect.left;
float y = cy + Math.abs(mTextRect.height()) / 2 - mTextRect.bottom;// always center vertical
canvas.drawText(getText(), i, i + 1, x, y, paint);
}

issue 4

由于继承于 EditText,当长按文字区域会弹出复制、粘贴、剪切菜单,而这功能是 PinView 所不需要的。

解决方案

1
2
setTextIsSelectable(false);
setLongClickable(false);

如果同时不需要键盘的左右移动键,可以用下面的方法

1
2
3
4
5
6
setTextIsSelectable(false);
@Override
protected MovementMethod getDefaultMovementMethod() {
return null;
}

总结

这是第一次写控件库,写完下来发觉真的需要很细心。而且很多方面都需要考虑,如何让控件更加容易使用,如何利用已有的属性,哪些属性可以提供给开发者自定义,诸如此类。同时又要在功能上进行取舍,精简逻辑的同时实现该有的功能,不做多余的事。

整个过程下来,学习的东西也不少,特别是了解到如何将自己的库放到 jcenter 中供其他人下载。