Android一步一步剖析+實現(xiàn)仿支付寶手勢密碼自定義View

最近項目需求:要求在項目中添加手勢密碼和指紋驗證,恰巧最近在苦練自定義View卸勺,于是參考了網(wǎng)上輪子和自己的理解,實現(xiàn)了如下的效果。
國際慣例:Without pic you say a JB(獎杯).


GIF.gif

這GIF做的是真的垃圾逻族,感興趣的去看Demo把,后面我會放上鏈接的骄崩。

一聘鳞、分析效果圖:
所有的自定義view都是通過分析效果圖薄辅,一點一點將效果圖分解成一個個模塊,然后單個模塊實現(xiàn)抠璃,最后拼裝成一個整體站楚,下面就通過手勢密碼的效果圖我們來剖析一波吧。


one.png

從上圖我們可以把View剖析如下:
(1)View的總大小我們可以通過手勢大View的寬度+手勢小View的高度搏嗡,通過onMeasure方法setMeasuredDimension(witdh窿春,width+minHeight)來賦予布局大小,minHeight可以根據(jù)實際情況自己賦值采盒。
(2)手勢大View可以通過onMeasure方法旧乞,通過比較寬高得到最小值,來設(shè)置手勢大View的正方形大小
(3)手勢小View同理于手勢大View磅氨,提示文字的位置尺栖,我們也很容易確認(rèn)。
(4)手勢大view的寬高得到了烦租,那么手勢大view每一個手勢點的坐標(biāo)和大小 我們就很容易得到延赌。
(5)相信大家初學(xué)Java的時候肯定做過,用 * 號打印各種圖形的操作叉橱,手勢view相當(dāng)于一個簡單的3*3矩陣挫以。我們知道了大小和坐標(biāo)很容易畫出來。
(6)小View也同理 可以實現(xiàn)窃祝,需要注意的是 在手勢密碼第一次注冊的時候存在小View掐松,在認(rèn)證的時候無小view,我們可以根據(jù)狀態(tài)锌杀,在onDraw中設(shè)置隱藏甩栈。

二、分析完后糕再,我們就一步一步來實現(xiàn)吧:

1量没、首先模板應(yīng)該具有通用性與可定制性,我們需要定義一個attrs突想。

通過效果圖分析殴蹄,我定義的attrs如下,在這里面猾担,手勢點我采用的是圖片(圖片可以讓手勢點更酷炫)

<declare-styleable name="SecurityCenter">
        <!-- 選中狀態(tài)的手勢點-->
        <attr name="selectedBitmap" format="reference"/>
        <!-- 未選中狀態(tài)的手勢點-->
        <attr name="unselectedBitmap" format="reference"/>
        <!-- 選中狀態(tài)的手勢小點-->
        <attr name="selectedBitmapSmall" format="reference"/>
        <!-- 未選中狀態(tài)的手勢小點-->
        <attr name="unselectedBitmapSmall" format="reference"/>
        <!-- 驗證失敗后再次驗證的攔截時間-->
        <attr name="waitTime" format="integer"/>
        <!-- 驗證的最大失敗次數(shù)-->
        <attr name="maxFailCounts" format="integer"/>
        <!-- 繪制時最少連接的點數(shù)-->
        <attr name="minPoint" format="integer"/>
        <!-- 字體的顏色-->
        <attr name="paintColor" format="color"/>
        <!-- 字體的大小-->
        <attr name="paintTextSize" format="dimension"/>
    </declare-styleable>

