最近想把工作这两年的东西好好写一写,一直觉得自己好像没做什么东西一样,写一写也能给自己一点自信,当然更像是一次总结。

安卓滑动验证模块是去年一个需求做的,当时也只是从网上找了一个不错的博客 cv 大法了一把,但是人家写的很详细,也是让我搞懂了一些东西,后面按 UI 要求改动了一些,就更加深刻了,从里面也学到了很多东西,下面好好说说。

具体效果

原版博客

前面先贴出原版博文,我做了一些修改但是不多,文章末尾贴代码吧

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

https://www.jianshu.com/p/9bf982da6e96

实现思路

原作者写的挺清楚了,我这也是说一下我的理解吧

  1. 继承 ImageView,初始化笔触,绘制验证码滑块路径
  2. 以背景图和路径擦出一个和背景图尺寸一样,但只包含滑块部分的 bitmap
  3. 以滑块 bitmap 提取一个同等大小只含滑块形状的底层阴影的 bitmap
  4. 通过外部 seekbar 更新滑动比例,触发invalidate 函数,重绘图形
  5. 先绘制空出部分的阴影,再绘制滑块,包含滑块的阴影和滑块的图形
  6. 在外部滑块停止后,进行验证,显示动画,触发里面验证的回调接口

其他细节就不详述了,代码里面注释很多,我也加了挺多注释,看文末代码吧

我的修改

因为需要,我也按着上面的代码做了一定修改,下面说说

阴影显示问题

直接使用原版的代码发现没有阴影,我还自己弄了一番,后面发现原博主的代码可以,只是使用的时候没有关闭硬件加速,把硬件加速关了,滑块阴影就可以显示了

//关闭硬件加速
setLayerType(LAYER_TYPE_SOFTWARE, null);
空余部分白边

按设计说的上面博客的空余部分不太容易发现,希望加一层白边,这里使用画笔 stroke 模式多描了一层白边,但是记得画完后恢复 fill 模式,因为画笔还需要绘制空余部分的阴影

//首先绘制验证码阴影,即待填充部分
if (mCaptchaPath != null) {//待填充阴影,此处为填充模式canvas.drawPath(mCaptchaPath, mPaint);//描绘白边mPaint.setStyle(Paint.Style.STROKE);mPaint.setColor(Color.WHITE);mPaint.setStrokeWidth(1);canvas.drawPath(mCaptchaPath, mPaint);//重置填充模式mPaint.setStyle(Paint.Style.FILL);mPaint.setColor(0xaa000000);
}
左右范围限制

由于和 seekbar 一起使用,seekbar 的左右还有边距,而且不好修改,加上被设计师狂喷说这个左右还能滑到头,不行之类的话,就加了点限制,看下面几点代码

 //初始偏移值,右边空余值,我这里代码里面写9和外部的seekbar比较适配public final int offset = 9;public static int dp2px(Context context, float dpValue) {final float scale = context.getResources().getDisplayMetrics().density;return (int) (dpValue * scale + 0.5f);}public int getMaxSwipeValue() {//返回控件宽度return mWidth - mCaptchaWidth - 2 * dp2px(getContext(), offset);}public void setCurrentSwipeValue(int value) {mDragerOffset = dp2px(getContext(), offset) + value;invalidate();}

主要就是设置默认的滑动位置,和外部滑块能够滑动的最大位置

适配问题

这里因为我使用了头条的适配方案,将屏幕宽度写死了为360dp,在某些大屏手机上会有问题,而且修改系统显示大小(非字体大小)也会造成问题,具体问题是滑块和底部图片的缩放不一致,验证的实际位置是准确的,但是显示的滑块却不准确,影响验证。

实际问题是在 onDraw 方法中的 canvas 的 density 和适配的 density 不一致问题,解决办法我摸索了很长一段时间,才发现很简单,把 onDraw 方法中的 canvas 改成图片的 density 就可以了。

//修改canvas的density,屏幕适配会导致density不一致
canvas.setDensity(mMaskBitmap.getDensity());

这里直接使用了 mMaskBitmap 的 density,完美解决,很OK!

其他

其他的就是简化了一下代码什么的,可以忽略。

结合Glide使用

这里也写一下用法吧,原来的 demo 不好用了,下面贴代码吧

  • Activity
