
分析
繼承關(guān)系
首先是一個輸入框,如果要手動實現(xiàn)一個輸入框View未免顯得太過麻煩纹笼,所以這里直接繼承自AppCompatEditText實現(xiàn)纹份。
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();
//設(shè)置數(shù)據(jù)長度
if (verifyCodeLength >= 0) {
setFilters(new InputFilter[]{new InputFilter.LengthFilter(verifyCodeLength)});
} else {
setFilters(new InputFilter[0]);
}
//禁止長按
setLongClickable(false);
//隱藏光標(biāo)
setCursorVisible(false);
//背景透明
setBackgroundColor(Color.TRANSPARENT);
}
}
在最后一個構(gòu)造方法中,獲取到一些初始化屬性的值廷痘,然后就是設(shè)置一些必要的UI顯示蔓涧,比如說限制輸入最大長度為驗證碼長度(這邊用的是InputFilter,感興趣的自行了解下)笋额,禁止長按蠢笋,隱藏光標(biāo),EditText背景透明(隱藏底部黑線)鳞陨,都是一些簡單的配置隱藏官方給的View的UI。
測量
實現(xiàn)一個自定義View包含layout瞻惋,measure和draw三個步驟厦滤,這里layout不需要,measure是需要實現(xiàn)的歼狼,其中
- View最小高度不得小于邊框高度掏导,這里的邊框高度就是小圓圈的直徑,如果小圓圈換成正方形就是正方形的邊長羽峰。
- View的最小寬度不得小于(邊框?qū)挾?* 邊框數(shù))+ (邊框間距 * 邊框數(shù)減1)趟咆,這個公式也很好理解添瓷,就是算四個格子加縫隙的寬度。這里在else里面做了一個簡單的處理值纱,就是讓View整體居中顯示鳞贷,根據(jù)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;
}
//最小寬度不得小于(邊框?qū)挾?* 邊框數(shù))+ (邊框間距 * 邊框數(shù)減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);
//設(shè)置重新測量
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
繪制
從圖中驗證碼輸入框部分可以看到搀愧,主要分為三部分的處理,一是繪制背景圈疆偿,二是繪制選中圈咱筛,三是繪制文字所以我在onDraw()方法里繪制了這幾部分
@Override
protected void onDraw(Canvas canvas) {
//獲取字體背景色
textColor = getCurrentTextColor();
//設(shè)置透明字體防止原來的輸入內(nèi)容顯示
setTextColor(Color.TRANSPARENT);
super.onDraw(canvas);
//等原來的繪制完再設(shè)置自定義的字體顏色
setTextColor(textColor);
//繪制背景
drawBackground(canvas);
//繪制驗證碼
drawVerifyText(canvas);
}
注:其中textColor是緩存下當(dāng)前設(shè)置的edittext的顏色,然后設(shè)置顏色為透明杆故,這樣在之后的輸入中就不會看到EditText默認(rèn)樣子的文本迅箩,但是這樣做又會影響我們要主動繪制文本顏色,所以在默認(rèn)的繪制完之后处铛,自定義的繪制之前設(shè)置回textColor饲趋。
首先看下繪制背景,繪制背景包含兩部分罢缸,一個是背景篙贸,一個是選中的前景,其中背景就是循環(huán)部分枫疆。下面看代碼中的注釋的詳細(xì)解釋
- 注釋1:這邊是配置一個矩形的位置爵川,這個矩形是驗證碼框的最大邊界。我們不管設(shè)置什么形狀息楔,都是在這個范圍之內(nèi)寝贡,其中animOffset是我做的一個小動畫,如果不想要的話值依,可以直接去掉圃泡。

- 注釋2部分:確定邊框后,通過setBounds定位愿险,setState繪制選擇器中哪一個drawable颇蜡,draw繪制,save保存辆亏,translate移動到下一個位置
- 注釋3部分:畫完之后要恢復(fù)到初始的狀態(tài),然后才能繪制后面的選中圖層风秤。
- 注釋4部分:選中圖層類似,要確定一個矩形邊界扮叨,然后根據(jù)當(dāng)前輸入的文本長度確定繪制位置缤弦,最后進(jìn)行繪制。
private void drawBackground(Canvas canvas) {
//輸入框矩形
Rect rect = new Rect();
//獲取當(dāng)前的保存狀態(tài)
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
//設(shè)置框邊界
strokeBackground.setBounds(rect);
//設(shè)置框狀態(tài)
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) {
//確定輸入內(nèi)容長度
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);
}
}
最后是繪制文本的部分彻磁,繪制文本重點關(guān)注x碍沐,y坐標(biāo)的確定狸捅。x坐標(biāo)是這樣確定的:

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坐標(biāo)以文字基線位置(左)位置為準(zhǔn)
int x = strokeWidth / 2 + (strokeWidth + strokePadding) * i - (rect.centerX());
//y坐標(biāo)以文字基線(下)位置為準(zhǔn)
int y = canvas.getHeight() / 2 + rect.height() / 2;
canvas.drawText(s, x, y, textPaint);
}
canvas.restoreToCount(count);
}
在這里還有一個輸入完成的回調(diào)累提,用于給外界獲取數(shù)據(jù)尘喝。
/**
* 輸入內(nèi)容監(jiān)聽器
*/
@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());
}
}
}
/**
* 回調(diào)
*/
public void setDataCall(DataCall<String> dataCall) {
this.dataCall = dataCall;
}
這里面有一些我自己封裝的方法,如DataCall(就是一個帶數(shù)據(jù)返回的接口)刻恭,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>
總結(jié)
對于這樣一個自定義View,繪制的內(nèi)容比較簡單鳍贾,沒有什么復(fù)雜的操作鞍匾,重點是確定輸入框的位置。然后處理官方View所顯示的內(nèi)容骑科。相對來說還是簡單的橡淑。