最近朋友被要求寫個(gè)九宮格手勢(shì)解鎖,于是,想著沒事也試了下,直接開始干吧。
本文參考了原博客做練手小項(xiàng)目玩,用于練習(xí)和總結(jié).
參照博客地址:
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
效果圖:

整個(gè)流程實(shí)現(xiàn)思路:
1.基于手機(jī)橫豎屏來展示,九宮格view需要顯示在屏幕居中的位置,確定好位置并初始化九個(gè)圓點(diǎn)。
2.判斷手機(jī)觸摸點(diǎn)是否作用在視圖的九個(gè)點(diǎn)上,如果在,則更改點(diǎn)的狀態(tài)
3.手指在九宮格視圖的作用域滑動(dòng),繪制兩點(diǎn)的連線,繼續(xù)滑動(dòng),則繼續(xù)繪制連線,被繪制了的點(diǎn)不允許再繪制,最后直至手指抬起。
4.手指抬起,校驗(yàn)路徑密碼,如果錯(cuò)誤,則更改點(diǎn)、線狀態(tài),并做提示,反之,則繪制成功,交由開發(fā)者自己實(shí)現(xiàn)。
5.優(yōu)化,擴(kuò)展
具體實(shí)現(xiàn):
一、確定視圖view的整體位置,確定每個(gè)點(diǎn)的位置并初始化