public class SwipeCheckActivity extends BaseActivity {//随便给一个必应的吧private static final List<String> URLS = Arrays.asList("https://api.dujin.org/bing/1920.php",);SwipeCaptchaView mSwipeCaptchaView;SeekBar mSeekBar;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_swipe_check);setTitle("验证码");mSwipeCaptchaView = findViewById(R.id.swipeCaptchaView);mSeekBar = findViewById(R.id.dragBar);mSwipeCaptchaView.setCurrentSwipeValue(0);mSwipeCaptchaView.setOnCaptchaMatchCallback(new SwipeCaptchaView.OnCaptchaMatchCallback() {@Overridepublic void matchSuccess(SwipeCaptchaView swipeCaptchaView) {mSeekBar.setEnabled(false);setResult(RESULT_OK);finish();}@Overridepublic void matchFailed(SwipeCaptchaView swipeCaptchaView) {Toast.makeText(SwipeCheckActivity.this,"验证失败!", 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) {mSwipeCaptchaView.matchCaptcha();}});//这里很可惜,使用图片缓存的话会有问题Log.e("SwipeCaptchaView", " System: density:" + getResources().getDisplayMetrics().densityDpi);Glide.with(SwipeCheckActivity.this).asBitmap().load(URLS.get((int) (Math.random() * URLS.size()))).apply(new RequestOptions().diskCacheStrategy(DiskCacheStrategy.NONE).skipMemoryCache(true)).addListener(new RequestListener<Bitmap>() {@Overridepublic boolean onLoadFailed(@Nullable GlideException e, Object model,Target<Bitmap> target, boolean isFirstResource) {return true;}@Overridepublic boolean onResourceReady(Bitmap resource, Object model,Target<Bitmap> target,DataSource dataSource, boolean isFirstResource) {mSwipeCaptchaView.setImageBitmap(resource);mSwipeCaptchaView.createCaptcha();return true;}}).into(mSwipeCaptchaView);}
}

注意

这里需要禁用一下缓存,不然获取不到图片的宽高,会闪退,我也没有什么好办法。

  • XML - layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:paddingLeft="14dp"android:paddingRight="14dp"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/check_hint"android:textColor="#ff303030"android:textSize="20sp"android:layout_marginTop="16dp"/><com.vcarecity.fdk.login.SwipeCaptchaViewandroid:id="@+id/swipeCaptchaView"android:layout_width="332dp"android:layout_height="188dp"android:scaleType="centerCrop"android:src="@drawable/bg_check_main"app:captchaHeight="36dp"app:captchaWidth="36dp"android:layout_marginTop="16dp"/><SeekBarandroid:id="@+id/dragBar"android:layout_width="332dp"android:layout_height="wrap_content"android:progressDrawable="@drawable/bg_check_dragbg"android:thumb="@drawable/icon_check_slider"android:layout_marginTop="12dp"android:maxHeight="14dp"android:minHeight="14dp"android:paddingLeft="10dp"android:paddingRight="10dp"android:paddingTop="4dp"android:paddingBottom="0dp"android:thumbOffset="9dp"/></LinearLayout>
  • XML - layout-v21
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:paddingLeft="14dp"android:paddingRight="14dp"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/check_hint"android:textColor="#ff303030"android:textSize="20sp"android:layout_marginTop="16dp"/><com.vcarecity.fdk.login.SwipeCaptchaViewandroid:id="@+id/swipeCaptchaView"android:layout_width="332dp"android:layout_height="188dp"android:scaleType="centerCrop"android:src="@drawable/bg_check_main"app:captchaHeight="36dp"app:captchaWidth="36dp"android:layout_marginTop="16dp"/><SeekBarandroid:id="@+id/dragBar"android:layout_width="332dp"android:layout_height="wrap_content"android:progressDrawable="@drawable/bg_check_dragbg"android:thumb="@drawable/icon_check_slider"android:layout_marginTop="12dp"android:splitTrack="false"android:maxHeight="14dp"android:minHeight="14dp"android:paddingLeft="10dp"android:paddingRight="10dp"android:paddingTop="4dp"android:paddingBottom="0dp"android:thumbOffset="9dp"/></LinearLayout>

这里 splitTrack 属性需要 v21 支持,主要解决在Android5.0以上 thumb 周围一圈白色不透明的问题。

  • Attrs
<declare-styleable name="SwipeCaptchaView">    <attr name="captchaWidth" format="dimension"/><attr name="captchaHeight" format="dimension"/><attr name="matchDeviation" format="dimension"/>
</declare-styleable>

这里设置了 XML 中可用的属性,写在 attrs.xml 文件中。

修改后的代码

这里太长了,还是放在博客末尾吧。

