Android自定義拼圖驗證碼

2019.7.5更新墓律,Android自定義點選驗證碼已完成寻狂。

先上效果圖弹沽,沒圖說個蛋蛋:

ezgif-3-a89a55a08fab.gif

從效果圖開始"臨摹"

分析

從上面的效果圖中,我們可以很直觀的看出一共包含三個元素:背景圖螃宙、空缺部分托呕、填充部分差牛,需要注意的是:
1. 空缺部分缺失的圖片剛好是填充部分
2. 我們把填充部分位置固定在左側音羞,而隨機生成空缺部分在右側,增加驗證難度

思路

  1. 準備背景圖片,通過canvas.drawBitmap()方法畫出背景圖
  2. 計算View寬高秀撇,隨機生成空缺部分的x坐標在(width/3, width)范圍超棺,固定填充部分的x左邊在(0,width/3)范圍內呵燕,保證填充部分和空缺部分在初始化時沒有重疊棠绘。(不嚴謹,具體數值還要結合空缺部分/填充部分尺寸詳細計算再扭,僅提供思路)氧苍。
  3. 先隨機生成空缺部分,然后根據空缺部分在原來Bitmap上的左邊生成一樣大小一樣形狀的圖片泛范,用于填充部分让虐。
  4. 然后重寫onTouchEvent方法,處理拖動時填充部分的位移敦跌,在MotionEvent.ACTION_UP條件下澄干,計算填充部分和空缺部分在畫布中的x坐標差值逛揩,判斷當差值小于闕值 dx 時柠傍,則認為通過驗證,否則調用 invalidate() 方法重新生成驗證碼辩稽。

主要代碼分析

這里重寫了onMeasure方法惧笛,根據我們準備的原圖片尺寸設置View寬高,并且重新生成和View一樣尺寸的背景圖newBgBitmap逞泄,統一尺寸以便后面我們對左邊的轉化患整。(這里曾經有些地方參照畫布尺寸計算,有些地方參照背景圖bitmap尺寸計算喷众,導致填充部分和空缺部分沒有吻合)各谚。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int minimumWidth = getSuggestedMinimumWidth();
        /*根據原背景圖寬高比設置畫布尺寸*/
        width = measureSize(minimumWidth, widthMeasureSpec);
        float scale = width / (float) bgBitmap.getWidth();
        height = (int) (bgBitmap.getHeight() * scale);
        setMeasuredDimension(width, height);

        /*根據畫布尺寸生成相同尺寸的背景圖*/
        newBgBitmap = clipBitmap(bgBitmap, width, height);
        /*根據新的背景圖生成填充部分*/
        srcBitmap = createSmallBitmap(newBgBitmap);
    }

設置畫筆的混合模式,生成一張自定義形狀的圖片供填充部分使用

    public Bitmap createSmallBitmap(Bitmap var) {
        Bitmap bitmap = Bitmap.createBitmap(shadowSize, shadowSize, Bitmap.Config.ARGB_8888);
        Canvas canvas1 = new Canvas(bitmap);
        canvas1.drawCircle(shadowSize / 2, shadowSize / 2, shadowSize / 2, paintSrc);
        /*設置混合模式*/
        paintSrc.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

        /*在指定范圍隨機生成空缺部分坐標到千,保證空缺部分出現在View右側*/
        int min = width / 3;
        int max = width - shadowSize / 2 - padding;
        Random random = new Random();
        shadowLeft = random.nextInt(max) % (max - min + 1) + min;
        Rect rect = new Rect(shadowLeft, (height - shadowSize) / 2, shadowSize + shadowLeft, (height + shadowSize) / 2);
        RectF rectF = new RectF(0, 0, shadowSize, shadowSize);
        canvas1.drawBitmap(var, rect, rectF, paintSrc);
        paintSrc.setXfermode(null);
        return bitmap;
    }

