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,猛的一看,似乎很简单,不就是一个矩形+上四个边可能出现的凹凸嘛。
凹凸的话,我们就是绘制一个半圆好了。
利用Path
的lineTo()
+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=“老板换码”/>
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 成功、失败的动画
最后
今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。
最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
【算法合集】
【延伸Android必备知识点】
【Android部分高级架构视频学习资源】
Android精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
3 适配ImageView的ScaleType。
4 成功、失败的动画
最后
今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。
最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
[外链图片转存中…(img-ecSh6sMQ-1714974417819)]
【算法合集】
[外链图片转存中…(img-CdGTkdUB-1714974417820)]
【延伸Android必备知识点】
[外链图片转存中…(img-YVDHx1ea-1714974417821)]
【Android部分高级架构视频学习资源】
Android精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!