每個(gè)圓點(diǎn)對(duì)象的屬性有:X/Y坐標(biāo),點(diǎn)擊狀態(tài),代表的密碼值,半徑。代碼如下:
* Created by lzr on 2021/3/23.
* Describe:繪制一個(gè)圓點(diǎn),攜帶x坐標(biāo),Y坐標(biāo),R半徑,狀態(tài),代表的值
*/
public class Point {
// 正常狀態(tài)
public static final int STATE_NORMAL = 1;
// 按下狀態(tài)
public static final int STATE_PRESS = 2;
// 錯(cuò)誤狀態(tài)
public static final int STATE_ERROR = 3;
public float x;
public float y;
private int num;
public float mRadius;
private int state = STATE_NORMAL;
/**
* 計(jì)算特定點(diǎn)和當(dāng)前點(diǎn)的距離
*
* @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;
}
}
初始化點(diǎn)位,以下代碼在onDraw()方法中,因?yàn)閛nLayout()后才能拿到控件寬高度,這里規(guī)定每個(gè)圓點(diǎn)的半徑為小格子的1/3,橫豎線交叉點(diǎn)就是圓點(diǎn)圓心,狀態(tài)起初統(tǒng)一為正常狀態(tài):
if (points == null || points.length <= 0) {
//當(dāng)前視圖的大小
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;
//每個(gè)點(diǎn)所占方格的寬度
int pointItemWidth = 0;
//橫屏的時(shí)候
if (mWidth > mHeight) {
offsetX = offset;
offsetY = 0;
pointItemWidth = mHeight / (mCount + 1);
}
//豎屏的時(shí)候
if (mWidth <= mHeight) {
offsetY = offset;
offsetX = 0;
pointItemWidth = mWidth / (mCount + 1);
}
//初始化3*3個(gè)點(diǎn)
initNineCirclePoint(mCount, pointItemWidth, offsetX, offsetY);
}
/**
* 初始化3*3個(gè)圓點(diǎn)
*
* @param count 多少行
* @param pItemOffset 每個(gè)格子的偏移量
* @param offsetX view的x軸偏移量
* @param offsetY view的y軸的偏移量
* /**
* * 坐標(biāo)分布
* * 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)
* *
* * 找出規(guī)律為:(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];
//將格子的偏移量作為圓點(diǎn)的半徑
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 + "]坐標(biāo)信息是: " + " x=" + offsetX + pItemOffset * (i + 1) + " y=" + offsetY + pItemOffset * (j + 1) + " mRadius=" + mRadius + " \n記錄原始密碼=" + (i + 1 + j * count) + " 記錄十六進(jìn)制密碼 = " + points[i][j].getNum());
}
}
}
接下來處理手勢(shì)滑動(dòng),主要在onTouchEvent()方法中獲取當(dāng)前的觸摸點(diǎn),并判斷觸摸點(diǎn)是否在九個(gè)目標(biāo)點(diǎn)內(nèi),如果在,篩選出來,并更改為按壓狀態(tài),:
/**
* 獲取選擇點(diǎn)的位置
*/
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++) {
//判斷觸摸的點(diǎn)是否在所有目標(biāo)點(diǎn)的繪制范圍內(nèi)
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:
// 獲取觸摸點(diǎn)在哪個(gè)目標(biāo)點(diǎn)的繪制范圍內(nèi)
selectP = getSelectedPointPosition();
if (selectP != null) {
isDraw = true;
//被選擇的點(diǎn)存入集合
mSelectedPoints.add(selectP);
}
break;
case MotionEvent.ACTION_MOVE:
selectP = getSelectedPointPosition();
//已經(jīng)選了的點(diǎn),不再重新被選擇
if (selectP != null && !mSelectedPoints.contains(selectP)) {
mSelectedPoints.add(selectP);
}
break;
case MotionEvent.ACTION_UP:
isDraw = false;
//驗(yàn)證密碼路徑
//xxxx
break;
}
invalidate();
return true;
}
每次重繪時(shí),都會(huì)繪制圓點(diǎn)、連線。繪制連線首先應(yīng)該先繪制兩個(gè)點(diǎn)A,B的線,A為起點(diǎn),B為終點(diǎn),當(dāng)繪制完成,將B點(diǎn)作為起點(diǎn),C點(diǎn)又作為終點(diǎn),最后呈現(xiàn)的就是幾個(gè)點(diǎn)的連線。
這里需要注意一下,手指在九宮格View上進(jìn)行移動(dòng)時(shí),可能會(huì)觸摸在圓點(diǎn)之外(mX,mY)的作用域,但是仍然應(yīng)該有連線牽引,這個(gè)線牽引的終點(diǎn)就是new Point(mX,mY),所以加了個(gè)isDraw標(biāo)志位,表示當(dāng)前正在繪制,即手指未抬起。
以下代碼主要是繪制功能,在onDraw()方法中進(jìn)行:
//繪制提示語(yǔ)
drawTextTips(canvas);
//繪制圓點(diǎn)
drawPoints(canvas);
//繪制連線
drawLines(canvas);
/**
* 繪制提示語(yǔ)
*
* @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);
}
/**
* 根據(jù)各個(gè)點(diǎn)狀態(tài)進(jìn)行重繪
*
* @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];
//不同狀態(tài)的繪制點(diǎn)
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;
}
}
}
}
/**
* 繪制幾個(gè)點(diǎn)的連線
*
* @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);
//將這個(gè)終點(diǎn)最為下一個(gè)起點(diǎn)
startP = endP;
}
if (isDraw) {
drawLine(canvas, startP, new Point(mX, mY, mRadius));
}
}
}
/**
* 繪制兩點(diǎn)之間的連線
*
* @param canvas
* @param startP 起始點(diǎn)
* @param endP 終止點(diǎn)
*/
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;
}
}
最后一旦手指抬起,就需要驗(yàn)證密碼路徑是否正確,這里我是用數(shù)字(十進(jìn)制)代表圓點(diǎn)的值,并轉(zhuǎn)為十六進(jìn)制存儲(chǔ),然后從已被選擇的點(diǎn)mSelectedPoints依次取出,與設(shè)定的密碼對(duì)比,如果錯(cuò)誤,可輸入次數(shù)減1,文案提示語(yǔ)、點(diǎn)、線都需要更改狀態(tài),提示完成后,更改圓點(diǎn)狀態(tài),清除連線,回到最初樣子。
若連續(xù)輸錯(cuò),則鎖定,無(wú)法繪制,如果在規(guī)定次數(shù)內(nèi)解開鎖,則做Toast提示用戶,可繼續(xù)下一操作。
/**
* 校驗(yàn)繪制的密碼路徑
*/
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, "校驗(yàn)密碼路徑: " + "本次選擇的密碼路徑:" + newPwdStringBuffer.toString() + "pwdStr" + pwdStr);
if (newPwdStringBuffer.toString().equals(pwdStr)) {
textTips = "解鎖成功";
//做其他操作
if (listener != null) {
listener.doUnLock();
}
//這里就重置所有的點(diǎn)的狀態(tài):
resetPoints();
} else {
inputCount--;
if (inputCount > 0) {
showAnimation();
textTips = "密碼錯(cuò)誤,你還可以輸入" + inputCount + "次";
}
//重新更改點(diǎn)、線狀態(tài)
resetPointsWithState(Point.STATE_ERROR);
postDelayed(new Runnable() {
@Override
public void run() {
if (inputCount == 0) {
textTips = "已鎖定,無(wú)法解鎖";
} else {
textTips = "請(qǐng)輸入解鎖圖案";
}
resetPoints();
invalidate();
}
}, 1000);
}
}
}
校驗(yàn)方法也可采取其他,這里只是做個(gè)簡(jiǎn)單實(shí)例。
case MotionEvent.ACTION_UP:
isDraw = false;
//驗(yàn)證密碼路徑
verifyPwdPath();
break;
到這里,九宮格解鎖就完成了。
后面我加了些屬性,比如:可以繪制count*count的矩陣,可以設(shè)置文本提示顏色,三種狀態(tài)的顏色,最大鎖定次數(shù)。
<declare-styleable name="CustomUnLockView">
<!-- 一行點(diǎn)的個(gè)數(shù),共有count*count個(gè)圓點(diǎn)密碼-->
<attr name="count" format="integer" />
<!-- 文本提示的顏色-->
<attr name="color_text_tips" format="color" />
<!-- 正常密碼圓點(diǎn)及線的顏色-->
<attr name="color_normal" format="color" />
<!-- 按壓密碼圓點(diǎn)及線的顏色-->
<attr name="color_press" format="color" />
<!-- 密碼錯(cuò)誤圓點(diǎn)及線的顏色-->
<attr name="color_error" format="color" />
<!-- 最大鎖定次數(shù)-->
<attr name="max_lock_times" format="integer" />
</declare-styleable>
總之,這篇文章也是熟悉并總結(jié)下自定義view,屬于自己練練手,起初寫之前覺得還挺難,現(xiàn)在覺得挺簡(jiǎn)單的,所以,重在動(dòng)手
附上github地址:https://github.com/yifentudou/CustomUnLockView