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

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()做的,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=“老板换码”/>

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() {

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

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

利用一些工具发现web端斗鱼,验证码图片和滑块图片都是接口返回的。

推测前端其实只返回后台:用户移动的距离或者距离的百分比

本例完全由前端实现验证码生成、验证功能,是因为:

1 练习自定义VIew,自己全部实现抠图 验证 绘制,感觉很酷。

2 我不会做后台,手动微笑。

核心点:

1 不规则图形Path的生成。

2 指定Path对Bitmap抠图,抗锯齿。

3 适配ImageView的ScaleType。

4 成功、失败的动画

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-2H98evSs-1715797213454)]

[外链图片转存中…(img-kr5cd5AS-1715797213455)]

[外链图片转存中…(img-bZT1SlFZ-1715797213456)]

[外链图片转存中…(img-MsLT75pq-1715797213457)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 15
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值