Android 自定义拼图验证码

作者: StormFeng | 来源:发表于2019-06-19 16:24 被阅读15次

    先上效果图,没图说个蛋蛋:

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

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

    相关文章

      网友评论

        本文标题:Android 自定义拼图验证码

        本文链接:https://www.haomeiwen.com/subject/tqjkqctx.html