1. 程式人生 > >Android 自定義 View——手勢密碼

Android 自定義 View——手勢密碼

    Android 自定義 View 當然是十分重要的,筆者這兩天寫了一個自定義 View 的手勢密碼,和大家分享分享:


    首先,我們來建立一個表示點的類,Point.java:

public class Point {

    // 點的三種狀態
    public static final int POINT_STATUS_NORMAL = 0;
    public static final int POINT_STATUS_CLICK = 1;
    public static final int POINT_STATUS_ERROR = 2;

    // 預設狀態
    public int state = POINT_STATUS_NORMAL;

    // 點的座標
    public float mX;
    public float mY;

    public Point(float x,float y){
        this.mX = x;
        this.mY = y;
    }

    // 獲取兩個點的距離
    public float getInstance(Point a){
        return (float) Math.sqrt((mX-a.mX)*(mX-a.mX)+(mY-a.mY)*(mY-a.mY));
    }

}

    然後我們建立一個 HandleLock.java 繼承自 View,並重寫其三種構造方法(不重寫帶兩個引數的構造方法會導致程式出錯):

    首先,我們先把後面需要用的變數寫出來,方便大家明白這些變數是幹嘛的:

// 三種畫筆
    private Paint mNormalPaint;
    private Paint mClickPaint;
    private Paint mErrorPaint;

    // 點的半徑
    private float mRadius;

    // 九個點,使用二維陣列
    private Point[][] mPoints = new Point[3][3];

    // 儲存手勢劃過的點
    private ArrayList<Point> mClickPointsList = new ArrayList<Point>();
    // 手勢的 x 座標,y 座標
    private float mHandleX;
    private float mHandleY;

    private OnDrawFinishListener mListener;

    // 儲存滑動路徑
    private StringBuilder mRoute = new StringBuilder();
    // 是否在畫錯誤狀態
    private boolean isDrawError = false;

    接下來我們來初始化資料:

// 初始化資料
    private void initData() {

        // 初始化三種畫筆,正常狀態為灰色,點下狀態為藍色,錯誤為紅色
        mNormalPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mNormalPaint.setColor(Color.parseColor("#ABABAB"));
        mClickPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mClickPaint.setColor(Color.parseColor("#1296db"));
        mErrorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mErrorPaint.setColor(Color.parseColor("#FB0C13"));

        // 獲取點間隔
        float offset = 0;
        if (getWidth() > getHeight()) {
            // 橫屏
            offset = getHeight() / 7;
            mRadius = offset / 2;
            mPoints[0][0] = new Point(getWidth() / 2 - offset * 2, offset + mRadius);
            mPoints[0][1] = new Point(getWidth() / 2, offset + mRadius);
            mPoints[0][2] = new Point(getWidth() / 2 + offset * 2, offset + mRadius);
            mPoints[1][0] = new Point(getWidth() / 2 - offset * 2, offset * 3 + mRadius);
            mPoints[1][1] = new Point(getWidth() / 2, offset * 3 + mRadius);
            mPoints[1][2] = new Point(getWidth() / 2 + offset * 2, offset * 3 + mRadius);
            mPoints[2][0] = new Point(getWidth() / 2 - offset * 2, offset * 5 + mRadius);
            mPoints[2][1] = new Point(getWidth() / 2, offset * 5 + mRadius);
            mPoints[2][2] = new Point(getWidth() / 2 + offset * 2, offset * 5 + mRadius);
        } else {
            // 豎屏
            offset = getWidth() / 7;
            mRadius = offset / 2;
            mPoints[0][0] = new Point(offset + mRadius, getHeight() / 2 - 2 * offset);
            mPoints[0][1] = new Point(offset * 3 + mRadius, getHeight() / 2 - 2 * offset);
            mPoints[0][2] = new Point(offset * 5 + mRadius, getHeight() / 2 - 2 * offset);
            mPoints[1][0] = new Point(offset + mRadius, getHeight() / 2);
            mPoints[1][1] = new Point(offset * 3 + mRadius, getHeight() / 2);
            mPoints[1][2] = new Point(offset * 5 + mRadius, getHeight() / 2);
            mPoints[2][0] = new Point(offset + mRadius, getHeight() / 2 + 2 * offset);
            mPoints[2][1] = new Point(offset * 3 + mRadius, getHeight() / 2 + 2 * offset);
            mPoints[2][2] = new Point(offset * 5 + mRadius, getHeight() / 2 + 2 * offset);
        }


    }

    大家可以看到,我來給點定座標是,是按照比較窄的邊的 1/7 作為點的直徑,這樣保證了,不管你怎麼定義 handleLock 的寬高,都可以使裡面的九個點看起來位置很舒服。

   接下來我們就需要寫一些函式,將點、線繪製到控制元件上,我自己把繪製分成了三部分,一部分是點,一部分是點與點之間的線,一部分是手勢的小點和手勢到最新點的線。