2袭灯、在View中接收賦值,點數(shù)圖片我用的bitmap绑嘹,如果無具體定義稽荧,這些屬性都會給他默認(rèn)值。代碼如下:

 public ChaosGestureView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.SecurityCenter);
        Drawable dw_selected = ta.getDrawable(R.styleable.SecurityCenter_selectedBitmap);
        Drawable dw_unSeclect = ta.getDrawable(R.styleable.SecurityCenter_unselectedBitmap);
        Drawable dw_selected_small = ta.getDrawable(R.styleable.SecurityCenter_selectedBitmapSmall);
        Drawable dw_unSeclect_small = ta.getDrawable(R.styleable.SecurityCenter_unselectedBitmapSmall);
        if (dw_selected!=null){
            selectedBitmap = ((BitmapDrawable) dw_selected).getBitmap();
        }else{
            selectedBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_finger_selected);
        }
        if (dw_unSeclect!=null){
            unSelectedBitmap = ((BitmapDrawable) dw_unSeclect).getBitmap();
        }else{
            unSelectedBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_finger_unselected);
        }
        if (dw_selected_small!=null){
            selectedBitmapSmall = ((BitmapDrawable) dw_selected_small).getBitmap();
        }else{
            selectedBitmapSmall = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_finger_selected_small);
        }
        if (dw_unSeclect_small!=null){
            unSelectedBitmapSmall= ((BitmapDrawable) dw_unSeclect_small).getBitmap();
        }else{
            unSelectedBitmapSmall = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_finger_unselected_new);
        }
        //等待時間,默認(rèn)30s
        waitTime = ta.getInteger(R.styleable.SecurityCenter_waitTime,30);
        //嘗試次數(shù)工腋,默認(rèn)5
        tempCount = ta.getInteger(R.styleable.SecurityCenter_maxFailCounts,5);
        //最小設(shè)置的點,默認(rèn)4個
        minPointNums = ta.getInteger(R.styleable.SecurityCenter_minPoint,4);
        //設(shè)置畫筆的顏色
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStrokeWidth(10);
        mPaint.setStyle(Paint.Style.STROKE);
        //畫筆的顏色
        int color = ta.getColor(R.styleable.SecurityCenter_paintColor, context.getResources().getColor(R.color.black));
        mPaint.setColor(color);
        //字體的大小
        float textsize = ta.getDimension(R.styleable.SecurityCenter_paintTextSize, 40);
        mPaint.setTextSize(textsize);
        //避免重新創(chuàng)建時候的錯誤
        ta.recycle();

        initView(context);
    }

3姨丈、在onMeasure中繪測手勢View布局大小,通過最開始的分析畅卓,都很容易理解。

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //width即為大View的單位寬 高
        int width = Math.min(widthSize, heightSize);
        if (widthMode == MeasureSpec.UNSPECIFIED) {
            width = heightSize;
        } else if (heightMode == MeasureSpec.UNSPECIFIED) {
            width = widthSize;
        }
        //大View一行3*1單位行高
        mLineHeight = width / 3;
        //大手勢View為邊長width的正方形,panelHeight是給小手勢view預(yù)留的空間
        setMeasuredDimension(width, width + panelHeight);
    }

4蟋恬、通過onSizeChange方法可以獲取翁潘,根據(jù)mLineHeight(3*1的行高) 的值,可以定義大手勢密碼點和小手勢密碼點的寬高歼争,然后通過Bitmap.createScaledBitmap方法拜马,設(shè)置好手勢點的大小圖。細(xì)節(jié)可以看代碼注解如下:

 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mPanelWidth = Math.min(w, h);
        //大手勢點寬度沐绒,為單位寬高的0.6倍俩莽,顯得更好看一些不會很滿
        pieceWidth = (int) (mLineHeight * 0.6f);
        //小手勢點寬度,同理
        pieceWidthSmall = (int) (mLineHeight * 0.15f);
        //畫出對應(yīng)手勢點的大小
        selectedBitmap = Bitmap.createScaledBitmap(selectedBitmap, (int) pieceWidth, (int) pieceWidth, false);
        unSelectedBitmap = Bitmap.createScaledBitmap(unSelectedBitmap, (int) pieceWidth, (int) pieceWidth, false);
        selectedBitmapSmall = Bitmap.createScaledBitmap(selectedBitmapSmall, (int) pieceWidthSmall, (int) pieceWidthSmall, false);
        unSelectedBitmapSmall = Bitmap.createScaledBitmap(unSelectedBitmapSmall, (int) pieceWidthSmall, (int) pieceWidthSmall, false);
    }

5洒沦、我們知道GestureView一般分為兩種狀態(tài)豹绪,一種是注冊狀態(tài)(包含小view的那種),另一種是認(rèn)證狀態(tài)(不包含小view的那種)申眼,所以我們要定義兩種狀態(tài),來區(qū)分使用情況蝉衣。

 //手勢初始化錄入狀態(tài)
    public static final int STATE_REGISTER = 101;
    //手勢確認(rèn) 使用狀態(tài)
    public static final int STATE_LOGIN = 100;
    //設(shè)置一個參數(shù)記錄當(dāng)前是出于初始化階段還是使用階段括尸,默認(rèn)為確認(rèn)狀態(tài)
    private int stateFlag = STATE_LOGIN;

