Android 高仿斗鱼滑动验证码

如下图。在Android上实现起来就不太容易,有些效果还是不如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 生成滑块BitmaponDraw()时,依次绘制:

1 验证码阴影

2 滑块

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

验证码区域的生成

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

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

完整代码在

https://github.com/mcxtzhang/SwipeCaptcha

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

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

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

可移动的验证码滑块

1 生成验证码阴影

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

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

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

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

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

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 高仿斗鱼滑动验证码,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

时间: 2016-11-23

Android控件SeekBar仿淘宝滑动验证效果

SeekBar是一个拖动条控件,最简单的案例就是我们的调节音量,还有音频视频的播放,传统的SeekBar样式,如图 传统的实现太简单,不足以让我们到能装逼的地步.本来是打算实现滴滴出行滑动完成订单的效果,可惜找不到效果图,今天也就用淘宝的滑动验证来作为实例 1.1 实现分析 SeekBar:使用progressDrawable属性自定义SeekBar 拖动块:使用thumb属性更改,其实就是一张图片 文字:使用RelativeLayout嵌套在一起 1.2 实现布局 <?xml version=

Android自定义滑动验证条的示例代码

本文介绍了Android自定义滑动验证条的示例代码,分享给大家,具体如下: *注:不知道为什么,h5的标签在这里没用了,所以我也只能用Markdown的语法来写了 项目地址:https://github.com/994866755/handsomeYe.seekbar.github.io 需求: 在我们的某些应用中需要滑动验证.比如说这个样子的: 刚开始我也很懵逼要怎么去弄,结果我去看了一些人的代码,有人是用自定义viewgroup去做,就是viewgroup包含滑动块和滑动条.但我觉得太麻烦,

Android自定义滑动删除效果的实现代码

先给大家展示下效果图,如果感觉不错,请参考实现代码: 序言 最近项目中需要用到滑动删除,然后去网上搜了一下,发现现有网上的各种解决办法各式各样,但是还是找不到一个能将所有细节和逻辑处理好的,至于滑动删除部分,我觉得处理的相对比较好的是 QQ(包括处理各种逻辑和细节);最终,苦寻无果,于是决定自己动手,丰衣足食 这篇文章将从现有 Android 滑动删除的痛点,到搭建好一个基本的框架,到最终提供一份完整的 Demo为止,争取为读者提供最大的可定制化 正文 一. 滑动删除的痛点 (1). 现有资料中

Android自定义View实现多边形统计图示例代码

前言   最近利用空闲时间学习了自定义View的一些知识,为了巩固,写了一个小东西,顺便分享出来,下面话不多说了,来一起看看详细的介绍吧. 简介   一个多边形统计图.边数,每个方向的值,每个点的文字等等都是可以设置的. 下面就来分析一下这个自定义View 这个view由以下几个部分组成 M层N边形 中心到各顶点的连线 填充区域 文字 @Override protected void onDraw(Canvas canvas) { if (!canDraw()) { return; } canv

Android自定义双向进度条的实现代码

想整个双向的进度条,就是可以选取播放范围的. 像这样: 然而官方控件里只有单向的.不要慌,我们自己画一个. 绘制一个进度条主要是三方面.1.样式,2.尺寸,3.操作监听. 完整代码来一遍: 注释基本上就把原理说明了一下. package util; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint;

Android 侧边滑动关闭Activity的示例代码

0.效果图 1.设置Activity样式属性 <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <item name="android:windowIsTranslucent">true</item> </style> 2.自定义侧边阴影视图 class SlideBackView extends View { pr

Laravel 5.5 的自定义验证对象/类示例代码详解

Laravel 5.5 将提供一个全新的自定义验证规则的对象,以作为原来的 Validator::extend 方法的替代. Laravel 5.5 将提供一个全新的自定义验证规则的对象,以作为原来的 Validator::extend 方法的替代..很多时候我们会直接用正则表达式来处理这种特殊的验证,也有时候我们会选择用 Validator::extend 来扩展一个自定义的规则.但在 Laravel 5.5 版本中,我们有了新的手段,只要定义一个实现 Illuminate\Contracts

Android 实现自定义圆形进度条的实例代码

Android 自定义圆形进度条 今天无意中发现一个圆形进度,想想自己实现一个,如下图: 基本思路是这样的: 1.首先绘制一个实心圆 2.绘制一个白色实心的正方形,遮住实心圆 3.在圆的中心动态绘制当前进度的百分比字符 4.绘制一个与之前实心圆相同颜色的空心圆 5.逐渐改变当前的百分比 6.根据百分比,逐渐改变正方形的大小,逐渐减小正方形的底部y轴的坐标,不断重绘,直到达到100% 首先看看自定义的属性 在values目录下新建attrs.xml内容如下: 定义绘制圆形的背景色,和绘制圆形的半径

Android 给图片加上水印的示例代码(支持logo+文字)

本文介绍了Android 给图片加上水印的示例代码(支持logo+文字),分享给大家,具体如下: 现在我们想要往图片上打上水印,该水印应符合这样的需求的: 支持logo+文字: 文字信息支持多行展示: 用户可以选择水印在图片上的生成位置(左上.右上.右下和左下). 粗略的结构图低配版大概就长这样... 水印结构图.png 现在提供这样的一种思路去实现这一个需求,我们可以通过自定义一个view,view的布局中包含logo.公司名称和相关信息,这个view就是我们要打上图片的水印. 这样的一个vi

Android自定义指示器时间轴效果实例代码详解

指示器时间轴在外卖.购物类的APP里会经常用到,效果大概就像下面这样,看了网上很多文章,大都是自己绘制,太麻烦,其实通过ListView就可以实现. 在Activity关联的布局文件activity_main.xml中放置一个ListView,代码如下.由于这个列表只是用于展示信息,并不需要用户去点击,所以将其clickable属性置为false:为了消除ListView点击产生的波纹效果,我们设置其listSelector属性的值为透明:我们不需要列表项之间的分割线,所以设置其divider属

微信小程序自定义音乐进度条的实例代码

需求:显示音乐播放按钮.可手动拖拽进度条:页面中含多个音乐,播放当前音乐时暂停其他音乐播放. 小程序自带标签 audio 小程序自带的audio标签含固定的样式,且有最小尺寸.目前项目也不含name和author字段,所以放弃audio标签. 实现效果图 初始化音乐数据 <text>{{currentProcess}}</text> <slider bindchange="" bindtouchstart="" bindtouchend