public class SwipeCaptchaView extends AppCompatImageView {//控件的宽高protected int mWidth;protected int mHeight;//验证码滑块的宽高private int mCaptchaWidth;private int mCaptchaHeight;//验证码的左上角(起点)的x yprivate int mCaptchaX;@SuppressWarnings("FieldCanBeLocal")private int mCaptchaY;private Random mRandom;private Paint mPaint;//验证码 阴影、抠图的Pathprivate Path mCaptchaPath;private PorterDuffXfermode mPorterDuffXfermode;//是否绘制滑块(验证失败闪烁动画用)private boolean isDrawMask;//滑块Bitmapprivate Bitmap mMaskBitmap;private Paint mMaskPaint;//用于绘制阴影的Paintprivate Paint mMaskShadowPaint;//滑块背后的阴影,需要关闭硬件加速才能显示private Bitmap mMaskShadowBitmap;//滑块的位移private int mDragerOffset;//是否处于验证模式,在验证成功后 为false,其余情况为trueprivate boolean isMatchMode;//验证的误差允许值private float mMatchDeviation;//验证失败的闪烁动画private ValueAnimator mFailAnim;//验证成功的白光一闪动画private boolean isShowSuccessAnim;private ValueAnimator mSuccessAnim;private Paint mSuccessPaint;//画笔private int mSuccessAnimOffset;//动画的offsetprivate Path mSuccessPath;//成功动画 平行四边形Pathpublic SwipeCaptchaView(Context context) {this(context, null);}public SwipeCaptchaView(Context context, AttributeSet attrs) {this(context, attrs, 0);}public SwipeCaptchaView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init(context, attrs, defStyleAttr);}private void init(Context context, AttributeSet attrs, int defStyleAttr) {//滑块默认尺寸int defaultSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics());mCaptchaHeight = defaultSize;mCaptchaWidth = defaultSize;//误差值mMatchDeviation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics());//设置的属性值TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SwipeCaptchaView, defStyleAttr, 0);int n = ta.getIndexCount();for (int i = 0; i < n; i++) {int attr = ta.getIndex(i);if (attr == R.styleable.SwipeCaptchaView_captchaHeight) {mCaptchaHeight = (int) ta.getDimension(attr, defaultSize);} else if (attr == R.styleable.SwipeCaptchaView_captchaWidth) {mCaptchaWidth = (int) ta.getDimension(attr, defaultSize);} else if (attr == R.styleable.SwipeCaptchaView_matchDeviation) {mMatchDeviation = ta.getDimension(attr, mMatchDeviation);}}ta.recycle();mRandom = new Random(System.nanoTime());mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);mPaint.setColor(0xaa000000);// 设置画笔遮罩滤镜mPaint.setMaskFilter(new BlurMaskFilter(20, BlurMaskFilter.Blur.SOLID));//滑块区域mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);mMaskPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);// 实例化阴影画笔mMaskShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);mMaskShadowPaint.setColor(Color.BLACK);mMaskShadowPaint.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.SOLID));mCaptchaPath = new Path();}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mWidth = w;mHeight = h;//动画区域 会用到宽高createMatchAnim();//尺寸改变,重置图形post(this::createCaptcha);}//验证动画初始化区域@SuppressWarnings("IntegerDivisionInFloatingPointContext")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(animation -> {float animatedValue = (float) animation.getAnimatedValue();isDrawMask = !(animatedValue < 0.5f);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(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/2*3, mHeight, new int[]{0x00ffffff, 0x88ffffff}, new float[]{0,0.5f},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();}//生成验证码区域public void createCaptcha() {if (getDrawable() != null) {resetFlags();createCaptchaPath();createMask();invalidate();}}//重置一些flasg, 开启验证模式private void resetFlags() {isMatchMode = true;}//生成验证码Pathprivate void createCaptchaPath() {//原本打算随机生成gap,后来发现 宽度/3 效果比较好,//int gap = mRandom.nextInt(mCaptchaWidth / 2);int gap = mCaptchaWidth / 3;//随机生成验证码阴影左上角 x y 点,mCaptchaX = mWidth / 2 + mRandom.nextInt(mWidth / 2 - 2 * mCaptchaWidth - gap);mCaptchaY = mCaptchaHeight + mRandom.nextInt(mHeight - 3 * 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();}//生成滑块private void createMask() {mMaskBitmap = getMaskBitmap(((BitmapDrawable) getDrawable()).getBitmap(), mCaptchaPath);//滑块阴影mMaskShadowBitmap = mMaskBitmap.extractAlpha();//拖动的位移重置mDragerOffset = dp2px(getContext(), offset);//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));//关闭硬件加速setLayerType(LAYER_TYPE_SOFTWARE, null);//绘制用于遮罩的圆形mCanvas.drawPath(mask, mMaskPaint);//设置遮罩模式(图像混合模式)mMaskPaint.setXfermode(mPorterDuffXfermode);//★考虑到scaleType等因素,要用Matrix对Bitmap进行缩放mCanvas.drawBitmap(mBitmap, getImageMatrix(), mMaskPaint);mMaskPaint.setXfermode(null);return tempBitmap;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);//继承自ImageView,所以Bitmap,ImageView已经帮我们draw好了。//我只在上面绘制和验证码相关的部分,//是否处于验证模式,在验证成功后 为false,其余情况为trueif (isMatchMode) {//绘制滑块// isDrawMask  绘制失败闪烁动画用if (null != mMaskBitmap && null != mMaskShadowBitmap && isDrawMask) {//修改canvas的density,屏幕适配会导致density不一致canvas.setDensity(mMaskBitmap.getDensity());//首先绘制验证码阴影,即待填充部分if (mCaptchaPath != null) {//待填充阴影,此处为填充模式canvas.drawPath(mCaptchaPath, mPaint);//描绘白边mPaint.setStyle(Paint.Style.STROKE);mPaint.setColor(Color.WHITE);mPaint.setStrokeWidth(1);canvas.drawPath(mCaptchaPath, mPaint);//重置填充模式mPaint.setStyle(Paint.Style.FILL);mPaint.setColor(0xaa000000);}// 先绘制阴影canvas.drawBitmap(mMaskShadowBitmap, -mCaptchaX + mDragerOffset, 0, mMaskShadowPaint);canvas.drawBitmap(mMaskBitmap, -mCaptchaX + mDragerOffset, 0, null);}//验证成功,白光扫过的动画,这一块动画感觉不完美,有提高空间if (isShowSuccessAnim) {canvas.translate(mSuccessAnimOffset, 0);canvas.drawPath(mSuccessPath, mSuccessPaint);}}}/*** 校验*/public void matchCaptcha() {if (null != onCaptchaMatchCallback && isMatchMode) {//这里验证逻辑,是通过比较,拖拽的距离 和 验证码起点x坐标。 默认3dp以内算是验证成功。if (Math.abs(mDragerOffset - mCaptchaX) < mMatchDeviation) {//成功的动画mSuccessAnim.start();} else {mFailAnim.start();}}}/*** 重置验证码滑动距离,(一般用于验证失败)*/public void resetCaptcha() {mDragerOffset = dp2px(getContext(), offset);invalidate();}/*** 最大可滑动值* @return 最大可滑动值,限制了右边空余值*/public int getMaxSwipeValue() {//返回控件宽度return mWidth - mCaptchaWidth - 2 * dp2px(getContext(), offset);}/*** 设置当前滑动值* @param value 当前滑动值,加上初始位移值*/public void setCurrentSwipeValue(int value) {mDragerOffset = dp2px(getContext(), offset) + value;invalidate();}/*** 验证码验证的回调*/public interface OnCaptchaMatchCallback {void matchSuccess(SwipeCaptchaView swipeCaptchaView);void matchFailed(SwipeCaptchaView swipeCaptchaView);}private OnCaptchaMatchCallback onCaptchaMatchCallback;public void setOnCaptchaMatchCallback(OnCaptchaMatchCallback onCaptchaMatchCallback) {this.onCaptchaMatchCallback = onCaptchaMatchCallback;}//留着吧,人写出来也不容易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;//以下是我写出了所有的计算公式后推的,不要问我过程,只可意会。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;//以下是我写出了所有的计算公式后推的,不要问我过程,只可意会。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);}}}//初始偏移值,右边空余值,我这里代码里面写9和外部的seekbar比较适配public final int offset = 9;public static int dp2px(Context context, float dpValue) {final float scale = context.getResources().getDisplayMetrics().density;return (int) (dpValue * scale + 0.5f);}
}