那么我們在每次注冊成功的時候,需要保存手勢狀態(tài)病毡,我們給狀態(tài)存在SharedPreferences中

 //成功后保存狀態(tài)
    private boolean saveState() {
        SharedPreferences sp = mContext.getSharedPreferences("STATE_DATA", Activity.MODE_PRIVATE);
        SharedPreferences.Editor edit = sp.edit();
        edit.putInt("state", stateFlag);
        return edit.commit();
    }

同理在初始化之前濒翻,我們要得到狀態(tài),判斷當(dāng)前view屬于什么狀態(tài)啦膜,這樣才能判斷onDraw中是否繪制小View

 //從SP中獲取當(dāng)前View處于什么狀態(tài)有送,默認(rèn)為初始化狀態(tài)
    private int getState() {
        SharedPreferences mSharedPreference = mContext.getSharedPreferences("STATE_DATA", Activity.MODE_PRIVATE);
        return mSharedPreference.getInt("state", STATE_REGISTER);
    }

6、根據(jù)狀態(tài)繪制手勢密碼點僧家,連接線雀摘,和提示文字

(1)繪制9個未選中狀態(tài)的大手勢View點,通過canvas.drawBitmap(Bitmap bitmap, float left, float top, Paint paint)方法繪制八拱,這里我們要注意位置計算的時候阵赠,只需要注意在android屏幕坐標(biāo)系里,左上角的位置是(0肌稻,0)清蚀,往右往下為正。

for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                canvas.drawBitmap(unSelectedBitmap, (float) (mLineHeight * (j + 0.5) - pieceWidth / 2), (float) (mLineHeight * (i + 0.5) - pieceWidth / 2 + panelHeight), mPaint);
            }
        }

可能這么說有點抽象爹谭,不過也就是把坐標(biāo)搞清楚了還是很簡單的枷邪,畫張圖配合你理解,圖中小View預(yù)留高度(panelHeight):


two.png

(2)可能到現(xiàn)在你就會很好奇诺凡,我設(shè)置的手勢連線到底是怎么存儲和校驗的呢东揣?問的好药薯! 這個問題我開始也思考了很久,有輪子是通過一個二維數(shù)組實現(xiàn)的救斑,通過這個二維數(shù)組我來了思路童本,聯(lián)想到了Bean。我用Bean存儲對應(yīng)點的X和Y的坐標(biāo)脸候,把每個點的實例加入一個List<>中穷娱,就完成了手勢繪制所有點的存儲。

bean的代碼如下:

//定義Bean运沦,來存儲手勢坐標(biāo)
    public class GestureBean {
        private int x;
        private int y;

        @Override
        public String toString() {
            return "GestureBean{" +
                    "x=" + x +
                    ", y=" + y +
                    '}';
        }

        public GestureBean(int x, int y) {
            this.x = x;
            this.y = y;
        }

        public int getX() {
            return x;
        }

        public void setX(int x) {
            this.x = x;
        }

        public int getY() {
            return y;
        }

        public void setY(int y) {
            this.y = y;
        }

        @Override
        public boolean equals(Object o) {
            return ((GestureBean) o).getX() == x && ((GestureBean) o).getY() == y;
        }
    }

(3)繪制連接線和選中點:連接線是通過過 onTouchEvent和onDraw泵额,配合畫出的,在OnTouchEvent中手指經(jīng)過的點都會存在listDatas集合中携添,再通過 invalidate();方法通知onDraw嫁盲,根據(jù)listDatas中的新增點數(shù),來繪制出選中點和點之間的連接線烈掠。再此只給出onDraw中的代碼羞秤,onTouchEvent中的邏輯會在下文詳細(xì)說明。

 //用于判斷狀態(tài)
        GestureBean firstGestrue = null;
        GestureBean currGestrue = null;
        if (!listDatas.isEmpty()) {

            firstGestrue = listDatas.get(0);
           //畫連接線
            for (int i = 1; i < listDatas.size(); i++) {
                currGestrue = listDatas.get(i);
                canvas.drawLine((float) (mLineHeight * (firstGestrue.getX() + 0.5)), (float) (mLineHeight * (firstGestrue.getY() + 0.5) + panelHeight), (float) (mLineHeight * (currGestrue.getX() + 0.5)), (float) (mLineHeight * (currGestrue.getY() + 0.5) + panelHeight), mPaint);
                firstGestrue = currGestrue;
            }
            //最后一條線
            lastGestrue = listDatas.get(listDatas.size() - 1);
            canvas.drawLine((float) (mLineHeight * (lastGestrue.getX() + 0.5)), (float) (mLineHeight * (lastGestrue.getY() + 0.5) + panelHeight), currX, currY, mPaint);

            //遍歷數(shù)組左敌,把把選中的點更換圖片
            for (GestureBean bean : listDatas) {
                canvas.drawBitmap(selectedBitmap, (float) (mLineHeight * (bean.getX() + 0.5) - pieceWidth / 2), (float) (mLineHeight * (bean.getY() + 0.5) + panelHeight - pieceWidth / 2), mPaint);
            }
        }

