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()方法中依次画出背景图、空缺部分、填充部分,注意先后顺序(具体细节自行处理,例如阴影、凹凸感等等)

    @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);
    }

草纸代码参考

随写随发布😛

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);
    }
}

下节预告:自定义点选验证码,效果图在文章开头已经放出了,就跟验证码死磕上了,哈哈。。。

2019.7.5更新,Android自定义点选验证码已完成。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容