结语

这个滑动验证虽然大部分是从别人那里复制来的,但是经过一番理解,再修改,还是大有收获吧,也希望新加的功能对读者有用,后面有机会把文中用到的一些 bitmap 处理的功能写写吧!

end

安卓图片滑动验证模块相关推荐

  1. Vue实现图片滑动验证

    Vue实现图片滑动验证 使用vue完成 使用vue完成 实习一个星期小白对工作安排的图片滑动验证分享 1.安装依赖 附上:https://www.npmjs.com/package/vue-monop ...

  2. vue实现图片滑动验证功能——功能实现

    图片滑动验证,是目前比较常见的验证方式,主要目的是防止用户利用机器人自动注册.登录.灌水. 目前vue技术日趋成熟,已经有专门针对图片滑动验证功能的插件了.具体使用方式如下: 1.安装插件--npm ...

  3. Java滑动验证_java图片滑动验证(登录验证)原理与实现方法详解

    本文实例讲述了java图片滑动验证(登录验证)原理与实现方法.分享给大家供大家参考,具体如下: 这是我简单做出的效果图,处理300X150px的校验图,并把图片发到前端,用时50毫秒左右,速度还是非常 ...

  4. 基于Java实现图片滑动验证(包含前端代码)

    前言 1.下面是一个效果展示: 2.先抱怨一下,在博客上面的抄袭真的非常严重,为了实现一个图片滑动验证,我搜索了挺久的资料,不过内容翻来覆去就是同样的内容,千篇一律,作者还各不相同:内容相同我就不多说 ...

  5. php 图片滑动验证 貳

    tp6方式: 注意:验证后销毁保存的图片(背景图和滑动图) <?php declare (strict_types = 1);namespace app\index\controller;use ...

  6. 前端滑动验证+拼图滑动验证效果

    相信大家都玩过B站,B站在登陆的时候有个拼图滑动验证,今天就整合一下前端实现的滑动验证 拖动滑动验证(无背景图片) <!DOCTYPE html> <html> <hea ...

  7. 【移动端】滑动验证致使整个屏幕都在动

    最近发现一款JQ图片滑动验证的代码.当手机端拖动按钮元素时,会触发手机浏览器自带事件,导致滑动失效或切屏,并且浏览器报错. [Intervention] Unable to preventDefaul ...

  8. Java图片滑动验证码

    目录 一.配置文件 二.图片 三.代码实现 前后端代码: java滚动验证码-前端和后端完整版_outdata的博客-CSDN博客_java图片滚动验证 图形滑动验证码JAVA实现[前后端结合]_Le ...

  9. html四张图片拼图,js实现html滑动图片拼图验证

    本文实例为大家分享了js实现html滑动图片拼图验证的具体代码,供大家参考,具体内容如下 html: Document 向右滑动验证 css: *{ margin: 0; padding: 0; } ...

