廢話不多說了,先給大家展示下自訂view效果圖,如果大家覺得還不錯的話,請繼續往下閱讀。
怎麼樣,這種驗證碼是不是很常見呢,下面我們就自己動手實現這種效果,自己動手,豐衣足食,哈哈~
一、 自訂view的步驟
自訂view一直被認為android進階通向高手的必經之路,其實自訂view好簡單,自訂view真正難的是如何繪製出高難度的圖形,這需要有好的數學功底(後悔沒有好好學數學了~),因為繪製圖形經常要計算座標點及類似的幾何變換等等。自訂view通常只需要以下幾個步驟:
寫一個類繼承View類;
重新View的構造方法;
測量View的大小,也就是重寫onMeasure()方法;
重新onDraw()方法。
其中第三步不是必須的,只有當系統無法確定自訂的view的大小的時候需要我們自己重寫onMeasure()方法來完成自訂view大小的測量,因為如果使用者(程式員)在使用我們的自訂view的時候沒有指定其精確大小(寬度或高度),如:布局檔案中layout_width或layout_heigth屬性值為wrap_content而不是match_parent或某個精確的值,那麼系統就不知道我們自訂view在onDraw()中繪製的圖形的大小,所以通常要讓我們自訂view支援wrap_content那麼我們就必須重寫onMeasure方法來告訴系統我們要繪製的view的大小(寬度和高度)。
還有,如果我們自訂view需要一些特殊的屬性,那麼我們還需要自訂屬性,這篇文章將會涉及到自訂屬性和上面的四個步驟的內容。
二、 自訂view的實現
要實現這種驗證碼控制項,我們需要先分析一下它要怎麼實現。通過看上面的效果圖,我們可以知道要實現這種效果,首先需要在繪製驗證碼字串,即圖中的文本部分,然後繪製一些幹擾點,再就是繪製幹擾線了,分析完畢。下面我們根據分析結果一步步實現這種效果。
1. 繼承View,重寫構造方法
寫一個類繼承View,然後重新它的構造方法
/*** Created by lt on 2016/3/2.*/public class ValidationCode extends View{/*** 在java代碼中建立view的時候調用,即new* @param context*/public ValidationCode(Context context) {this(context,null);}/*** 在xml布局檔案中使用view但沒有指定style的時候調用* @param context* @param attrs*/public ValidationCode(Context context, AttributeSet attrs) {this(context, attrs, 0);}/*** 在xml布局檔案中使用view並指定style的時候調用* @param context* @param attrs* @param defStyleAttr*/public ValidationCode(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);// 做一些初始化工作init();}}
View有三個構造方法,一般的做法都是讓一個參數和兩個參數的構造方法調用三個構造參數的方法,這三個構造方法的調用情況看方法上面的注釋。在這個構造方法裡面我們先做一些初始化隨機驗證碼字串,畫筆等工作:
/*** 初始化一些資料*/private void init() {// 產生隨機數字和字母組合mCodeString = getCharAndNumr(mCodeCount);// 初始化文字畫筆mTextPaint = new Paint();mTextPaint.setStrokeWidth(3); // 畫筆大小為3mTextPaint.setTextSize(mTextSize); // 設定文字大小// 初始化幹擾點畫筆mPointPaint = new Paint();mPointPaint.setStrokeWidth(6);mPointPaint.setStrokeCap(Paint.Cap.ROUND); // 設定斷點處為圓形// 初始化幹擾線畫筆mPathPaint = new Paint();mPathPaint.setStrokeWidth(5);mPathPaint.setColor(Color.GRAY);mPathPaint.setStyle(Paint.Style.STROKE); // 設定畫筆為空白心mPathPaint.setStrokeCap(Paint.Cap.ROUND); // 設定斷點處為圓形// 取得驗證碼字串顯示的寬度值mTextWidth = mTextPaint.measureText(mCodeString);}
到這裡,我們就完成了自訂View步驟中的前面的兩小步了,接下來就是完成第三步,即重寫onMeasure()進行我們自訂view大小(寬高)的測量了:
2. 重寫onMeasure(),完成View大小的測量
/*** 要像layout_width和layout_height屬性支援wrap_content就必須重新這個方法* @param widthMeasureSpec* @param heightMeasureSpec*/@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// 分別測量控制項的寬度和高度,基本為模板方法int measureWidth = measureWidth(widthMeasureSpec);int measureHeight = measureHeight(heightMeasureSpec);// 其實這個方法最終會調用setMeasuredDimension(int measureWidth,int measureHeight);// 將測量出來的寬高設定進去完成測量setMeasuredDimension(measureWidth, measureHeight);}
測量寬度的方法:
/*** 測量寬度* @param widthMeasureSpec*/private int measureWidth(int widthMeasureSpec) {int result = (int) (mTextWidth*1.8f);int widthMode = MeasureSpec.getMode(widthMeasureSpec);int widthSize = MeasureSpec.getSize(widthMeasureSpec);if(widthMode == MeasureSpec.EXACTLY){// 精確測量模式,即布局檔案中layout_width或layout_height一般為精確的值或match_parentresult = widthSize; // 既然是精確模式,那麼直接返回測量的寬度即可}else{if(widthMode == MeasureSpec.AT_MOST) {// 最大值模式,即布局檔案中layout_width或layout_height一般為wrap_contentresult = Math.min(result,widthSize);}}return result;}
測量高度的方法:
/*** 測量高度* @param heightMeasureSpec*/private int measureHeight(int heightMeasureSpec) {int result = (int) (mTextWidth/1.6f);int heightMode = MeasureSpec.getMode(heightMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec);if(heightMode == MeasureSpec.EXACTLY){// 精確測量模式,即布局檔案中layout_width或layout_height一般為精確的值或match_parentresult = heightSize; // 既然是精確模式,那麼直接返回測量的寬度即可}else{if(heightMode == MeasureSpec.AT_MOST) {// 最大值模式,即布局檔案中layout_width或layout_height一般為wrap_contentresult = Math.min(result,heightSize);}}return result;}
說明:其實onMeasure()方法最終會調用setMeasuredDimension(int measureWidth,int measureHeight);將測量出來的寬高設定進去完成測量,而我們要做的就是測量得到寬度和高度的值,測量寬度和高度的方法最重要的就是得到當使用者(程式員)沒有給我們的控制項指定精確的值(具體數值或match_parent)時合適的寬度和高度,所以,以上測量寬度和高度的方法基本上是一個模板方法,要做的就是得到result的一個合適的值,這裡我們無需關注給result的那個值,因為這個值根據控制項算出來的一個合適的值(也許不是很合適)。
完成了控制項的測量,那麼接下來我們還要完成控制項的繪製這一大步,也就是自訂view的核心的一步重寫onDraw()方法繪製圖形。
3. 重寫onDraw(),繪製圖形
根據我們上面的分析,我們需要繪製驗證碼文本字串,幹擾點,幹擾線。由於幹擾點和幹擾線需要座標和路徑來繪製, 所以在繪製之前先做一些初始化隨機幹擾點座標和幹擾線路徑:
private void initData() {// 擷取控制項的寬和高,此時已經測量完成mHeight = getHeight();mWidth = getWidth();mPoints.clear();// 產生幹擾點座標for(int i=0;i<150;i++){PointF pointF = new PointF(mRandom.nextInt(mWidth)+10,mRandom.nextInt(mHeight)+10);mPoints.add(pointF);}mPaths.clear();// 產生幹擾線座標for(int i=0;i<2;i++){Path path = new Path();int startX = mRandom.nextInt(mWidth/3)+10;int startY = mRandom.nextInt(mHeight/3)+10;int endX = mRandom.nextInt(mWidth/2)+mWidth/2-10;int endY = mRandom.nextInt(mHeight/2)+mHeight/2-10;path.moveTo(startX,startY);path.quadTo(Math.abs(endX-startX)/2,Math.abs(endY-startY)/2,endX,endY);mPaths.add(path);}}
有了這些資料之後,我們可以開始繪製圖形了。
(1)繪製驗證碼文本字串
由於驗證碼文本字串是隨機產生的,所以我們需要利用代碼來隨機產生這種隨機驗證碼:
/*** java產生隨機數字和字母組合* @param length[產生隨機數的長度]* @return*/public static String getCharAndNumr(int length) {String val = "";Random random = new Random();for (int i = 0; i < length; i++) {// 輸出字母還是數字String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num";// 字串if ("char".equalsIgnoreCase(charOrNum)) {// 取得大寫字母還是小寫字母int choice = random.nextInt(2) % 2 == 0 ? 65 : 97;val += (char) (choice + random.nextInt(26));} else if ("num".equalsIgnoreCase(charOrNum)) { // 數字val += String.valueOf(random.nextInt(10));}}return val;}
這種代碼是java基礎,相信大家都看得懂,看不懂也沒關係,這種代碼網上隨便一搜就有,其實我也是直接從網上搜的,嘿嘿~。
android的2D圖形api canvas提供了drawXXX()方法來完成各種圖形的繪製,其中就有drawText()方法來繪製文本,同時還有drawPosText()在給定的座標點上繪製文本,drawTextOnPath()在給定途徑上繪製圖形。仔細觀察上面的效果圖,發現文本有的不是水平的,即有的被傾斜了,這就可以給我們的驗證碼提升一定的識別難度,要實現文字傾斜效果,我們可以通過drawTextOnPath()在給定路徑繪製文本達到傾斜效果,然而這種方法實現比較困難(座標點和路徑難以計算),所以,我們可以通過canvas提供的位置變換方法rorate()結合drawText()實現文本傾斜效果。
int length = mCodeString.length();float charLength = mTextWidth/length;for(int i=1;i<=length;i++){int offsetDegree = mRandom.nextInt(15);// 這裡只會產生0和1,如果是1那麼正旋轉正角度,否則旋轉負角度offsetDegree = mRandom.nextInt(2) == 1?offsetDegree:-offsetDegree;canvas.save();canvas.rotate(offsetDegree, mWidth / 2, mHeight / 2);// 給畫筆設定隨機顏色,+20是為了去除一些邊界值mTextPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);canvas.drawText(String.valueOf(mCodeString.charAt(i - 1)), (i-1) * charLength * 1.6f+30, mHeight * 2 / 3f, mTextPaint);canvas.restore();}
這段代碼通過for迴圈分別繪製驗證碼字串中的每個字元,每繪製一個字元都將畫布旋轉一個隨機的正負角度,然後通過drawText()方法繪製字元,每個字元的繪製起點座標根據字元的長度和位置不同而不同,這個自己計算,這裡也許也不是很合適。要注意的是,每次對畫布canvas進行位置變換的時候都要先調用canvas.save()方法儲存好之前繪製的圖形,繪製結束後調用canvas.restore()恢複畫布的位置,以便下次繪製圖形的時候不會由於之前畫布的位置變化而受影響。
(2)繪製幹擾點
// 產生幹擾效果1 -- 幹擾點for(PointF pointF : mPoints){mPointPaint.setARGB(255,mRandom.nextInt(200)+20,mRandom.nextInt(200)+20,mRandom.nextInt(200)+20);canvas.drawPoint(pointF.x,pointF.y,mPointPaint);}
給幹擾點畫筆設定隨機顏色,然後根據隨機產生的點的座標利用canvas.drawPoint()繪製點。
(3)繪製幹擾線
// 產生幹擾效果2 -- 幹擾線for(Path path : mPaths){mPathPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);canvas.drawPath(path, mPathPaint);}
給幹擾線畫筆設定隨機顏色,然後根據隨機產生路徑利用canvas.drawPath()繪製貝茲路徑,從而繪製出幹擾線。
4. 重寫onTouchEvent,定製View事件
這裡做這一步是為了實現當我們點擊我們的自訂View的時候,完成一些操作,即定製View事件。這裡,我們需要當使用者點擊驗證碼控制項的時候,改變驗證碼的文本字串。
@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()){case MotionEvent.ACTION_DOWN:// 重建隨機數字和字母組合mCodeString = getCharAndNumr(mCodeCount);invalidate();break;default:break;}return super.onTouchEvent(event);}
OK,到這裡我們的這個自訂View就基本完成了,可能大家會問,這個自訂View是不是擴充性太差了,定製性太低了,說好的自訂屬性呢?跑哪裡去了。不要急,下面我們就來自訂我們自己View的屬性,自訂屬性。
5. 自訂屬性,提高自訂View的可定製性
(1)在資源檔attrs.xml檔案中定義我們的屬性(集)
<?xml version="1.0" encoding="utf-8"?><resources><declare-styleable name="IndentifyingCode"><attr name="codeCount" format="integer|reference"></attr><attr name="textSize" format="dimension"></attr></declare-styleable></resources>
說明:
在attrs.xml檔案中的attr節點中定義我們的屬性,定義屬性需要name屬性工作表示我們的屬性值,同時需要format屬性工作表示屬性值的格式,其格式有很多種,如果屬性值可以使多種格式,那麼格式間用”|”分開;
declare-styleable節點用來定義我們自訂屬性集,其name屬性指定了該屬性集的名稱,可以任意,但一般為自訂控制項的名稱;
如果屬性已經定義了(如layout_width),那麼可以直接引用該屬性,不要指定格式了。
(2)在布局檔案中引用自訂屬性,注意需要引入命名空間
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:lt="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"><com.lt.identifyingcode.ValidationCodeandroid:id="@+id/validationCode"android:layout_width="wrap_content"android:layout_centerInParent="true"lt:textSize="25sp"android:background="@android:color/darker_gray"android:layout_height="wrap_content"/></RelativeLayout>
引入命名空間在現在只需要添加xmlns:lt="http://schemas.android.com/apk/res-auto"即可(lt換成你自己的命名空間名稱),而在以前引入命名空間方式為xmlns:custom="http://schemas.android.com/apk/res/com.example.customview01",res後面的包路徑指的是項目的package`
(3)在構造方法中擷取自訂屬性的值
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IndentifyingCode);mCodeCount = typedArray.getInteger(R.styleable.IndentifyingCode_codeCount, 5); // 擷取布局中驗證碼位元屬性值,預設為5個// 擷取布局中驗證碼文字的大小,預設為20spmTextSize = typedArray.getDimension(R.styleable.IndentifyingCode_textSize, typedArray.getDimensionPixelSize(R.styleable.IndentifyingCode_textSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, getResources().getDisplayMetrics())));// 一個好的習慣是用完資源要記得回收,就想開啟資料庫和IO流用完後要記得關閉一樣typedArray.recycle();
OK,自訂屬性也完成了,值也擷取到了,那麼我們只需要將定製的屬性值在我們onDraw()繪製的時候使用到就行了,自訂屬性就是這麼簡單~,看到這裡,也許有點混亂了,看一下完整代碼整理一下。
package com.lt.identifyingcode;import android.content.Context;import android.content.res.TypedArray;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Path;import android.graphics.PointF;import android.util.AttributeSet;import android.util.TypedValue;import android.view.MotionEvent;import android.view.View;import java.util.ArrayList;import java.util.Random;/*** Created by lt on 2016/3/2.*/public class ValidationCode extends View{/*** 控制項的寬度*/private int mWidth;/*** 控制項的高度*/private int mHeight;/*** 驗證碼文本畫筆*/private Paint mTextPaint; // 文本畫筆/*** 幹擾點座標的集合*/private ArrayList<PointF> mPoints = new ArrayList<PointF>();private Random mRandom = new Random();;/*** 幹擾點畫筆*/private Paint mPointPaint;/*** 繪製貝茲路徑的路徑集合*/private ArrayList<Path> mPaths = new ArrayList<Path>();/*** 幹擾線畫筆*/private Paint mPathPaint;/*** 驗證碼字串*/private String mCodeString;/*** 驗證碼的位元*/private int mCodeCount;/*** 驗證碼字元的大小*/private float mTextSize;/*** 驗證碼字串的顯示寬度*/private float mTextWidth;/*** 在java代碼中建立view的時候調用,即new* @param context*/public ValidationCode(Context context) {this(context,null);}/*** 在xml布局檔案中使用view但沒有指定style的時候調用* @param context* @param attrs*/public ValidationCode(Context context, AttributeSet attrs) {this(context, attrs, 0);}/*** 在xml布局檔案中使用view並指定style的時候調用* @param context* @param attrs* @param defStyleAttr*/public ValidationCode(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);getAttrValues(context, attrs);// 做一些初始化工作init();}/*** 擷取布局檔案中的值* @param context*/private void getAttrValues(Context context,AttributeSet attrs) {TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IndentifyingCode);mCodeCount = typedArray.getInteger(R.styleable.IndentifyingCode_codeCount, 5); // 擷取布局中驗證碼位元屬性值,預設為5個// 擷取布局中驗證碼文字的大小,預設為20spmTextSize = typedArray.getDimension(R.styleable.IndentifyingCode_textSize, typedArray.getDimensionPixelSize(R.styleable.IndentifyingCode_textSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, getResources().getDisplayMetrics())));// 一個好的習慣是用完資源要記得回收,就想開啟資料庫和IO流用完後要記得關閉一樣typedArray.recycle();}/*** 要像layout_width和layout_height屬性支援wrap_content就必須重新這個方法* @param widthMeasureSpec* @param heightMeasureSpec*/@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// 分別測量控制項的寬度和高度,基本為模板方法int measureWidth = measureWidth(widthMeasureSpec);int measureHeight = measureHeight(heightMeasureSpec);// 其實這個方法最終會調用setMeasuredDimension(int measureWidth,int measureHeight);// 將測量出來的寬高設定進去完成測量setMeasuredDimension(measureWidth, measureHeight);}@Overrideprotected void onDraw(Canvas canvas) {// 初始化資料initData();int length = mCodeString.length();float charLength = mTextWidth/length;for(int i=1;i<=length;i++){int offsetDegree = mRandom.nextInt(15);// 這裡只會產生0和1,如果是1那麼正旋轉正角度,否則旋轉負角度offsetDegree = mRandom.nextInt(2) == 1?offsetDegree:-offsetDegree;canvas.save();canvas.rotate(offsetDegree, mWidth / 2, mHeight / 2);// 給畫筆設定隨機顏色mTextPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);canvas.drawText(String.valueOf(mCodeString.charAt(i - 1)), (i-1) * charLength * 1.6f+30, mHeight * 2 / 3f, mTextPaint);canvas.restore();}// 產生幹擾效果1 -- 幹擾點for(PointF pointF : mPoints){mPointPaint.setARGB(255,mRandom.nextInt(200)+20,mRandom.nextInt(200)+20,mRandom.nextInt(200)+20);canvas.drawPoint(pointF.x,pointF.y,mPointPaint);}// 產生幹擾效果2 -- 幹擾線for(Path path : mPaths){mPathPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);canvas.drawPath(path, mPathPaint);}}private void initData() {// 擷取控制項的寬和高,此時已經測量完成mHeight = getHeight();mWidth = getWidth();mPoints.clear();// 產生幹擾點座標for(int i=0;i<150;i++){PointF pointF = new PointF(mRandom.nextInt(mWidth)+10,mRandom.nextInt(mHeight)+10);mPoints.add(pointF);}mPaths.clear();// 產生幹擾線座標for(int i=0;i<2;i++){Path path = new Path();int startX = mRandom.nextInt(mWidth/3)+10;int startY = mRandom.nextInt(mHeight/3)+10;int endX = mRandom.nextInt(mWidth/2)+mWidth/2-10;int endY = mRandom.nextInt(mHeight/2)+mHeight/2-10;path.moveTo(startX,startY);path.quadTo(Math.abs(endX-startX)/2,Math.abs(endY-startY)/2,endX,endY);mPaths.add(path);}}/*** 初始化一些資料*/private void init() {// 產生隨機數字和字母組合mCodeString = getCharAndNumr(mCodeCount);// 初始化文字畫筆mTextPaint = new Paint();mTextPaint.setStrokeWidth(3); // 畫筆大小為3mTextPaint.setTextSize(mTextSize); // 設定文字大小// 初始化幹擾點畫筆mPointPaint = new Paint();mPointPaint.setStrokeWidth(6);mPointPaint.setStrokeCap(Paint.Cap.ROUND); // 設定斷點處為圓形// 初始化幹擾線畫筆mPathPaint = new Paint();mPathPaint.setStrokeWidth(5);mPathPaint.setColor(Color.GRAY);mPathPaint.setStyle(Paint.Style.STROKE); // 設定畫筆為空白心mPathPaint.setStrokeCap(Paint.Cap.ROUND); // 設定斷點處為圓形// 取得驗證碼字串顯示的寬度值mTextWidth = mTextPaint.measureText(mCodeString);}/*** java產生隨機數字和字母組合* @param length[產生隨機數的長度]* @return*/public static String getCharAndNumr(int length) {String val = "";Random random = new Random();for (int i = 0; i < length; i++) {// 輸出字母還是數字String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num";// 字串if ("char".equalsIgnoreCase(charOrNum)) {// 取得大寫字母還是小寫字母int choice = random.nextInt(2) % 2 == 0 ? 65 : 97;val += (char) (choice + random.nextInt(26));} else if ("num".equalsIgnoreCase(charOrNum)) { // 數字val += String.valueOf(random.nextInt(10));}}return val;}@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()){case MotionEvent.ACTION_DOWN:// 重建隨機數字和字母組合mCodeString = getCharAndNumr(mCodeCount);invalidate();break;default:break;}return super.onTouchEvent(event);}/*** 測量寬度* @param widthMeasureSpec*/private int measureWidth(int widthMeasureSpec) {int result = (int) (mTextWidth*1.8f);int widthMode = MeasureSpec.getMode(widthMeasureSpec);int widthSize = MeasureSpec.getSize(widthMeasureSpec);if(widthMode == MeasureSpec.EXACTLY){// 精確測量模式,即布局檔案中layout_width或layout_height一般為精確的值或match_parentresult = widthSize; // 既然是精確模式,那麼直接返回測量的寬度即可}else{if(widthMode == MeasureSpec.AT_MOST) {// 最大值模式,即布局檔案中layout_width或layout_height一般為wrap_contentresult = Math.min(result,widthSize);}}return result;}/*** 測量高度* @param heightMeasureSpec*/private int measureHeight(int heightMeasureSpec) {int result = (int) (mTextWidth/1.6f);int heightMode = MeasureSpec.getMode(heightMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec);if(heightMode == MeasureSpec.EXACTLY){// 精確測量模式,即布局檔案中layout_width或layout_height一般為精確的值或match_parentresult = heightSize; // 既然是精確模式,那麼直接返回測量的寬度即可}else{if(heightMode == MeasureSpec.AT_MOST) {// 最大值模式,即布局檔案中layout_width或layout_height一般為wrap_contentresult = Math.min(result,heightSize);}}return result;}/*** 擷取驗證碼字串,進行匹配的時候只需要字串比較即可(具體比較規則自己決定)* @return 驗證碼字串*/public String getCodeString() {return mCodeString;}}
總結:這裡與其說自訂View到不如說是繪製圖形,關鍵在於座標點的計算,這裡在計算座標上也許不太好,以上是給大家分享Android自訂view製作絢麗的驗證碼,希望對大家有所協助!大家有什麼好的思路或者建議希望可以留言告訴我,感激不盡~。