天天看點

自定義控件九宮格滑動解鎖

1. 前言

  • 最近想給自己做的的app添加一個滑動解鎖的功能,用的是樂視的手機,就模仿它的效果實作.
  • 視訊示範一下效果
  • GitHub

2. LockPoint實體

  • 每個點是一個實體(LockPoint)用來存儲這個點的所有資訊,包括點的實體位置(x,y)和點的index位置(0-8)
class LockPoint {
        // 點的位置 0-8
        int index;
        // 點的x,y坐标
        float x, y;
        // 構造方法,初始化一個點
        LockPoint(int index, float x, float y) {
            this.index = index;
            this.x = x;
            this.y = y;
        }
        // 構造方法,從另一個點初始化
        LockPoint(LockPoint p) {
            this.x = p.x;
            this.y = p.y;
            this.index = p.index;
        }
        // 預設構造方法,初始化為一個空的點
        LockPoint() {
            this.x = -;
            this.y = -;
            this.index = -;
        }
        // 判斷該點是不是一個空的點
        boolean isEmpty() {
            return this.x == - && this.y == -;
        }
        // 重新給位置指派
        void init(float x, float y) {
            this.x = x;
            this.y = y;
        }
        // 設定為另一點的值
        void init(LockPoint p) {
            this.x = p.x;
            this.y = p.y;
            this.index = p.index;
        }
        // 判斷一個位置是不是在該點觸摸範圍内,touchSensitiveRange為觸摸有效半徑
        boolean isTouchIn(float judgeX, float judgeY) {
            return judgeX < x + touchSensitiveRange &&
                    judgeX > x - touchSensitiveRange &&
                    judgeY < y + touchSensitiveRange &&
                    judgeY > y - touchSensitiveRange;
        }

        // 重寫equals和hashCode
        @Override
        public boolean equals(Object o) {
            LockPoint p = (LockPoint) o;
            return p.x == x && p.y == y;
        }

        @Override
        public int hashCode() {
            return ;
        }

        String out(String tag) {
            return tag + " : x = " + x + " , y = " + y;
        }
    }
           

3. 初始化

  • 初始化九個點的位置,需要根據控件的大小動态計算,是以在onMeare()之後進行
  • 需求是需要将九個點放在控件中間,來适應控件大小的變化,首先确定第一個點距離左邊的距離startSpace,兩個點之間的距離 =(控件寬度 - 2 * startSpace)/2
int size = getMeasuredWidth();
        // 将寬高設定為一樣的,正方形
        setMeasuredDimension(size, size);
        // 初始化螢幕中的九個點的位置和下标
        initLockPointArray = new LockPoint[];
        // startSpace 為距離左邊的距離,計算九個點的位置,保證九個點放在控件中間
        if (startSpace == AUTO_START_SPACING) {
            //預設是控件的1/4
            startSpace = size / ;
        }
        // 計算每兩個點之間的間隔
        internalSpace = (size -  * startSpace) / ;
           
  • 初始化九個點的位置
// 初始化九個點的位置
            int index = ;
            for (int i = ; i < ; i++) {
                for (int j = ; j < ; j++) {
                    initLockPointArray[index] = new LockPoint(index, startSpace + j * internalSpace, startSpace + i * internalSpace);
                    index++;
                }
            }
           
  • onMeasure()完整代碼
// onMeasure之後初始化資料
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int size = getMeasuredWidth();
        // 将寬高設定為一樣的,正方形
        setMeasuredDimension(size, size);
        // 初始化螢幕中的九個點的位置和下标
        if (initLockPointArray == null) {
            initLockPointArray = new LockPoint[];
            // startSpace 為距離左邊的距離,計算九個點的位置放在控件中間
            if (startSpace == AUTO_START_SPACING) {
                startSpace = size / ;
            }
            // 計算每兩個點之間的間隔
            internalSpace = (size -  * startSpace) / ;
            // 初始化九個點的位置
            int index = ;
            for (int i = ; i < ; i++) {
                for (int j = ; j < ; j++) {
                    initLockPointArray[index] = new LockPoint(index, startSpace + j * internalSpace, startSpace + i * internalSpace);
                    index++;
                }
            }
            // 為了在preview時能看到效果
            if (isInEditMode()) {
                historyPointList.addAll(Arrays.asList(initLockPointArray));
            }
        }
    }
           

4. onDraw

  • 繪制過程大緻分為三個步驟
  • <1> 繪制九個點,這是每次都需要繪制的
