Android自定義view制作絢麗的驗(yàn)證碼
廢話(huà)不多說(shuō)了,先給大家展示下自定義view效果圖,如果大家覺(jué)得還不錯(cuò)的話(huà),請(qǐng)繼續(xù)往下閱讀。
怎么樣,這種驗(yàn)證碼是不是很常見(jiàn)呢,下面我們就自己動(dòng)手實(shí)現(xiàn)這種效果,自己動(dòng)手,豐衣足食,哈哈~
一、 自定義view的步驟
自定義view一直被認(rèn)為android進(jìn)階通向高手的必經(jīng)之路,其實(shí)自定義view好簡(jiǎn)單,自定義view真正難的是如何繪制出高難度的圖形,這需要有好的數(shù)學(xué)功底(后悔沒(méi)有好好學(xué)數(shù)學(xué)了~),因?yàn)槔L制圖形經(jīng)常要計(jì)算坐標(biāo)點(diǎn)及類(lèi)似的幾何變換等等。自定義view通常只需要以下幾個(gè)步驟:
寫(xiě)一個(gè)類(lèi)繼承View類(lèi);
重新View的構(gòu)造方法;
測(cè)量View的大小,也就是重寫(xiě)onMeasure()方法;
重新onDraw()方法。
其中第三步不是必須的,只有當(dāng)系統(tǒng)無(wú)法確定自定義的view的大小的時(shí)候需要我們自己重寫(xiě)onMeasure()方法來(lái)完成自定義view大小的測(cè)量,因?yàn)槿绻脩?hù)(程序員)在使用我們的自定義view的時(shí)候沒(méi)有指定其精確大?。▽挾然蚋叨龋纾翰季治募衛(wèi)ayout_width或layout_heigth屬性值為wrap_content而不是match_parent或某個(gè)精確的值,那么系統(tǒng)就不知道我們自定義view在onDraw()中繪制的圖形的大小,所以通常要讓我們自定義view支持wrap_content那么我們就必須重寫(xiě)onMeasure方法來(lái)告訴系統(tǒng)我們要繪制的view的大?。▽挾群透叨龋?。
還有,如果我們自定義view需要一些特殊的屬性,那么我們還需要自定義屬性,這篇文章將會(huì)涉及到自定義屬性和上面的四個(gè)步驟的內(nèi)容。
二、 自定義view的實(shí)現(xiàn)
要實(shí)現(xiàn)這種驗(yàn)證碼控件,我們需要先分析一下它要怎么實(shí)現(xiàn)。通過(guò)看上面的效果圖,我們可以知道要實(shí)現(xiàn)這種效果,首先需要在繪制驗(yàn)證碼字符串,即圖中的文本部分,然后繪制一些干擾點(diǎn),再就是繪制干擾線(xiàn)了,分析完畢。下面我們根據(jù)分析結(jié)果一步步實(shí)現(xiàn)這種效果。
1. 繼承View,重寫(xiě)構(gòu)造方法
寫(xiě)一個(gè)類(lèi)繼承View,然后重新它的構(gòu)造方法
/** * Created by lt on 2016/3/2. */ public class ValidationCode extends View{ /** * 在java代碼中創(chuàng)建view的時(shí)候調(diào)用,即new * @param context */ public ValidationCode(Context context) { this(context,null); } /** * 在xml布局文件中使用view但沒(méi)有指定style的時(shí)候調(diào)用 * @param context * @param attrs */ public ValidationCode(Context context, AttributeSet attrs) { this(context, attrs, 0); } /** * 在xml布局文件中使用view并指定style的時(shí)候調(diào)用 * @param context * @param attrs * @param defStyleAttr */ public ValidationCode(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 做一些初始化工作 init(); } }
View有三個(gè)構(gòu)造方法,一般的做法都是讓一個(gè)參數(shù)和兩個(gè)參數(shù)的構(gòu)造方法調(diào)用三個(gè)構(gòu)造參數(shù)的方法,這三個(gè)構(gòu)造方法的調(diào)用情況看方法上面的注釋。在這個(gè)構(gòu)造方法里面我們先做一些初始化隨機(jī)驗(yàn)證碼字符串,畫(huà)筆等工作:
/** * 初始化一些數(shù)據(jù) */ private void init() { // 生成隨機(jī)數(shù)字和字母組合 mCodeString = getCharAndNumr(mCodeCount); // 初始化文字畫(huà)筆 mTextPaint = new Paint(); mTextPaint.setStrokeWidth(3); // 畫(huà)筆大小為3 mTextPaint.setTextSize(mTextSize); // 設(shè)置文字大小 // 初始化干擾點(diǎn)畫(huà)筆 mPointPaint = new Paint(); mPointPaint.setStrokeWidth(6); mPointPaint.setStrokeCap(Paint.Cap.ROUND); // 設(shè)置斷點(diǎn)處為圓形 // 初始化干擾線(xiàn)畫(huà)筆 mPathPaint = new Paint(); mPathPaint.setStrokeWidth(5); mPathPaint.setColor(Color.GRAY); mPathPaint.setStyle(Paint.Style.STROKE); // 設(shè)置畫(huà)筆為空心 mPathPaint.setStrokeCap(Paint.Cap.ROUND); // 設(shè)置斷點(diǎn)處為圓形 // 取得驗(yàn)證碼字符串顯示的寬度值 mTextWidth = mTextPaint.measureText(mCodeString); }
到這里,我們就完成了自定義View步驟中的前面的兩小步了,接下來(lái)就是完成第三步,即重寫(xiě)onMeasure()進(jìn)行我們自定義view大?。▽捀撸┑臏y(cè)量了:
2. 重寫(xiě)onMeasure(),完成View大小的測(cè)量
/** * 要像layout_width和layout_height屬性支持wrap_content就必須重新這個(gè)方法 * @param widthMeasureSpec * @param heightMeasureSpec */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 分別測(cè)量控件的寬度和高度,基本為模板方法 int measureWidth = measureWidth(widthMeasureSpec); int measureHeight = measureHeight(heightMeasureSpec); // 其實(shí)這個(gè)方法最終會(huì)調(diào)用setMeasuredDimension(int measureWidth,int measureHeight); // 將測(cè)量出來(lái)的寬高設(shè)置進(jìn)去完成測(cè)量 setMeasuredDimension(measureWidth, measureHeight); }
測(cè)量寬度的方法:
/** * 測(cè)量寬度 * @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){ // 精確測(cè)量模式,即布局文件中l(wèi)ayout_width或layout_height一般為精確的值或match_parent result = widthSize; // 既然是精確模式,那么直接返回測(cè)量的寬度即可 }else{ if(widthMode == MeasureSpec.AT_MOST) { // 最大值模式,即布局文件中l(wèi)ayout_width或layout_height一般為wrap_content result = Math.min(result,widthSize); } } return result; }
測(cè)量高度的方法:
/** * 測(cè)量高度 * @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){ // 精確測(cè)量模式,即布局文件中l(wèi)ayout_width或layout_height一般為精確的值或match_parent result = heightSize; // 既然是精確模式,那么直接返回測(cè)量的寬度即可 }else{ if(heightMode == MeasureSpec.AT_MOST) { // 最大值模式,即布局文件中l(wèi)ayout_width或layout_height一般為wrap_content result = Math.min(result,heightSize); } } return result; }
說(shuō)明:其實(shí)onMeasure()方法最終會(huì)調(diào)用setMeasuredDimension(int measureWidth,int measureHeight);將測(cè)量出來(lái)的寬高設(shè)置進(jìn)去完成測(cè)量,而我們要做的就是測(cè)量得到寬度和高度的值,測(cè)量寬度和高度的方法最重要的就是得到當(dāng)用戶(hù)(程序員)沒(méi)有給我們的控件指定精確的值(具體數(shù)值或match_parent)時(shí)合適的寬度和高度,所以,以上測(cè)量寬度和高度的方法基本上是一個(gè)模板方法,要做的就是得到result的一個(gè)合適的值,這里我們無(wú)需關(guān)注給result的那個(gè)值,因?yàn)檫@個(gè)值根據(jù)控件算出來(lái)的一個(gè)合適的值(也許不是很合適)。
完成了控件的測(cè)量,那么接下來(lái)我們還要完成控件的繪制這一大步,也就是自定義view的核心的一步重寫(xiě)onDraw()方法繪制圖形。
3. 重寫(xiě)onDraw(),繪制圖形
根據(jù)我們上面的分析,我們需要繪制驗(yàn)證碼文本字符串,干擾點(diǎn),干擾線(xiàn)。由于干擾點(diǎn)和干擾線(xiàn)需要坐標(biāo)和路徑來(lái)繪制, 所以在繪制之前先做一些初始化隨機(jī)干擾點(diǎn)坐標(biāo)和干擾線(xiàn)路徑:
private void initData() { // 獲取控件的寬和高,此時(shí)已經(jīng)測(cè)量完成 mHeight = getHeight(); mWidth = getWidth(); mPoints.clear(); // 生成干擾點(diǎn)坐標(biāo) for(int i=0;i<150;i++){ PointF pointF = new PointF(mRandom.nextInt(mWidth)+10,mRandom.nextInt(mHeight)+10); mPoints.add(pointF); } mPaths.clear(); // 生成干擾線(xiàn)坐標(biāo) 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); } }
有了這些數(shù)據(jù)之后,我們可以開(kāi)始繪制圖形了。
(1)繪制驗(yàn)證碼文本字符串
由于驗(yàn)證碼文本字符串是隨機(jī)生成的,所以我們需要利用代碼來(lái)隨機(jī)生成這種隨機(jī)驗(yàn)證碼:
/** * java生成隨機(jī)數(shù)字和字母組合 * @param length[生成隨機(jī)數(shù)的長(zhǎng)度] * @return */ public static String getCharAndNumr(int length) { String val = ""; Random random = new Random(); for (int i = 0; i < length; i++) { // 輸出字母還是數(shù)字 String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num"; // 字符串 if ("char".equalsIgnoreCase(charOrNum)) { // 取得大寫(xiě)字母還是小寫(xiě)字母 int choice = random.nextInt(2) % 2 == 0 ? 65 : 97; val += (char) (choice + random.nextInt(26)); } else if ("num".equalsIgnoreCase(charOrNum)) { // 數(shù)字 val += String.valueOf(random.nextInt(10)); } } return val; }
這種代碼是java基礎(chǔ),相信大家都看得懂,看不懂也沒(méi)關(guān)系,這種代碼網(wǎng)上隨便一搜就有,其實(shí)我也是直接從網(wǎng)上搜的,嘿嘿~。
android的2D圖形api canvas提供了drawXXX()方法來(lái)完成各種圖形的繪制,其中就有drawText()方法來(lái)繪制文本,同時(shí)還有drawPosText()在給定的坐標(biāo)點(diǎn)上繪制文本,drawTextOnPath()在給定途徑上繪制圖形。仔細(xì)觀察上面的效果圖,發(fā)現(xiàn)文本有的不是水平的,即有的被傾斜了,這就可以給我們的驗(yàn)證碼提升一定的識(shí)別難度,要實(shí)現(xiàn)文字傾斜效果,我們可以通過(guò)drawTextOnPath()在給定路徑繪制文本達(dá)到傾斜效果,然而這種方法實(shí)現(xiàn)比較困難(坐標(biāo)點(diǎn)和路徑難以計(jì)算),所以,我們可以通過(guò)canvas提供的位置變換方法rorate()結(jié)合drawText()實(shí)現(xiàn)文本傾斜效果。
int length = mCodeString.length(); float charLength = mTextWidth/length; for(int i=1;i<=length;i++){ int offsetDegree = mRandom.nextInt(15); // 這里只會(huì)產(chǎn)生0和1,如果是1那么正旋轉(zhuǎn)正角度,否則旋轉(zhuǎn)負(fù)角度 offsetDegree = mRandom.nextInt(2) == 1?offsetDegree:-offsetDegree; canvas.save(); canvas.rotate(offsetDegree, mWidth / 2, mHeight / 2); // 給畫(huà)筆設(shè)置隨機(jī)顏色,+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(); }
這段代碼通過(guò)for循環(huán)分別繪制驗(yàn)證碼字符串中的每個(gè)字符,每繪制一個(gè)字符都將畫(huà)布旋轉(zhuǎn)一個(gè)隨機(jī)的正負(fù)角度,然后通過(guò)drawText()方法繪制字符,每個(gè)字符的繪制起點(diǎn)坐標(biāo)根據(jù)字符的長(zhǎng)度和位置不同而不同,這個(gè)自己計(jì)算,這里也許也不是很合適。要注意的是,每次對(duì)畫(huà)布canvas進(jìn)行位置變換的時(shí)候都要先調(diào)用canvas.save()方法保存好之前繪制的圖形,繪制結(jié)束后調(diào)用canvas.restore()恢復(fù)畫(huà)布的位置,以便下次繪制圖形的時(shí)候不會(huì)由于之前畫(huà)布的位置變化而受影響。
(2)繪制干擾點(diǎn)
// 產(chǎn)生干擾效果1 -- 干擾點(diǎn) 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); }
給干擾點(diǎn)畫(huà)筆設(shè)置隨機(jī)顏色,然后根據(jù)隨機(jī)產(chǎn)生的點(diǎn)的坐標(biāo)利用canvas.drawPoint()繪制點(diǎn)。
(3)繪制干擾線(xiàn)
// 產(chǎn)生干擾效果2 -- 干擾線(xiàn) for(Path path : mPaths){ mPathPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20); canvas.drawPath(path, mPathPaint); }
給干擾線(xiàn)畫(huà)筆設(shè)置隨機(jī)顏色,然后根據(jù)隨機(jī)產(chǎn)生路徑利用canvas.drawPath()繪制貝塞爾曲線(xiàn),從而繪制出干擾線(xiàn)。
4. 重寫(xiě)onTouchEvent,定制View事件
這里做這一步是為了實(shí)現(xiàn)當(dāng)我們點(diǎn)擊我們的自定義View的時(shí)候,完成一些操作,即定制View事件。這里,我們需要當(dāng)用戶(hù)點(diǎn)擊驗(yàn)證碼控件的時(shí)候,改變驗(yàn)證碼的文本字符串。
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: // 重新生成隨機(jī)數(shù)字和字母組合 mCodeString = getCharAndNumr(mCodeCount); invalidate(); break; default: break; } return super.onTouchEvent(event); }
OK,到這里我們的這個(gè)自定義View就基本完成了,可能大家會(huì)問(wèn),這個(gè)自定義View是不是擴(kuò)展性太差了,定制性太低了,說(shuō)好的自定義屬性呢?跑哪里去了。不要急,下面我們就來(lái)自定義我們自己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>
說(shuō)明:
在attrs.xml文件中的attr節(jié)點(diǎn)中定義我們的屬性,定義屬性需要name屬性表示我們的屬性值,同時(shí)需要format屬性表示屬性值的格式,其格式有很多種,如果屬性值可以使多種格式,那么格式間用”|”分開(kāi);
declare-styleable節(jié)點(diǎn)用來(lái)定義我們自定義屬性集,其name屬性指定了該屬性集的名稱(chēng),可以任意,但一般為自定義控件的名稱(chēng);
如果屬性已經(jīng)定義了(如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>
引入命名空間在現(xiàn)在只需要添加xmlns:lt="http://schemas.android.com/apk/res-auto"即可(lt換成你自己的命名空間名稱(chēng)),而在以前引入命名空間方式為xmlns:custom="http://schemas.android.com/apk/res/com.example.customview01",res后面的包路徑指的是項(xiàng)目的package`
(3)在構(gòu)造方法中獲取自定義屬性的值
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IndentifyingCode); mCodeCount = typedArray.getInteger(R.styleable.IndentifyingCode_codeCount, 5); // 獲取布局中驗(yàn)證碼位數(shù)屬性值,默認(rèn)為5個(gè) // 獲取布局中驗(yàn)證碼文字的大小,默認(rèn)為20sp mTextSize = typedArray.getDimension(R.styleable.IndentifyingCode_textSize, typedArray.getDimensionPixelSize(R.styleable.IndentifyingCode_textSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, getResources().getDisplayMetrics()))); // 一個(gè)好的習(xí)慣是用完資源要記得回收,就想打開(kāi)數(shù)據(jù)庫(kù)和IO流用完后要記得關(guān)閉一樣 typedArray.recycle();
OK,自定義屬性也完成了,值也獲取到了,那么我們只需要將定制的屬性值在我們onDraw()繪制的時(shí)候使用到就行了,自定義屬性就是這么簡(jiǎn)單~,看到這里,也許有點(diǎn)混亂了,看一下完整代碼整理一下。
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; /** * 驗(yàn)證碼文本畫(huà)筆 */ private Paint mTextPaint; // 文本畫(huà)筆 /** * 干擾點(diǎn)坐標(biāo)的集合 */ private ArrayList<PointF> mPoints = new ArrayList<PointF>(); private Random mRandom = new Random();; /** * 干擾點(diǎn)畫(huà)筆 */ private Paint mPointPaint; /** * 繪制貝塞爾曲線(xiàn)的路徑集合 */ private ArrayList<Path> mPaths = new ArrayList<Path>(); /** * 干擾線(xiàn)畫(huà)筆 */ private Paint mPathPaint; /** * 驗(yàn)證碼字符串 */ private String mCodeString; /** * 驗(yàn)證碼的位數(shù) */ private int mCodeCount; /** * 驗(yàn)證碼字符的大小 */ private float mTextSize; /** * 驗(yàn)證碼字符串的顯示寬度 */ private float mTextWidth; /** * 在java代碼中創(chuàng)建view的時(shí)候調(diào)用,即new * @param context */ public ValidationCode(Context context) { this(context,null); } /** * 在xml布局文件中使用view但沒(méi)有指定style的時(shí)候調(diào)用 * @param context * @param attrs */ public ValidationCode(Context context, AttributeSet attrs) { this(context, attrs, 0); } /** * 在xml布局文件中使用view并指定style的時(shí)候調(diào)用 * @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); // 獲取布局中驗(yàn)證碼位數(shù)屬性值,默認(rèn)為5個(gè) // 獲取布局中驗(yàn)證碼文字的大小,默認(rèn)為20sp mTextSize = typedArray.getDimension(R.styleable.IndentifyingCode_textSize, typedArray.getDimensionPixelSize(R.styleable.IndentifyingCode_textSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, getResources().getDisplayMetrics()))); // 一個(gè)好的習(xí)慣是用完資源要記得回收,就想打開(kāi)數(shù)據(jù)庫(kù)和IO流用完后要記得關(guān)閉一樣 typedArray.recycle(); } /** * 要像layout_width和layout_height屬性支持wrap_content就必須重新這個(gè)方法 * @param widthMeasureSpec * @param heightMeasureSpec */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 分別測(cè)量控件的寬度和高度,基本為模板方法 int measureWidth = measureWidth(widthMeasureSpec); int measureHeight = measureHeight(heightMeasureSpec); // 其實(shí)這個(gè)方法最終會(huì)調(diào)用setMeasuredDimension(int measureWidth,int measureHeight); // 將測(cè)量出來(lái)的寬高設(shè)置進(jìn)去完成測(cè)量 setMeasuredDimension(measureWidth, measureHeight); } @Override protected void onDraw(Canvas canvas) { // 初始化數(shù)據(jù) initData(); int length = mCodeString.length(); float charLength = mTextWidth/length; for(int i=1;i<=length;i++){ int offsetDegree = mRandom.nextInt(15); // 這里只會(huì)產(chǎn)生0和1,如果是1那么正旋轉(zhuǎn)正角度,否則旋轉(zhuǎn)負(fù)角度 offsetDegree = mRandom.nextInt(2) == 1?offsetDegree:-offsetDegree; canvas.save(); canvas.rotate(offsetDegree, mWidth / 2, mHeight / 2); // 給畫(huà)筆設(shè)置隨機(jī)顏色 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(); } // 產(chǎn)生干擾效果1 -- 干擾點(diǎn) 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); } // 產(chǎn)生干擾效果2 -- 干擾線(xiàn) 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() { // 獲取控件的寬和高,此時(shí)已經(jīng)測(cè)量完成 mHeight = getHeight(); mWidth = getWidth(); mPoints.clear(); // 生成干擾點(diǎn)坐標(biāo) for(int i=0;i<150;i++){ PointF pointF = new PointF(mRandom.nextInt(mWidth)+10,mRandom.nextInt(mHeight)+10); mPoints.add(pointF); } mPaths.clear(); // 生成干擾線(xiàn)坐標(biāo) 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); } } /** * 初始化一些數(shù)據(jù) */ private void init() { // 生成隨機(jī)數(shù)字和字母組合 mCodeString = getCharAndNumr(mCodeCount); // 初始化文字畫(huà)筆 mTextPaint = new Paint(); mTextPaint.setStrokeWidth(3); // 畫(huà)筆大小為3 mTextPaint.setTextSize(mTextSize); // 設(shè)置文字大小 // 初始化干擾點(diǎn)畫(huà)筆 mPointPaint = new Paint(); mPointPaint.setStrokeWidth(6); mPointPaint.setStrokeCap(Paint.Cap.ROUND); // 設(shè)置斷點(diǎn)處為圓形 // 初始化干擾線(xiàn)畫(huà)筆 mPathPaint = new Paint(); mPathPaint.setStrokeWidth(5); mPathPaint.setColor(Color.GRAY); mPathPaint.setStyle(Paint.Style.STROKE); // 設(shè)置畫(huà)筆為空心 mPathPaint.setStrokeCap(Paint.Cap.ROUND); // 設(shè)置斷點(diǎn)處為圓形 // 取得驗(yàn)證碼字符串顯示的寬度值 mTextWidth = mTextPaint.measureText(mCodeString); } /** * java生成隨機(jī)數(shù)字和字母組合 * @param length[生成隨機(jī)數(shù)的長(zhǎng)度] * @return */ public static String getCharAndNumr(int length) { String val = ""; Random random = new Random(); for (int i = 0; i < length; i++) { // 輸出字母還是數(shù)字 String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num"; // 字符串 if ("char".equalsIgnoreCase(charOrNum)) { // 取得大寫(xiě)字母還是小寫(xiě)字母 int choice = random.nextInt(2) % 2 == 0 ? 65 : 97; val += (char) (choice + random.nextInt(26)); } else if ("num".equalsIgnoreCase(charOrNum)) { // 數(shù)字 val += String.valueOf(random.nextInt(10)); } } return val; } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: // 重新生成隨機(jī)數(shù)字和字母組合 mCodeString = getCharAndNumr(mCodeCount); invalidate(); break; default: break; } return super.onTouchEvent(event); } /** * 測(cè)量寬度 * @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){ // 精確測(cè)量模式,即布局文件中l(wèi)ayout_width或layout_height一般為精確的值或match_parent result = widthSize; // 既然是精確模式,那么直接返回測(cè)量的寬度即可 }else{ if(widthMode == MeasureSpec.AT_MOST) { // 最大值模式,即布局文件中l(wèi)ayout_width或layout_height一般為wrap_content result = Math.min(result,widthSize); } } return result; } /** * 測(cè)量高度 * @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){ // 精確測(cè)量模式,即布局文件中l(wèi)ayout_width或layout_height一般為精確的值或match_parent result = heightSize; // 既然是精確模式,那么直接返回測(cè)量的寬度即可 }else{ if(heightMode == MeasureSpec.AT_MOST) { // 最大值模式,即布局文件中l(wèi)ayout_width或layout_height一般為wrap_content result = Math.min(result,heightSize); } } return result; } /** * 獲取驗(yàn)證碼字符串,進(jìn)行匹配的時(shí)候只需要字符串比較即可(具體比較規(guī)則自己決定) * @return 驗(yàn)證碼字符串 */ public String getCodeString() { return mCodeString; } }
總結(jié):這里與其說(shuō)自定義View到不如說(shuō)是繪制圖形,關(guān)鍵在于坐標(biāo)點(diǎn)的計(jì)算,這里在計(jì)算坐標(biāo)上也許不太好,以上是給大家分享Android自定義view制作絢麗的驗(yàn)證碼,希望對(duì)大家有所幫助!大家有什么好的思路或者建議希望可以留言告訴我,感激不盡~。
- Android自定義控件通用驗(yàn)證碼輸入框的實(shí)現(xiàn)
- Android自定義控件深入學(xué)習(xí) Android生成隨機(jī)驗(yàn)證碼
- Android自定義View獲取注冊(cè)驗(yàn)證碼倒計(jì)時(shí)按鈕
- Android自定義Chronometer實(shí)現(xiàn)短信驗(yàn)證碼秒表倒計(jì)時(shí)功能
- Android自定義View實(shí)現(xiàn)隨機(jī)驗(yàn)證碼
- Android自定義方框EditText注冊(cè)驗(yàn)證碼
- Android自定義View實(shí)現(xiàn)驗(yàn)證碼
- Android自定義控件實(shí)現(xiàn)驗(yàn)證碼倒計(jì)時(shí)
- Android自定義View繪制隨機(jī)生成圖片驗(yàn)證碼
- Android View教程之自定義驗(yàn)證碼輸入框效果
相關(guān)文章
android滑動(dòng)解鎖震動(dòng)效果的開(kāi)啟和取消
在4.0的圓環(huán)滑動(dòng)解鎖中,我們點(diǎn)擊下去的時(shí)候會(huì)有震動(dòng)效果,因?yàn)檫@個(gè)控件設(shè)置的震動(dòng)效果沒(méi)有綁定設(shè)置中設(shè)置的觸摸振動(dòng)開(kāi)關(guān)來(lái)取消振動(dòng)效果,下邊這個(gè)例子實(shí)現(xiàn)了開(kāi)啟和取消的方法2013-06-06Android實(shí)現(xiàn)五子棋游戲(局域網(wǎng)版)
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)局域網(wǎng)版的五子棋游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05Android 監(jiān)聽(tīng)Notification 被清除實(shí)例代碼
本文主要介紹Android 監(jiān)聽(tīng)Notification 事件,這里給大家提供實(shí)例代碼進(jìn)行參考,有需要的小伙伴可以參考下2016-07-07android實(shí)現(xiàn)主動(dòng)連接和被動(dòng)連接的藍(lán)牙聊天功能
這篇文章主要為大家詳細(xì)介紹了android實(shí)現(xiàn)主動(dòng)連接和被動(dòng)連接的藍(lán)牙聊天功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06利用源碼編譯Android系統(tǒng)的APK和可執(zhí)行命令的方法
這篇文章主要介紹了利用源碼編譯Android系統(tǒng)的APK和可執(zhí)行命令的方法,示例在Linux系統(tǒng)環(huán)境上進(jìn)行構(gòu)建,需要的朋友可以參考下2016-02-02Android實(shí)現(xiàn)EditText內(nèi)容保存為Bitmap的方法
這篇文章主要介紹了Android實(shí)現(xiàn)EditText內(nèi)容保存為Bitmap的方法,涉及Android中saveBitmap方法的簡(jiǎn)單使用技巧,需要的朋友可以參考下2016-01-01Android常用三方庫(kù)混淆規(guī)則整理(小結(jié))
這篇文章主要介紹了Android常用三方庫(kù)混淆規(guī)則整理(小結(jié)),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-07-07