在onDraw()方法中依次畫出背景圖昌渤、空缺部分、填充部分憔四,注意先后順序(具體細節(jié)自行處理膀息,例如陰影、凹凸感等等)

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        RectF rectF = new RectF(0, 0, width, height);
        /*畫背景圖*/
        canvas.drawBitmap(newBgBitmap, null, rectF, paintSrc);

        bgPaint.setColor(Color.parseColor("#000000"));
        /*畫空缺部分周圍陰影*/
        canvas.drawCircle(shadowLeft + shadowSize / 2, height / 2, shadowSize / 2, bgPaint);
        /*畫空缺部分*/
        canvas.drawCircle(shadowLeft + shadowSize / 2, height / 2, shadowSize / 2, paintShadow);

        Rect rect = new Rect(srcLeft, (height - shadowSize) / 2, shadowSize + srcLeft, (height + shadowSize) / 2);

        bgPaint.setColor(Color.parseColor("#FFFFFF"));
        /*畫填充部分周圍陰影*/
        canvas.drawCircle(srcLeft + shadowSize / 2, height / 2, shadowSize / 2, bgPaint);
        /*畫填充部分*/
        canvas.drawBitmap(srcBitmap, null, rect, paintSrc);
    }

草紙代碼參考

隨寫隨發(fā)布??

package com.example.qingfengwei.myapplication;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;

import java.util.Random;


public class SlidingVerificationView extends View {

    private Bitmap bgBitmap;
    private Bitmap newBgBitmap;
    private Bitmap srcBitmap;

    private Paint paintShadow;
    private Paint paintSrc;
    private float curX;
    private float lastX;

    private int dx;
    private int shadowSize = dp2px(60);
    private int padding = dp2px(40);
    private int shadowLeft;
    private int srcLeft = padding;

    private int width, height;

    private Paint bgPaint;

    private OnVerifyListener listener;

    public SlidingVerificationView(Context context) {
        this(context, null);
    }

    public SlidingVerificationView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SlidingVerificationView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        paintShadow = new Paint();
        paintShadow.setAntiAlias(true);
        paintShadow.setColor(Color.parseColor("#AA000000"));


        paintSrc = new Paint();
        paintSrc.setAntiAlias(true);
        paintSrc.setFilterBitmap(true);
        paintSrc.setStyle(Paint.Style.FILL_AND_STROKE);
        paintSrc.setColor(Color.WHITE);

        bgPaint = new Paint();
        bgPaint.setMaskFilter(new BlurMaskFilter(5, BlurMaskFilter.Blur.OUTER));
        bgPaint.setAntiAlias(true);
        bgPaint.setStyle(Paint.Style.FILL);

        bgBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.syzt);
    }

    public void setVerifyListener(OnVerifyListener listener) {
        this.listener = listener;
    }

    public Bitmap clipBitmap(Bitmap bm, int newWidth, int newHeight) {
        int width = bm.getWidth();
        int height = bm.getHeight();
        float scaleWidth = ((float) newWidth) / width;
        float scaleHeight = ((float) newHeight) / height;
        Matrix matrix = new Matrix();
        matrix.postScale(scaleWidth, scaleHeight);
        return Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true);
    }


    public Bitmap createSmallBitmap(Bitmap var) {
        Bitmap bitmap = Bitmap.createBitmap(shadowSize, shadowSize, Bitmap.Config.ARGB_8888);
        Canvas canvas1 = new Canvas(bitmap);
        canvas1.drawCircle(shadowSize / 2, shadowSize / 2, shadowSize / 2, paintSrc);
        /*設置混合模式*/
        paintSrc.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));


        /*在指定范圍隨機生成空缺部分坐標了赵,保證空缺部分出現在View右側*/
        int min = width / 3;
        int max = width - shadowSize / 2 - padding;
        Random random = new Random();
        shadowLeft = random.nextInt(max) % (max - min + 1) + min;
        Rect rect = new Rect(shadowLeft, (height - shadowSize) / 2, shadowSize + shadowLeft, (height + shadowSize) / 2);
        RectF rectF = new RectF(0, 0, shadowSize, shadowSize);
        canvas1.drawBitmap(var, rect, rectF, paintSrc);
        paintSrc.setXfermode(null);
        return bitmap;
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        curX = event.getRawX();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = event.getRawX();
                break;
            case MotionEvent.ACTION_MOVE:
                dx = (int) (curX - lastX);
                srcLeft = dx + padding;
                invalidate();
                break;
            case MotionEvent.ACTION_UP:

                boolean isSuccess = Math.abs(srcLeft - shadowLeft) < 8;

                if (isSuccess) {
                    Toast.makeText(getContext(), "驗證成功!", Toast.LENGTH_SHORT).show();
                    Log.d("w", "check success!");
                } else {
                    Toast.makeText(getContext(), "驗證失敗!", Toast.LENGTH_SHORT).show();
                    Log.d("w", "check fail!");
                    srcBitmap = createSmallBitmap(newBgBitmap);
                    srcLeft = padding;
                    invalidate();
                }

                if (listener != null) {
                    listener.onResult(isSuccess);
                }
                break;
        }

        return true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int minimumWidth = getSuggestedMinimumWidth();
        /*根據原背景圖寬高比設置畫布尺寸*/
        width = measureSize(minimumWidth, widthMeasureSpec);
        float scale = width / (float) bgBitmap.getWidth();
        height = (int) (bgBitmap.getHeight() * scale);
        setMeasuredDimension(width, height);

        /*根據畫布尺寸生成相同尺寸的背景圖*/
        newBgBitmap = clipBitmap(bgBitmap, width, height);
        /*根據新的背景圖生成填充部分*/
        srcBitmap = createSmallBitmap(newBgBitmap);

    }

    private int measureSize(int defaultSize, int measureSpec) {
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        int result = defaultSize;
        switch (mode) {
            case MeasureSpec.UNSPECIFIED:
                result = defaultSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                result = size;
                break;
        }
        return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        RectF rectF = new RectF(0, 0, width, height);
        /*畫背景圖*/
        canvas.drawBitmap(newBgBitmap, null, rectF, paintSrc);

        bgPaint.setColor(Color.parseColor("#000000"));
        /*畫空缺部分周圍陰影*/
        canvas.drawCircle(shadowLeft + shadowSize / 2, height / 2, shadowSize / 2, bgPaint);
        /*畫空缺部分*/
        canvas.drawCircle(shadowLeft + shadowSize / 2, height / 2, shadowSize / 2, paintShadow);

        Rect rect = new Rect(srcLeft, (height - shadowSize) / 2, shadowSize + srcLeft, (height + shadowSize) / 2);

        bgPaint.setColor(Color.parseColor("#FFFFFF"));
        /*畫填充部分周圍陰影*/
        canvas.drawCircle(srcLeft + shadowSize / 2, height / 2, shadowSize / 2, bgPaint);
        /*畫填充部分*/
        canvas.drawBitmap(srcBitmap, null, rect, paintSrc);
    }

    public static int dp2px(float dp) {
        float density = Resources.getSystem().getDisplayMetrics().density;
        return (int) (density * dp + 0.5f);
    }
}

