Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android應用 手勢密碼的實現(三)

Android應用 手勢密碼的實現(三)

編輯:關於Android編程

本文目的如下:

1、加一個設置初始密碼的功能

2、讓手勢單點生效

3、讓繪制路徑的中間點自動加入軌跡(例如選中第一排的1位和3位時2位也能自動選中)

4、一些其它方面的優化

 

一、加入設置初始密碼的功能

設置初始密碼時,需要用戶先輸入一個合法的密碼(例如長度不小於4),然後提示輸入確認密碼,請求用戶再次輸入,直到再次輸入正確。

也就是說,第一次只做長度判斷,長度足夠就 setAnswer(),第二次就進入正常判斷流程 checkAnswer() ,因此我們需要一個 boolean 型來標識是否是初次設置密碼。

邏輯比較簡單,在 ACTION_UP 事件的回調裡判斷 firstSet 標志,為 false 時 checkAnswer(),為 true 時 setAnswer()。

首先我們需要一個 firstSet 標識和 set 方法:

 

// 默認false
private boolean isFirstSet = false;
// 是否是初次設置密碼
public void isFirstSet(boolean isFirstSet) {
    this.isFirstSet = isFirstSet;
}

 

 

接下來我們需要一個回調接口,在OnGestureLockViewListener接口裡新增一個onFirstSetPattern():
// listener
public interface OnGestureLockViewListener {
    // 單獨選中的元素的id
    void onBlockSelected(int cId);

    // 是否匹配
    void onGestureEvent(boolean matched);

    // 超過嘗試次數
    void onUnmatchedExceedBoundary();

    // 首次設置密碼
    void onFirstSetPattern(boolean patternOk);
}

我為了看起來方便就沒有遵循編碼規范,大家不要學我:P

另外這個onBlockSelected()個人感覺實際用處不大,刪掉也行。

 

ACTION_UP事件裡原來的回調部分是這樣的:

case MotionEvent.ACTION_UP:

    mPaint.setColor(mFingerUpColor);
    mPaint.setAlpha(255);
    this.mTryTimes--;

    // 回調是否成功
    if (mOnGestureLockViewListener != null && mChoose.size() > 0)
    {
        isAnswerRight = checkAnswer();
        mOnGestureLockViewListener.onGestureEvent(isAnswerRight);
        setViewColor(isAnswerRight);
        if (this.mTryTimes == 0)
        {
            mOnGestureLockViewListener.onUnmatchedExceedBoundary();
        }
    }

    ...

 

 

現在我們既然有setViewColor()方法那麼原來的mPaint.setColor和setAlpha就可以刪掉了,而且我想要密碼輸錯時再this.mTryTimes--(這樣可以避免最後一次輸對密碼時調用onUnmatchedExceedBoundary())

具體邏輯比較簡單,就不畫圖了(設置初始密碼時最小長度我設的4):

 

case MotionEvent.ACTION_UP:
    // 回調是否成功
    if (mOnGestureLockViewListener != null && mChoose.size() > 0) {
        //如果是初次設置圖案,不需要checkAnswer(),但需要setAnswer()
        if (isFirstSet) {
            boolean patternOk = !(mChoose.size() < 4);
            if (patternOk) {
                setAnswer(mChoose);
                isFirstSet = false;
            }
            setViewColor(patternOk);
            mOnGestureLockViewListener.onFirstSetPattern(patternOk);
        } else {
            isAnswerRight = checkAnswer();
            if (!isAnswerRight) {
                this.mTryTimes--;
            }
            setViewColor(isAnswerRight);
            mOnGestureLockViewListener.onGestureEvent(isAnswerRight);
            if (this.mTryTimes == 0) {
                // 剩余次數為0時進行一些處理(在使用該手勢密碼的activity中覆寫)
                mOnGestureLockViewListener.onUnmatchedExceedBoundary();
            }
        }
    }

    ...

有人可能注意到這裡的setAnswer()參數錯了,其實是我為了方便重新寫了個setAnswer()方法,也重新定義了mAnswer變量:

 

 

/**
 * 存儲答案
 */
private List mAnswer = new ArrayList<>();

/**
 * 設置答案
 * @param answer
 */
private void setAnswer(List answer) {
    // 直接用賦值語句賦的是對象的引用,新人比較容易坑在這種地方
    this.mAnswer.clear();
    for (int i = 0; i < answer.size(); i++) {
        this.mAnswer.add(answer.get(i));
    }
}

 

很明顯我不希望外部使用傳入List的方式設置答案,所以重載setAnswer()並對外公開。

由於服務器返回數據多為字符串,所以我這裡也采用String型,具體想怎樣設計答案的規范可以自由發揮:

 

/**
 * 對外公布設置答案的方法
 * @param answer
 */