注冊手勢成功的時候需要將手勢集合瘾蛋,保存,用于下一次校驗矫限,我們存在SharedPreference中(注意:這個手勢View只適用于本機攔截哺哼,所以存SharedPresference就夠了)

 //將點的xy list存入sp
    private boolean saveToSharedPrefference(List<GestureBean> data) {
        SharedPreferences sp = mContext.getSharedPreferences("GESTURAE_DATA", Activity.MODE_PRIVATE);
        SharedPreferences.Editor edit = sp.edit();
        //存入多少個點
        edit.putInt("data_size", data.size()); /*sKey is an array*/
        //和每個店的坐標(biāo)
        for (int i = 0; i < data.size(); i++) {
            edit.remove("data_" + i);
            edit.putString("data_" + i, data.get(i).getX() + " " + data.get(i).getY());
        }
        return edit.commit();
    }

獲取存儲集合:,我們再存的時候和取得時候,可以先存一個錄入點的數(shù)量叼风,更方便做判斷取董。

 //讀取之前保存的List
    public List<GestureBean> loadSharedPrefferenceData() {
        List<GestureBean> list = new ArrayList<>();
        SharedPreferences mSharedPreference = mContext.getSharedPreferences("GESTURAE_DATA", Activity.MODE_PRIVATE);
        //取出點數(shù)
        int size = mSharedPreference.getInt("data_size", 0);
        //和坐標(biāo)
        for (int i = 0; i < size; i++) {
            String str = mSharedPreference.getString("data_" + i, "0 0");
            list.add(new GestureBean(Integer.parseInt(str.split(" ")[0]), Integer.parseInt(str.split(" ")[1])));
        }
        return list;
    }

(4) 小圖和文字的繪制就很簡單了,參考前幾項 我就直接給代碼了(剛才開會无宿,思路被干擾了茵汰。。懈贺。经窖。)

 //如果處于初始化狀態(tài)
        if (stateFlag == STATE_REGISTER) {
            //繪制上面的提示點  不需要提示點
            drawTipsPoint(canvas);
        } else {
            //上面的是文字 點沒了
            drawTipsText(canvas);
        }

需要注意的是,小View在完成第一次繪制的時候梭灿,第二次繪制的時候需要保存第一次的樣式画侣,通過list存儲比較,如下代碼實現(xiàn)堡妒。

 //繪制提示點
    private void drawTipsPoint(Canvas canvas) {
        //寬度為View寬度的一半
        float widthMiddleX = mPanelWidth / 2;
        //確定好相關(guān)坐標(biāo)配乱,找出第一個點的中心點
        float firstX = widthMiddleX - pieceWidthSmall / 4 - pieceWidthSmall / 2 - pieceWidthSmall;
        float firstY = panelHeight / 2 - pieceWidthSmall / 2 - pieceWidthSmall - pieceWidthSmall / 4 - 10;
        //畫點,由于沒有選中,畫9個未選中點
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                canvas.drawBitmap(unSelectedBitmapSmall, (float) (firstX + j * (pieceWidthSmall * 1.25)), (float) (firstY + i * (pieceWidthSmall * 1.25)), mPaint);
            }
        }
        //第二次確認(rèn)前的小手勢密碼·顯示第一次劃過的痕跡
        if (listDatasCopy != null && !listDatasCopy.isEmpty()) {
            for (GestureBean bean : listDatasCopy) {
                canvas.drawBitmap(selectedBitmapSmall, (float) (firstX + bean.getX() * (pieceWidthSmall * 1.25)), (float) (firstY + bean.getY() * (pieceWidthSmall * 1.25)), mPaint);
            }
        }
        //隨著手指ActionMove來改變選中點的顏色
        else if (listDatas != null && !listDatas.isEmpty()) {
            for (GestureBean bean : listDatas) {
                canvas.drawBitmap(selectedBitmapSmall, (float) (firstX + bean.getX() * (pieceWidthSmall * 1.25)), (float) (firstY + bean.getY() * (pieceWidthSmall * 1.25)), mPaint);
            }
        }
        drawMessage(canvas, "繪制解鎖圖案", mError);
    }

