自定義 View 之實現(xiàn)九宮格鎖屏效果

博主聲明:

轉(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)下面這樣的一個九宮格解鎖效果霍衫。

image

我們首要做的第一步就是把這九個點給繪制出來候引,這個應(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);
        }
    }

你會看到如下的效果

image

那么到了這一步,就算是成功了一小步了傻寂。接著我們應(yīng)該去添加手勢識別的事件息尺,可以根據(jù)我們的手指滑動去繪制一條解鎖的路徑。這一步比較關(guān)鍵疾掰,首先我們得判斷手指是否真的碰到了小圓搂誉,這里就要做一下碰撞檢測的邏輯,沒觸碰到静檬,當然就不會畫路徑了炭懊。

那么怎么才算觸碰到圓呢,這個還是比較簡單的拂檩。因為圓一周的距離都是一樣的侮腹,我們?nèi)ヅ袛嘤|摸點有沒有在圓內(nèi)部,就是判斷觸摸點與圓心的距離是否小于半徑了稻励,如下如:

image

代碼就是計算兩個點的距離

    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制的就是正常的圓了弹灭。好了督暂,一起看一下效果吧

image

還行吧,至少效果達到了穷吮,樣子雖然難看了一點點逻翁,不過我們后面可以用圖片來替換掉。現(xiàn)在我們來繪制路徑的效果捡鱼,繪制路徑這里非嘲嘶兀坑啊,我用循環(huán)遍歷來繪制 Path 的時候驾诈,會出現(xiàn)線亂連的效果缠诅,就是這個樣子的

image

起初還想著為什么會這樣呢,可能是我的 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 一下即可弯淘,看效果吧

image

好了绿店,這樣就可以了。我們剛剛改了一下代碼庐橙,為每一個點標上了數(shù)字假勿,順序是 123... ,這個大家都懂的态鳖。這里做標記是為了能夠提供九宮格鎖的驗證效果转培,我們把點連起來就是一串數(shù)字,然后我們?nèi)テヅ湟呀?jīng)設(shè)置的密碼就好了浆竭。

我這里設(shè)置默認密碼為 2 4 8 6浸须,這四個數(shù)字相連惨寿。我們來試試效果吧

image

最后,本案例的完整代碼如下:

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 可以很好的解決這個問題恕汇。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市或辖,隨后出現(xiàn)的幾起案子瘾英,更是在濱河造成了極大的恐慌,老刑警劉巖颂暇,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缺谴,死亡現(xiàn)場離奇詭異,居然都是意外死亡耳鸯,警方通過查閱死者的電腦和手機湿蛔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來县爬,“玉大人阳啥,你說我怎么就攤上這事〔圃” “怎么了察迟?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長耳高。 經(jīng)常有香客問我扎瓶,道長,這世上最難降的妖魔是什么泌枪? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任栗弟,我火速辦了婚禮,結(jié)果婚禮上工闺,老公的妹妹穿的比我還像新娘乍赫。我一直安慰自己瓣蛀,他們只是感情好,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布雷厂。 她就那樣靜靜地躺著惋增,像睡著了一般。 火紅的嫁衣襯著肌膚如雪改鲫。 梳的紋絲不亂的頭發(fā)上诈皿,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音像棘,去河邊找鬼稽亏。 笑死,一個胖子當著我的面吹牛缕题,可吹牛的內(nèi)容都是我干的截歉。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼烟零,長吁一口氣:“原來是場噩夢啊……” “哼瘪松!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起锨阿,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤宵睦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后墅诡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體壳嚎,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年末早,在試婚紗的時候發(fā)現(xiàn)自己被綠了烟馅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡荐吉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出口渔,到底是詐尸還是另有隱情样屠,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布缺脉,位于F島的核電站痪欲,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏攻礼。R本人自食惡果不足惜业踢,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望礁扮。 院中可真熱鬧知举,春花似錦瞬沦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽击罪。三九已至马绝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間氛堕,已是汗流浹背立肘。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工边坤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谅年。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓茧痒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親踢故。 傳聞我的和親對象是個殘疾皇子文黎,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容