自定义 View 之实现九宫格锁屏效果
博主聲明:
轉載請在開頭附加本文鏈接及作者信息,并標記為轉載。本文由博主?威威喵?原創,請多支持與指教。
本文首發于此? ?博主:威威喵??|??博客主頁:https://blog.csdn.net/smile_running
? ? Android 鎖屏功能是我們最常用的、最經常接觸的一個軟件之一了吧,因為我個人也是使用的 Android 手機,雖然手機不怎么好,但是也有鎖屏這個功能。雖然現在的手機都是指紋解鎖,但是我的手機解鎖功能,它被我設置了一種模式,就是當我離開手機超過一定時間,或者說手機很長一段時間處于沒有使用狀態時,它就會自動給你鎖上,而且這個鎖只能靠屏幕設定的九宮格鎖來解除這個模式,不能通過指紋來解鎖。
? ? 說了這么多,當然并不是做這個功能,我個人認為它的實現肯定是利用了手機的傳感器來判斷你的手機是否處在被你使用的狀態,至于怎么實現的,還需進一步考慮。不過呢,我們今天要實現的肯定也和上面我提到的東西有關,那就是 Android 的九宮格鎖屏功能,不僅是 Android 的手機,蘋果的也都有這樣的功能。
? ? 首先,這個功能的實用程度不用我多說了吧,雖然現在都是指紋識別的,或者更高級的人臉識別,但是九宮格畢竟是經典中的經典,雖然之前還有數字按鈕的解鎖功能,但是九宮格看起來檔次就高了一些。
? ? 好吧,我們直接來看看如何實現下面這樣的一個九宮格解鎖效果。
? ? 我們首要做的第一步就是把這九個點給繪制出來,這個應該很簡單吧,一個循環計算一下每排點的坐標,然后繪制三排即可。然后還需要繪制一個文字的提示文本,關鍵代碼如下:
/*** 添加 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);}}? ? 你會看到如下的效果
? ?那么到了這一步,就算是成功了一小步了。接著我們應該去添加手勢識別的事件,可以根據我們的手指滑動去繪制一條解鎖的路徑。這一步比較關鍵,首先我們得判斷手指是否真的碰到了小圓,這里就要做一下碰撞檢測的邏輯,沒觸碰到,當然就不會畫路徑了。
? ? 那么怎么才算觸碰到圓呢,這個還是比較簡單的。因為圓一周的距離都是一樣的,我們去判斷觸摸點有沒有在圓內部,就是判斷觸摸點與圓心的距離是否小于半徑了,如下如:
?代碼就是計算兩個點的距離
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));}? ? 好了,現在我們有了可以判斷觸摸點可以在圓內的邏輯,可以開始寫手勢識別的事件了。首先,我們要搞清楚兩個狀態,一個是正常狀態的圓,全部畫粉紅色,另一個是被按下的圓,我們畫其他顏色。
? ? 第二個關鍵思路,我們在手指按下且繼續移動的時候,不同顏色的點必定也會隨之增加,我這里采取了遍歷的做法,因為我們每一個圓的圓心坐標都是唯一的,這里采用不能有重復的 Set 集合來保存被按下的點。我們在 MOVE 事件里面,通過遍歷固定圓點和手指觸摸點進行判斷是否包含在圓內,是就添加到 Set 集合,然后在 onDraw 里面把被按下的集合點全部繪制出來。那么,代碼如下:
@Overridepublic 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 事件做一樣的操作就行。然后是繪制代碼
@Overrideprotected 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 的時候,我們狀態就變為正常,繪制的就是正常的圓了。好了,一起看一下效果吧
? ? 還行吧,至少效果達到了,樣子雖然難看了一點點,不過我們后面可以用圖片來替換掉?,F在我們來繪制路徑的效果,繪制路徑這里非常坑啊,我用循環遍歷來繪制 Path 的時候,會出現線亂連的效果,就是這個樣子的
? ? 起初還想著為什么會這樣呢,可能是我的 for 循環把 path.lineTo 搞亂了嗎,不應該啊。后來寫我們的序號的時候,也就是按下的圓點的號碼,它也是亂的,我立馬反應過來,原來是我傻了,用了一個 ArraySet 去保存,難怪會亂序,我靠,搞了我好久,這里一定要記住這個坑啊,不細心的話,還真不知道哪里寫錯了,后來我用了 LinkedHashSet 就正常了。代碼如下:
if (mHasPath) {mPath.reset();moveFirst = 0;for (NumberPoint point : mPasswordSet) {if ((++moveFirst) == 1)mPath.moveTo(point.x, point.y);elsemPath.lineTo(point.x, point.y);}//如果需要繪制路徑canvas.drawPath(mPath, mPathPaint);}代碼還是比較簡單的,如果是第一個點的話,要 moveTo 一下即可,看效果吧
? ? 好了,這樣就可以了。我們剛剛改了一下代碼,為每一個點標上了數字,順序是 123... ,這個大家都懂的。這里做標記是為了能夠提供九宮格鎖的驗證效果,我們把點連起來就是一串數字,然后我們去匹配已經設置的密碼就好了。
? ? 我這里設置默認密碼為 2 4 8 6,這四個數字相連。我們來試試效果吧
?最后,本案例的完整代碼如下:
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;}@Overrideprotected 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);elsemPath.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);}}@Overridepublic 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 給一起連上,我這個案例里面沒有去實現它,有需要的話自己去搜索看看,如何寫代碼。因為我案例中用到了 Path 路徑的方式,我后來想一想,還是用 Line 更直接,用 drawLine 可以很好的解決這個問題。
總結
以上是生活随笔為你收集整理的自定义 View 之实现九宫格锁屏效果的全部內容,希望文章能夠幫你解決所遇到的問題。