LockPoint tempPoint;
        for (int i = ; i < initLockPointArray.length; i++) {
            canvas.drawCircle(initLockPointArray[i].x, initLockPointArray[i].y, pointRadius, pointPaint);
        }
           
  • <2> 繪制已經劃過的點
// 繪制之前觸過存儲起來的的點,繪制第i個點和i+1個點之間的線
        if (historyPointList.size() > ) {
            for (int i = ; i < historyPointList.size() - ; i++) {
                canvas.drawLine(historyPointList.get(i).x, historyPointList.get(i).y, historyPointList.get(i + ).x, historyPointList.get(i + ).y, linePaint);
            }
        }
           
  • <3> 繪制觸摸點和最後一個點的連線
// 畫最後一個點和觸摸的點之間的線
        if (currentLockPoint != null
                && currentLockPoint.x != - && currentLockPoint.y != -
                && touchPoint.x != - && touchPoint.y != -) {
            canvas.drawLine(currentLockPoint.x, currentLockPoint.y, touchPoint.x, touchPoint.y, linePaint);
        }
           

5. 事件處理

  • 對使用者touch事件進行處理
    1. 要記錄目前觸摸的點,用于繪制跟随手指的連線
    2. 檢測觸摸的點是不是在九個點中某個點的範圍内,如果是的話該點要加入被觸摸點的清單中
    3. 當手指擡起時,清除資料,恢複初始狀态
@Override
    public boolean onTouchEvent(MotionEvent event) {

        if (!isEnabled() || isEventOver)
            return false;

        int action = MotionEventCompat.getActionMasked(event);
        switch (action) {
            // 重新初始化觸摸點
            case MotionEvent.ACTION_DOWN:
                touchPoint.init(event.getX(), event.getY());
                break;
            // 移動時檢測是否在觸摸範圍内
            case MotionEvent.ACTION_MOVE:
                touchPoint.init(event.getX(), event.getY());
                LockPoint tempPoint;
                for (int i = ; i < initLockPointArray.length; i++) {
                    tempPoint = initLockPointArray[i];
                    if (!historyPointList.contains(tempPoint)
                            && tempPoint.isTouchIn(event.getX(), event.getY())) {
                        historyPointList.add(new LockPoint(tempPoint));
                        currentLockPoint.init(tempPoint);
                        break;
                    }
                }
                break;
            // 擡起時結束,重新初始化
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                touchPoint.init(-, -);
                currentLockPoint.init(-, -);
                historyPointList.clear();
                break;
        }
        postInvalidate();
        return true;
    }
           

6. 優化-多點觸控事件處理

  • 使用者在觸摸螢幕時可能有多個手指在操作,上面的代碼在單指時沒有問題,相容多點觸控的思路是:
    1. 當使用者觸發down事件時,我們可以擷取到一個pointerId,這個id唯一的标志了這個指頭,後面發生的所有事件都使用用這個pointerId來擷取,隻處理這個指頭的事件,避免事件的錯亂。
    2. 當我們開始的時候标志的那個手指擡起來了怎麼辦呢,兩個解決方法,第一個就是直接結束整個流程,相當于單指時手指擡起。第二個方法就是轉移事件,當一個指頭擡起時,從該事件中擷取還沒擡起的手指,更改标志的pointerId,事件就轉移到了另一個手指上,我們關心就是新手指的觸摸啦
    3. 關于對于事件進行處理的相關機制可以看Android事件機制,寫的都是比較基本的東西,後面慢慢完善,不過了解擷取多指的事件9⃣️綽綽有餘啦
  • 話不多說,上代碼,比較需要注意的地方我都标注在注釋中,友善查找
