写点什么

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

发布于: 2021 年 11 月 07 日

*/


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


</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


publi


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


c void matchSuccess(SwipeCaptchaView swipeCaptchaView) {


Toast.makeText(MainActivity.this, "恭喜你啊 验证成功 可以搞事情了", Toast.LENGTH_SHORT).show();


mSeekBar.setEnabled(false);

评论

发布
暂无评论
【Android】仿斗鱼滑动拼图验证码控件