Android UI系列之仿斗鱼滑动拼图验证码控件

来源:互联网 发布:php开发简历自己缺点 编辑:程序博客网 时间:2024/05/16 11:16

转载自:http://blog.csdn.net/zxt0601/article/details/53315975

斗鱼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();    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

关于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);                }            }*/        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116

这里用的是推导之后的公式,没推导前的也在注释里。 
简单说,先计算出中点和半径,利用三次贝塞尔曲线绘制一个圆(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;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
    //抠图    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;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

其实这里我也走了一些曲折的路,我先是用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();这句话是为了在绘制出的滑块周围也绘制一圈阴影,加强立体效果。 
仔细看下图效果,周边又一圈立体阴影的效果:

绘制

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);            }        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

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));
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

值得说的就是,配合滑块滑动,是利用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();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

滑动、验证、动画

上一节完成后,我们的滑动验证码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();    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

校验:

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

    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;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
    /**     * 校验     */    public void matchCaptcha() {        if (null != onCaptchaMatchCallback && isMatchMode) {            //这里验证逻辑,是通过比较,拖拽的距离 和 验证码起点x坐标。 默认3dp以内算是验证成功。            if (Math.abs(mDragerOffset - mCaptchaX) < mMatchDeviation) {                //成功的动画                mSuccessAnim.start();            } else {                mFailAnim.start();            }        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

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

动画:

动画里要用到宽高,所以它是在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();    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

代码很简单,修改的一些布尔值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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

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();                    }                });    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

总结

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

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

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

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

原创粉丝点击