当前位置:网站首页>图片验证码控件
图片验证码控件
2022-06-29 09:16:00 【禽兽先生不禽兽】
现在的应用各种验证方式五花八门,从最开始的数字验证,到后来的数字图片变成动态图,再到图片验证,还有 12306 那令人发指的:
无论方式怎么变化,都是为了保证有人使用机器恶意注册、登陆等,加大服务器的负担和消费。最近是简单实现了一下图片验证码,在优化过一次后决定记录一下它的实现原理。
最后的效果是这样:
1 思路
首先,我们需要对图片进行处理,从中挖取一个用于验证的拼图块,等待验证的图片也就是原图挖取拼图块后的样子,然后我们可以拖动进度条来改变拼图块的位置,最后松手的时候判断拼图块的位置是否在原图被挖取的位置附近就行了。2 挖取拼图块
这种情况我们必然要用自定义 View 的,首先我们加载一个图片,将图片挖掉一个拼图块,再在底部画一个进度条:public class ImageCheckCodeView extends View {
private Paint mPaint; //画笔,画圆角矩形的进度条,画进度游标
private Bitmap mBitmap; //进行图片验证的原图
private Bitmap waitCheckBitmap; //等待验证的图片,是原图挖取掉一个拼图块后的图片
private Puzzle puzzle; //进行验证的拼图块
private RectF roundRectF; //进度条的圆角矩形
private float progress = 0; //当前手机的滑动进度
private int seekBarHeight; //进度条的高度
private boolean startImageCheck = false; //开始图片验证的标志位
private Rect puzzleSrc; //拼图块绘制区域
private Rect puzzleDst; //拼图块显示区域
public void setBitmap(Bitmap bitmap) {
this.mBitmap = bitmap;
if (getMeasuredWidth() == 0 || getMeasuredHeight() == 0) {
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
initBitmap();
}
});
} else {
initBitmap();
}
}
public ImageCheckCodeView(Context context) {
this(context, null);
}
public ImageCheckCodeView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ImageCheckCodeView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//初始化画笔
mPaint = new Paint();
//创建进度条的圆角矩形的显示区域
roundRectF = new RectF();
//创建拼图块对象
puzzle = new Puzzle();
//拼图块的绘制区域和显示区域
puzzleSrc = new Rect();
puzzleDst = new Rect();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
//如果没有指定宽,默认宽度为屏幕宽
width = getContext().getResources().getDisplayMetrics().widthPixels;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
//如果没有指定高,默认高度为屏幕高的 1/3
height = getContext().getResources().getDisplayMetrics().heightPixels / 3;
}
setMeasuredDimension(width, height);
initBitmap();
}
/**
* Description:初始化图片,获取拼图块和挖取拼图块后的等待验证的图片
* Date:2018/2/7
*/
private void initBitmap() {
if (mBitmap == null) {
mBitmap = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.a);
}
//设置进度条的高度为控件高度的 1/10
seekBarHeight = getMeasuredHeight() / 10;
//设置进度条现在在控件底部,距离底部的距离也为控件高度的 1/10
roundRectF.set(0, getMeasuredHeight() - seekBarHeight * 2, getMeasuredWidth(), getMeasuredHeight() - seekBarHeight);
//将控件宽高传递给 puzzle 对象
puzzle.setContainerWidth(getMeasuredWidth());
puzzle.setContainerHeight(getMeasuredHeight());
//将原图传递给 puzzle 对象,puzzle 内部会将图片处理成拼图块的样子
puzzle.setBitmap(mBitmap);
//根据原图和 puzzle 对象获取挖取拼图块后的图片,即等待验证的图片
waitCheckBitmap = BitmapUtil.getWaitCheckBitmap(mBitmap, puzzle, getMeasuredWidth(), getMeasuredHeight());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(waitCheckBitmap, 0, 0, null);
//画灰色,50% 透明度的圆角矩形的指示条
mPaint.setColor(Color.argb(128, 128, 128, 128));
canvas.drawRoundRect(roundRectF, 45, 45, mPaint);
}
}其中 Puzzle 对象如下,在父容器测量到宽高之后传递该对象中,根据父容器的宽高来决定拼图块的宽高,宽我是取的父容器的 1/5,高是父容器的 1/4,然后再随机生成一下拼图块的位置,x 和 y 是拼图块左上角的横纵坐标:
public class Puzzle {
private int containerWidth; //容器宽度
private int containerHeight; //容器高度
private int x; //拼图块左上角横坐标
private int y; //拼图块左上角纵坐标
private int width; //拼图块的宽
private int height; //拼图块的高
private Bitmap bitmap; //原图
public Puzzle() {
}
public void setContainerWidth(int containerWidth) {
this.containerWidth = containerWidth;
x = new Random().nextInt(containerWidth
- containerWidth / 5 //减去拼图块宽度,保证拼图块全部显示
);
width = containerWidth / 5;
}
public void setContainerHeight(int containerHeight) {
this.containerHeight = containerHeight;
y = new Random().nextInt(containerHeight
- containerHeight / 5 //减去拼图块高度,保证拼图块全部显示
);
height = containerHeight / 4;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public Bitmap getBitmap() {
return bitmap;
}
public void setBitmap(Bitmap bitmap) {
this.bitmap = BitmapUtil.getPuzzleBitmap(bitmap, this, containerWidth, containerHeight);
}
}BitmapUtil 中有两个处理图片的方法,这两个方法内部实现差不多,主要区别是 serXfermode() 来决定显示交集部分还是非交集部分,拼图块的绘制主要涉及到贝塞尔曲线的计算,不懂的朋友可以搜一下贝塞尔曲线。
public class BitmapUtil {
/**
* Description:获取拼图块
* Date:2018/2/7
*/
public static Bitmap getPuzzleBitmap(Bitmap bitmap, Puzzle puzzle, int width, int height) {
//创建一个拼图块大小的图片
Bitmap mBitmap = Bitmap.createBitmap(puzzle.getWidth(), puzzle.getHeight(), Bitmap.Config.ARGB_8888);
Canvas mCanvas = new Canvas(mBitmap);
Paint mPaint = new Paint();
mPaint.setAntiAlias(true);
//画拼图块的路径
Path mPath = new Path();
mPath.moveTo(0, puzzle.getHeight() / 4);
mPath.lineTo(puzzle.getWidth() / 3, puzzle.getHeight() / 4);
mPath.cubicTo(puzzle.getWidth() / 6, 0
, puzzle.getWidth() - puzzle.getWidth() / 6, 0
, puzzle.getWidth() - puzzle.getWidth() / 3, puzzle.getHeight() / 4);
mPath.lineTo(puzzle.getWidth(), puzzle.getHeight() / 4);
mPath.lineTo(puzzle.getWidth(), puzzle.getHeight());
mPath.lineTo(0, puzzle.getHeight());
mPath.lineTo(0, puzzle.getHeight() - puzzle.getHeight() / 4);
mPath.cubicTo(puzzle.getWidth() / 3, puzzle.getHeight() - puzzle.getHeight() / 8
, puzzle.getWidth() / 3, puzzle.getHeight() / 4 + puzzle.getHeight() / 8
, 0, puzzle.getHeight() / 2);
mPath.lineTo(0, puzzle.getHeight() / 4);
mCanvas.drawPath(mPath, mPaint);
//画拼图块的图片
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
//拼图块显示的位置根据图片与控件的比例决定
Rect src = new Rect((int) ((double) bitmap.getWidth() / (double) width * puzzle.getX())
, (int) ((double) bitmap.getHeight() / (double) height * puzzle.getY())
, (int) ((double) bitmap.getWidth() / (double) width * (puzzle.getX() + puzzle.getWidth()))
, (int) ((double) bitmap.getHeight() / (double) height * (puzzle.getY() + puzzle.getHeight())));
Rect dst = new Rect(0, 0, puzzle.getWidth(), puzzle.getHeight());
mCanvas.drawBitmap(bitmap, src, dst, mPaint);
//在拼图块外围画一个轮廓,显眼一点
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(5f);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setXfermode(null);
mCanvas.drawPath(mPath, mPaint);
return mBitmap;
}
/**
* Description:获取等待验证的图片
* Date:2018/2/7
*/
public static Bitmap getWaitCheckBitmap(Bitmap bitmap, Puzzle puzzle, int width, int height) {
//创建一个与控件宽高相等的图片
Bitmap mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas mCanvas = new Canvas(mBitmap);
Paint mPaint = new Paint();
mPaint.setAntiAlias(true);
Path mPath = new Path();
//挖取掉拼图块
mPath.moveTo(puzzle.getX(), puzzle.getY() + puzzle.getHeight() / 4);
mPath.lineTo(puzzle.getX() + puzzle.getWidth() / 3, puzzle.getY() + puzzle.getHeight() / 4);
mPath.cubicTo(puzzle.getX() + puzzle.getWidth() / 6, puzzle.getY()
, puzzle.getX() + puzzle.getWidth() - puzzle.getWidth() / 6, puzzle.getY()
, puzzle.getX() + puzzle.getWidth() - puzzle.getWidth() / 3, puzzle.getY() + puzzle.getHeight() / 4);
mPath.lineTo(puzzle.getX() + puzzle.getWidth(), puzzle.getY() + puzzle.getHeight() / 4);
mPath.lineTo(puzzle.getX() + puzzle.getWidth(), puzzle.getY() + puzzle.getHeight());
mPath.lineTo(puzzle.getX(), puzzle.getY() + puzzle.getHeight());
mPath.lineTo(puzzle.getX(), puzzle.getY() + puzzle.getHeight() - puzzle.getHeight() / 4);
mPath.cubicTo(puzzle.getX() + puzzle.getWidth() / 3, puzzle.getY() + puzzle.getHeight() - puzzle.getHeight() / 8
, puzzle.getX() + puzzle.getWidth() / 3, puzzle.getY() + puzzle.getHeight() / 4 + puzzle.getHeight() / 8
, puzzle.getX(), puzzle.getY() + puzzle.getHeight() / 2);
mPath.lineTo(puzzle.getX(), puzzle.getY() + puzzle.getHeight() / 4);
mCanvas.drawPath(mPath, mPaint);
//图片显示在控件范围内
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
Rect dst = new Rect(0, 0, width, height);
mCanvas.drawBitmap(bitmap, src, dst, mPaint);
return mBitmap;
}
}
目前的效果如下:

成功获取到了挖取拼图块后的等待验证的图片,还有一个进度条,拼图块暂时还没画出来,不着急,一般是滑动的时候才会出现拼图块,接下来我们就来处理滑动。
3 处理滑动
在按下时开始滑动(要从左边小于控件宽度 1/10 的地方按下时才算有效),
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (event.getRawX() < getMeasuredWidth() / 10) {
startImageCheck = true;
progress = event.getRawX() < seekBarHeight / 2
? seekBarHeight / 2
: event.getRawX() > getMeasuredWidth() - seekBarHeight / 2 ? getMeasuredWidth() - seekBarHeight / 2 : event.getRawX();
}
invalidate();
return true;
case MotionEvent.ACTION_MOVE:
if (!startImageCheck) {
break;
}
progress = event.getRawX() < seekBarHeight / 2
? seekBarHeight / 2
: event.getRawX() > getMeasuredWidth() - seekBarHeight / 2 ? getMeasuredWidth() - seekBarHeight / 2 : event.getRawX();
invalidate();
break;
case MotionEvent.ACTION_UP:
if (!startImageCheck) {
break;
}
progress = 0;
startImageCheck = false;
invalidate();
break;
}
return false;
} //开始验证后重绘触摸点和拼图块的位置
if (startImageCheck) {
puzzleSrc.set(0, 0, puzzle.getWidth(), puzzle.getHeight());
puzzleDst.set((int) progress - puzzle.getWidth() / 2, puzzle.getY(), (int) progress + puzzle.getWidth() - puzzle.getWidth() / 2, puzzle.getY() + puzzle.getHeight());
canvas.drawBitmap(puzzle.getBitmap(), puzzleSrc, puzzleDst, null);
//画触摸点
mPaint.setColor(Color.BLACK);
canvas.drawCircle(progress, getMeasuredHeight() - seekBarHeight - seekBarHeight / 2, seekBarHeight / 2, mPaint);
}现在每次重绘时如果是开始验证的情况就会绘制拼图块和进度条中的点:

4 验证结果回调
最后我们需要提供一个外部接口来告知是否验证成功。
public interface OnCheckResultCallback {
void onSuccess();
void onFailure();
}定义一个存储回调接口的变量和提供一个设置回调接口的方法:
private OnCheckResultCallback onCheckResultCallback;
public void setOnCheckResultCallback(OnCheckResultCallback onCheckResultCallback) {
this.onCheckResultCallback = onCheckResultCallback;
}最后在手指抬起时,判断一下验证结果并回调结果(我这里的判断条件是抬起时手指的位置在拼图块中心点左右误差不超过拼图块宽度 1/20 即验证成功):
if (onCheckResultCallback == null) {
break;
}
if (event.getRawX() > puzzle.getX() + puzzle.getWidth() / 2 - puzzle.getWidth() / 20
&& event.getRawX() < puzzle.getX() + puzzle.getWidth() / 2 + puzzle.getWidth() / 20) {
//松手时触摸点在拼图块中心的横坐标左右偏差不超过拼图块宽度的 1/20则验证成功
onCheckResultCallback.onSuccess();
} else {
onCheckResultCallback.onFailure();
}这样就达到文章开头的效果了。
5 总结
6 Github 传送门
边栏推荐
- FreeRTOS(九)——队列
- JVM之TLAB
- 详细分析PBot挖矿病毒家族行为和所利用漏洞原理,提供蓝军详细防护建议
- Flutter 基础组件之 ListView
- 安装Anaconda后启动JupyterLab需要输入密码
- A method of creating easy to manage and maintain thread by C language
- 我想知道如何免费网上注册股票开户?另外,手机开户安全么?
- Generic paging framework
- Data visualization: the significance of data visualization
- leetcode MYSQL数据库题目180
猜你喜欢

