寫一個圖案解鎖控件

寫一個圖案解鎖控件

雖然網上有很多的關於圖案解鎖的現成輪子,但是 ,有什麼比自己寫一個輪子更帶勁的事情呢?

首先展示效果:

實現分析

屬性分析

應爲這是一個自定義控件,網上很多的輪子都是通過替換圖片來實現的,但是,我並不想使用圖片(最主要,不會切圖)。那麼我就需要考慮,我應該使用那些自定義屬性。

通過觀察別人的輪子,發現主要的自定義屬性有這些:

  1. 正常情況下的點顏色
  2. 按下時點的顏色
  3. 錯誤時點的顏色
  4. 連接時線的顏色
  5. 錯誤時線的顏色

PS:應該還有一個點的半徑,但是我想想還是寫死了算了。

實現分析

先上一張圖:

圖有點醜,見諒

圖中的9個實心點,就是代表我們所需要繪製的九宮的9個點的位置,整個區域是一個正方形(不要問我爲什麼是正方形),可以從圖中看到水平和豎直的3個點把分別把橫豎方向上分成了4部分,我們計算的座標時每個點取1/4就行了。

行爲分析

  1. 在進行連線時,同一個點不能被鏈接兩次
  2. 橫,豎,斜三個方向上不能有跳過 。以橫向舉例,比如選擇了第一個點,不能跳過第二個點,而去直接連接第三個點。

代碼實現

### 自定義屬性

經過上面的分析,我可以開始動手了。

首先定義5個屬性:

 <declare-styleable name="PatternDeblockView">
        <!--  按下時的圓的顏色-->
        <attr name="pressed_color" format="color"></attr>
        <!--錯誤時的圓的顏色-->
        <attr name="error_color" format="color"></attr>
        <!--平常時的顏色-->
        <attr name="normal_color" format="color"></attr>
        <!--錯誤時的線的顏色-->
        <attr name="error_line_color" format="color"></attr>
        <!--滑動時的線上的顏色-->
        <attr name="pressed_line_color" format="color"></attr>
    </declare-styleable>

當5個屬性定義完成後,獲取這些屬性:

 private void initAttr(Context context, AttributeSet attrs) {
        if (null != attrs) {
            TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.PatternDeblockView);
            int len = array.getIndexCount();
            for (int i = 0; i < len; i++) {
                int attr = array.getIndex(i);
                switch (attr) {
                    case R.styleable.PatternDeblockView_pressed_color:
                        mPressedColor = array.getColor(attr, Color.BLUE);
                        break;
                    case R.styleable.PatternDeblockView_error_color:
                        mErrorColor = array.getColor(attr, Color.RED);
                        break;
                    case R.styleable.PatternDeblockView_normal_color:
                        mNormalColor = array.getColor(attr, Color.GRAY);
                        break;
                    case R.styleable.PatternDeblockView_pressed_line_color:
                        mPressedLineColor = array.getColor(attr, Color.BLUE);
                        break;
                    case R.styleable.PatternDeblockView_error_line_color:
                        mErrorLineColor = array.getColor(attr, Color.RED);
                        break;
                }
            }
        }
        mChoosePoints = new ArrayList<Point>();
        mChoosePassword = new ArrayList<Integer>();
        //設置半徑
        mRadius = 40;
        mPaint = new Paint();
        mPaint.setStrokeWidth(5);//設置線寬
        mPaint.setAntiAlias(true);
    }

在獲取自定義屬性值得同時對一些屬性進行設置,在這裏我設置了圓的半徑和畫筆的線寬。該方法需要在重寫的構造方法中調用。

測量

自定義屬性獲取完成後,我需要通過重寫onMeasure()方法,獲取我當前控件的寬高值:

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        width = measureWidth(widthMeasureSpec);
        height = measureHeight(heightMeasureSpec);
        setMeasuredDimension(width, height);
    }
private int measureHeight(int heightMeasureSpec) {
        int height = 0;
        int mode = MeasureSpec.getMode(heightMeasureSpec);
        int size = MeasureSpec.getSize(heightMeasureSpec);

        if (mode == MeasureSpec.EXACTLY) {
            height = size;
        } else {
            height = 200;
        }
        return height;
    }

measureWidth()measureHeight()方法類似,這幾個方法都是基本通用的形式。

繪製

這一步是兩個關鍵部分的其中一個。

在重寫的onDraw()方法中,我需要把九宮格的九宮點,按照之前的分析畫出來:

protected void onDraw(Canvas canvas) {
        //沒有進行初始化
        if (mPoints == null) {
            // 進行初始化
            initPaints();
        }

        drawPoints(canvas, mPoints);
        drawLine(canvas, movingX, movingY);

        super.onDraw(canvas);
    }