效果圖如下:


three.png

繪制文字搬泥,確定好大體坐標(biāo)就可了桑寨,在小view下面,很好理解忿檩,直接給代碼了:


    //繪制提示語
    private void drawTipsText(Canvas canvas) {
        float widthMiddleX = mPanelWidth / 2;
        mPaint.setStyle(Paint.Style.FILL);
        int widthStr1 = (int) mPaint.measureText("輸入手勢來解鎖");
        float baseX = widthMiddleX - widthStr1 / 2;
        float baseY = panelHeight / 2 + 50;
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        float fontTotalHeight = fontMetrics.bottom - fontMetrics.top;
        float offY = fontTotalHeight / 2 - fontMetrics.bottom - 30;
        float newY = baseY + offY;
        canvas.drawText("輸入手勢來解鎖", baseX, newY, mPaint);
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStrokeWidth(10);
    }

7尉尾、手勢密碼設(shè)置,必然要處理OnTouchEvent燥透,這里的邏輯才是關(guān)鍵沙咏,我會詳細(xì)分析。

(1)在這里我們封裝的比較完善班套,我處理了驗證超過驗證次數(shù)會攔截手勢View肢藐,這里面算是后期完善,但是為大家梳理思路的話就顯得比較冗余吱韭,直接貼代碼吆豹,先pass掉,敢興趣去看demo:

 if (mTimeOut) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    break;
                case MotionEvent.ACTION_MOVE:
                    break;
                case MotionEvent.ACTION_UP:
                    if (0 < leftTime && leftTime <= 30) {
                        AlertUtil.t(mContext, "嘗試次數(shù)達(dá)到最大," + leftTime + "s后重試");
                    }

                    return true;
            }
        }

(2)首先我們要判斷我們的OnTouch事件理盆,是否在大View的范圍內(nèi)痘煤,由于坐標(biāo)開始規(guī)范的很清楚,這個很好判斷

   if (event.getY() >= ((mLineHeight * (0 + 0.5) - pieceWidth / 2 + panelHeight))){
  //得到XY用于判斷 手指處于哪個點
            int x = (int) ((event.getY() - panelHeight) / mLineHeight);
            int y = (int) (event.getX() / mLineHeight);

            //當(dāng)前手指的坐標(biāo)
            currX = event.getX();
            currY = event.getY();
}

(3)MotionEvent.ACTION_DOWN: 當(dāng)手指按下去的時候熏挎,我們要判斷按下去的點速勇,處于哪一個大手勢點范圍內(nèi),并把它加入List<Bean>中坎拐。通知onDraw重繪,如上文所說的那樣养匈,把點改為選中狀態(tài)哼勇。

case MotionEvent.ACTION_DOWN:
                    lastGestrue = null;

                    if (currX >= 0 && currX <= mPanelWidth && currY >= panelHeight && currY <= panelHeight + mPanelWidth) {
                        if (currY <= (x + 0.5) * mLineHeight + pieceWidth / 2 + panelHeight && currY >= (x + 0.5) * mLineHeight - pieceWidth / 2 + panelHeight &&
                                currX <= (y + 0.5) * mLineHeight + pieceWidth / 2 && currX >= (y + 0.5) * mLineHeight - pieceWidth / 2) {
                            //判斷當(dāng)前手指處于哪個點范圍內(nèi),如果點沒存在listData,存進(jìn)去呕乎,第一個點
                            if (!listDatas.contains(new GestureBean(y, x))) {
                                listDatas.add(new GestureBean(y, x));
                            }
                        }
                    }
                    //重繪一次积担,第一個點顯示被選中了
                    invalidate();
                    break;