// 處理觸摸事件,支援多點觸摸
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // fast stop
        if (!isEnabled() || isEventOver)
            return false;
        // pointerIndex 是事件的在event中的下标
        int pointerIndex;
        // 擷取事件掩碼
        int action = MotionEventCompat.getActionMasked(event);
        switch (action) {
            // 重新初始化觸摸點
            case MotionEvent.ACTION_DOWN:
                // pointerId 記錄目前激活的pointerId
                activePointerId = event.getPointerId();
                // 根據pointerId查找事件在event中的位置
                pointerIndex = event.findPointerIndex(activePointerId);
                // 根據位置擷取到具體的事件的坐标,這裡獲得的坐标就是我們要記住的那個指頭的坐标
                touchPoint.init(event.getX(pointerIndex), event.getY(pointerIndex));
                break;
            case MotionEvent.ACTION_MOVE:
                // 手指移動時還是根據激活的pointerId擷取下标index,來進行後續操作,避免事件錯亂
                pointerIndex = event.findPointerIndex(activePointerId);
                // pointerIndex < 0表示手指的事件擷取不到了,結束響應事件
                if (pointerIndex < ) {
                    Log.e(TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
                    cancelLockDraw();
                    return false;
                }
                // 根據移動的位置擷取坐标,初始化touchPoint的值
                touchPoint.init(event.getX(pointerIndex), event.getY(pointerIndex));
                LockPoint tempPoint;
                // 檢索觸摸點有沒有在九個點中的某一個的觸摸範圍内
                for (int i = ; i < initLockPointArray.length; i++) {
                    tempPoint = initLockPointArray[i];
                    if (!historyPointList.contains(tempPoint)
                            && tempPoint.isTouchIn(event.getX(pointerIndex), event.getY(pointerIndex))) {
                        LockPoint centerPoint = findCenterPoint(tempPoint);
                        // 優化,查找兩個點之間的點,後面會有介紹
                        if (!centerPoint.isEmpty()) {
                            activePoint(centerPoint);
                        }
                        activePoint(tempPoint);
                        break;
                    }
                }
                break;
            case MotionEventCompat.ACTION_POINTER_UP:
                // 多指操作中 非 最後一個手指擡起時觸發ACTION_POINTER_UP,此時要擷取還在螢幕上的其他手指轉移事件的對象
                onSecondaryPointerUp(event);
                break;
            case MotionEvent.ACTION_UP:
                // 最後的手指擡起觸發 ACTION_UP
                pointerIndex = event.findPointerIndex(activePointerId);
                if (pointerIndex < ) {
                    Log.e(TAG, "Got ACTION_UP event but don't have an active pointer id.");
                    activePointerId = INVALID_POINTER;
                    return false;
                }
                // 釋出繪制的結果,可能是監聽回調之類的
                publishResult();
                // 置為-1
                activePointerId = INVALID_POINTER;
                break;
            case MotionEvent.ACTION_CANCEL:
                // 類似up
                cancelLockDraw();
                activePointerId = INVALID_POINTER;
                break;
        }
        postInvalidate();
        return true;
    }
           
  • 轉移焦點的方法,在各種控件的源代碼中随處可見,我也是拷貝出來直接用的,邏輯不是很複雜
/**
     * 當一個手機擡起時,轉移焦點
     *
     * @param ev 事件
     */
private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
        if (pointerId == activePointerId) {
            // This was our active pointer going up. Choose a new
            // active pointer and adjust accordingly.
            final int newPointerIndex = pointerIndex ==  ?  : ;
            activePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
        }
    }
           
  • 釋出結果
/**
     * 釋出繪制結果
     */
    private void publishResult() {
        if (listener != null) {
            isEventOver = true;
            StringBuilder sb = new StringBuilder();
            for (LockPoint lockPoint : historyPointList) {
                sb.append(lockPoint.index);
            }
            String passWd = sb.toString();
            boolean isFinish = listener.onFinish(LockView.this, passWd, passWd.length());
            if (isFinish) {
                // 輸入合法
                touchPoint.init(currentLockPoint);
            } else {
                // 輸入不合法
                cancelLockDraw();
                isEventOver = false;
            }
        } else {
            cancelLockDraw();
        }
    }
           
  • 回複初始狀态,因為在多處調用了,貼一下
/**
     * 結束繪制,恢複初始狀态
     */
    private void cancelLockDraw() {
        touchPoint.init(-, -);
        currentLockPoint.init(-, -);
        historyPointList.clear();
        postInvalidate();
    }
           

7. 優化-自動添加兩點之間連線上的點

  • 當滑動時越過中間的點之間連接配接兩端,自動查找和添加兩點之間的點,手機上的滑動解鎖也是這樣的邏輯,不然會導緻圖形很繁瑣,不美觀而且不符合常見邏輯。也就是說如果目前激發的點和上一個激發的點之間有沒有激發的點,那麼自動給他激發。
  • 首先如果兩個點是相鄰的或者是對角線上相鄰,那麼中間一定不會有空下來的點,需要排除這個情況
/**
     * 檢測相鄰
     *
     * @param p1 點1
     * @param p2 點2
     * @return p1和p2是否相鄰,斜對角也算相鄰
     */
    private boolean isAdjacentPoint(LockPoint p1, LockPoint p2) {
        // internalSpace是初始化時兩個點之間的距離,都是簡單的計算和情況羅列
        if ((p1.x == p2.x && Math.abs(p1.y - p2.y) == internalSpace)
                || (p1.y == p2.y && Math.abs(p1.x - p2.x) == internalSpace)
                || (Math.abs(p1.x - p2.x) == internalSpace && Math.abs(p1.y - p2.y) == internalSpace)) {
            Log.e(TAG, "相鄰點,不處理");
            return true;
        }
        return false;
    }
           
  • 然後如何判斷一個點位于首尾兩個激發點的中間,思路是當這個點在兩個點的連線上時且不是首尾兩個點就是中間的點。判斷的根據是斜率是不是相等,就是國中的數學問題啦。
/**
     * 判斷c點是不是在p1-p2的直線上
     *
     * @param p1 起始點
     * @param p2 終止點
     * @param c  判斷的點
     * @return 是否在該線上
     */
    private boolean isInLine(LockPoint p1, LockPoint p2, LockPoint c) {
        float k1 = (p1.x - p2.x) * f / (p1.y - p2.y);
        float k2 = (p1.x - c.x) * f / (p1.y - c.y);
        return k1 == k2;
    }
           
  • 最後整合一下,去掉不必要的判斷,在touch事件中調用
/**
     * 檢測目前激活的點和上一個激活點之間的是否有沒有激發的點
     *
     * @param activePoint 目前被激發的點
     * @return 目前激活的點和上一個激活點之間的是否有沒有激發的點,沒有傳回empty的{@link LockPoint#isEmpty()}
     */
    private LockPoint findCenterPoint(LockPoint activePoint) {
        LockPoint rstPoint = new LockPoint();
        // 隻有一個點不需要比較
        if (historyPointList.size() < ) {
            return rstPoint;
        }
        LockPoint tempPoint;
        // 擷取上個點
        LockPoint preActivePoint = historyPointList.get(historyPointList.size() - );
        // 兩個點是不是相鄰的,是相鄰的是堅決不會中間有點被空出來的
        if (isAdjacentPoint(preActivePoint, activePoint))
            return rstPoint;

        for (int i = ; i < initLockPointArray.length; i++) {
            tempPoint = initLockPointArray[i];
            // 沒有被觸摸過 && 不是首點 && 不是尾點
            if (!historyPointList.contains(tempPoint) && !preActivePoint.equals(tempPoint) && !activePoint.equals(tempPoint)) { 
                // 在連線上
                if (isInLine(preActivePoint, activePoint, tempPoint)) {
                    Log.e(TAG, "點線上上 " + tempPoint.out("temp") + " " + preActivePoint.out("pre") + " " + activePoint.out("active"));
                    rstPoint.init(tempPoint);
                    break;
                }
            }
        }
        return rstPoint;
    }
           
  • 在onTouchEvent中調用
LockPoint centerPoint = findCenterPoint(tempPoint);
        // 優化,查找兩個點之間的點
        if (!centerPoint.isEmpty()) {
           activePoint(centerPoint);
        }
        activePoint(tempPoint);
           

8. 優化-給被觸摸的點添加動畫

  • 當手指觸摸到一個點時,添加一個縮放動畫來回報觸摸操作
  • 思路時,當觸摸到一個點時使用ValueAnimator開啟動畫,不斷改變半徑的值,在繪制時達到實作縮放的效果
/**
     * 開始縮放動畫
     */
    private void startScaleAnimation() {

        if (mScaleAnimator == null) {
            mScaleAnimator = ValueAnimator.ofFloat(f, scaleMax, f);
            mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float scale = (float) animation.getAnimatedValue();
                    // 不斷改變半徑的值
                    scalePointRadius = pointRadius * scale;
                    postInvalidate();
                }
            });
            mScaleAnimator.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {

                }

                @Override
                public void onAnimationEnd(Animator animation) {
                        // 動畫結束後初始化回标準半徑的值
                    scalePointRadius = pointRadius;
                }

                @Override
                public void onAnimationCancel(Animator animation) {

                }

                @Override
                public void onAnimationRepeat(Animator animation) {

                }
            });
            mScaleAnimator.setDuration(scaleAnimDuration);
        }
        if (mScaleAnimator.isRunning())
            mScaleAnimator.end();
        mScaleAnimator.start();
    }
           
  • 同時在onDraw()方法中對剛剛觸摸的點要進行繪制,更改onDraw()方法中繪制九個點的部分,對剛剛觸摸的點使用縮放後的半徑繪制。
