最近朋友被要求写个九宫格手势解锁,于是,想着没事也试了下,直接开始干吧。
本文参考了原博客做练手小项目玩,用于练习和总结.
参照博客地址:
https://blog.csdn.net/smile_Running/article/details/87453435
https://blog.csdn.net/qq_34161388/article/details/74387263?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-5&spm=1001.2101.3001.4242
效果图:
整个流程实现思路:
1.基于手机横竖屏来展示,九宫格view需要显示在屏幕居中的位置,确定好位置并初始化九个圆点。
2.判断手机触摸点是否作用在视图的九个点上,如果在,则更改点的状态
3.手指在九宫格视图的作用域滑动,绘制两点的连线,继续滑动,则继续绘制连线,被绘制了的点不允许再绘制,最后直至手指抬起。
4.手指抬起,校验路径密码,如果错误,则更改点、线状态,并做提示,反之,则绘制成功,交由开发者自己实现。
5.优化,扩展
具体实现:
一、确定视图view的整体位置,确定每个点的位置并初始化
每个圆点对象的属性有:X/Y坐标,点击状态,代表的密码值,半径。代码如下:
* Created by lzr on 2021/3/23.
* Describe:绘制一个圆点,携带x坐标,Y坐标,R半径,状态,代表的值
*/
public class Point {
// 正常状态
public static final int STATE_NORMAL = 1;
// 按下状态
public static final int STATE_PRESS = 2;
// 错误状态
public static final int STATE_ERROR = 3;
public float x;
public float y;
private int num;
public float mRadius;
private int state = STATE_NORMAL;
/**
* 计算特定点和当前点的距离
*
* @param point
* @return
*/
public float getInstanceWithPoint(Point point) {
return (float) Math.sqrt(Math.pow(point.x - x, 2) + Math.pow(point.y - y, 2));
}
public String getNum() {
return Integer.toHexString(num);
}
public void setNum(int num) {
this.num = num;
}
public Point(float x, float y, float mRadius) {
this.x = x;
this.y = y;
this.mRadius = mRadius;
}
public float getmRadius() {
return mRadius;
}
public void setmRadius(float mRadius) {
this.mRadius = mRadius;
}
public int getState() {
return state;
}
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
public void setState(int state) {
this.state = state;
}
}
初始化点位,以下代码在onDraw()方法中,因为onLayout()后才能拿到控件宽高度,这里规定每个圆点的半径为小格子的1/3,横竖线交叉点就是圆点圆心,状态起初统一为正常状态:
if (points == null || points.length <= 0) {
//当前视图的大小
mWidth = getWidth() - getPaddingLeft() - getPaddingRight();
mHeight = getHeight() - getPaddingTop() - getPaddingBottom();
Log.i(TAG, "init: mWidth =" + mWidth + " mHeight" + mHeight);
//九宫格需要居中显示,偏移量
offset = Math.abs(mWidth - mHeight) / 2;
//x/y轴上的偏移量
int offsetX = 0;
offsetY = 0;
//每个点所占方格的宽度
int pointItemWidth = 0;
//横屏的时候
if (mWidth > mHeight) {
offsetX = offset;
offsetY = 0;
pointItemWidth = mHeight / (mCount + 1);
}
//竖屏的时候
if (mWidth <= mHeight) {
offsetY = offset;
offsetX = 0;
pointItemWidth = mWidth / (mCount + 1);
}
//初始化3*3个点
initNineCirclePoint(mCount, pointItemWidth, offsetX, offsetY);
}
/**
* 初始化3*3个圆点
*
* @param count 多少行
* @param pItemOffset 每个格子的偏移量
* @param offsetX view的x轴偏移量
* @param offsetY view的y轴的偏移量
* /**
* * 坐标分布
* * 1(0,0) 2(1,0) 3(2,0)
* *
* * 4(0,1) 5(1,1) 6(2,1)
* *
* * 7(0,2) 8(1,2) 9(2,2)
* *
* * 找出规律为:(point.x +1) + point.y* 3
*/
private void initNineCirclePoint(int count, int pItemOffset, int offsetX, int offsetY) {
Log.i(TAG, "count=" + count + " pItemOffset=" + pItemOffset + " offsetX=" + offsetX + " offsetY=" + offsetY);
points = new Point[count][count];
//将格子的偏移量作为圆点的半径
mRadius = pItemOffset / 3;
//行
for (int i = 0; i < count; i++) {
//列
for (int j = 0; j < count; j++) {
points[i][j] = new Point(offsetX + pItemOffset * (i + 1), offsetY + pItemOffset * (j + 1), mRadius);
points[i][j].setNum(i + 1 + j * count);
Log.i(TAG, "points[" + i + "][" + j + "]坐标信息是: " + " x=" + offsetX + pItemOffset * (i + 1) + " y=" + offsetY + pItemOffset * (j + 1) + " mRadius=" + mRadius + " \n记录原始密码=" + (i + 1 + j * count) + " 记录十六进制密码 = " + points[i][j].getNum());
}
}
}
接下来处理手势滑动,主要在onTouchEvent()方法中获取当前的触摸点,并判断触摸点是否在九个目标点内,如果在,筛选出来,并更改为按压状态,:
/**
* 获取选择点的位置
*/
private Point getSelectedPointPosition() {
Point point = new Point(mX, mY, mRadius);
for (int i = 0; i < points.length; i++) {
for (int j = 0; j < points[i].length; j++) {
//判断触摸的点是否在所有目标点的绘制范围内
if (points[i][j].getInstanceWithPoint(point) < mRadius) {
points[i][j].setState(Point.STATE_PRESS);
return points[i][j];
}
}
}
return null;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mX = event.getX();
mY = event.getY();
if (inputCount == 0) {
return true;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 获取触摸点在哪个目标点的绘制范围内
selectP = getSelectedPointPosition();
if (selectP != null) {
isDraw = true;
//被选择的点存入集合
mSelectedPoints.add(selectP);
}
break;
case MotionEvent.ACTION_MOVE:
selectP = getSelectedPointPosition();
//已经选了的点,不再重新被选择
if (selectP != null && !mSelectedPoints.contains(selectP)) {
mSelectedPoints.add(selectP);
}
break;
case MotionEvent.ACTION_UP:
isDraw = false;
//验证密码路径
//xxxx
break;
}
invalidate();
return true;
}
每次重绘时,都会绘制圆点、连线。绘制连线首先应该先绘制两个点A,B的线,A为起点,B为终点,当绘制完成,将B点作为起点,C点又作为终点,最后呈现的就是几个点的连线。
这里需要注意一下,手指在九宫格View上进行移动时,可能会触摸在圆点之外(mX,mY)的作用域,但是仍然应该有连线牵引,这个线牵引的终点就是new Point(mX,mY),所以加了个isDraw标志位,表示当前正在绘制,即手指未抬起。
以下代码主要是绘制功能,在onDraw()方法中进行:
//绘制提示语
drawTextTips(canvas);
//绘制圆点
drawPoints(canvas);
//绘制连线
drawLines(canvas);
/**
* 绘制提示语
*
* @param canvas
*/
private void drawTextTips(Canvas canvas) {
mTextTipsPaint.setTextAlign(Paint.Align.CENTER);
//字体比例按屏幕比例来,满屏为20dp,越小字越小
mTextTipsPaint.setTextSize(dp2px((float) mWidth / Math.min(phoneWidth, phoneHeight) * defalut_text_tips_size));
canvas.drawText(textTips, (float) mWidth / 2, offsetY, mTextTipsPaint);
}
/**
* 根据各个点状态进行重绘
*
* @param canvas
*/
private void drawPoints(Canvas canvas) {
for (int i = 0; i < points.length; i++) {
for (int j = 0; j < points[i].length; j++) {
Point point = points[i][j];
//不同状态的绘制点
switch (point.getState()) {
case Point.STATE_ERROR:
canvas.drawCircle(point.x, point.y, point.mRadius, mErrorPaint);
break;
case Point.STATE_NORMAL:
canvas.drawCircle(point.x, point.y, point.mRadius, mNormalPaint);
break;
case Point.STATE_PRESS:
canvas.drawCircle(point.x, point.y, point.mRadius, mPressPaint);
break;
}
}
}
}
/**
* 绘制几个点的连线
*
* @param canvas
*/
private void drawLines(Canvas canvas) {
if (mSelectedPoints.size() > 0) {
Point startP = mSelectedPoints.get(0);
Point endP;
for (int i = 1; i < mSelectedPoints.size(); i++) {
endP = mSelectedPoints.get(i);
drawLine(canvas, startP, endP);
//将这个终点最为下一个起点
startP = endP;
}
if (isDraw) {
drawLine(canvas, startP, new Point(mX, mY, mRadius));
}
}
}
/**
* 绘制两点之间的连线
*
* @param canvas
* @param startP 起始点
* @param endP 终止点
*/
private void drawLine(Canvas canvas, Point startP, Point endP) {
switch (startP.getState()) {
case Point.STATE_PRESS:
canvas.drawLine(startP.x, startP.y, endP.x, endP.y, mPressPaint);
break;
case Point.STATE_ERROR:
canvas.drawLine(startP.x, startP.y, endP.x, endP.y, mErrorPaint);
break;
}
}
最后一旦手指抬起,就需要验证密码路径是否正确,这里我是用数字(十进制)代表圆点的值,并转为十六进制存储,然后从已被选择的点mSelectedPoints依次取出,与设定的密码对比,如果错误,可输入次数减1,文案提示语、点、线都需要更改状态,提示完成后,更改圆点状态,清除连线,回到最初样子。
若连续输错,则锁定,无法绘制,如果在规定次数内解开锁,则做Toast提示用户,可继续下一操作。
/**
* 校验绘制的密码路径
*/
private void verifyPwdPath() {
newPwdStringBuffer.setLength(0);
if (mSelectedPoints != null) {
for (int i = 0; i < mSelectedPoints.size(); i++) {
Point point = mSelectedPoints.get(i);
newPwdStringBuffer.append(point.getNum());
Log.i(TAG, "已选择的密码路径有序取出为: " + point.getNum());
}
}
if (newPwdStringBuffer != null) {
Log.i(TAG, "校验密码路径: " + "本次选择的密码路径:" + newPwdStringBuffer.toString() + "pwdStr" + pwdStr);
if (newPwdStringBuffer.toString().equals(pwdStr)) {
textTips = "解锁成功";
//做其他操作
if (listener != null) {
listener.doUnLock();
}
//这里就重置所有的点的状态:
resetPoints();
} else {
inputCount--;
if (inputCount > 0) {
showAnimation();
textTips = "密码错误,你还可以输入" + inputCount + "次";
}
//重新更改点、线状态
resetPointsWithState(Point.STATE_ERROR);
postDelayed(new Runnable() {
@Override
public void run() {
if (inputCount == 0) {
textTips = "已锁定,无法解锁";
} else {
textTips = "请输入解锁图案";
}
resetPoints();
invalidate();
}
}, 1000);
}
}
}
校验方法也可采取其他,这里只是做个简单实例。
case MotionEvent.ACTION_UP:
isDraw = false;
//验证密码路径
verifyPwdPath();
break;
到这里,九宫格解锁就完成了。
后面我加了些属性,比如:可以绘制count*count的矩阵,可以设置文本提示颜色,三种状态的颜色,最大锁定次数。
总之,这篇文章也是熟悉并总结下自定义view,属于自己练练手,起初写之前觉得还挺难,现在觉得挺简单的,所以,重在动手
附上github地址:https://github.com/yifentudou/CustomUnLockView