(4)MotionEvent.ACTION_MOVE: 手指在View上滑動,滑動到哪個點就把猬仁,哪個點的坐標(biāo)add到List<Baen>中帝璧。在通知重繪

 case MotionEvent.ACTION_MOVE:
                    //手指移動在大View范圍內(nèi)
                    if (currX >= 0 && currX <= mPanelWidth && currY >= panelHeight && currY <= panelHeight + mPanelWidth) {
                        //縮小響應(yīng)范圍 在此處需要注意的是 x跟currX在物理方向上是反的哦
                        if (currY <= (x + 0.5) * mLineHeight + pieceWidth / 2 + panelHeight && currY >= (x + 0.5) * mLineHeight - pieceWidth / 2 + panelHeight &&
                                currX <= (y + 0.5) * mLineHeight + pieceWidth / 2 && currX >= (y + 0.5) * mLineHeight - pieceWidth / 2) {
                            //滑倒的店處于哪個點范圍內(nèi),如果點沒存在listData,存進(jìn)去
                            if (!listDatas.contains(new GestureBean(y, x))) {
                                listDatas.add(new GestureBean(y, x));
//
                            }
                        }
                    }
                    //重繪
                    invalidate();
                    break;

(5)MotionEvent.ACTION_UP: 分為兩種情況湿刽,
1的烁、認(rèn)證狀態(tài),會從loadSharedPrefferenceData獲取到以前的listdatas做比較诈闺,判斷是否成功
2渴庆、注冊狀態(tài),會比較第一次的listdatas,來判斷兩次驗證是否一致,從而襟雷,處理成功和失敗的邏輯刃滓。

 case MotionEvent.ACTION_UP:
                    if (lastGestrue != null) {
                        currX = (float) ((lastGestrue.getX() + 0.5) * mLineHeight);
                        currY = (float) ((lastGestrue.getY() + 0.5) * mLineHeight);
                    }

                    //如果View處于認(rèn)證狀態(tài)
                    if (stateFlag == STATE_LOGIN) {
                        //相同那么認(rèn)證成功
                        if (listDatas.equals(loadSharedPrefferenceData())) {
                            mError = false;
                            postListener(true);
                            invalidate();
                            listDatas.clear();
                            return true;
                        } else {

                            if (--tempCount == 0) {//嘗試次數(shù)達(dá)到上限
                                mError = true;
                                mTimeOut = true;
                                listDatas.clear();
                                Date date = new Date();
                                PreferenceCache.putGestureTime(date.getTime());
                                mTimerTask = new InnerTimerTask(handler);
                                mTimer.schedule(mTimerTask, 0, 1000);
                                invalidate();
                                return true;
                            }
                            mError = true;
                            AlertUtil.t(mContext, "手勢錯誤,還可以再輸入" + tempCount + "次");
                            listDatas.clear();
                        }

                    }
                    //View處于注冊狀態(tài)
                    else if (stateFlag == STATE_REGISTER) {
                        //第一次認(rèn)證狀態(tài)
                        if (listDatasCopy == null || listDatasCopy.isEmpty()) {
                            if (listDatas.size() < minPointNums) {
                                listDatas.clear();
                                mError = true;
                                AlertUtil.t(mContext, "點數(shù)不能小于" + minPointNums + "個");
                                invalidate();
                                return true;
                            }
                            listDatasCopy.addAll(listDatas);
                            listDatas.clear();
                            mError = false;
                            AlertUtil.t(mContext, "請再一次繪制");
                        } else {
                            //兩次認(rèn)證成功
                            if (listDatas.equals(listDatasCopy)) {
                                saveToSharedPrefference(listDatas);
                                mError = false;
                                stateFlag = STATE_LOGIN;
                                postListener(true);
                                saveState();
                            } else {
                                mError = true;
                                AlertUtil.t(mContext, "與上次手勢繪制不一致,請重新設(shè)置");
                            }
                            listDatas.clear();
                            invalidate();
                            return true;
                        }
                    }
                    invalidate();
                    break;

至此,手勢View的所有邏輯大概已經(jīng)清楚了耸弄,下面做的是需要完善咧虎。

三·、完善View计呈,設(shè)置接口調(diào)用砰诵,失敗倒計時,和關(guān)閉View是清理當(dāng)前SP緩存震叮。

1胧砰、仔細(xì)看的朋友會發(fā)現(xiàn),ACTION.UP中有 postListener(true)這些東西苇瓣,這就是我定義的接口尉间,來返回認(rèn)證狀態(tài)。
(1)定義接口,三個參分別為:view所處狀態(tài)击罪,存儲的List哲嘲,是否成功(注冊或認(rèn)證)

 //定義接口 ,傳遞View狀態(tài)
    public interface GestureCallBack{
        void gestureVerifySuccessListener(int stateFlag, List<GestureBean> data, boolean success);
    }

