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

2023-10-17 08:30

本文主要是介绍【Android】仿斗鱼滑动拼图验证码控件,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
转载请标明出处:
http://blog.csdn.net/zxt0601/article/details/53315975
本文出自:【张旭童的博客】(http://blog.csdn.net/zxt0601)
代码传送门:喜欢的话,随手点个star。多谢
https://github.com/mcxtzhang/SwipeCaptcha

概述

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

上周一总监让我研究一波滑动验证码,说项目可能会上。我想了一下好像在斗鱼、淘宝都见过,结果下了这两个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是指凹凸的起点和顶点的距离。

    //生成验证码Pathprivate 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一块bitmapBitmap 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,都是和动画相关的:
代码如下:

    @Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);//继承自ImageView,所以Bitmap,ImageView已经帮我们draw好了。//我只在上面绘制和验证码相关的部分,//是否处于验证模式,在验证成功后 为false,其余情况为trueif (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() {@Overridepublic void onAnimationEnd(Animator animation) {onCaptchaMatchCallback.matchFailed(SwipeCaptchaView.this);}});mFailAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic 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() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mSuccessAnimOffset = (int) animation.getAnimatedValue();invalidate();}});mSuccessAnim.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationStart(Animator animation) {isShowSuccessAnim = true;}@Overridepublic 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;@Overrideprotected 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() {@Overridepublic void onClick(View v) {mSwipeCaptchaView.createCaptcha();mSeekBar.setEnabled(true);mSeekBar.setProgress(0);}});mSwipeCaptchaView.setOnCaptchaMatchCallback(new SwipeCaptchaView.OnCaptchaMatchCallback() {@Overridepublic void matchSuccess(SwipeCaptchaView swipeCaptchaView) {Toast.makeText(MainActivity.this, "恭喜你啊 验证成功 可以搞事情了", Toast.LENGTH_SHORT).show();mSeekBar.setEnabled(false);}@Overridepublic void matchFailed(SwipeCaptchaView swipeCaptchaView) {Toast.makeText(MainActivity.this, "你有80%的可能是机器人,现在走还来得及", Toast.LENGTH_SHORT).show();swipeCaptchaView.resetCaptcha();mSeekBar.setProgress(0);}});mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {mSwipeCaptchaView.setCurrentSwipeValue(progress);}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {//随便放这里是因为控件mSeekBar.setMax(mSwipeCaptchaView.getMaxSwipeValue());}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {Log.d("zxt", "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]");mSwipeCaptchaView.matchCaptcha();}});//从网络加载图片也okGlide.with(this).load("http://www.investide.cn/data/edata/image/20151201/20151201180507_281.jpg").asBitmap().into(new SimpleTarget<Bitmap>() {@Overridepublic 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 成功、失败的动画


QQ搞基交流群:
557266366

这篇关于【Android】仿斗鱼滑动拼图验证码控件的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/224185

相关文章

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

android-opencv-jni

//------------------start opencv--------------------@Override public void onResume(){ super.onResume(); //通过OpenCV引擎服务加载并初始化OpenCV类库,所谓OpenCV引擎服务即是 //OpenCV_2.4.3.2_Manager_2.4_*.apk程序包,存

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

Android 10.0 mtk平板camera2横屏预览旋转90度横屏拍照图片旋转90度功能实现

1.前言 在10.0的系统rom定制化开发中,在进行一些平板等默认横屏的设备开发的过程中,需要在进入camera2的 时候,默认预览图像也是需要横屏显示的,在上一篇已经实现了横屏预览功能,然后发现横屏预览后,拍照保存的图片 依然是竖屏的,所以说同样需要将图片也保存为横屏图标了,所以就需要看下mtk的camera2的相关横屏保存图片功能, 如何实现实现横屏保存图片功能 如图所示: 2.mtk

android应用中res目录说明

Android应用的res目录是一个特殊的项目,该项目里存放了Android应用所用的全部资源,包括图片、字符串、颜色、尺寸、样式等,类似于web开发中的public目录,js、css、image、style。。。。 Android按照约定,将不同的资源放在不同的文件夹中,这样可以方便的让AAPT(即Android Asset Packaging Tool , 在SDK的build-tools目

Android fill_parent、match_parent、wrap_content三者的作用及区别

这三个属性都是用来适应视图的水平或者垂直大小,以视图的内容或尺寸为基础的布局,比精确的指定视图的范围更加方便。 1、fill_parent 设置一个视图的布局为fill_parent将强制性的使视图扩展至它父元素的大小 2、match_parent 和fill_parent一样,从字面上的意思match_parent更贴切一些,于是从2.2开始,两个属性都可以使用,但2.3版本以后的建议使

Spring 验证码(kaptcha)

首先引入需要的jar包: <dependency><groupId>com.github.axet</groupId><artifactId>kaptcha</artifactId><version>0.0.9</version></dependency> 配置验证码相关设置: <bean id="captchaProducer" class="com.

Android Environment 获取的路径问题

1. 以获取 /System 路径为例 /*** Return root of the "system" partition holding the core Android OS.* Always present and mounted read-only.*/public static @NonNull File getRootDirectory() {return DIR_ANDR

lvgl8.3.6 控件垂直布局 label控件在image控件的下方显示

在使用 LVGL 8.3.6 创建一个垂直布局,其中 label 控件位于 image 控件下方,你可以使用 lv_obj_set_flex_flow 来设置布局为垂直,并确保 label 控件在 image 控件后添加。这里是如何步骤性地实现它的一个基本示例: 创建父容器:首先创建一个容器对象,该对象将作为布局的基础。设置容器为垂直布局:使用 lv_obj_set_flex_flow 设置容器