最近想把工作这两年的东西好好写一写,一直觉得自己好像没做什么东西一样,写一写也能给自己一点自信,当然更像是一次总结。
安卓滑动验证模块是去年一个需求做的,当时也只是从网上找了一个不错的博客 cv 大法了一把,但是人家写的很详细,也是让我搞懂了一些东西,后面按 UI 要求改动了一些,就更加深刻了,从里面也学到了很多东西,下面好好说说。
具体效果
原版博客
前面先贴出原版博文,我做了一些修改但是不多,文章末尾贴代码吧
【Android】仿斗鱼滑动拼图验证码控件
https://www.jianshu.com/p/9bf982da6e96
实现思路
原作者写的挺清楚了,我这也是说一下我的理解吧
- 继承 ImageView,初始化笔触,绘制验证码滑块路径
- 以背景图和路径擦出一个和背景图尺寸一样,但只包含滑块部分的 bitmap
- 以滑块 bitmap 提取一个同等大小只含滑块形状的底层阴影的 bitmap
- 通过外部 seekbar 更新滑动比例,触发invalidate 函数,重绘图形
- 先绘制空出部分的阴影,再绘制滑块,包含滑块的阴影和滑块的图形
- 在外部滑块停止后,进行验证,显示动画,触发里面验证的回调接口
其他细节就不详述了,代码里面注释很多,我也加了挺多注释,看文末代码吧
我的修改
因为需要,我也按着上面的代码做了一定修改,下面说说
阴影显示问题
直接使用原版的代码发现没有阴影,我还自己弄了一番,后面发现原博主的代码可以,只是使用的时候没有关闭硬件加速,把硬件加速关了,滑块阴影就可以显示了
//关闭硬件加速
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;
@Override
protected 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() {
@Override
public void matchSuccess(SwipeCaptchaView swipeCaptchaView) {
mSeekBar.setEnabled(false);
setResult(RESULT_OK);
finish();
}
@Override
public void matchFailed(SwipeCaptchaView swipeCaptchaView) {
Toast.makeText(SwipeCheckActivity.this,
"验证失败!", 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) {
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>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model,
Target<Bitmap> target, boolean isFirstResource) {
return true;
}
@Override
public 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">
<TextView
android:layout_width=