1. 程式人生 > >Android自定義view(一),打造絢麗的驗證碼

Android自定義view(一),打造絢麗的驗證碼

前言:我相信信念的力量,信念可以支撐起一個人,一個名族,一個國家。正如“人沒有夢想和鹹魚有什麼區別”一樣,我有信念,有理想,所以我正在努力向著夢想前進~。

自定義view,如果是我,我首先要看到自定義view的效果圖,然後再想想怎麼實現這種效果或功能,所以先貼上自定義驗證碼控制元件的效果圖:

這裡寫圖片描述

怎麼樣,這種驗證碼是不是很常見呢,下面我們就自己動手實現這種效果,自己動手,豐衣足食,哈哈~

一、 自定義view的步驟

自定義view一直被認為android進階通向高手的必經之路,其實自定義view好簡單,自定義view真正難的是如何繪製出高難度的圖形,這需要有好的數學功底(後悔沒有好好學數學了~),因為繪製圖形經常要計算座標點及類似的幾何變換等等。自定義view通常只需要以下幾個步驟:

  1. 寫一個類繼承View類;
  2. 重新View的構造方法;
  3. 測量View的大小,也就是重寫onMeasure()方法;
  4. 重新onDraw()方法。

其中第三步不是必須的,只有當系統無法確定自定義的view的大小的時候需要我們自己重寫onMeasure()方法來完成自定義view大小的測量,因為如果使用者(程式設計師)在使用我們的自定義view的時候沒有指定其精確大小(寬度或高度),如:佈局檔案中layout_widthlayout_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); // 畫筆大小為3
    mTextPaint.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
*/
@Override
protected 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_parent
        result = widthSize; // 既然是精確模式,那麼直接返回測量的寬度即可
    }else{
        if(widthMode == MeasureSpec.AT_MOST) {
            // 最大值模式,即佈局檔案中layout_width或layout_height一般為wrap_content
            result = 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_parent
        result = heightSize; // 既然是精確模式,那麼直接返回測量的寬度即可
    }else{
        if(heightMode == MeasureSpec.AT_MOST) {
            // 最大值模式,即佈局檔案中layout_width或layout_height一般為wrap_content
            result = 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事件。這裡,我們需要當用戶點選驗證碼控制元件的時候,改變驗證碼的文字字串。

@Override
public 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.ValidationCode
        android: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個
// 獲取佈局中驗證碼文字的大小,預設為20sp
mTextSize = 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個
        // 獲取佈局中驗證碼文字的大小,預設為20sp
        mTextSize = 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
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        // 分別測量控制元件的寬度和高度,基本為模板方法
        int measureWidth = measureWidth(widthMeasureSpec);
        int measureHeight = measureHeight(heightMeasureSpec);

        // 其實這個方法最終會呼叫setMeasuredDimension(int measureWidth,int measureHeight);
        // 將測量出來的寬高設定進去完成測量
        setMeasuredDimension(measureWidth, measureHeight);
    }

    @Override
    protected 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); // 畫筆大小為3
        mTextPaint.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;
    }

    @Override
    public 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_parent
            result = widthSize; // 既然是精確模式,那麼直接返回測量的寬度即可
        }else{
            if(widthMode == MeasureSpec.AT_MOST) {
                // 最大值模式,即佈局檔案中layout_width或layout_height一般為wrap_content
                result = 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_parent
            result = heightSize; // 既然是精確模式,那麼直接返回測量的寬度即可
        }else{
            if(heightMode == MeasureSpec.AT_MOST) {
                // 最大值模式,即佈局檔案中layout_width或layout_height一般為wrap_content
                result = Math.min(result,heightSize);
            }
        }
        return result;
    }


    /**
     * 獲取驗證碼字串,進行匹配的時候只需要字串比較即可(具體比較規則自己決定)
     * @return 驗證碼字串
     */
    public String getCodeString() {
        return mCodeString;
    }
}

總結:這裡與其說自定義View到不如說是繪製圖形,關鍵在於座標點的計算,這裡在計算座標上也許不太好,大家有什麼好的思路或者建議希望可以留言告訴我,感激不盡~。