// 繪制九個點,當動畫在執行時被激活的點會被放大
        LockPoint tempPoint;
        for (int i = ; i < initLockPointArray.length; i++) {
            tempPoint = initLockPointArray[i];
            // 最後觸摸的點
            if (currentLockPoint != null && currentLockPoint.equals(tempPoint)) {
                canvas.drawCircle(tempPoint.x, tempPoint.y, scalePointRadius, pointPaint);
            } else {
                canvas.drawCircle(tempPoint.x, tempPoint.y, pointRadius, pointPaint);
            }
        }
           

9. 回調

  • 使用監聽将結果回調給使用者,在ACTION_UP時釋出結果
public interface OnLockFinishListener {
        /**
         * 
         * @param lockView 控件
         * @param passWd 密碼
         * @param passWsLength 密碼長度
         * @return 當傳回true時,畫面将會定格在繪制結束後的狀态,比如當密碼輸入正确的時候
         * 傳回false時,畫面會重新初始化回初始狀态,比如密碼重新二次輸入确認或者密碼錯誤的時候
         */
        boolean onFinish(LockView lockView, String passWd, int passWsLength);
    }


    /**
     * 釋出繪制結果
     */
    private void publishResult() {
        if (listener != null) {
            isEventOver = true;
            StringBuilder sb = new StringBuilder();
            for (LockPoint lockPoint : historyPointList) {
                sb.append(lockPoint.index);
            }
            String passWd = sb.toString();
            boolean isFinish = listener.onFinish(LockView.this, passWd, passWd.length());
            if (isFinish) {
                // 畫面定格
                touchPoint.init(currentLockPoint);
            } else {
                // 恢複初始化
                cancelLockDraw();
                isEventOver = false;
            }
        } else {
            cancelLockDraw();
        }
    }
           

10. 綜上

  • 還遺留了一個點,就是自動添加中間的點時應該也是有動畫效果的,暫時還沒做,有空補上吧,希望大家指正。

11. 完整代碼

/**
 * Project  : CdLibsTest
 * Package  : com.march.cdlibstest.widget
 * CreateAt : 2016/11/26
 * Describe : 自定義控件實作九宮格滑動解鎖
 *
 * @author chendong
 */
public class LockView extends View {

    public static final String TAG = "LOCK_VIEW";
    private static final int INVALID_POINTER = -;
    private static final int AUTO_START_SPACING = -;
    private static final int DEFAULT_MIN_POINT_NUM = ;
    // 激活的觸摸點id
    private int activePointerId = INVALID_POINTER;


    // 四邊的間隔,預設是控件的1/4
    private int startSpace;
    // 兩點間隔
    private int internalSpace;

    // 點的半徑
    private int pointRadius;
    // 動畫scale的半徑
    private float scalePointRadius;
    // 觸摸半徑,在點的一定範圍内觸發
    private int touchSensitiveRange;
    // 線寬度
    private int lineWidth;
    // 點顔色
    private int pointColor;
    // 線顔色
    private int lineColor;

    // 縮放的大小
    private float scaleMax;
    // 動畫時間
    private int scaleAnimDuration = ;
    // 本次繪制結束,調用init()方法恢複初始化
    private boolean isEventOver = false;


    class LockPoint {
        // 點的位置 0-8
        int index;
        //  點的x,y坐标
        float x, y;
        // 構造方法,初始化一個點
        LockPoint(int index, float x, float y) {
            this.index = index;
            this.x = x;
            this.y = y;
        }
        // 構造方法,從另一個點初始化
        LockPoint(LockPoint p) {
            this.x = p.x;
            this.y = p.y;
            this.index = p.index;
        }
        // 預設構造方法,初始化為一個空的點
        LockPoint() {
            this.x = -;
            this.y = -;
            this.index = -;
        }
        // 判斷該點是不是一個空的點
        boolean isEmpty() {
            return this.x == - && this.y == -;
        }
        // 重新給位置指派
        void init(float x, float y) {
            this.x = x;
            this.y = y;
        }
        // 設定為另一點的值
        void init(LockPoint p) {
            this.x = p.x;
            this.y = p.y;
            this.index = p.index;
        }
        // 判斷一個位置是不是在該點觸摸範圍内,touchSensitiveRange為觸摸有效半徑
        boolean isTouchIn(float judgeX, float judgeY) {
            return judgeX < x + touchSensitiveRange &&
                    judgeX > x - touchSensitiveRange &&
                    judgeY < y + touchSensitiveRange &&
                    judgeY > y - touchSensitiveRange;
        }

        // 重寫equals和hashCode
        @Override
        public boolean equals(Object o) {
            LockPoint p = (LockPoint) o;
            return p.x == x && p.y == y;
        }

        @Override
        public int hashCode() {
            return ;
        }

