博主聲明:
轉(zhuǎn)載請在開頭附加本文鏈接及作者信息糯而,并標記為轉(zhuǎn)載。本文由博主 威威喵 原創(chuàng),請多支持與指教。
本文首發(fā)于此 博主:威威喵 | 博客主頁:https://blog.csdn.net/smile_running
Android 鎖屏功能是我們最常用的延刘、最經(jīng)常接觸的一個軟件之一了吧,因為我個人也是使用的 Android 手機六敬,雖然手機不怎么好碘赖,但是也有鎖屏這個功能。雖然現(xiàn)在的手機都是指紋解鎖外构,但是我的手機解鎖功能普泡,它被我設(shè)置了一種模式,就是當我離開手機超過一定時間审编,或者說手機很長一段時間處于沒有使用狀態(tài)時劫哼,它就會自動給你鎖上,而且這個鎖只能靠屏幕設(shè)定的九宮格鎖來解除這個模式割笙,不能通過指紋來解鎖。
說了這么多眯亦,當然并不是做這個功能伤溉,我個人認為它的實現(xiàn)肯定是利用了手機的傳感器來判斷你的手機是否處在被你使用的狀態(tài),至于怎么實現(xiàn)的妻率,還需進一步考慮乱顾。不過呢,我們今天要實現(xiàn)的肯定也和上面我提到的東西有關(guān)宫静,那就是 Android 的九宮格鎖屏功能走净,不僅是 Android 的手機,蘋果的也都有這樣的功能孤里。
首先伏伯,這個功能的實用程度不用我多說了吧,雖然現(xiàn)在都是指紋識別的捌袜,或者更高級的人臉識別说搅,但是九宮格畢竟是經(jīng)典中的經(jīng)典,雖然之前還有數(shù)字按鈕的解鎖功能虏等,但是九宮格看起來檔次就高了一些弄唧。
好吧,我們直接來看看如何實現(xiàn)下面這樣的一個九宮格解鎖效果霍衫。
我們首要做的第一步就是把這九個點給繪制出來候引,這個應(yīng)該很簡單吧,一個循環(huán)計算一下每排點的坐標敦跌,然后繪制三排即可澄干。然后還需要繪制一個文字的提示文本,關(guān)鍵代碼如下:
/**
* 添加 9 個點
*/
private void addNinePoints() {
mPoints.clear();
for (int i = 1; i <= 3; i++) {
// 第一排 第 2 個點
float x1 = mCircleX;
float y = mCircleY / 2 * i;// 每一排 y 坐標都是一個樣的
mPoint = new PointF(x1, y);
mPoints.add(mPoint);
// 第一排 第 1 個點
float x2 = mCircleX / 3;
mPoint = new PointF(x2, y);
mPoints.add(mPoint);
// 第一排 第 3 個點
float x3 = mWidth - mCircleX / 3;
mPoint = new PointF(x3, y);
mPoints.add(mPoint);
}
}
private void drawCircles(Canvas canvas) {
canvas.drawText("請輸入解鎖圖案", mWidth / 2 - mPaint.getTextSize() * 3.5f, mHeight / 9, mPaint);
for (PointF point : mPoints) {
canvas.drawCircle(point.x, point.y, mCircleRadius, mPaint);
}
}
你會看到如下的效果
那么到了這一步,就算是成功了一小步了傻寂。接著我們應(yīng)該去添加手勢識別的事件息尺,可以根據(jù)我們的手指滑動去繪制一條解鎖的路徑。這一步比較關(guān)鍵疾掰,首先我們得判斷手指是否真的碰到了小圓搂誉,這里就要做一下碰撞檢測的邏輯,沒觸碰到静檬,當然就不會畫路徑了炭懊。
那么怎么才算觸碰到圓呢,這個還是比較簡單的拂檩。因為圓一周的距離都是一樣的侮腹,我們?nèi)ヅ袛嘤|摸點有沒有在圓內(nèi)部,就是判斷觸摸點與圓心的距離是否小于半徑了稻励,如下如:
代碼就是計算兩個點的距離
private boolean isInsideCircle(float downX, float downY, float circleX, float circleY) {
return mCircleRadius > Math.sqrt(Math.pow(downX - circleX, 2) + Math.pow(downY - circleY, 2));
}
好了父阻,現(xiàn)在我們有了可以判斷觸摸點可以在圓內(nèi)的邏輯,可以開始寫手勢識別的事件了望抽。首先加矛,我們要搞清楚兩個狀態(tài),一個是正常狀態(tài)的圓煤篙,全部畫粉紅色斟览,另一個是被按下的圓,我們畫其他顏色辑奈。
第二個關(guān)鍵思路苛茂,我們在手指按下且繼續(xù)移動的時候,不同顏色的點必定也會隨之增加鸠窗,我這里采取了遍歷的做法妓羊,因為我們每一個圓的圓心坐標都是唯一的,這里采用不能有重復(fù)的 Set 集合來保存被按下的點稍计。我們在 MOVE 事件里面侍瑟,通過遍歷固定圓點和手指觸摸點進行判斷是否包含在圓內(nèi),是就添加到 Set 集合丙猬,然后在 onDraw 里面把被按下的集合點全部繪制出來涨颜。那么,代碼如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
float downX = event.getX();
float downY = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
for (PointF point : mPoints) {
// 取出每一個圓的圓心坐標
mStatus = MOVE;
float circleX = point.x;
float circleY = point.y;
if (isInsideCircle(downX, downY, circleX, circleY)) {
mSelectedPoints.add(point);
}
}
break;
case MotionEvent.ACTION_UP:
mStatus = NORMAL;
mSelectedPoints.clear();
break;
}
invalidate();
return true;
}
這里的 DOWN 事件其實可以不用處理茧球,與 MOVE 事件做一樣的操作就行庭瑰。然后是繪制代碼
@Override
protected void onDraw(Canvas canvas) {
switch (mStatus) {
case NORMAL:
drawCircles(canvas);
break;
case MOVE:
drawCircles(canvas);
if (mSelectedPoints.size() > 0) {
for (PointF point : mSelectedPoints)
canvas.drawCircle(point.x, point.y, mCircleRadius, mSelectedPaint);
}
break;
}
}
當手指 Up 的時候,我們狀態(tài)就變?yōu)檎G缆瘢L制的就是正常的圓了弹灭。好了督暂,一起看一下效果吧
還行吧,至少效果達到了穷吮,樣子雖然難看了一點點逻翁,不過我們后面可以用圖片來替換掉。現(xiàn)在我們來繪制路徑的效果捡鱼,繪制路徑這里非嘲嘶兀坑啊,我用循環(huán)遍歷來繪制 Path 的時候驾诈,會出現(xiàn)線亂連的效果缠诅,就是這個樣子的
起初還想著為什么會這樣呢,可能是我的 for 循環(huán)把 path.lineTo 搞亂了嗎乍迄,不應(yīng)該啊管引。后來寫我們的序號的時候,也就是按下的圓點的號碼闯两,它也是亂的褥伴,我立馬反應(yīng)過來,原來是我傻了漾狼,用了一個 ArraySet 去保存噩翠,難怪會亂序,我靠邦投,搞了我好久,這里一定要記住這個坑啊擅笔,不細心的話志衣,還真不知道哪里寫錯了,后來我用了 LinkedHashSet 就正常了猛们。代碼如下:
if (mHasPath) {
mPath.reset();
moveFirst = 0;
for (NumberPoint point : mPasswordSet) {
if ((++moveFirst) == 1)
mPath.moveTo(point.x, point.y);
else
mPath.lineTo(point.x, point.y);
}
//如果需要繪制路徑
canvas.drawPath(mPath, mPathPaint);
}
代碼還是比較簡單的念脯,如果是第一個點的話,要 moveTo 一下即可弯淘,看效果吧
好了绿店,這樣就可以了。我們剛剛改了一下代碼庐橙,為每一個點標上了數(shù)字假勿,順序是 123... ,這個大家都懂的态鳖。這里做標記是為了能夠提供九宮格鎖的驗證效果转培,我們把點連起來就是一串數(shù)字,然后我們?nèi)テヅ湟呀?jīng)設(shè)置的密碼就好了浆竭。
我這里設(shè)置默認密碼為 2 4 8 6浸须,這四個數(shù)字相連惨寿。我們來試試效果吧
最后,本案例的完整代碼如下:
package nd.no.xww.qqmessagedragview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* @author xww
* @desciption :
* @date 2019/8/4
* @time 14:28
* 博主:威威喵
* 博客:https://blog.csdn.net/smile_Running
*/
@RequiresApi(api = Build.VERSION_CODES.M)
public class UnlockView extends View {
// 默認九宮格圖像解鎖密碼
private final String DEF_PASSWORD = "2486";
private boolean unlock = false;
private Paint mNormalPaint;
private Paint mSelectedPaint;
private Paint mPathPaint;
private float mWidth;
private float mHeight;
private float mCircleX;
private float mCircleY;
private float mCircleRadius;
// 儲存不被按下的圓
private List<NumberPoint> mNormalPoints;
//儲存在手指移動后被按下的圓
private Set<NumberPoint> mSelectedPoints;
private int mPreSize;
private Path mPath;
private int moveFirst;
//是否繪制路徑
private boolean mHasPath;
private final int NORMAL = 1;
private final int MOVE = 3;
private int mStatus = NORMAL;
private int mDwonStartX;
private int mDwonStartY;
private Set<NumberPoint> mPasswordSet;
private void init() {
mNormalPaint = getPaint(Color.parseColor("#f81B60"));
mSelectedPaint = getPaint(Color.parseColor("#aaaaff"));
mPathPaint = getPaint(Color.parseColor("#aaaaff"));
mPathPaint.setStyle(Paint.Style.STROKE);
mPathPaint.setStrokeWidth(15f);
mNormalPoints = new ArrayList<>();
mSelectedPoints = new ArraySet<>();
mPasswordSet = new LinkedHashSet<>();
mWidth = getResources().getDisplayMetrics().widthPixels;
mHeight = getResources().getDisplayMetrics().heightPixels;
mCircleX = mWidth / 2;
mCircleY = mHeight / 2;
mCircleRadius = mCircleX / 5;
mPath = new Path();
mPath.setFillType(Path.FillType.EVEN_ODD);
addNinePoints();// 初始化 9 個點
setDrawPath(true);
}
private Paint getPaint(int color) {
Paint paint = new Paint();
paint.setDither(true);
paint.setAntiAlias(true);
paint.setTextSize(70f);
paint.setColor(color);
return paint;
}
public UnlockView(Context context) {
this(context, null);
}
public UnlockView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public UnlockView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/**
* 添加 9 個點
*/
private void addNinePoints() {
mNormalPoints.clear();
NumberPoint mPoint = null;
for (int i = 1; i <= 3; i++) {
// 第一排 第 1 個點
float x2 = mCircleX / 3;
float y = mCircleY / 2 * i;// 每一排 y 坐標都是一個樣的
mPoint = new NumberPoint(1 + (i - 1) * 3);
mPoint.set(x2, y);
mNormalPoints.add(mPoint);
// 第一排 第 2 個點
float x1 = mCircleX;
mPoint = new NumberPoint(2 + (i - 1) * 3);
mPoint.set(x1, y);
mNormalPoints.add(mPoint);
// 第一排 第 3 個點
float x3 = mWidth - mCircleX / 3;
mPoint = new NumberPoint(3 + (i - 1) * 3);
mPoint.set(x3, y);
mNormalPoints.add(mPoint);
}
}
public void setDrawPath(boolean hasPath) {
this.mHasPath = hasPath;
}
@Override
protected void onDraw(Canvas canvas) {
switch (mStatus) {
case NORMAL:
drawCircles(canvas);
break;
case MOVE:
drawCircles(canvas);
if (mSelectedPoints.size() > 0) {
for (NumberPoint point : mSelectedPoints) {
canvas.drawCircle(point.x, point.y, mCircleRadius, mSelectedPaint);
mPasswordSet.add(point);
}
if (mHasPath) {
mPath.reset();
moveFirst = 0;
for (NumberPoint point : mPasswordSet) {
if ((++moveFirst) == 1)
mPath.moveTo(point.x, point.y);
else
mPath.lineTo(point.x, point.y);
}
//如果需要繪制路徑
canvas.drawPath(mPath, mPathPaint);
}
}
break;
}
}
private void drawCircles(Canvas canvas) {
if (unlock) {
mNormalPaint.setColor(Color.parseColor("#00ff00"));
canvas.drawText("解鎖成功", mWidth / 2 - mNormalPaint.getTextSize() * 2f, mHeight / 9, mNormalPaint);
} else {
mNormalPaint.setColor(Color.parseColor("#ff0000"));
canvas.drawText("請輸入解鎖圖案", mWidth / 2 - mNormalPaint.getTextSize() * 3.5f, mHeight / 9, mNormalPaint);
}
for (NumberPoint point : mNormalPoints) {
canvas.drawCircle(point.x, point.y, mCircleRadius, mNormalPaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float downX = event.getX();
float downY = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
for (NumberPoint point : mNormalPoints) {
// 取出每一個圓的圓心坐標
unlock = false;
mStatus = MOVE;
float circleX = point.x;
float circleY = point.y;
if (isInsideCircle(downX, downY, circleX, circleY)) {
mSelectedPoints.add(point);
}
}
break;
case MotionEvent.ACTION_UP:
mStatus = NORMAL;
StringBuffer password = new StringBuffer();
for (NumberPoint point : mPasswordSet) {
password.append(point.getNumber());
}
Log.i("========", "password: " + password);
if (DEF_PASSWORD.equals(password.toString())) {
unlock = true;
}
mPasswordSet.clear();
mSelectedPoints.clear();
mPath.reset();
moveFirst = 0;
break;
}
invalidate();
return true;
}
private boolean isInsideCircle(float downX, float downY, float circleX, float circleY) {
return mCircleRadius > Math.sqrt(Math.pow(downX - circleX, 2) + Math.pow(downY - circleY, 2));
}
public class NumberPoint extends PointF {
private int number;
public NumberPoint(int number) {
this.number = number;
}
public int getNumber() {
return number;
}
}
}
好吧删窒,趕緊試試運行效果吧裂垦。不過我這個案例還有一個問題沒有解決,就比如我們從 4 開始然后上面或下面一點繞過 5 直接連 6也可以肌索,我通過對比了一下我的真實手機上的九宮格鎖屏蕉拢,它的做法是當我們想要從 4 繞過 5 去連 6 時,會直接把 5 給一起連上驶社,我這個案例里面沒有去實現(xiàn)它企量,有需要的話自己去搜索看看,如何寫代碼亡电。因為我案例中用到了 Path 路徑的方式届巩,我后來想一想,還是用 Line 更直接份乒,用 drawLine 可以很好的解決這個問題恕汇。