(2)實例化接口媳禁,當(dāng)前Activity必須繼承接口

 //讓當(dāng)前的Activity繼承View的接口
        try {
            gestureCallBack = (GestureCallBack) mContext;
        } catch (final ClassCastException e) {
            throw new ClassCastException(mContext.toString() + " must implement GestureCallBack");
        }

(3)給接口傳遞眠副,實時數(shù)據(jù)Action.UP

  //給接口傳遞數(shù)據(jù)
    private void postListener(boolean success) {
        if (gestureCallBack != null) {
            gestureCallBack.gestureVerifySuccessListener(stateFlag, listDatas, success);
        }
    }

2、驗證失敗倒計時竣稽,通過Handler囱怕,Timer和TimerTask實現(xiàn)
(1)、內(nèi)部類TimeTask

 //定義一個內(nèi)部TimerTask類用于記錄毫别,錯誤倒計時
    static class InnerTimerTask extends TimerTask{
        Handler handler;

        public InnerTimerTask(Handler handler) {
            this.handler = handler;
        }

        @Override
        public void run() {
            handler.sendMessage(handler.obtainMessage());
        }
    }

(2)實例化娃弓,上次錯誤時間也是存儲在SharedPreference,waiTime為定義好的超時時間岛宦。代碼如下

 mTimer = new Timer();
        //計算上次失敗時間與現(xiàn)在的時間差
        try {
            long lastTime = PreferenceCache.getGestureTime();
            Date date = new Date();
            if (lastTime !=0 && (date.getTime()-lastTime)/1000<waitTime){
                //失敗時間未到台丛,還處于鎖定狀態(tài)
                mTimeOut = true;
                leftTime = (int)(waitTime-((date.getTime()-lastTime))/1000);
                mTimerTask = new InnerTimerTask(handler);
                mTimer.schedule(mTimerTask,0,1000);
            }else{
                mTimeOut = false;
                leftTime = waitTime;
            }

        }catch (RuntimeException e){
            e.printStackTrace();
        }

(3)Handler處理消息:

 //接受TimerTask消息,通知UI
    private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
           leftTime--;
           if (leftTime == 0){
               if (mTimer != null)
                   mTimerTask.cancel();
               mTimeOut = false;
               AlertUtil.t(mContext,"請繪制解鎖圖案");
               mError = false;
               invalidate();
               //將計時信息還原
               reSet();
               return;
           }
           mError = true;
           invalidate();
        }
    };

3、清理手勢View緩存砾肺,用于關(guān)閉View或者修改密碼

//清除以前保存的狀態(tài)挽霉,用于關(guān)閉View
   public boolean clearCache() {
       SharedPreferences sp = mContext.getSharedPreferences("STATE_DATA", Activity.MODE_PRIVATE);
       SharedPreferences.Editor edit = sp.edit();
       edit.putInt("state", STATE_REGISTER);
       stateFlag = STATE_REGISTER;
       invalidate();
       return edit.commit();
   }
   //用于更改手勢密碼,清除以前密碼
   public boolean clearCacheLogin() {
       SharedPreferences sp = mContext.getSharedPreferences("STATE_DATA", Activity.MODE_PRIVATE);
       SharedPreferences.Editor edit = sp.edit();
       edit.putInt("state", STATE_LOGIN);
       stateFlag = STATE_LOGIN;
       invalidate();
       return edit.commit();
   }

四变汪、簡單使用:
1侠坎、以設(shè)置手勢密碼 為例:
(1)XML布局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.chaos.chaossecuritycenter.activity.SettingPatternPswActivity">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="50dp">
        <TextView
            android:id="@+id/tv_setting_back"
            android:layout_marginLeft="10dp"
            android:textSize="16sp"
            android:drawableLeft="@mipmap/back"
            android:textColor="@color/bak_blue"
            android:gravity="center_vertical"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:text="返回"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="設(shè)置手勢密碼"
            android:textColor="@color/black"
            android:textSize="20sp" />
    </RelativeLayout>


    <com.chaos.chaossecuritycenter.weight.ChaosGestureView
        android:id="@+id/gesture"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginLeft="40dp"
        android:layout_marginRight="40dp"
        android:layout_marginTop="40dp"
        android:layout_weight="1"
        app:selectedBitmap="@mipmap/icon_finger_selected"
        app:unselectedBitmap="@mipmap/icon_finger_unselected"
        app:selectedBitmapSmall="@mipmap/icon_finger_selected_small"
        app:unselectedBitmapSmall="@mipmap/icon_finger_unselected_new"
        app:waitTime="30"
        app:maxFailCounts="5"
        app:minPoint="4"
        app:paintColor="@color/bak_blue"
        app:paintTextSize="15sp"
       />
