首先,咱們來看看實現的是怎麼樣的效果:java
若是咱們拿到這樣的UI,想到的佈局應該是用4個EditText
包在橫向的LinearLayout
裏面,但今天要講的View,因此咱們決定用一個自定義的EditText
畫出來。android
學到什麼?git
功能需求github
思路canvas
徹底重畫一個EditText
,就包含了測量佈局
和從新繪製
這兩個關鍵步驟。好了,到這裏理一下總體的思路:markdown
EditText
畫布,從新繪製方框onTextChanged
但知足驗證碼個數的時候調用自動完成方法開始動手ide
準備開始了,果斷繼承一個AppCompatEditText
來初始化基本參數先:oop
/** * 驗證碼輸入框,重寫EditText的繪製方法實現。 * @author RAE */ public class CodeEditText extends AppCompatEditText { // 驗證碼文本顏色 private int mTextColor; // 輸入的最大長度 private int mMaxLength = 4; // 邊框寬度 private int mStrokeWidth; // 邊框高度 private int mStrokeHeight; // 邊框之間的距離 private int mStrokePadding = 20; // 用矩形來保存方框的位置、大小信息 private final Rect mRect = new Rect(); // 方框的背景 private Drawable mStrokeDrawable; /** * 構造方法 * */ public CodeEditText(Context context, AttributeSet attrs) { super(context, attrs); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CodeEditText); int indexCount = typedArray.getIndexCount(); for (int i = 0; i < indexCount; i++) { int index = typedArray.getIndex(i); if (index == R.styleable.CodeEditText_strokeHeight) { this.mStrokeHeight = (int) typedArray.getDimension(index, 60); } else if (index == R.styleable.CodeEditText_strokeWidth) { this.mStrokeWidth = (int) typedArray.getDimension(index, 60); } else if (index == R.styleable.CodeEditText_strokePadding) { this.mStrokePadding = (int) typedArray.getDimension(index, 20); } else if (index == R.styleable.CodeEditText_strokeBackground) { this.mStrokeDrawable = typedArray.getDrawable(index); } else if (index == R.styleable.CodeEditText_strokeLength) { this.mMaxLength = typedArray.getInteger(index, 4); } } typedArray.recycle(); if (mStrokeDrawable == null) { throw new NullPointerException("stroke drawable not allowed to be null!"); } setMaxLength(mMaxLength); setLongClickable(false); // 去掉背景顏色 setBackgroundColor(Color.TRANSPARENT); // 不顯示光標 setCursorVisible(false); } @Override public boolean onTextContextMenuItem(int id) { return false; } /** * 設置最大長度 */ private void setMaxLength(int maxLength) { if (maxLength >= 0) { setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxLength)}); } else { setFilters(new InputFilter[0]); } } } 複製代碼
開始測量佈局佈局
初始化完了就要開始測量佈局了,計算公式爲:動畫
輸入框寬度 = 邊框寬度 * 數量 + 邊框間距 *(數量-1)
@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 < mStrokeHeight) { height = mStrokeHeight; } // 輸入框寬度 = 邊框寬度 * 數量 + 邊框間距 *(數量-1) int recommendWidth = mStrokeWidth * mMaxLength + mStrokePadding * (mMaxLength - 1); // 判斷寬度是否小於推薦寬度 if (width < recommendWidth) { width = recommendWidth; } widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, widthMode); heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, heightMode); // 設置測量佈局 setMeasuredDimension(widthMeasureSpec, heightMeasureSpec); } 複製代碼
畫家登場
來到最重要的步驟了,重畫輸入框!來一步步看代碼註釋:
@Override protected void onDraw(Canvas canvas) { // 重繪背景顏色 drawStrokeBackground(canvas); // 重繪文本 drawText(canvas); } 複製代碼
繪製背景方框
/** * 繪製方框 */ private void drawStrokeBackground(Canvas canvas) { // 下面繪製方框背景顏色 // 肯定反饋位置 mRect.left = 0; mRect.top = 0; mRect.right = mStrokeWidth; mRect.bottom = mStrokeHeight; int count = canvas.getSaveCount(); // 當前畫布保存的狀態 canvas.save(); // 保存畫布 for (int i = 0; i < mMaxLength; i++) { mStrokeDrawable.setBounds(mRect); // 設置位置 mStrokeDrawable.setState(new int[]{android.R.attr.state_enabled}); // 設置圖像狀態 mStrokeDrawable.draw(canvas); // 畫到畫布上 // 肯定下一個方框的位置 float dx = mRect.right + mStrokePadding; // X座標位置 // 保存畫布 canvas.save(); // [注意細節] 移動畫布到下一個位置 canvas.translate(dx, 0); } // [注意細節] 把畫布還原到畫反饋以前的狀態,這樣就還原到最初位置了 canvas.restoreToCount(count); // 畫布歸位 canvas.translate(0, 0); // 下面繪製高亮狀態的邊框 // 當前高亮的索引 int activatedIndex = Math.max(0, getEditableText().length()); mRect.left = mStrokeWidth * activatedIndex + mStrokePadding * activatedIndex; mRect.right = mRect.left + mStrokeWidth; mStrokeDrawable.setState(new int[]{android.R.attr.state_focused}); mStrokeDrawable.setBounds(mRect); mStrokeDrawable.draw(canvas); } 複製代碼
通常畫布的移動canvas.translate(x,y)
會結合canvas.save();
來使用。
一、調用canvas.save();
保存當前畫布的狀態,用PS來解析就是按下ctrl +s
鍵,而後幫你新建一個新的圖層。你以後畫的內容不會影響到以前畫的內容,要回到以前的狀態就調用canvas.restoreToCount(count)
來還原。
二、把畫布的位置移到下一個位置canvas.translate(x,y)
,下圖所示,你會發現方框在畫布中的位置沒有發生變化而是畫布距離發生了變化。這就是畫布平移的效果了。
畫驗證碼文字
/** * 重繪文本 */ private void drawText(Canvas canvas) { int count = canvas.getSaveCount(); canvas.translate(0, 0); int length = getEditableText().length(); for (int i = 0; i < length; i++) { String text = String.valueOf(getEditableText().charAt(i)); TextPaint textPaint = getPaint(); textPaint.setColor(getCurrentTextColor()); // 獲取文本大小 textPaint.getTextBounds(text, 0, 1, mRect); // 計算(x,y) 座標 int x = mStrokeWidth / 2 + (mStrokeWidth + mStrokePadding) * i - (mRect.centerX()); int y = canvas.getHeight() / 2 + mRect.height() / 2; canvas.drawText(text, 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 == mMaxLength) { hideSoftInput(); if (mOnInputFinishListener != null) { mOnInputFinishListener.onTextFinish(getEditableText().toString(), mMaxLength); } } } 複製代碼
到這裏你能大概理解畫布的概念了,本文完。