上來首先,對九個點有沒有初始化進行判斷,如果已經初始化了,就直接畫點和畫線,如果沒有初始化,先進行初始化操作:

   private void initPaints() {

        float offsetX = 0;
        float offsetY = 0;

        min = Math.min(width, height);//獲取最小值
        if (width >= height) {//如果寬比高大
            offsetX = (width - height) / 2;
        } else {//如果寬比高小
            offsetY = (height - width) / 2;
        }
        mPoints = new Point[3][3];

        //初始化完成
        mPoints[0][0] = new Point(offsetX + min / 4, offsetY + min / 4, 1);
        mPoints[0][1] = new Point(offsetX + min / 2, offsetY + min / 4, 2);
        mPoints[0][2] = new Point(offsetX + min - min / 4, offsetY + min / 4, 3);

        mPoints[1][0] = new Point(offsetX + min / 4, offsetY + min / 2, 4);
        mPoints[1][1] = new Point(offsetX + min / 2, offsetY + min / 2, 5);
        mPoints[1][2] = new Point(offsetX + min - min / 4, offsetY + min / 2, 6);

        mPoints[2][0] = new Point(offsetX + min / 4, offsetY + min - min / 4, 7);
        mPoints[2][1] = new Point(offsetX + min / 2, offsetY + min - min / 4, 8);
        mPoints[2][2] = new Point(offsetX + min - min / 4, offsetY + min - min / 4, 9);
    }

通過使用3*3的二維數組進行點的初始化,按照下圖所示進行說明

由於我們要把九宮放在我們自定義控件展示的中間位置,因此需要獲取控件實際的寬和高並獲取最小的一個,如上圖所示的情況,由於height大於width,因此,在y方向要加上偏移,由於加上偏移後要使得九宮位於中間,因此y的偏移量爲(height-widht)/2。反之。x方向的偏移爲(width-height)/2

在計算完偏移後就能通過4等分的原則,得到所用點的具體座標。把9點的位置計算完成後,進行畫點:

    private void drawPoints(Canvas canvas, Point[][] mPoints) {
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                Point point = mPoints[i][j];
                if (point.state == Point.NORMALSTATE) {
                    mPaint.setColor(mNormalColor);
                } else if (point.state == Point.PRESSEDSTATE) {
                    mPaint.setColor(mPressedColor);
                } else {
                    mPaint.setColor(mErrorColor);
                }
                canvas.drawCircle(point.x, point.y, mRadius, mPaint);
            }
        }
    }

在畫點的時候需要注意的是,根據點的狀態,繪製不同的顏色。接下的的任務,就是畫線了,劃線部分,需要結合對Touch事件的處理一起描述:

 private void drawLine(Canvas canvas, float movingX, float movingY) {
        float secondX = 0;
        float secondY = 0;
        float firstX = 0;
        float firstY = 0;
        if (isError) {
            mPaint.setColor(mErrorLineColor);//設置錯誤時的線顏色
        } else {
            mPaint.setColor(mPressedLineColor);
        }
        int len = mChoosePoints.size();

        if (len >= 1) {
            firstX = mChoosePoints.get(0).x;
            firstY = mChoosePoints.get(0).y;
        }
        //畫點中的線
        for (int i = 1; i < len; i++) {
            secondX = mChoosePoints.get(i).x;
            secondY = mChoosePoints.get(i).y;

            canvas.drawLine(firstX, firstY, secondX, secondY, mPaint);
            firstX = secondX;
            firstY = secondY;
        }

        if (!isFinish) {
            //畫點到點的線
            if (len >= 1) {
                canvas.drawLine(firstX, firstY, movingX, movingY, mPaint);
            }
        }

    }

首先是把已經選中的點之間用線連接起來。也要注意狀態。如果沒有移動事件沒有結束同時沒有點和最後一個點連接起來,需要把最後一個點和當其的手指點擊位置連接起來。

處理Touch事件

這部分是重中之重,先上代碼:

public boolean onTouchEvent(MotionEvent event) {
        //獲得當其移動位置
        movingX = event.getX();
        movingY = event.getY();
        Point point = null;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!isFinish) {
                    point = choosePoints(movingX, movingY);//判斷落下的點是否在9個點上
                    if (null != point) {
                        mChoosePoints.add(point);//如果不爲空加入選中的集合中
                        mChoosePassword.add(point.index);
                        isChosen = true;// 表示已經與了選擇
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                //移動時有兩情況需要進考慮,1:在點的範圍時需要把點選中,2:重複點不計算,3:一條線上 不能跳過選擇
                if (!isFinish) {
                    isRepeat = checkIsRepeat(movingX, movingY);
                    if (!isRepeat) {//如果不在已經選中的點中,判讀是否在點陣中
                        point = choosePoints(movingX, movingY);
                        //進行跳過選擇
                        if (null != point) {
                            mChoosePoints.add(point);
                            mChoosePassword.add(point.index);
                        }
                        parsePoint();
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                isFinish = true;
                //對點數進行判讀 如果連起來的點數少於5設置成
                int len = mChoosePoints.size();
                if (len == 0) {
                    isChosen = false;
                    isFinish = false;
                }
                if (!isPassword) {//不是密碼模式
                    if (len < 5) {
                        isError = true;
                        for (int i = 0; i < len; i++) {
                            point = mChoosePoints.get(i);
                            point.state = Point.ERRORSTATE;
                        }
                        if (null != mListener) {
                            mListener.drawMessage(false, null);
                        }
                    } else {
                        if (null != mListener) {
                            mListener.drawMessage(true, mChoosePassword);
                        }
                    }
                } else {//是密碼模式
                    if (mChoosePassword.equals(mPassword)) {
                        if (null != mListener) {
                            mListener.enterPass(true);
                        }
                    } else {
                        if (null != mListener) {
                            //把顏色改變
                            len = mChoosePoints.size();
                            for (int i = 0; i < len; i++) {
                                point = mChoosePoints.get(i);
                                point.state = Point.ERRORSTATE;
                            }
                            mListener.enterPass(false);
                        }
                    }
                }
                break;
        }

        postInvalidate();// 進行重繪
        return true;
    }

在該Touch事件中,需要處理3個Action,ACTION_DOWNACTION_MOVEACTION_UP三個事件。

ACTION_DOWN事件中需要處理,當前用戶點擊的區域是否在點的範圍中,如果在範圍中,就把該點添加的被選中的集合中。並把選中的狀態進行更改。

判斷是否在點的範圍中也很簡單:

 private Point choosePoints(float movingX, float movingY) {
        Log.i(TAG, "movingX:" + movingX + ";;;;;movingY:" + movingY);
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                Point point = mPoints[i][j];
                if (Point.isInPoint(point, movingX, movingY, mRadius)) {
                    //修改點的狀態(被選中)
                    point.state = Point.PRESSEDSTATE;
                    return point;
                }
            }
        }
        return null;
    }

ACTION_MOVE事件中需要對下面三種情況進處理。

  1. 在點的範圍時需要把點選中。
  2. 重複點不計算。
  3. 一條線上不能跳過選擇。、

第一種情況和ACTION_DOWN的處理方式一致,第二種情況處理如下:

private boolean checkIsRepeat(float movingX, float movingY) {
        int len = mChoosePoints.size();
        for (int i = 0; i < len; i++) {
            if (Point.isInPoint(mChoosePoints.get(i), movingX, movingY, mRadius)) {
                return true;
            }
        }
        return false;
    }

也是很簡單的循環。

針對第三種情況,我只想到瞭如下的實現方式,講道理,應該有其他的方式進行判斷:

 private void parsePoint() {
        int len = mChoosePoints.size();
        //如果被選中的點個數不大於1
        if (len <= 1) {
            return;
        }

        //拿到最後最後兩個Point
        Point a = mChoosePoints.get(len - 1);
        Point b = mChoosePoints.get(len - 2);

        Point p = null;
        float x = 0;
        float y = 0;

        if (a.x == b.x && Math.abs(a.y - b.y) == min / 2) {//在同一條豎線
            Log.i(TAG, "a.x=b.x");
            x = a.x;
            y = a.y - (a.y - b.y) / 2;
        } else if (Math.abs(a.x - b.x) == min / 2 && a.y == b.y) {//在同一條直線
            x = a.x - (a.x - b.x) / 2;
            y = a.y;
        } else if (Math.abs(a.x - b.x) == min / 2 && Math.abs(a.y - b.y) == min / 2) {//在同一條斜線上
            x = a.x - (a.x - b.x) / 2;
            y = a.y - (a.y - b.y) / 2;
        }
        p = choosePoints(x, y);
        Log.i(TAG, "X:" + x + ";;;;;Y:" + y);
        if (null != p) {
            p.state = Point.PRESSEDSTATE;
            mChoosePoints.add(len - 1, p);
            mChoosePassword.add(len - 1, p.index);
        }
    }

在這裏,我是通過計算座標的之間的差進行的,比較蠢。

ACTION_UP的處理就比較簡單了,如果不是密碼模式,就檢測已經選中點的數量,如果超過範圍用正常的方式展示,如果在範圍之內,使用錯誤的方式展示。

最後就是調用postInvalidate()方法進行重繪。

恩,大體應該就是這麼多。

最後說兩句

雖然,整個小控件完成了,但是在編寫這的控件中發現了很多問題,

  1. 命名的問題,命名感覺亂七八糟的。
  2. 思考問題的邏輯還是存在一些問題,邏輯不嚴謹,對於空判斷等一些可能會造成異常的條件,沒有進行控制。

以後好好努力吧。

整個項目我放在了github上,有興趣的可以看看代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章