</LinearLayout>

布局預(yù)覽:


four.png

Java代碼(設(shè)置手勢密碼頁面):

public class SettingPatternPswActivity extends AppCompatActivity implements ChaosGestureView.GestureCallBack{
    private TextView tv_back;
    private ChaosGestureView gestureView;
    private int jumpFlg;
    private int flag;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_setting_pattern_psw);
        jumpFlg = getIntent().getIntExtra("jumpFlg", 0);
        flag = getIntent().getIntExtra("flag", 0);
        initView();
    }

    private void initView() {
        tv_back = (TextView) findViewById(R.id.tv_setting_back);
        gestureView = (ChaosGestureView) findViewById(R.id.gesture);
        gestureView.setGestureCallBack(this);
        //不調(diào)用這個方法會造成第二次啟動程序直接進(jìn)入手勢識別而不是手勢設(shè)置
        gestureView.clearCache();
        tv_back.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                finish();
            }
        });
    }

    @Override
    public void gestureVerifySuccessListener(int stateFlag, List<ChaosGestureView.GestureBean> data, boolean success) {
        if (stateFlag == GestureView.STATE_LOGIN) {
            PreferenceCache.putGestureFlag(true);
            finish();
        }
    }

    @Override
    public void onPointerCaptureChanged(boolean hasCapture) {

    }
}

五:總結(jié)
這個自定義手勢密碼,是我參照個別輪子+我本人的理解疫衩,仿照支付寶手勢密碼設(shè)計的硅蹦,整體流暢我已帶大家分析了一波荣德。該View還有很多需要完善的地方,我以后會慢慢完善童芹,有什么指教或者疑問涮瞻,請大家在下面留言。

Demo:
1假褪、手勢密碼自定義View
2署咽、指紋驗證(由于仿支付寶安全的Demo,含指紋我就一塊做了生音,用的三方)

Demo地址:https://github.com/ChaosOctopus/ChaosSecurityCenter

如果覺得對您有用宁否,請給我一個贊,或者一個Star缀遍。


US2WT57LJJS5THY%NWL9VG5.jpg
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末慕匠,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子域醇,更是在濱河造成了極大的恐慌台谊,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件譬挚,死亡現(xiàn)場離奇詭異锅铅,居然都是意外死亡,警方通過查閱死者的電腦和手機减宣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門盐须,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人漆腌,你說我怎么就攤上這事贼邓。” “怎么了闷尿?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵立帖,是天一觀的道長。 經(jīng)常有香客問我悠砚,道長,這世上最難降的妖魔是什么堂飞? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任灌旧,我火速辦了婚禮,結(jié)果婚禮上绰筛,老公的妹妹穿的比我還像新娘枢泰。我一直安慰自己,他們只是感情好铝噩,可當(dāng)我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布衡蚂。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪毛甲。 梳的紋絲不亂的頭發(fā)上年叮,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天,我揣著相機與錄音玻募,去河邊找鬼只损。 笑死,一個胖子當(dāng)著我的面吹牛七咧,可吹牛的內(nèi)容都是我干的跃惫。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼艾栋,長吁一口氣:“原來是場噩夢啊……” “哼爆存!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蝗砾,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤先较,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后遥诉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拇泣,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年矮锈,在試婚紗的時候發(fā)現(xiàn)自己被綠了霉翔。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡苞笨,死狀恐怖债朵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瀑凝,我是刑警寧澤序芦,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站粤咪,受9級特大地震影響谚中,放射性物質(zhì)發(fā)生泄漏建瘫。R本人自食惡果不足惜缩多,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望狐援。 院中可真熱鬧囊拜,春花似錦某筐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽身诺。三九已至,卻和暖如春抄囚,著一層夾襖步出監(jiān)牢的瞬間霉赡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工怠苔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留同廉,地道東北人。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓柑司,卻偏偏與公主長得像迫肖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子攒驰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,685評論 2 360

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