下節(jié)預告:自定義點選驗證碼潜支,效果圖在文章開頭已經放出了,就跟驗證碼死磕上了柿汛,哈哈冗酿。。。

2019.7.5更新裁替,Android自定義點選驗證碼已完成鸠窗。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市胯究,隨后出現的幾起案子稍计,更是在濱河造成了極大的恐慌,老刑警劉巖裕循,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件臣嚣,死亡現場離奇詭異,居然都是意外死亡剥哑,警方通過查閱死者的電腦和手機硅则,發(fā)現死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來株婴,“玉大人怎虫,你說我怎么就攤上這事±Ы椋” “怎么了大审?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長座哩。 經常有香客問我徒扶,道長,這世上最難降的妖魔是什么根穷? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任姜骡,我火速辦了婚禮,結果婚禮上屿良,老公的妹妹穿的比我還像新娘圈澈。我一直安慰自己,他們只是感情好尘惧,可當我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布康栈。 她就那樣靜靜地躺著,像睡著了一般褥伴。 火紅的嫁衣襯著肌膚如雪谅将。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天重慢,我揣著相機與錄音饥臂,去河邊找鬼。 笑死似踱,一個胖子當著我的面吹牛隅熙,可吹牛的內容都是我干的稽煤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼囚戚,長吁一口氣:“原來是場噩夢啊……” “哼酵熙!你這毒婦竟也來了?” 一聲冷哼從身側響起驰坊,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤匾二,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后拳芙,有當地人在樹林里發(fā)現了一具尸體察藐,經...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年舟扎,在試婚紗的時候發(fā)現自己被綠了分飞。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡睹限,死狀恐怖譬猫,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情羡疗,我是刑警寧澤染服,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站顺囊,受9級特大地震影響肌索,放射性物質發(fā)生泄漏蕉拢。R本人自食惡果不足惜特碳,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望晕换。 院中可真熱鬧午乓,春花似錦、人聲如沸闸准。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽夷家。三九已至蒸其,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間库快,已是汗流浹背摸袁。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留义屏,地道東北人靠汁。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓蜂大,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蝶怔。 傳聞我的和親對象是個殘疾皇子奶浦,可洞房花燭夜當晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內容