美文网首页Android自定义ViewAndroid拾萃
【控件】仿斗鱼滑动拼图验证码控件

【控件】仿斗鱼滑动拼图验证码控件

作者: 锐心凌志 | 来源:发表于2018-04-27 13:02 被阅读92次

    概述

    本篇滑动验证码的代码其实上周四就写好了,结果周末赶上找房子,搬家,累掉了半条命,赶紧写篇博客恢复恢复元气。

    另外上次简书莫名其妙封我号,也不给我一个说法,当天又解封了。这个事我也挺不爽的,吐个槽

    上周一总监让我研究一波滑动验证码,说项目可能会上。我想了一下好像在斗鱼、淘宝都见过,结果下了这两个app,发现怎么点也出不来滑动验证码。于是,我就去web端斗鱼看了一下,果然,每次登陆都会出现验证码。
    好吧,那我们这次的目标就定为 在 Android端app上,自定义View,仿一个web端滑动验证码吧
    (后话,做到后面发现我有点蠢了,我应该直接模仿app端的,很多效果在web端应该很好实现 ,但是在Android端就不那么好整了。,例如验证成功的白光扫过动画,如下图。在Android上实现起来就不太容易,有些效果还是不如web端酷炫。)

    斗鱼web端效果 我们的Demo,Ac娘镇楼

    (图很渣,也忽略底下的SeekBar,这不是重点)
    一些动画,效果录不出来了,大家可以去斗鱼web端看一下,然后下载Demo看一下,效果还是可以的。
    代码 传送门:
    https://github.com/mcxtzhang/SwipeCaptcha

    我们的Demo和web端基本上一样。

    那么本控件包含不仅包含以下功能:

    • 随机区域起点(左上角x,y)生成一个验证码阴影。
    • 验证码拼图 凹凸图形会随机变换。
    • 验证码区域宽高可自定义。
    • 抠图验证码区域,绘制一个用于联动滑动的验证码滑块。
    • 验证失败,会闪烁几下然后回到原点。
    • 验证成功,会有白光扫过的动画。

    分解一下验证码核心实现思路:

    • 控件继承自ImageView。理由:
      1 如果放在项目中用,验证码图片希望可以是接口返回。ImageView以及其子类支持花式加载图片。
      2 继承自ImageView,绘制图片本身不用我们干预,也不用我们操心scaleType,节省很多工作。
    • onSizeChanged()方法中生成 和 控件宽高相关的属性值:
      1 初始化时随机生成验证码区域起点
      2 生成验证码区域Path
      3 生成滑块Bitmap
    • onDraw()时,依次绘制:
      1 验证码阴影
      2 滑块

    核心工作是以上,可是实现起来还是有很多坑的,下面一步一步来吧。


    验证码区域的生成

    这里我省略自定义View的几个基础步骤:

    • 在attrs.xml定义属性
    • 在View的构造函数里获取attrs属性
    • 一些Paint,Path的初始化工作

    完整代码在
    https://github.com/mcxtzhang/SwipeCaptcha
    可以下载后对照阅读,效果更佳。

    首先思考,验证码区域包含:

    • 绘制在图片上的验证码阴影
    • 可移动的验证码滑块

    1 生成验证码阴影

    我们用Path存储验证码区域,
    所以这一步最重要是生成验证码区域的Path。
    查看竞品(斗鱼web端)如下,

    斗鱼验证码原型.png

    so,我们这里要绘制一个矩形+四边可能会有随机的凹凸,凹凸可以用半圆来替代。
    我们如下编写:
    代码配有注释,gap是指凹凸的起点和顶点的距离。

        //生成验证码Path
        private void createCaptchaPath() {
            //原本打算随机生成gap,后来发现 宽度/3 效果比较好,
            int gap = mRandom.nextInt(mCaptchaWidth / 2);
            gap = mCaptchaWidth / 3;
    
            //随机生成验证码阴影左上角 x y 点,
            mCaptchaX = mRandom.nextInt(mWidth - mCaptchaWidth - gap);
            mCaptchaY = mRandom.nextInt(mHeight - mCaptchaHeight - gap);
    
            mCaptchaPath.reset();
            mCaptchaPath.lineTo(0, 0);
    
            //从左上角开始 绘制一个不规则的阴影
            mCaptchaPath.moveTo(mCaptchaX, mCaptchaY);//左上角
            mCaptchaPath.lineTo(mCaptchaX + gap, mCaptchaY);
            //draw一个随机凹凸的圆
            drawPartCircle(new PointF(mCaptchaX + gap, mCaptchaY),
                    new PointF(mCaptchaX + gap * 2, mCaptchaY),
                    mCaptchaPath, mRandom.nextBoolean());
    
            mCaptchaPath.lineTo(mCaptchaX + mCaptchaWidth, mCaptchaY);//右上角
            mCaptchaPath.lineTo(mCaptchaX + mCaptchaWidth, mCaptchaY + gap);
            //draw一个随机凹凸的圆
            drawPartCircle(new PointF(mCaptchaX + mCaptchaWidth, mCaptchaY + gap),
                    new PointF(mCaptchaX + mCaptchaWidth, mCaptchaY + gap * 2),
                    mCaptchaPath, mRandom.nextBoolean());
    
            mCaptchaPath.lineTo(mCaptchaX + mCaptchaWidth, mCaptchaY + mCaptchaHeight);//右下角
            mCaptchaPath.lineTo(mCaptchaX + mCaptchaWidth - gap, mCaptchaY + mCaptchaHeight);
            //draw一个随机凹凸的圆
            drawPartCircle(new PointF(mCaptchaX + mCaptchaWidth - gap, mCaptchaY + mCaptchaHeight),
                    new PointF(mCaptchaX + mCaptchaWidth - gap * 2, mCaptchaY + mCaptchaHeight),
                    mCaptchaPath, mRandom.nextBoolean());
    
            mCaptchaPath.lineTo(mCaptchaX, mCaptchaY + mCaptchaHeight);//左下角
            mCaptchaPath.lineTo(mCaptchaX, mCaptchaY + mCaptchaHeight - gap);
            //draw一个随机凹凸的圆
            drawPartCircle(new PointF(mCaptchaX, mCaptchaY + mCaptchaHeight - gap),
                    new PointF(mCaptchaX, mCaptchaY + mCaptchaHeight - gap * 2),
                    mCaptchaPath, mRandom.nextBoolean());
    
            mCaptchaPath.close();
        }
    
    

    关于drawPartCircle(),它的功能是传入起点、终点坐标,以及需要凹还是凸,和绘制的Path。它会在Path上绘制一个凹、凸的半圆。
    代码如下:

     /**
         * 传入起点、终点 坐标、凹凸和Path。
         * 会自动绘制凹凸的半圆弧
         *
         * @param start 起点坐标
         * @param end   终点坐标
         * @param path  半圆会绘制在这个path上
         * @param outer 是否凸半圆
         */
        public static void drawPartCircle(PointF start, PointF end, Path path, boolean outer) {
            float c = 0.551915024494f;
            //中点
            PointF middle = new PointF(start.x + (end.x - start.x) / 2, start.y + (end.y - start.y) / 2);
            //半径
            float r1 = (float) Math.sqrt(Math.pow((middle.x - start.x), 2) + Math.pow((middle.y - start.y), 2));
            //gap值
            float gap1 = r1 * c;
    
            if (start.x == end.x) {
                //绘制竖直方向的
    
                //是否是从上到下
                boolean topToBottom = end.y - start.y > 0 ? true : false;
                //以下是我写出了所有的计算公式后推的,不要问我过程,只可意会。
                int flag;//旋转系数
                if (topToBottom) {
                    flag = 1;
                } else {
                    flag = -1;
                }
                if (outer) {
                    //凸的 两个半圆
                    path.cubicTo(start.x + gap1 * flag, start.y,
                            middle.x + r1 * flag, middle.y - gap1 * flag,
                            middle.x + r1 * flag, middle.y);
                    path.cubicTo(middle.x + r1 * flag, middle.y + gap1 * flag,
                            end.x + gap1 * flag, end.y,
                            end.x, end.y);
                } else {
                    //凹的 两个半圆
                    path.cubicTo(start.x - gap1 * flag, start.y,
                            middle.x - r1 * flag, middle.y - gap1 * flag,
                            middle.x - r1 * flag, middle.y);
                    path.cubicTo(middle.x - r1 * flag, middle.y + gap1 * flag,
                            end.x - gap1 * flag, end.y,
                            end.x, end.y);
                }
            } else {
                //绘制水平方向的
    
                //是否是从左到右
                boolean leftToRight = end.x - start.x > 0 ? true : false;
                //以下是我写出了所有的计算公式后推的,不要问我过程,只可意会。
                int flag;//旋转系数
                if (leftToRight) {
                    flag = 1;
                } else {
                    flag = -1;
                }
                if (outer) {
                    //凸 两个半圆
                    path.cubicTo(start.x, start.y - gap1 * flag,
                            middle.x - gap1 * flag, middle.y - r1 * flag,
                            middle.x, middle.y - r1 * flag);
                    path.cubicTo(middle.x + gap1 * flag, middle.y - r1 * flag,
                            end.x, end.y - gap1 * flag,
                            end.x, end.y);
                } else {
                    //凹 两个半圆
                    path.cubicTo(start.x, start.y + gap1 * flag,
                            middle.x - gap1 * flag, middle.y + r1 * flag,
                            middle.x, middle.y + r1 * flag);
                    path.cubicTo(middle.x + gap1 * flag, middle.y + r1 * flag,
                            end.x, end.y + gap1 * flag,
                            end.x, end.y);
                }
    
    /*
                没推导之前的公式在这里
                if (start.x < end.x) {
                    if (outer) {
                        //上左半圆 顺时针
                        path.cubicTo(start.x, start.y - gap1,
                                middle.x - gap1, middle.y - r1,
                                middle.x, middle.y - r1);
    
                        //上右半圆:顺时针
                        path.cubicTo(middle.x + gap1, middle.y - r1,
                                end.x, end.y - gap1,
                                end.x, end.y);
                    } else {
                        //下左半圆 逆时针
                        path.cubicTo(start.x, start.y + gap1,
                                middle.x - gap1, middle.y + r1,
                                middle.x, middle.y + r1);
    
                        //下右半圆 逆时针
                        path.cubicTo(middle.x + gap1, middle.y + r1,
                                end.x, end.y + gap1,
                                end.x, end.y);
                    }
                } else {
                    if (outer) {
                        //下右半圆 顺时针
                        path.cubicTo(start.x, start.y + gap1,
                                middle.x + gap1, middle.y + r1,
                                middle.x, middle.y + r1);
                        //下左半圆 顺时针
                        path.cubicTo(middle.x - gap1, middle.y + r1,
                                end.x, end.y + gap1,
                                end.x, end.y);
                    }
                }*/
            }
        }
    
    

    这里用的是推导之后的公式,没推导前的也在注释里。
    简单说,先计算出中点和半径,利用三次贝塞尔曲线绘制一个圆(c和gap1 都是和三次贝塞尔曲线相关)。关于三次贝塞尔曲线就不展开了,网上很多资料,我也是现学的。
    这里关于绘制验证码阴影Path,还有一段曲折心路历程,
    绘制出来的效果如下:

    左边是滑块,右边是阴影

    )

    心路历程(可以不看)
    验证码Path,猛的一看,似乎很简单,不就是一个矩形+上四个边可能出现的凹凸嘛。
    凹凸的话,我们就是绘制一个半圆好了。
    利用PathlineTo()+addCircle()似乎可以很轻松的实现?
    最开始我是这么做的,结果发现画出来的Path是多段的Path,闭合后,无法形成一个完整阴影区域。更无法用于下一步验证码滑块bitmap的生成。
    好,看来是addCircle()的锅,导致了Path被分割成多段。那我用arcTo()好了,结果发现arcTo不像addCircle()那样可以设置绘图的方向,(顺时针,逆时针),这当时可把我难住了,因为不能逆时针的话,上、右边的凹就画不出来。所以我放弃了,我转用贝塞尔曲线绘制这个凹凸。
    文章写到这里,我突然发现自己智障了,sweepAngle传入负值不就可以逆时针了吗。如:arcTo(oval, 180, -180);
    所以说写博客是有很大好处的,写博客时大脑也是高速旋转,因为生怕写出错误,一是误导别人,二是丢人。大脑高速运转说不定就想通了以前想不通的问题。
    于是我就脑残的用sin+二阶贝尔赛曲线去绘制这个半圆了,为什么用它们呢?因为当初我绘制波浪滚动的时候用的sin函数+二阶贝塞尔模拟波浪,于是我就惯性思维的也这么解决了。结果呢?绘制出来的凹凸不够圆啊,sin函数还是比不过圆是不是。
    于是我就走上了用三节贝塞尔曲线模拟圆的路。
    看来我当初写这一块代码的时候,脑子确实不太清醒,不过也有收获。又复习了一遍Path的几个函数和贝塞尔曲线。

    2 抠图:验证码滑块的生成

    验证码Path生成好了后,我要根据Path去生成验证码滑块。那么第一步就是要抠图了。
    代码如下:

        //生成滑块
        private void craeteMask() {
            mMaskBitmap = getMaskBitmap(((BitmapDrawable) getDrawable()).getBitmap(), mCaptchaPath);
            //滑块阴影
            mMaskShadowBitmap = mMaskBitmap.extractAlpha();
            //拖动的位移重置
            mDragerOffset = 0;
            //isDrawMask  绘制失败闪烁动画用
            isDrawMask = true;
        }
    
    
        //抠图
        private Bitmap getMaskBitmap(Bitmap mBitmap, Path mask) {
            //以控件宽高 create一块bitmap
            Bitmap tempBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
            //把创建的bitmap作为画板
            Canvas mCanvas = new Canvas(tempBitmap);
            //有锯齿 且无法解决,所以换成XFermode的方法做
            //mCanvas.clipPath(mask);
            // 抗锯齿
            mCanvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
            //绘制用于遮罩的圆形
            mCanvas.drawPath(mask, mMaskPaint);
            //设置遮罩模式(图像混合模式)
            mMaskPaint.setXfermode(mPorterDuffXfermode);
            //★考虑到scaleType等因素,要用Matrix对Bitmap进行缩放
            mCanvas.drawBitmap(mBitmap, getImageMatrix(), mMaskPaint);
            mMaskPaint.setXfermode(null);
            return tempBitmap;
        }
    
    

    其实这里我也走了一些曲折的路,我先是用canvas.clipPath(path)抠的图,结果发现有锯齿,搜了很多资料也没搞定。于是我又回到了Xfermode的路上,将其设置为mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
    先绘制dst,即遮罩验证码Path,然后再绘制src:Bitmap,取交集即可完成抠图。
    这里有一些需要注意的地方:

    • src的Bitmap是取ImageView本身的bitmap。
    • 创建的新Bitmap的宽高取控件的宽高
    • 它们两者的宽高很大可能是不同的,这就是ImageView参数scaleType的作用。所以我们取出ImageView的Matrix 用于绘制src的Bitmap。这样抠出来的Bitmap区域就和第1步遮盖住的区域是一样的了。

    mMaskShadowBitmap = mMaskBitmap.extractAlpha();这句话是为了在绘制出的滑块周围也绘制一圈阴影,加强立体效果。
    仔细看下图效果,周边又一圈立体阴影的效果:

    image

    绘制

    onDraw()方法其实比较简单,只不过在其中加入了一些布尔类型的flag,都是和动画相关的:
    代码如下:

        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            //继承自ImageView,所以Bitmap,ImageView已经帮我们draw好了。
            //我只在上面绘制和验证码相关的部分,
    
            //是否处于验证模式,在验证成功后 为false,其余情况为true
            if (isMatchMode) {
                //首先绘制验证码阴影
                if (mCaptchaPath != null) {
                    canvas.drawPath(mCaptchaPath, mPaint);
                }
                //绘制滑块
                // isDrawMask  绘制失败闪烁动画用
                if (null != mMaskBitmap && null != mMaskShadowBitmap && isDrawMask) {
                    // 先绘制阴影
                    canvas.drawBitmap(mMaskShadowBitmap, -mCaptchaX + mDragerOffset, 0, mMaskShadowPaint);
                    canvas.drawBitmap(mMaskBitmap, -mCaptchaX + mDragerOffset, 0, null);
                }
                //验证成功,白光扫过的动画,这一块动画感觉不完美,有提高空间
                if (isShowSuccessAnim) {
                    canvas.translate(mSuccessAnimOffset, 0);
                    canvas.drawPath(mSuccessPath, mSuccessPaint);
                }
            }
        }
    
    

    mPaint如下定义: 所以绘制出阴影也有一些阴影效果。

            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
            mPaint.setColor(0x77000000);
            //mPaint.setStyle(Paint.Style.STROKE);
            // 设置画笔遮罩滤镜
            mPaint.setMaskFilter(new BlurMaskFilter(20, BlurMaskFilter.Blur.SOLID));
    
    

    值得说的就是,配合滑块滑动,是利用mDragerOffset,默认是0,滑动时mDragerOffset增加,滑块右移,反之亦然。
    验证成功的白光扫过动画,是利用canvas.translate()做的,mSuccessPathmSuccessPaint如下:

            mSuccessPaint = new Paint();
            mSuccessPaint.setShader(new LinearGradient(0, 0, width, 0, new int[]{
                    0x11ffffff, 0x88ffffff}, null,
                    Shader.TileMode.MIRROR));
            //模仿斗鱼 是一个平行四边形滚动过去
            mSuccessPath = new Path();
            mSuccessPath.moveTo(0, 0);
            mSuccessPath.rLineTo(width, 0);
            mSuccessPath.rLineTo(width / 2, mHeight);
            mSuccessPath.rLineTo(-width, 0);
            mSuccessPath.close();
    
    

    滑动、验证、动画

    上一节完成后,我们的滑动验证码View已经可以正常绘制出来了,现在我们为它增加一些方法,让它可以联动滑动、验证功能和动画。

    联动滑动:

    上一节也提到,滑动主要是改变mDragerOffset的值,然后重绘自己->ondraw(),根据mDragerOffset偏移滑块Bitmap的绘制。

        /**
         * 重置验证码滑动距离,(一般用于验证失败)
         */
        public void resetCaptcha() {
            mDragerOffset = 0;
            invalidate();
        }
    
        /**
         * 最大可滑动值
         * @return
         */
        public int getMaxSwipeValue() {
            //return ((BitmapDrawable) getDrawable()).getBitmap().getWidth() - mCaptchaWidth;
            //返回控件宽度
            return mWidth - mCaptchaWidth;
        }
    
        /**
         * 设置当前滑动值
         * @param value
         */
        public void setCurrentSwipeValue(int value) {
            mDragerOffset = value;
            invalidate();
        }
    
    

    校验:

    校验的话,需要引入一个回调接口:

    
        public interface OnCaptchaMatchCallback {
            void matchSuccess(SwipeCaptchaView swipeCaptchaView);
    
            void matchFailed(SwipeCaptchaView swipeCaptchaView);
        }
    
        /**
         * 验证码验证的回调
         */
        private OnCaptchaMatchCallback onCaptchaMatchCallback;
    
        public OnCaptchaMatchCallback getOnCaptchaMatchCallback() {
            return onCaptchaMatchCallback;
        }
    
        /**
         * 设置验证码验证回调
         *
         * @param onCaptchaMatchCallback
         * @return
         */
        public SwipeCaptchaView setOnCaptchaMatchCallback(OnCaptchaMatchCallback onCaptchaMatchCallback) {
            this.onCaptchaMatchCallback = onCaptchaMatchCallback;
            return this;
        }
    
    
        /**
         * 校验
         */
        public void matchCaptcha() {
            if (null != onCaptchaMatchCallback && isMatchMode) {
                //这里验证逻辑,是通过比较,拖拽的距离 和 验证码起点x坐标。 默认3dp以内算是验证成功。
                if (Math.abs(mDragerOffset - mCaptchaX) < mMatchDeviation) {
                    //成功的动画
                    mSuccessAnim.start();
                } else {
                    mFailAnim.start();
                }
            }
    
        }
    
    

    成功、失败的回调是在动画结束时通知的。

    动画:

    动画里要用到宽高,所以它是在onSizeChanged()方法里被调用的。

    //验证动画初始化区域
        private void createMatchAnim() {
            mFailAnim = ValueAnimator.ofFloat(0, 1);
            mFailAnim.setDuration(100)
                    .setRepeatCount(4);
            mFailAnim.setRepeatMode(ValueAnimator.REVERSE);
            //失败的时候先闪一闪动画 斗鱼是 隐藏-显示 -隐藏 -显示
            mFailAnim.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    onCaptchaMatchCallback.matchFailed(SwipeCaptchaView.this);
                }
            });
            mFailAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float animatedValue = (float) animation.getAnimatedValue();
                    if (animatedValue < 0.5f) {
                        isDrawMask = false;
                    } else {
                        isDrawMask = true;
                    }
                    invalidate();
                }
            });
    
            int width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, getResources().getDisplayMetrics());
            mSuccessAnim = ValueAnimator.ofInt(mWidth + width, 0);
            mSuccessAnim.setDuration(500);
            mSuccessAnim.setInterpolator(new FastOutLinearInInterpolator());
            mSuccessAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    mSuccessAnimOffset = (int) animation.getAnimatedValue();
                    invalidate();
                }
            });
            mSuccessAnim.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationStart(Animator animation) {
                    isShowSuccessAnim = true;
                }
    
                @Override
                public void onAnimationEnd(Animator animation) {
                    onCaptchaMatchCallback.matchSuccess(SwipeCaptchaView.this);
                    isShowSuccessAnim = false;
                    isMatchMode = false;
                }
            });
            mSuccessPaint = new Paint();
            mSuccessPaint.setShader(new LinearGradient(0, 0, width, 0, new int[]{
                    0x11ffffff, 0x88ffffff}, null,
                    Shader.TileMode.MIRROR));
            //模仿斗鱼 是一个平行四边形滚动过去
            mSuccessPath = new Path();
            mSuccessPath.moveTo(0, 0);
            mSuccessPath.rLineTo(width, 0);
            mSuccessPath.rLineTo(width / 2, mHeight);
            mSuccessPath.rLineTo(-width, 0);
            mSuccessPath.close();
        }
    
    

    代码很简单,修改的一些布尔值flag,在onDraw()方法里会用到,结合onDraw()一看便懂。


    Demo

    这一节,我们联动SeekBar滑动起来。
    xml如下:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
        ......
    >
    
        <com.mcxtzhang.captchalib.SwipeCaptchaView
            android:id="@+id/swipeCaptchaView"
            android:layout_width="300dp"
            android:layout_height="150dp"
            android:layout_centerHorizontal="true"
            android:scaleType="centerCrop"
            android:src="@drawable/pic11"
            app:captchaHeight="30dp"
            app:captchaWidth="30dp"/>
    
        <SeekBar
            android:id="@+id/dragBar"
            android:layout_width="320dp"
            android:layout_height="60dp"
            android:layout_below="@id/swipeCaptchaView"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="30dp"
            android:progressDrawable="@drawable/dragbg"
            android:thumb="@drawable/thumb_bg"/>
    
        <Button
            android:id="@+id/btnChange"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:text="老板换码"/>
    </RelativeLayout>
    
    

    UI就是文首那张图的样子,
    完整Activity代码:

    public class MainActivity extends AppCompatActivity {
        SwipeCaptchaView mSwipeCaptchaView;
        SeekBar mSeekBar;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mSwipeCaptchaView = (SwipeCaptchaView) findViewById(R.id.swipeCaptchaView);
            mSeekBar = (SeekBar) findViewById(R.id.dragBar);
            findViewById(R.id.btnChange).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mSwipeCaptchaView.createCaptcha();
                    mSeekBar.setEnabled(true);
                    mSeekBar.setProgress(0);
                }
            });
            mSwipeCaptchaView.setOnCaptchaMatchCallback(new SwipeCaptchaView.OnCaptchaMatchCallback() {
                @Override
                public void matchSuccess(SwipeCaptchaView swipeCaptchaView) {
                    Toast.makeText(MainActivity.this, "恭喜你啊 验证成功 可以搞事情了", Toast.LENGTH_SHORT).show();
                    mSeekBar.setEnabled(false);
                }
    
                @Override
                public void matchFailed(SwipeCaptchaView swipeCaptchaView) {
                    Toast.makeText(MainActivity.this, "你有80%的可能是机器人,现在走还来得及", Toast.LENGTH_SHORT).show();
                    swipeCaptchaView.resetCaptcha();
                    mSeekBar.setProgress(0);
                }
            });
            mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
    
                @Override
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    mSwipeCaptchaView.setCurrentSwipeValue(progress);
                }
    
                @Override
                public void onStartTrackingTouch(SeekBar seekBar) {
                    //随便放这里是因为控件
                    mSeekBar.setMax(mSwipeCaptchaView.getMaxSwipeValue());
                }
    
                @Override
                public void onStopTrackingTouch(SeekBar seekBar) {
                    Log.d("zxt", "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]");
                    mSwipeCaptchaView.matchCaptcha();
                }
            });
    
            //从网络加载图片也ok
            Glide.with(this)
                    .load("http://www.investide.cn/data/edata/image/20151201/20151201180507_281.jpg")
                    .asBitmap()
                    .into(new SimpleTarget<Bitmap>() {
                        @Override
                        public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) {
                            mSwipeCaptchaView.setImageBitmap(resource);
                            mSwipeCaptchaView.createCaptcha();
                        }
                    });
        }
    }
    
    

    总结

    代码传送门 喜欢的话,随手点个star。多谢
    https://github.com/mcxtzhang/SwipeCaptcha
    包含完整Demo和SwipeCaptchaView。

    利用一些工具发现web端斗鱼,验证码图片和滑块图片都是接口返回的。
    推测前端其实只返回后台:用户移动的距离或者距离的百分比

    本例完全由前端实现验证码生成、验证功能,是因为:
    1 练习自定义VIew,自己全部实现抠图 验证 绘制,感觉很酷。
    2 我不会做后台,手动微笑。

    核心点:
    1 不规则图形Path的生成。
    2 指定Path对Bitmap抠图,抗锯齿。
    3 适配ImageView的ScaleType。
    4 成功、失败的动画

    作者:张旭童
    链接:https://www.jianshu.com/p/9bf982da6e96
    來源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    相关文章

      网友评论

        本文标题:【控件】仿斗鱼滑动拼图验证码控件

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