最新文章

  1. oracle job 时间间隔,关于job的时间间隔的一个问题
  2. 面试时算法题的解答思路
  3. CCNA 实验三 3.2 2 默认路由
  4. OpenCV信息流Alpha遮罩
  5. c++如何将两个if函数合并_设计一个 add 函数
  6. 互联网公司Java面试总结
  7. php 覆盖相同键的值,php - PHP数组使用相同的字符串键组合值 - SO中文参考 - www.soinside.com...
  8. 研讨会 | CCF TF 第 17 期:认知计算产业化落地
  9. mysql if语句后面执行两个语句_MySQL的if,case语句使用总结
  10. 帝豪gl车机系统降级_何以剑指合资?帝豪GL/英朗底盘对比
  11. FortiGate设备管理
  12. android常用刷机指令,【高级技术】Android刷机常用adb指令集合
  13. 【马克思主义基本原理】--第二章--实践与认识及其发展规律
  14. 一直想当5G老大的美国,现在进展怎样了?
  15. 修改360企业版杀毒软件备注名的方法
  16. “黎明”号新任务继续“锁定”谷神星
  17. c语言键盘函数key,c语言获得键盘的按键
  18. 如何让虚拟角色自然融入现实?
  19. #Python3中检测字符串是否只由大写英文字母或者数字组成issupper()
  20. Zynq-7000 - Which IBIS models should be used for Zynq-7000 devices?

热门文章

  1. UE4 BSP画刷知识点
  2. uni-app 关于微信小程序分享,app微信聊天界面和朋友圈分享
  3. Web API 项目报“Unable to resolve service for type ”
  4. 【理解】Kalman卡尔曼滤波器 附pythonmatlab代码
  5. 作团队感悟(17)----小结一下我的作团队感悟
  6. 软件测试工程师的主要“工作内容”和职责是什么?
  7. 在springboot中配置redis集群
  8. 少儿Python编程_第十八讲 搭建网站
  9. fastadmin操作中添加自定义按钮,点击弹出表格
  10. java 打印图片_java实现图片的打印