// 畫點,按照我們選擇的半徑畫九個圓
    private void drawPoints(Canvas canvas) {
        // 便利所有的點,並且判斷這些點的狀態
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                Point point = mPoints[i][j];
                switch (point.state) {
                    case Point.POINT_STATUS_NORMAL:
                        canvas.drawCircle(point.mX, point.mY, mRadius, mNormalPaint);
                        break;
                    case Point.POINT_STATUS_CLICK:
                        canvas.drawCircle(point.mX, point.mY, mRadius, mClickPaint);
                        break;
                    case Point.POINT_STATUS_ERROR:
                        canvas.drawCircle(point.mX, point.mY, mRadius, mErrorPaint);
                        break;
                    default:
                        break;

                }
            }
        }
    }
    // 畫點與點之間的線
    private void drawLines(Canvas canvas) {
        // 判斷手勢是否已經劃過點了
        if (mClickPointsList.size() > 0) {
            Point prePoint = mClickPointsList.get(0);
            // 將所有已選擇點的按順序連線
            for (int i = 1; i < mClickPointsList.size(); i++) {
                // 判斷已選擇點的狀態
                if (prePoint.state == Point.POINT_STATUS_CLICK) {
                    mClickPaint.setStrokeWidth(7);
                    canvas.drawLine(prePoint.mX, prePoint.mY, mClickPointsList.get(i).mX, mClickPointsList.get(i).mY, mClickPaint);
                }
                if (prePoint.state == Point.POINT_STATUS_ERROR) {
                    mErrorPaint.setStrokeWidth(7);
                    canvas.drawLine(prePoint.mX, prePoint.mY, mClickPointsList.get(i).mX, mClickPointsList.get(i).mY, mErrorPaint);
                }
                prePoint = mClickPointsList.get(i);
            }

        }

    }
    // 畫手勢點
    private void drawFinger(Canvas canvas) {
        // 有選擇點後再出現手勢點
        if (mClickPointsList.size() > 0) {
            canvas.drawCircle(mHandleX, mHandleY, mRadius / 2, mClickPaint);
        }
        // 最新點到手指的連線,判斷是否有已選擇的點,有才能畫
        if (mClickPointsList.size() > 0) {
            canvas.drawLine(mClickPointsList.get(mClickPointsList.size() - 1).mX, mClickPointsList.get(mClickPointsList.size() - 1).mY,
                    mHandleX, mHandleY, mClickPaint);
        }
    }

    上面的程式碼我們看到需要使用到手勢劃過的點,我們是怎麼選擇的呢?

// 獲取手指移動中選取的點
private int[] getPositions() {
    Point point = new Point(mHandleX, mHandleY);
    int[] position = new int[2];
    // 遍歷九個點,看手勢的座標是否在九個圓內,有則返回這個點的兩個下標
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            if (mPoints[i][j].getInstance(point) <= mRadius) {
                position[0] = i;
                position[1] = j;
                return position;
            }
        }

    }
    return null;
}

    我們需要重寫其 onTouchEvent 來通過手勢動作來提交選擇的點,並更新檢視:

// 重寫點選事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 獲取手勢的座標
        mHandleX = event.getX();
        mHandleY = event.getY();
        int[] position;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                position = getPositions();
                // 判斷點下時是否選擇到點
                if (position != null) {
                    // 新增到已選擇點中,並改變其狀態
                    mClickPointsList.add(mPoints[position[0]][position[1]]);
                    mPoints[position[0]][position[1]].state = Point.POINT_STATUS_CLICK;
                    // 儲存路徑,依次儲存其橫縱下標
                    mRoute.append(position[0]);
                    mRoute.append(position[1]);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                position = getPositions();
                // 判斷手勢移動時是否選擇到點
                if (position != null) {
                    // 判斷當前選擇的點是否已經被選擇過
                    if (!mClickPointsList.contains(mPoints[position[0]][position[1]])) {
                        // 新增到已選擇點中,並改變其狀態
                        mClickPointsList.add(mPoints[position[0]][position[1]]);
                        mPoints[position[0]][position[1]].state = Point.POINT_STATUS_CLICK;
                        // 儲存路徑,依次儲存其橫縱下標
                        mRoute.append(position[0]);
                        mRoute.append(position[1]);
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                // 重置資料
                resetData();
                break;
            default:
                break;
        }
        // 更新檢視
        invalidate();

        return true;
    }
// 重置資料
    private void resetData() {
        // 將所有選擇過的點的狀態改為正常
        for (Point point :
                mClickPointsList) {
            point.state = Point.POINT_STATUS_NORMAL;
        }
        // 清空已選擇點
        mClickPointsList.clear();
        // 清空儲存的路徑
        mRoute = new StringBuilder();
        // 不再畫錯誤狀態
        isDrawError = false;
    }

    那我們怎麼繪製檢視呢?我們通過重寫其 onDraw() 方法:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 判斷是否畫錯誤狀態,畫錯誤狀態不需要畫手勢點已經於最新選擇點的連線
        if (isDrawError) {
            drawPoints(canvas);
            drawLines(canvas);
        } else {
            drawPoints(canvas);
            drawLines(canvas);
            drawFinger(canvas);
        }
    }

    那麼這個手勢密碼繪製過程就結束了,但是整個控制元件還沒有結束,我們還需要給它一個監聽器,監聽其繪製完成,選擇後續事件:

private OnDrawFinishListener mListener;

    // 定義繪製完成的介面
    public interface OnDrawFinishListener {
        public boolean drawFinish(String route);
    }

    // 定義繪製完成的方法,傳入介面
    public void setOnDrawFinishListener(OnDrawFinishListener listener) {
        this.mListener = listener;
    }

    然後我們就需要在手勢離開的時候 ,來進行繪製完成時的事件:

case MotionEvent.ACTION_UP:
                // 完成時回撥繪製完成的方法,返回比對結果,判斷手勢密碼是否正確
                mListener.drawFinish(mRoute.toString());
                // 返回錯誤,則將所有已選擇點狀態改為錯誤
                if (!mListener.drawFinish(mRoute.toString())) {
                    for (Point point :
                            mClickPointsList) {
                        point.state = Point.POINT_STATUS_ERROR;
                    }
                    // 將是否繪製錯誤設為 true
                    isDrawError = true;
                    // 重新整理檢視
                    invalidate();
                    // 這裡我們使用 handler 非同步操作,使其錯誤狀態保持 0.5s
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            if (!mListener.drawFinish(mRoute.toString())) {
                                Message message = new Message();
                                message.arg1 = 0;
                                handler.sendMessage(message);
                            }
                        }
                    }).run();
                } else {
                    resetData();
                }
                invalidate();

                break;
private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.arg1) {
                case 0:
                    try {
                        // 沉睡 0.5s
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 重置資料,並重新整理檢視
                    resetData();
                    invalidate();
                    break;
                default:
                    break;
            }

        }
    };

    好了,handleLock,整個過程就結束了,筆者這裡定義了一個監聽器只是給大家提供一種思路,筆者將儲存的大路徑傳給了使用者,是為了保證使用者可以自己儲存密碼,並作相關操作,大家也可以使用 HandleLock 來  儲存密碼,不傳給使用者,根據自己的需求寫出更多更豐富的監聽器,而且這裡筆者在 MotionEvent.ACTION_UP 中直接回調了 drawFinish() 方法,就意味著要使用該 HandleLock 就必須給它設定監聽器。

    接下來我們說說 HandleLock 的使用,首先是在佈局檔案中使用:

<com.example.a01378359.testapp.lock.HandleLock
        android:id="@+id/handlelock_test"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    接下來是程式碼中使用:

handleLock = findViewById(R.id.handlelock_test);
        handleLock.setOnDrawFinishListener(new HandleLock.OnDrawFinishListener() {
            @Override
            public boolean drawFinish(String route) {
                // 第一次滑動,則儲存密碼
                if (count == 0){
                    password = route;
                    count++;
                    Toast.makeText(LockTestActivity.this,"已儲存密碼",Toast.LENGTH_SHORT).show();
                    return true;
                }else {
                    // 與儲存密碼比較,返回結果,並且做出相應事件
                    if (password.equals(route)){
                        Toast.makeText(LockTestActivity.this,"密碼正確",Toast.LENGTH_SHORT).show();
                        return true;
                    }else {
                        Toast.makeText(LockTestActivity.this,"密碼錯誤",Toast.LENGTH_SHORT).show();
                        return false;
                    }
                }
            }
        });
    專案地址:原始碼