        String out(String tag) {
            return tag + " : x = " + x + " , y = " + y;
        }
    }

    // 動畫
    private ValueAnimator mScaleAnimator;
    // 初始化的九個點
    private LockPoint[] initLockPointArray;
    // 觸摸過的點淚飙
    private List<LockPoint> historyPointList;

    // 觸摸的點
    private LockPoint touchPoint;
    // 目前最後一個激活的點
    private LockPoint currentLockPoint;

    // 畫線
    private Paint linePaint;
    // 畫點
    private Paint pointPaint;

    // 監聽
    private OnLockFinishListener listener;

    public interface OnLockFinishListener {
        /**
         *
         * @param lockView 控件
         * @param passWd 密碼
         * @param passWsLength 密碼長度
         * @return 當傳回true時,畫面将會定格在繪制結束後的狀态
         * 傳回false時,畫面會重新初始化回初始狀态
         */
        boolean onFinish(LockView lockView, String passWd, int passWsLength);
    }

    public LockView(Context context) {
        this(context, null);
    }

    public LockView(Context context, AttributeSet attrs) {
        this(context, attrs, );
    }

    public LockView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LockView);
        float density = getResources().getDisplayMetrics().density;

        pointRadius = (int) typedArray.getDimension(R.styleable.LockView_lock_pointRadius, ( * density));
        scalePointRadius = pointRadius;
        touchSensitiveRange = (int) typedArray.getDimension(R.styleable.LockView_lock_touchSensitiveRange, pointRadius * );
        startSpace = (int) typedArray.getDimension(R.styleable.LockView_lock_startSpace, AUTO_START_SPACING);
        lineWidth = (int) typedArray.getDimension(R.styleable.LockView_lock_lineWidth, ( * density));

        lineColor = typedArray.getColor(R.styleable.LockView_lock_lineColor, Color.WHITE);
        pointColor = typedArray.getColor(R.styleable.LockView_lock_pointColor, Color.WHITE);

        scaleAnimDuration = typedArray.getInt(R.styleable.LockView_lock_scaleAnimDuration, );
        scaleMax = typedArray.getFloat(R.styleable.LockView_lock_scaleMax, f);
        typedArray.recycle();

        historyPointList = new ArrayList<>();
        touchPoint = new LockPoint();
        currentLockPoint = new LockPoint();

        pointPaint = new Paint();
        pointPaint.setAntiAlias(true);
        pointPaint.setColor(pointColor);
        pointPaint.setStyle(Paint.Style.FILL_AND_STROKE);

        linePaint = new Paint();
        linePaint.setAntiAlias(true);
        linePaint.setStrokeWidth(lineWidth);
        linePaint.setColor(lineColor);
        linePaint.setStyle(Paint.Style.STROKE);
    }

    public void setListener(OnLockFinishListener listener) {
        this.listener = listener;
    }


    /**
     * 開始縮放動畫
     */
    private void startScaleAnimation() {

        if (mScaleAnimator == null) {
            mScaleAnimator = ValueAnimator.ofFloat(f, scaleMax, f);
            mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float scale = (float) animation.getAnimatedValue();
                    scalePointRadius = pointRadius * scale;
                    postInvalidate();
                }
            });
            mScaleAnimator.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {

                }

                @Override
                public void onAnimationEnd(Animator animation) {
                    scalePointRadius = pointRadius;
                }

                @Override
                public void onAnimationCancel(Animator animation) {

                }

                @Override
                public void onAnimationRepeat(Animator animation) {

                }
            });
            mScaleAnimator.setDuration(scaleAnimDuration);
        }
        if (mScaleAnimator.isRunning())
            mScaleAnimator.end();
        mScaleAnimator.start();
    }

    // 處理觸摸事件,支援多點觸摸
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // fast stop
        if (!isEnabled() || isEventOver)
            return false;
        // pointerIndex 是事件的在event中的下标
        int pointerIndex;
        // 擷取事件掩碼
        int action = MotionEventCompat.getActionMasked(event);
        switch (action) {
            // 重新初始化觸摸點
            case MotionEvent.ACTION_DOWN:
                // pointerId 記錄目前激活的pointerId
                activePointerId = event.getPointerId();
                // 根據pointerId查找事件在event中的位置
                pointerIndex = event.findPointerIndex(activePointerId);
                // 根據位置擷取到具體的事件的坐标,這裡獲得的坐标就是我們要記住的那個指頭的坐标
                touchPoint.init(event.getX(pointerIndex), event.getY(pointerIndex));
                break;
            case MotionEvent.ACTION_MOVE:
                // 手指移動時還是根據激活的pointerId擷取下标index,來進行後續操作,避免事件錯亂
                pointerIndex = event.findPointerIndex(activePointerId);
                // pointerIndex < 0表示手指的事件擷取不到了,結束響應事件
                if (pointerIndex < ) {
                    Log.e(TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
                    cancelLockDraw();
                    return false;
                }
                // 根據移動的位置擷取坐标,初始化touchPoint的值
                touchPoint.init(event.getX(pointerIndex), event.getY(pointerIndex));
                LockPoint tempPoint;
                // 檢索觸摸點有沒有在九個點中的某一個的觸摸範圍内
                for (int i = ; i < initLockPointArray.length; i++) {
                    tempPoint = initLockPointArray[i];
                    if (!historyPointList.contains(tempPoint)
                            && tempPoint.isTouchIn(event.getX(pointerIndex), event.getY(pointerIndex))) {
                        LockPoint centerPoint = findCenterPoint(tempPoint);
                        if (!centerPoint.isEmpty()) {
                            activePoint(centerPoint);
                        }
                        activePoint(tempPoint);
                        break;
                    }
                }
                break;
            case MotionEventCompat.ACTION_POINTER_UP:
                // 多指操作中 非 最後一個手指擡起時觸發ACTION_POINTER_UP,此時要擷取還在螢幕上的其他手指轉移事件的對象
                onSecondaryPointerUp(event);
                break;
            case MotionEvent.ACTION_UP:
                // 最後的手指擡起觸發 ACTION_UP
                pointerIndex = event.findPointerIndex(activePointerId);
                if (pointerIndex < ) {
                    Log.e(TAG, "Got ACTION_UP event but don't have an active pointer id.");
                    activePointerId = INVALID_POINTER;
                    return false;
                }
                // 釋出繪制的結果,可能是監聽回調之類的
                publishResult();
                // 置為-1
                activePointerId = INVALID_POINTER;
                break;
            case MotionEvent.ACTION_CANCEL:
                // 類似up
                cancelLockDraw();
                activePointerId = INVALID_POINTER;
                break;
        }
        postInvalidate();
        return true;
    }


    /**
     * 釋出繪制結果
     */
    private void publishResult() {
        if (listener != null) {
            isEventOver = true;
            StringBuilder sb = new StringBuilder();
            for (LockPoint lockPoint : historyPointList) {
                sb.append(lockPoint.index);
            }
            String passWd = sb.toString();
            boolean isFinish = listener.onFinish(LockView.this, passWd, passWd.length());
            if (isFinish) {
                // 輸入合法
                touchPoint.init(currentLockPoint);
            } else {
                // 輸入不合法
                cancelLockDraw();
                isEventOver = false;
            }
        } else {
            cancelLockDraw();
        }
    }

    /**
     * 當一個手機擡起時,轉移焦點
     *
     * @param ev 事件
     */
    private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
        if (pointerId == activePointerId) {
            // This was our active pointer going up. Choose a new
            // active pointer and adjust accordingly.
            final int newPointerIndex = pointerIndex ==  ?  : ;
            activePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
        }
    }

    /**
     * 檢測目前激活的點和上一個激活點之間的是否有沒有激發的點
     *
     * @param activePoint 目前被激發的點
     * @return 目前激活的點和上一個激活點之間的是否有沒有激發的點,沒有傳回empty的{@link LockPoint#isEmpty()}
     */
    private LockPoint findCenterPoint(LockPoint activePoint) {
        LockPoint rstPoint = new LockPoint();
        // 隻有一個點不需要比較
        if (historyPointList.size() < ) {
            return rstPoint;
        }
        LockPoint tempPoint;
        // 擷取上個點
        LockPoint preActivePoint = historyPointList.get(historyPointList.size() - );
        // 兩個點是不是相鄰的,是相鄰的是堅決不會中間有點被空出來的
        if (isAdjacentPoint(preActivePoint, activePoint))
            return rstPoint;

        for (int i = ; i < initLockPointArray.length; i++) {
            tempPoint = initLockPointArray[i];
            // 沒有被觸摸過 && 不是首點 && 不是尾點
            if (!historyPointList.contains(tempPoint) && !preActivePoint.equals(tempPoint) && !activePoint.equals(tempPoint)) {
                if (isInLine(preActivePoint, activePoint, tempPoint)) {
                    Log.e(TAG, "點線上上 " + tempPoint.out("temp") + " " + preActivePoint.out("pre") + " " + activePoint.out("active"));
                    rstPoint.init(tempPoint);
                    break;
                }
            }
        }
        return rstPoint;
    }


    /**
     * 檢測相鄰
     *
     * @param p1 點1
     * @param p2 點2
     * @return p1和p2是否相鄰,斜對角也算相鄰
     */
    private boolean isAdjacentPoint(LockPoint p1, LockPoint p2) {
        if ((p1.x == p2.x && Math.abs(p1.y - p2.y) == internalSpace)
                || (p1.y == p2.y && Math.abs(p1.x - p2.x) == internalSpace)
                || (Math.abs(p1.x - p2.x) == internalSpace && Math.abs(p1.y - p2.y) == internalSpace)) {
            Log.e(TAG, "相鄰點,不處理");
            return true;
        }
        return false;
    }


    /**
     * 判斷c點是不是在p1-p2的直線上
     *
     * @param p1 起始點
     * @param p2 終止點
     * @param c  判斷的點
     * @return 是否在該線上
     */
    private boolean isInLine(LockPoint p1, LockPoint p2, LockPoint c) {
        float k1 = (p1.x - p2.x) * f / (p1.y - p2.y);
        float k2 = (p1.x - c.x) * f / (p1.y - c.y);
        return k1 == k2;
    }

    /**
     * 激活該點,該點将會添加到選中點清單中,然後執行動畫
     *
     * @param tempPoint 被激活的點
     */
    private void activePoint(LockPoint tempPoint) {
        historyPointList.add(new LockPoint(tempPoint));
        currentLockPoint.init(tempPoint);
        startScaleAnimation();
        postInvalidate();
    }


    public void init() {
        isEventOver = false;
        cancelLockDraw();
    }

    /**
     * 結束繪制,恢複初始狀态
     */
    private void cancelLockDraw() {
        touchPoint.init(-, -);
        currentLockPoint.init(-, -);
        historyPointList.clear();
        postInvalidate();
    }


    // onMeasure之後初始化資料
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int size = getMeasuredWidth();
        // 将寬高設定為一樣的,正方形
        setMeasuredDimension(size, size);
        // 初始化螢幕中的九個點的位置和下标
        if (initLockPointArray == null) {
            initLockPointArray = new LockPoint[];
            // startSpace 為距離左邊的距離,計算九個點的位置放在控件中間
            if (startSpace == AUTO_START_SPACING) {
                startSpace = size / ;
            }
            // 計算每兩個點之間的間隔
            internalSpace = (size -  * startSpace) / ;
            // 初始化九個點的位置
            int index = ;
            for (int i = ; i < ; i++) {
                for (int j = ; j < ; j++) {
                    initLockPointArray[index] = new LockPoint(index, startSpace + j * internalSpace, startSpace + i * internalSpace);
                    index++;
                }
            }
            // 為了在preview時能看到效果
            if (isInEditMode()) {
                historyPointList.addAll(Arrays.asList(initLockPointArray));
            }
        }
    }

    private void log(Object... objs) {
        StringBuilder sb = new StringBuilder();
        for (Object obj : objs) {
            sb.append(obj.toString()).append("   ");
        }
        Log.e(TAG, sb.toString());
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // fast stop
        if (initLockPointArray == null)
            return;

        log(currentLockPoint.out("current"), touchPoint.out("touch"));

        // 畫最後一個點和觸摸的點之間的線
        if (currentLockPoint != null
                && currentLockPoint.x != - && currentLockPoint.y != -
                && touchPoint.x != - && touchPoint.y != -) {
            canvas.drawLine(currentLockPoint.x, currentLockPoint.y, touchPoint.x, touchPoint.y, linePaint);
        }

        // 繪制之前觸過存儲起來的的點
        if (historyPointList.size() > ) {
            for (int i = ; i < historyPointList.size() - ; i++) {
                canvas.drawLine(historyPointList.get(i).x, historyPointList.get(i).y, historyPointList.get(i + ).x, historyPointList.get(i + ).y, linePaint);
            }
        }

        // 繪制九個點,當動畫在執行時被激活的點會被放大
        LockPoint tempPoint;
        for (int i = ; i < initLockPointArray.length; i++) {
            tempPoint = initLockPointArray[i];
            if (currentLockPoint != null && currentLockPoint.equals(tempPoint)) {
                canvas.drawCircle(tempPoint.x, tempPoint.y, scalePointRadius, pointPaint);
            } else {
                canvas.drawCircle(tempPoint.x, tempPoint.y, pointRadius, pointPaint);

            }
        }
    }
}