public void setAnswer(String answer) {
    mAnswer.clear();
    for (int i = 0; i < answer.length(); i++) {
        mAnswer.add((int) answer.charAt(i) - 48);
    }
}

 

checkAnswer也得改:

 

private boolean checkAnswer() {
    return mChoose.equals(mAnswer);
}

接下來在MainActivity中更新代碼:

 

 

mGesture = (GestureLockViewGroup) findViewById(R.id.gesture_lock_view_group);
        mGesture.setAnswer("12369");
        mGesture.isFirstSet(true);// 調用
        mGesture.setOnGestureLockViewListener(new GestureLockViewGroup.OnGestureLockViewListener() {
            @Override
            public void onBlockSelected(int cId) {
            }

            @Override
            public void onGestureEvent(boolean matched) {
                Toast.makeText(mContext, "Matched:"+matched,Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onUnmatchedExceedBoundary() {
                Toast.makeText(mContext, "錯誤5次...",Toast.LENGTH_SHORT).show();
                mGesture.setUnMatchExceedBoundary(5);
            }

            @Override
            public void onFirstSetPattern(boolean patternOk) {
                if (patternOk) {
                    Toast.makeText(mContext, "請再次輸入以確認",Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(mContext, "需要四個點以上",Toast.LENGTH_SHORT).show();
                }
   }
});
當然很明顯因為有 isFirstSet(true),所以setAnswer("12369")語句已經無效了。

 

另外設置初始密碼時應該對 showPath 為 false 的情況進行規避,即設置初始密碼的前提下令showPath方法失效,由於set方法的調用有先後順序,因此兩個方法都要做改動:

 

// 對外公開的set方法
public void setShowPath(boolean showPath) {
    this.showPath = showPath || isFirstSet;
}
// 是否是初次設置密碼
public void isFirstSet(boolean isFirstSet) {
    this.isFirstSet = isFirstSet;
    setShowPath(true);// 強制顯示路徑
}
現在還有個問題就是確認時輸錯5次也會引發onUnmatchedExceedBoundary()回調,最簡單的解決方案就是在isFirstSet為true時設置mTryTimes為一個較大的數字,例如1000,或者外部調用已給的public方法setUnMatchExceedBoundary()。

 

 

二、單點生效

當前的手勢密碼控件在單擊某個點的時候並不會觸發答案判斷,而我想仿支付寶,所以先看看代碼。

 

ACTION_DOWN:

只有個reset(),而添加第一個點的代碼在 ACTION_MOVE 裡,因此只要不觸發 ACTION_MOVE,就無法添加第一個點,我們要做的就是點擊時就觸發MOVE事件的代碼,讓 ACTION_DOWN 走到 ACTION_MOVE 裡。

所以直接把 ACTION_DOWN 裡的 break 刪掉就好:
switch (action)
{
    case MotionEvent.ACTION_DOWN:
        // 重置
        reset();
    case MotionEvent.ACTION_MOVE:
        // 初始化畫筆為藍色
        setViewColor(true);
        GestureLockView child = getChildIdByPos(x, y);
        
        ...

 

三、讓繪制路徑中的點自動選擇

現在的手勢如果滑快一點,很容易出現如下這種狀況(第二排第三個點漏掉了):這是因為滑動過快導致的觸摸事件xy坐標值變動太快,造成某個區域都沒有觸發 ACTION_MOVE 事件,也就無法進行子View的添加了。

 

我想避免太過復雜的情況影響到用戶輸入,因此我想寫個方法,在每次獲取新child的cId時,先判斷這兩個點之間的直線上是否還有其余的點,如果有那麼先加上該點,需求簡單的時候(例如只有3*3)可以直接排列好幾種情況進行添加,但當行列數為4、5、...時,情況就復雜多了,一排可能忽略了兩個點三個點。

合適的算法我暫時也沒有,自己寫了個比較粗糙的方法,邏輯有點亂,注釋也懶得加了,有心的可以自己寫:

 

// n * n的陣列,首位從0起算,計算公式:cId = x + n*y + 1
private int checkChoose(int cId) {
    if (mChoose == null || mChoose.size() < 1) {
        return -1;
    }
    int lastX, lastY;
    int nowX, nowY;
    int lastId = mChoose.get(mChoose.size() - 1);

    lastX = (lastId - 1) % mCount;
    lastY = (lastId - 1) / mCount;

    nowX = (cId - 1) % mCount;
    nowY = (cId - 1) / mCount;

    int signX = compare(lastX, nowX);
    int signY = compare(lastY, nowY);
    // 比較x軸y軸間距
    int copiesX = (nowX - lastX) * signX;
    int copiesY = (nowY - lastY) * signY;
    int copies = copiesX > copiesY ? copiesY : copiesX;

    if (copiesX == 1 || copiesY == 1) {
        return -1;
    }

    if (signX == 0 || signY == 0) {
        return lastX + signX + (lastY + signY) * mCount + 1;
    }

    if (copies > 1 && copiesX % copies == 0 && copiesY % copies == 0) {
        return lastX + copiesX / copies * signX
                + (lastY + copiesY / copies * signY) * mCount + 1;
    }

    return -1;
}

private int compare(int last, int now) {
    if (now > last) {
        return 1;
    } else if (now < last) {
        return -1;
    } else {
        return 0;
    }
}
簡單說下邏輯(建議略過這一段):

 

checkChoose()方法取mChoose的前一個點(lastId)與當前選中的點(cId)進行比較,先根據Id值計算出該點對應的圓圈陣列橫縱坐標值。

例如,一個4*4的手勢陣列,前一個點的 lastId = 1 = 0 + 0*4 + 1,當前選中點 cId = 16 = 3 + 3*4 + 1,即 lastId坐標(0,0),cId坐標(3,3)。

那麼copiesX即為 |3-0| = +3,copiesY同理也為+3,copies取值較小的一方,copies為0說明在同一行或同一列(即signX==0 || signY==0),進行特殊處理,為1時說明兩點之間無遺漏,返回-1,大於1時進行邊的分割,長邊能整除短邊時說明有遺漏的點。

例如3 / 3 = 1說明長邊短邊都分成3份,那麼中間勢必遺漏了兩個點,返回時x坐標值 + signX *1,y坐標值+ signY *1。在圖例的前提下return 6,指第二排第二個點。

又例如 copiesX = 2,copiesY = 3時(n>=4),說明兩點之間的X軸相隔一排,Y軸相隔兩排,選中的兩個點作對角,構成的陣列是3*4,中間沒有能添加的點(左)。

又例如copiesX = 2,copiesY = 4時(n>=5),說明兩點之間的X軸相隔一排,Y軸相隔三排,選中的兩個點作對角,構成的陣列是3*5,那麼第三排的第二個點可以添加(右)。

\ \

邏輯十分蛋疼,讀者請自行梳理。。。

 

因為返回的是依序遞增的點,因此在 ACTION_MOVE 中是使用while 循環對返回值進行判斷,直到返回-1才退出:

 

case MotionEvent.ACTION_MOVE:
    // 初始化畫筆為藍色
    setViewColor(true);
    GestureLockView child = getChildIdByPos(x, y);

    if (child != null)
    {
        int cId = child.getId();
        if (!mChoose.contains(cId))
        {
            // 循環加入中間點
            int subId = checkChoose(cId);
            Log.e(TAG, "SubId:" +subId);
            while (subId != -1) {
                // 1、這部分代碼和以下 2 部分基本一樣,可以抽離出一個方法
                mChoose.add(subId);
                GestureLockView subChild = mGestureLockViews[subId - 1];
                subChild.setMode(GestureLockView.Mode.STATUS_FINGER_ON, showPath);
                // onBlockSelected(cId)這個回調目前基本沒用,不想要可以考慮刪掉
                if (mOnGestureLockViewListener != null) {
                    mOnGestureLockViewListener.onBlockSelected(cId);
                }
                // 設置指引線的起點
                mLastPathX = subChild.getLeft() / 2 + subChild.getRight() / 2;
                mLastPathY = subChild.getTop() / 2 + subChild.getBottom() / 2;
                // 非第一個,將兩者使用線連上
                mPath.lineTo(mLastPathX, mLastPathY);
                // 繼續循環
                subId = checkChoose(cId);
            }

            // 2、中間點加入完成,繼續加入當前選擇的點
            mChoose.add(cId);
            child.setMode(GestureLockView.Mode.STATUS_FINGER_ON, showPath);
            if (mOnGestureLockViewListener != null)
                mOnGestureLockViewListener.onBlockSelected(cId);
            // 設置指引線的起點
            mLastPathX = child.getLeft() / 2 + child.getRight() / 2;
            mLastPathY = child.getTop() / 2 + child.getBottom() / 2;

            if (mChoose.size() == 1) {
                // 當前添加為第一個
                mPath.moveTo(mLastPathX, mLastPathY);
            } else {
                // 非第一個,將兩者使用線連上
                mPath.lineTo(mLastPathX, mLastPathY);
            }
        }
    }
    // 指引線的終點
    mTmpTarget.x = x;
    mTmpTarget.y = y;
    break;

到這裡就結束了,最後的效果,不論如何連接,中間的點總是可以自行填補的:

四、一些其它方面的優化與修改

例如添加一些方法為自己的APP定制功能,降低耦合和冗余代碼,或者修改下功能外觀等等(例如在checkPositionInChild()中修改靈敏度參數等)還有優化下邏輯算法等等,主要就是自己看代碼了。

好吧這條就只是湊個字數。。。

下一篇文章會講講手勢密碼在APP中的應用。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved