Android拼图滑块验证码控件
2018-01-30 11:04
567 查看
地址:Captcha Github
概述
验证码是可以区分用户是人还是计算机。可以防止破解密码、刷票等恶意行为。客户端上多数用在关键操作上,比如购买、登录、注册等场景。本文将介绍Android拼图滑块验证码控件是如何一步一步编写形成的。希望能帮助到大家。 当然,如果仅仅是实现效果如何实现就太没技术含量了。本文将通过介绍如何编写此控件的同时,介绍Android自定义控件的一些技巧。
效果图
分析
由效果图可以看到一张图片,一个可拖动滑块条,拖动滑块条,滑块拼图跟随滑动,滑动拼图到正确位置并松开手指,图下方会出现验证信息,滑动到错误位置并松开,也会出现验证信息。 看到这样的效果,很多人估计都能看出来,这是一个ViewGroup包装了一个ImageView和Seekbar。没错,控件确实是LinearLayout包裹着一个ImageView和Seekar,然而Android自带的ImageView和Seekbar并不能实现效果,需要改造。如何改造后面再说。现在来看看整体控件的结构:
从图看到,控件是由PictureVertifyView和TextSeekbar组合在一个Captcha里面。也就是前面所说的一个LinearLayout包裹一个ImageView和Seekbar,并将这些形成一个小团体。其中团体的老大我就叫他Captcha,PictureVertifyView和TextSeekbar就是他的小跟班,社团跟客户谈业务只能由老大进行,老大将工作吩咐给跟班,跟班们根据自己的能力做自己的本职,PictureVertifyView就是做拼图这块的,TextSeekbar就是做滑动条这块的,最终组合成一个能完成客户需求的整体。
废话不多说,开始写干货。
实现
1. 编写PictureVertifyView类
PictureVertifyView就是效果图上部分的拼图块,也是Captcha控件的核心。主要完成拼图的绘制,逻辑。
1.1PictureVertifyView类的状态
首先PictureVertifyView有6种状态(点击、滑动、松开手指、验证成功、验证失败、初始化),这些状态将影响PictureVertifyView的绘制。至于怎么影响,后面将会说明。private static final int STATE_DOWN = 1; private static final int STATE_MOVE = 2; private static final int STATE_LOOSEN = 3; private static final int STATE_IDEL = 4; private static final int STATE_ACCESS = 5; private static final int STATE_UNACCESS = 6; private int mState = STATE_IDEL;
//手指按下 void down(int progress) { startTouchTime = System.currentTimeMillis(); mState = STATE_DOWN; currentPosition = (int) (progress / 100f * (getWidth() - Utils.dp2px(getContext(), 50))); invalidate(); } //手指滑动,改变缺块图片位置 void move(int progress) { mState = STATE_MOVE; currentPosition = (int) (progress / 100f * (getWidth() - Utils.dp2px(getContext(), 50))); invalidate(); } //手指松开,进行验证 void loose() { mState = STATE_LOOSEN; looseTime = System.currentTimeMillis(); checkAccess(); invalidate(); } //复位到初始状态,当复位的初始状态,所有东西都会重置 void reset() { mState = STATE_IDEL; verfityBlock.recycle(); verfityBlock = null; info = null; blockShape = null; invalidate(); } //验证不通过 void unAccess() { mState = STATE_UNACCESS; invalidate(); } //验证通过 void access() { mState = STATE_ACCESS; invalidate(); }
1.2拼图缺块位置信息生成
拼图缺块信息既是拼图缺块的位置,这里将封装成一个实体类PositionINfo如下:
public static class PositionInfo { //缺块在整张图片的左上角x轴位置 int left; //缺块在整张图片的左上角y轴位置 int top; public PositionInfo(int left, int top) { this.left = left; this.top = top; } }
对于验证码拼图缺块的位置是要在图片范围内随机生成的。代码如下:
@Override public PositionInfo getBlockPostionInfo(int width, int height) { Random random = new Random(); int edge = Utils.dp2px(getContext(), 50); int left = random.nextInt(width - edge); //Avoid robot frequently and quickly click the start point to access the captcha. if (left < edge) { left = edge; } int top = random.nextInt(height - edge); if (top < 0) { top = 0; } return new PositionInfo(left, top); }
这里面对缺块作了边界限制,如下图:
从图上看到,图片的左上角的点坐标限制在里面的矩形中,这样做的目的是防止缺块超出图片边界。看到这里有人要疑惑,为了防止缺块超出整张图片范围,限制右、下边界可以理解,为什么还要限制左边界。原因很简单,Captcha是基于按下和松开TextSeekbar来判断是否要进行验证。万一机器人模拟快速点击滑动条的初始位置,而缺块正好又生成在图片最左边,这样不就不需要滑动就能通过验证了吗。虽然缺块生成在图片的最左边几率很小,但是我还是拒绝这种欧气的产生。
1.3缺块的形状
缺块的形状就是一个Path类,如何编写?这还用问吗?你弄个圆也行,弄个三角形也行,这里我就弄个不知道名字的形状好了,代码如下:
public Path getBlockShape(int blockSize) { int gap = Utils.dp2px(getContext(), blockSize/5f); Path path = new Path(); path.moveTo(0, gap); path.rLineTo(Utils.dp2px(getContext(), blockSize/2.5f), 0); path.rLineTo(0, -gap); path.rLineTo(gap, 0); path.rLineTo(0, gap); path.rLineTo(2 * gap, 0); path.rLineTo(0, 4 * gap); path.rLineTo(-5 * gap, 0); path.rLineTo(0, -1.5f * gap); path.rLineTo(gap, 0); path.rLineTo(0, -gap); path.rLineTo(-gap, 0); path.close(); return path; } //不过注意了,你设计的Path的狂傲要限制在blockSize(缺块大小)内。
1.4生成缺块图片
上面介绍了缺块的形状,这里我们讲讲如何生成缺块图片。缺块图片是要在原图根据缺块形状裁的,至于怎么裁,代码如下:
private Bitmap createBlockBitmap() { //创建一张白纸 Bitmap tempBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); //将这张白纸片放入画布中 Canvas canvas = new Canvas(tempBitmap); //由于PictureVertifyView是继承ImageView,这里我们直接拿到ImageView的Drawable图片并限制其宽高以达到Drawable是和ImageView的宽高是一致的 getDrawable().setBounds(0, 0, getWidth(), getHeight()); //把画布的可画范围限制在缺块形状中,当然这个blockShape已经根据缺块位置进行偏移 canvas.clipPath(blockShape); //画画 getDrawable().draw(canvas); //先忽略下面这句好吗 mStrategy.decoreateSwipeBlockBitmap(canvas,blockShape); return cropBitmap(tempBitmap); }
这时tempBitmap是这样的:外矩形是透明的,内矩形就是缺块的内容,而整张tempBitmap的大小跟ImageView的大小一样,很多无用面积,而我们需要的是里面那张。别急cropBitmp方法就是帮我们裁出里面那部分。
private Bitmap cropBitmap(Bitmap bmp) { Bitmap result = null; int size = Utils.dp2px(getContext(), blockSize); //一句代码就裁出来了,就是这么简单 result = Bitmap.createBitmap(bmp, info.left, info.top, size, size); bmp.recycle(); return result; }
1.5 绘制
在看绘制代码前,我们先来看看下面这张图,看看怎么绘制。
实际上绘制并不复杂,就是在ImageView的基础上加上一个缺块的阴影遮盖原来图片的一部分,再加上裁出来的缺块图片。由于本控件的滑动条是水平滑动的,所以缺块图片和缺块阴影在同一水平线上,只是水平位置不同。验证是否通过是判断currentPostion和left是否大概相等。怎么个大概?我代码是写10个像素。
好了,贴代码:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制前,我们要判断是否有缺块的位置信息,缺块形状和图片,没有的话要先获得它们的实例,其中mStrategy的作用,我们后面将会说到。 if (info == null) { info = mStrategy.getBlockPostionInfo(getWidth(), getHeight()); } if (blockShape == null) { blockShape = mStrategy.getBlockShape(blockSize); blockShape.offset(info.left, info.top); } if (verfityBlock == null) { verfityBlock = createBlockBitmap(); } //上文已经说过,状态会影响绘制内容,其中当状态为非验证成功时,会绘制阴影,当状态为滑动或初始时绘制滑动的缺块图片(写代码最怕为改名字,有些名字很尴尬,请多多包涵) if (mState != STATE_ACCESS) { canvas.drawPath(blockShape, shadowPaint); } if (mState == STATE_MOVE || mState == STATE_IDEL) canvas.drawBitmap(verfityBlock, currentPosition, info.top, bitmapPaint); } }
1.6 验证算法
前面提到,当松开手指,缺块图片和缺块阴影重合的时候就是验证通过,否则失败,其代码很简单:
private void checkAccess() { //判断currentPostion(滑块水平位置)和info.left(阴影水平位置)是否在容差以内 if (Math.abs(currentPosition - info.left) < TOLERANCE) { access(); //listener监听器用于监听验证成功失败事件 if (listener != null) { //小操作,用于记录用户验证所需要的时间,详细看工程代码 long deltaTime = looseTime - startTouchTime; listener.onAccess(deltaTime); } } else { unAccess(); if (listener != null) { listener.onFailed(); } } }
2.编写TextSeekbar
TextSeekbar就是效果图下面那个滑动条,它相当于团队里老板的市场调研,观察客户的喜好再告诉给老板,老板再叫PictureVertifyView做什么。TextSeekbar继承于Seekbar,功能很简单,就是在绘制完Seekbar的时候多绘制一行字,代码如下:@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawText("向右滑动滑块完成拼图", getWidth() / 2, getHeight() / 2 + textPaint.getTextSize() / 2 - 4, textPaint); } //注意,这个地方有点偷工减料了,文字垂直方向的偏移并没有准确计算,只是在手机上调试视觉上就是差不多这样,这里深感抱歉。
3.编写Captcha老大
前文说过,Captcha就是团队老大,外面客户谈业务只能跟它谈,再将工作分配给小弟。Captcha就相当于团队的门面,其实门面嘛是可有可无的。有了PictureVertifyView和TextSeekbar就可以进行业务工作,但是国不能一日无君,社团不能一日无老大。老大是对内统筹下属工作,对外是外界与团队进行沟通的对象。 Captcha其实就是一个LinearLayout包裹PictureVertifyView和TextSeekbar的组合控件,当然也可以是FrameLayout等。
Captcha的编写与编写组合控件的步骤一样,编写xml布局文件,编写控制逻辑。
3.1xml布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/container_backgroud" android:padding="10dp" android:orientation="vertical"> <FrameLayout android:layout_width="match_parent" android:paddingLeft="10dp" android:paddingRight="10dp" android:layout_height="200dp"> <com.luozm.captcha.PictureVertifyView android:id="@+id/vertifyView" android:layout_width="match_parent" android:layout_height="200dp" android:scaleType="fitXY" /> <LinearLayout android:visibility="gone" android:id="@+id/accessRight" android:background="#7F000000" android:orientation="horizontal" android:layout_gravity="bottom" android:layout_width="match_parent" android:layout_height="28dp"> <ImageView android:src="@drawable/right" android:layout_marginLeft="10dp" android:layout_width="20dp" android:layout_gravity="center" android:layout_height="20dp" /> <TextView android:id="@+id/accessText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginLeft="10dp" android:textColor="#FFFFFF" android:text="验证通过,耗时1000毫秒" android:textSize="14sp"/> </LinearLayout> <LinearLayout android:id="@+id/accessFailed" android:background="#7F000000" android:orientation="horizontal" android:visibility="gone" android:layout_gravity="bottom" android:layout_width="match_parent" android:layout_height="28dp"> <ImageView android:src="@drawable/wrong" android:layout_marginLeft="10dp" android:layout_width="20dp" android:layout_gravity="center" android:layout_height="20dp" /> <TextView android:id="@+id/accessFailedText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginLeft="10dp" android:textColor="#FFFFFF" android:textSize="14sp"/> </LinearLayout> </FrameLayout> <com.luozm.captcha.TextSeekbar android:id="@+id/seekbar" android:layout_gravity="center" style="@style/MySeekbarSytle" android:splitTrack="false" android:thumbOffset="0dp" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" /> </LinearLayout>
布局很简单,除了前文介绍的TextSeekbar和PictureVertifyView外还有一个覆盖在PictureVertifyView底部的用于显示验证信息的布局。
3.2控制逻辑
Captcha的作用就是统筹子控件的逻辑,相当于一个Activity界面统筹各个控件工作完成一个功能。在这里,Captcha主要是通过TextSeekbar的拖动事件触发PictureVertifyView状态的改变。其代码如下:
seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (isDown) { isDown = false; if (progress > 10) { isResponse = false; } else { isResponse = true; accessFailed.setVisibility(GONE); vertifyView.down(0); } } if (isResponse) { vertifyView.move(progress); } else { seekBar.setProgress(0); isResponse = false; } } @Override public void onStartTrackingTouch(SeekBar seekBar) { isDown = true; } @Override public void onStopTrackingTouch(SeekBar seekBar) { if (isResponse) { vertifyView.loose(); } } });
由于Android自带的Seekbar的progress值是随用户手指的位置改变,即使手指并非按下滑动块也能改变其值。因此,上面代码作了用户按下滑动块才能拖动,避免progress值的瞬变。
4.策略模式
刚写这个控件的时候,只是为了完成项目中登录验证的功能,PictureVertifyView的拼图形状,拼图效果都是写死的。而为了让控件的使用者可以自行定制想要的拼图效果,采用策略模式对控件进行大改造,将定义拼图效果的方法移至策略类当中。其类图大概如下图: 读者在前文中应该留意到mStrategy这个引用。这就是PictureVertifyView通过引用CaptchaStrategy的一个实现类从而确定自己要用到的拼图效果。CaptchaStrategy的代码如下:
public abstract class CaptchaStrategy { protected Context mContext; public CaptchaStrategy(Context ctx) { this.mContext = ctx; } protected Context getContext() { return mContext; } /** * 定义缺块的形状 * * @param blockSize 单位dp,注意转化为px * @return path of the shape */ public abstract Path getBlockShape(int blockSize); /** * 定义缺块的位置信息生成算法 * * @param width picture width * @param height picture height * @return position info of the block */ public abstract PictureVertifyView.PositionInfo getBlockPostionInfo(int width, int height); /** * 获得缺块阴影的Paint */ public abstract Paint getBlockShadowPaint(); /** * 获得滑块图片的Paint */ public abstract Paint getBlockBitmapPaint(); /** * 装饰滑块图片,在绘制图片后执行,即绘制滑块前景 */ public void decoreateSwipeBlockBitmap(Canvas canvas,Path shape) { } }
它的默认实现类为DefaultCaptchaStrategy,代码如下:
public class DefaultCaptchaStrategy extends CaptchaStrategy { public DefaultCaptchaStrategy(Context ctx) { super(ctx); } @Override public Path getBlockShape(int blockSize) { int gap = Utils.dp2px(getContext(), blockSize/5f); Path path = new Path(); path.moveTo(0, gap); path.rLineTo(Utils.dp2px(getContext(), blockSize/2.5f), 0); path.rLineTo(0, -gap); path.rLineTo(gap, 0); path.rLineTo(0, gap); path.rLineTo(2 * gap, 0); path.rLineTo(0, 4 * gap); path.rLineTo(-5 * gap, 0); path.rLineTo(0, -1.5f * gap); path.rLineTo(gap, 0); path.rLineTo(0, -gap); path.rLineTo(-gap, 0); path.close(); return path; } @Override public PictureVertifyView.PositionInfo getBlockPostionInfo(int width, int height) { Random random = new Random(); int edge = Utils.dp2px(getContext(), 50); int left = random.nextInt(width - edge); //Avoid robot frequently and quickly click the start point to access the captcha. if (left < edge) { left = edge; } int top = random.nextInt(height - edge); if (top < 0) { top = 0; } return new PictureVertifyView.PositionInfo(left, top); } @Override public Paint getBlockShadowPaint() { Paint shadowPaint = new Paint(); shadowPaint.setColor(Color.parseColor("#000000")); shadowPaint.setAlpha(165); return shadowPaint; } @Override public Paint getBlockBitmapPaint() { Paint paint = new Paint(); return paint; } @Override public void decoreateSwipeBlockBitmap(Canvas canvas, Path shape) { Paint paint = new Paint(); paint.setColor(Color.parseColor("#FFFFFF")); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(10); paint.setPathEffect(new DashPathEffect(new float[]{20,20},10)); Path path = new Path(shape); canvas.drawPath(path,paint); } }
总结
通过拼图滑块验证码控件的编写,了解到自定义控件的一些技巧和步骤。此外还能免费在项目中用到拼图验证码(因为市面上网易的云盾验证码,极验都是付费的)。 最后,给伸手党一个福利,控件已上传到jcenter。使用者可移至gayhub查看使用。
相关文章推荐
- Android拼图滑块验证码控件
- Android拼图滑块验证码控件
- 【Android】仿斗鱼滑动拼图验证码控件
- Android自定义View 做个简单的验证码控件
- Android手机验证码倒计时控件
- Android拆轮子系列之写验证码控件的方法
- Android UI系统控件进阶(二)—下拉列表控件Spinner,进度条ProgressBar,滑块控件SeekBar,评分控件RatingBar
- Android自定义View--验证码控件
- 仿斗鱼滑动拼图验证码控件
- Android自定义View入门之简单验证码控件
- 极验验证——滑块拼图验证码
- Android 验证码控件 (计时器简单实现)
- Android原生Switch控件滑块thumb卡住问题的解决方法
- android 闪屏页的倒计时和发送验证码的倒计时控件
- Android实现滑动选择控件实例代码
- android学习记录(九)AppWidget桌面控件轻松实现
- Android 高仿微信头像截取 打造不一样的自定义控件
- android中设置控件获得焦点
- android控件详解----TextView
- 【读书笔记-《Android游戏编程之从零开始》】3.Android 游戏开发常用的系统控件(Button、Layout、ImageButton)