Deep Learning-based Automated Delineation of Head and Neck Malignant Lesions from PET Images

A 3D Dual Path U-Net of Cancer Segmentation Based on MRI

Kicad learning notes - shortcut keys

C语言实现一种创建易管理易维护线程的方法

容器

Gross Tumor Volume Segmentation for Head and Neck Cancer Radiotherapy using Deep Dense Multi-modalit

Custom MVC framework implementation

Flutter 基础组件之 ListView

Flutter 基础组件之 Container

力扣94二叉树的中序遍历
随机推荐
Fully Automated Gross Tumor Volume Delineation From PET in Head and Neck Cancer Using Deep Learning
分布式和集群分不清,我们讲讲两个厨子炒菜的故事
FreeRTOS(九)——队列
Data governance: data standard management (Part III)
Leetcode skimming -- teponacci sequence
監控數據源連接池使用情况
Introduction to Chang'an chain data storage and construction of MySQL storage environment
linux环境下安装配置redis,并设置开机自启动
Monitoring data source connection pool usage
Implementation of multi key state machine based on STM32 standard library
IDEA自动补全
Hystrix熔断器:服务熔断与服务降级
内网穿透工具frp使用入门
GCC and makefile
FreeRTOS (VIII) - time management
RecyclerView刷新闪烁与删除Item时崩溃问题
数据源连接池未关闭的问题 Could not open JDBC Connection for transaction
JS obtain mobile phone model and system version
请用已学过的知识编写程序,找出小甲鱼藏在下边这个长字符串中的密码,密码的埋藏点符合以下规律:
JVM之方法的绑定机制