欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android 高仿斗鱼滑动验证码

程序员文章站 2024-02-28 12:41:34
如下图。在android上实现起来就不太容易,有些效果还是不如web端酷炫。) 我们的demo,ac娘镇楼 (图很渣,也忽略底下的seekbar,这不是...

如下图。在android上实现起来就不太容易,有些效果还是不如web端酷炫。)

Android 高仿斗鱼滑动验证码

Android 高仿斗鱼滑动验证码

我们的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 生成滑块bitmapondraw()时,依次绘制:

1 验证码阴影

2 滑块

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

验证码区域的生成

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

在attrs.xml定义属性在view的构造函数里获取attrs属性一些paint,path的初始化工作

完整代码在

https://github.com/mcxtzhang/swipecaptcha

可以下载后对照阅读,效果更佳。

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

绘制在图片上的验证码阴影

可移动的验证码滑块

1 生成验证码阴影

我们用path存储验证码区域,

所以这一步最重要是生成验证码区域的path。

查看竞品(斗鱼web端)如下,

Android 高仿斗鱼滑动验证码

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,还有一段曲折心路历程,绘制出来的效果如下:

Android 高仿斗鱼滑动验证码

左边是滑块,右边是阴影

心路历程(可以不看)

验证码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();这句话是为了在绘制出的滑块周围也绘制一圈阴影,加强立体效果。

仔细看下图效果,周边又一圈立体阴影的效果:

Android 高仿斗鱼滑动验证码

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()做的,msuccesspath和msuccesspaint如下:

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 成功、失败的动画

以上所述是小编给大家介绍的android 高仿斗鱼滑动验证码,希望对大家有所帮助