分析
继承关系
首先是一个输入框,如果要手动实现一个输入框View未免显得太过麻烦,所以这里直接继承自AppCompatEditText实现。
public class VerifyCodeView extends AppCompatEditText{
public VerifyCodeView(Context context) {
this(context, null);
}
public VerifyCodeView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.editTextStyle);
}
@SuppressLint("ResourceAsColor")
public VerifyCodeView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.VerifyCodeView);
verifyCodeLength = typedArray.getInteger(R.styleable.VerifyCodeView_verifyCodeLength, 4);
strokeWidth = (int) typedArray.getDimension(R.styleable.VerifyCodeView_strokeWidth, ScreenUtil.dip2px(App.context, 50));
strokeHeight = (int) typedArray.getDimension(R.styleable.VerifyCodeView_strokeHeight, ScreenUtil.dip2px(App.context, 50));
strokePadding = (int) typedArray.getDimension(R.styleable.VerifyCodeView_strokePadding, ScreenUtil.dip2px(App.context, 30));
strokeBackground = typedArray.getDrawable(R.styleable.VerifyCodeView_strokeBackground);
typedArray.recycle();
//设置数据长度
if (verifyCodeLength >= 0) {
setFilters(new InputFilter[]{new InputFilter.LengthFilter(verifyCodeLength)});
} else {
setFilters(new InputFilter[0]);
}
//禁止长按
setLongClickable(false);
//隐藏光标
setCursorVisible(false);
//背景透明
setBackgroundColor(Color.TRANSPARENT);
}
}
在最后一个构造方法中,获取到一些初始化属性的值,然后就是设置一些必要的UI显示,比如说限制输入最大长度为验证码长度(这边用的是InputFilter,感兴趣的自行了解下),禁止长按,隐藏光标,EditText背景透明(隐藏底部黑线),都是一些简单的配置隐藏官方给的View的UI。
测量
实现一个自定义View包含layout,measure和draw三个步骤,这里layout不需要,measure是需要实现的,其中
- View最小高度不得小于边框高度,这里的边框高度就是小圆圈的直径,如果小圆圈换成正方形就是正方形的边长。
- View的最小宽度不得小于(边框宽度 * 边框数)+ (边框间距 * 边框数减1),这个公式也很好理解,就是算四个格子加缝隙的宽度。这里在else里面做了一个简单的处理,就是让View整体居中显示,根据startPos的配置,可以从startPos位置开始画起。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//最小高度不得低于边框高度
if (height < strokeHeight) {
height = strokeHeight;
}
//最小宽度不得小于(边框宽度 * 边框数)+ (边框间距 * 边框数减1)
int editViewWidth = (strokeWidth * verifyCodeLength + strokePadding * (verifyCodeLength - 1));
if (width < editViewWidth) {
width = editViewWidth;
} else {
//左边起始
startPos = width / 2 - editViewWidth / 2;
}
//重新生成spec
widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, widthMode);
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, heightMode);
//设置重新测量
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
绘制
从图中验证码输入框部分可以看到,主要分为三部分的处理,一是绘制背景圈,二是绘制选中圈,三是绘制文字所以我在onDraw()方法里绘制了这几部分
@Override
protected void onDraw(Canvas canvas) {
//获取字体背景色
textColor = getCurrentTextColor();
//设置透明字体防止原来的输入内容显示
setTextColor(Color.TRANSPARENT);
super.onDraw(canvas);
//等原来的绘制完再设置自定义的字体颜色
setTextColor(textColor);
//绘制背景
drawBackground(canvas);
//绘制验证码
drawVerifyText(canvas);
}
注:其中textColor是缓存下当前设置的edittext的颜色,然后设置颜色为透明,这样在之后的输入中就不会看到EditText默认样子的文本,但是这样做又会影响我们要主动绘制文本颜色,所以在默认的绘制完之后,自定义的绘制之前设置回textColor。
首先看下绘制背景,绘制背景包含两部分,一个是背景,一个是选中的前景,其中背景就是循环部分。下面看代码中的注释的详细解释
- 注释1:这边是配置一个矩形的位置,这个矩形是验证码框的最大边界。我们不管设置什么形状,都是在这个范围之内,其中animOffset是我做的一个小动画,如果不想要的话,可以直接去掉。
- 注释2部分:确定边框后,通过setBounds定位,setState绘制选择器中哪一个drawable,draw绘制,save保存,translate移动到下一个位置
- 注释3部分:画完之后要恢复到初始的状态,然后才能绘制后面的选中图层。
- 注释4部分:选中图层类似,要确定一个矩形边界,然后根据当前输入的文本长度确定绘制位置,最后进行绘制。
private void drawBackground(Canvas canvas) {
//输入框矩形
Rect rect = new Rect();
//获取当前的保存状态
canvas.translate(startPos, 0);
int count = canvas.getSaveCount();
canvas.save();
//绘制几个框
for (int i = 0; i < verifyCodeLength; i++) {
//注释1
rect.left = 0;
rect.top = strokeHeight - animOffset;
rect.right = rect.left + strokeWidth;
rect.bottom = rect.top + strokeHeight;
if (rect.top != 0) {
animOffset += 2;
}
//注释2
//设置框边界
strokeBackground.setBounds(rect);
//设置框状态
strokeBackground.setState(new int[]{android.R.attr.state_enabled});
//绘制
strokeBackground.draw(canvas);
//Ctrl s
canvas.save();
//位移
canvas.translate(rect.right + strokePadding, 0);
}
//注释3
canvas.restoreToCount(count);
canvas.translate(0, 0);
//注释4
if (getEditableText().length() != verifyCodeLength) {
//确定输入内容长度
int current = Math.max(0, getEditableText().length());
//确定方框左右边界
rect.left = (strokeWidth + strokePadding) * current;
rect.right = rect.left + strokeWidth;
strokeBackground.setState(new int[]{android.R.attr.state_focused});
strokeBackground.setBounds(rect);
strokeBackground.draw(canvas);
}
}
最后是绘制文本的部分,绘制文本重点关注x,y坐标的确定。x坐标是这样确定的:
private void drawVerifyText(Canvas canvas) {
Rect rect = new Rect();
canvas.translate(0, 0);
int count = canvas.getSaveCount();
canvas.save();
int length = getEditableText().length();
TextPaint textPaint = getPaint();
for (int i = 0; i < length; i++) {
String s = String.valueOf(getEditableText().charAt(i));
textPaint.setColor(textColor);
textPaint.setFakeBoldText(true);
textPaint.getTextBounds(s, 0, 1, rect);
//计算位置
//x坐标以文字基线位置(左)位置为准
int x = strokeWidth / 2 + (strokeWidth + strokePadding) * i - (rect.centerX());
//y坐标以文字基线(下)位置为准
int y = canvas.getHeight() / 2 + rect.height() / 2;
canvas.drawText(s, x, y, textPaint);
}
canvas.restoreToCount(count);
}
在这里还有一个输入完成的回调,用于给外界获取数据。
/**
* 输入内容监听器
*/
@Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
super.onTextChanged(text, start, lengthBefore, lengthAfter);
int textLength = getEditableText().length();
if (textLength == verifyCodeLength) {
Systems.hideIME();
if (dataCall != null) {
dataCall.back(getEditableText().toString());
}
}
}
/**
* 回调
*/
public void setDataCall(DataCall<String> dataCall) {
this.dataCall = dataCall;
}
这里面有一些我自己封装的方法,如DataCall(就是一个带数据返回的接口),hideIME(隐藏输入法)等。最后贴一下selector:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true">
<shape android:shape="oval">
<stroke android:width="2dp"
android:color="#FFFB9C00" />
<corners android:radius="4dp" />
</shape>
</item>
<item>
<shape android:shape="oval">
<stroke android:width="2dp" android:color="#EEEEEE" />
<corners android:radius="4dp" />
</shape>
</item>
</selector>
总结
对于这样一个自定义View,绘制的内容比较简单,没有什么复杂的操作,重点是确定输入框的位置。然后处理官方View所显示的内容。相对来说还是简单的。