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

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


GIF.gif

這GIF做的是真的垃圾,感興趣的去看Demo把,后面我會(huì)放上鏈接的。

一、分析效果圖:
所有的自定義view都是通過(guò)分析效果圖,一點(diǎn)一點(diǎn)將效果圖分解成一個(gè)個(gè)模塊,然后單個(gè)模塊實(shí)現(xiàn),最后拼裝成一個(gè)整體,下面就通過(guò)手勢(shì)密碼的效果圖我們來(lái)剖析一波吧。


one.png

從上圖我們可以把View剖析如下:
(1)View的總大小我們可以通過(guò)手勢(shì)大View的寬度+手勢(shì)小View的高度,通過(guò)onMeasure方法setMeasuredDimension(witdh,width+minHeight)來(lái)賦予布局大小,minHeight可以根據(jù)實(shí)際情況自己賦值。
(2)手勢(shì)大View可以通過(guò)onMeasure方法,通過(guò)比較寬高得到最小值,來(lái)設(shè)置手勢(shì)大View的正方形大小
(3)手勢(shì)小View同理于手勢(shì)大View,提示文字的位置,我們也很容易確認(rèn)。
(4)手勢(shì)大view的寬高得到了,那么手勢(shì)大view每一個(gè)手勢(shì)點(diǎn)的坐標(biāo)和大小 我們就很容易得到。
(5)相信大家初學(xué)Java的時(shí)候肯定做過(guò),用 * 號(hào)打印各種圖形的操作,手勢(shì)view相當(dāng)于一個(gè)簡(jiǎn)單的3*3矩陣。我們知道了大小和坐標(biāo)很容易畫(huà)出來(lái)。
(6)小View也同理 可以實(shí)現(xiàn),需要注意的是 在手勢(shì)密碼第一次注冊(cè)的時(shí)候存在小View,在認(rèn)證的時(shí)候無(wú)小view,我們可以根據(jù)狀態(tài),在onDraw中設(shè)置隱藏。

二、分析完后,我們就一步一步來(lái)實(shí)現(xiàn)吧:

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

通過(guò)效果圖分析,我定義的attrs如下,在這里面,手勢(shì)點(diǎn)我采用的是圖片(圖片可以讓手勢(shì)點(diǎn)更酷炫)

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

2、在View中接收賦值,點(diǎn)數(shù)圖片我用的bitmap,如果無(wú)具體定義,這些屬性都會(huì)給他默認(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);
        }
        //等待時(shí)間,默認(rèn)30s
        waitTime = ta.getInteger(R.styleable.SecurityCenter_waitTime,30);
        //嘗試次數(shù),默認(rèn)5
        tempCount = ta.getInteger(R.styleable.SecurityCenter_maxFailCounts,5);
        //最小設(shè)置的點(diǎn),默認(rèn)4個(gè)
        minPointNums = ta.getInteger(R.styleable.SecurityCenter_minPoint,4);
        //設(shè)置畫(huà)筆的顏色
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStrokeWidth(10);
        mPaint.setStyle(Paint.Style.STROKE);
        //畫(huà)筆的顏色
        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)建時(shí)候的錯(cuò)誤
        ta.recycle();

        initView(context);
    }

3、在onMeasure中繪測(cè)手勢(shì)View布局大小,通過(guò)最開(kāi)始的分析,都很容易理解。

 @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;
        //大手勢(shì)View為邊長(zhǎng)width的正方形,panelHeight是給小手勢(shì)view預(yù)留的空間
        setMeasuredDimension(width, width + panelHeight);
    }

4、通過(guò)onSizeChange方法可以獲取,根據(jù)mLineHeight(3*1的行高) 的值,可以定義大手勢(shì)密碼點(diǎn)和小手勢(shì)密碼點(diǎn)的寬高,然后通過(guò)Bitmap.createScaledBitmap方法,設(shè)置好手勢(shì)點(diǎn)的大小圖。細(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);
        //大手勢(shì)點(diǎn)寬度,為單位寬高的0.6倍,顯得更好看一些不會(huì)很滿(mǎn)
        pieceWidth = (int) (mLineHeight * 0.6f);
        //小手勢(shì)點(diǎn)寬度,同理
        pieceWidthSmall = (int) (mLineHeight * 0.15f);
        //畫(huà)出對(duì)應(yīng)手勢(shì)點(diǎn)的大小
        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),一種是注冊(cè)狀態(tài)(包含小view的那種),另一種是認(rèn)證狀態(tài)(不包含小view的那種),所以我們要定義兩種狀態(tài),來(lái)區(qū)分使用情況。

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

那么我們?cè)诿看巫?cè)成功的時(shí)候,需要保存手勢(shì)狀態(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)繪制手勢(shì)密碼點(diǎn),連接線(xiàn),和提示文字

(1)繪制9個(gè)未選中狀態(tài)的大手勢(shì)View點(diǎn),通過(guò)canvas.drawBitmap(Bitmap bitmap, float left, float top, Paint paint)方法繪制,這里我們要注意位置計(jì)算的時(shí)候,只需要注意在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);
            }
        }

可能這么說(shuō)有點(diǎn)抽象,不過(guò)也就是把坐標(biāo)搞清楚了還是很簡(jiǎn)單的,畫(huà)張圖配合你理解,圖中小View預(yù)留高度(panelHeight):


two.png

(2)可能到現(xiàn)在你就會(huì)很好奇,我設(shè)置的手勢(shì)連線(xiàn)到底是怎么存儲(chǔ)和校驗(yàn)的呢?問(wèn)的好! 這個(gè)問(wèn)題我開(kāi)始也思考了很久,有輪子是通過(guò)一個(gè)二維數(shù)組實(shí)現(xiàn)的,通過(guò)這個(gè)二維數(shù)組我來(lái)了思路,聯(lián)想到了Bean。我用Bean存儲(chǔ)對(duì)應(yīng)點(diǎn)的X和Y的坐標(biāo),把每個(gè)點(diǎn)的實(shí)例加入一個(gè)List<>中,就完成了手勢(shì)繪制所有點(diǎn)的存儲(chǔ)。

bean的代碼如下:

//定義Bean,來(lái)存儲(chǔ)手勢(shì)坐標(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)繪制連接線(xiàn)和選中點(diǎn):連接線(xiàn)是通過(guò)過(guò) onTouchEvent和onDraw,配合畫(huà)出的,在OnTouchEvent中手指經(jīng)過(guò)的點(diǎn)都會(huì)存在listDatas集合中,再通過(guò) invalidate();方法通知onDraw,根據(jù)listDatas中的新增點(diǎn)數(shù),來(lái)繪制出選中點(diǎn)和點(diǎn)之間的連接線(xiàn)。再此只給出onDraw中的代碼,onTouchEvent中的邏輯會(huì)在下文詳細(xì)說(shuō)明。

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

            firstGestrue = listDatas.get(0);
           //畫(huà)連接線(xiàn)
            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;
            }
            //最后一條線(xiàn)
            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ù)組,把把選中的點(diǎn)更換圖片
            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);
            }
        }

注冊(cè)手勢(shì)成功的時(shí)候需要將手勢(shì)集合,保存,用于下一次校驗(yàn),我們存在SharedPreference中(注意:這個(gè)手勢(shì)View只適用于本機(jī)攔截,所以存SharedPresference就夠了)

 //將點(diǎn)的xy list存入sp
    private boolean saveToSharedPrefference(List<GestureBean> data) {
        SharedPreferences sp = mContext.getSharedPreferences("GESTURAE_DATA", Activity.MODE_PRIVATE);
        SharedPreferences.Editor edit = sp.edit();
        //存入多少個(gè)點(diǎn)
        edit.putInt("data_size", data.size()); /*sKey is an array*/
        //和每個(gè)店的坐標(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();
    }

獲取存儲(chǔ)集合:,我們?cè)俅娴臅r(shí)候和取得時(shí)候,可以先存一個(gè)錄入點(diǎn)的數(shù)量,更方便做判斷。

 //讀取之前保存的List
    public List<GestureBean> loadSharedPrefferenceData() {
        List<GestureBean> list = new ArrayList<>();
        SharedPreferences mSharedPreference = mContext.getSharedPreferences("GESTURAE_DATA", Activity.MODE_PRIVATE);
        //取出點(diǎn)數(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) 小圖和文字的繪制就很簡(jiǎn)單了,參考前幾項(xiàng) 我就直接給代碼了(剛才開(kāi)會(huì),思路被干擾了。。。。)

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

需要注意的是,小View在完成第一次繪制的時(shí)候,第二次繪制的時(shí)候需要保存第一次的樣式,通過(guò)list存儲(chǔ)比較,如下代碼實(shí)現(xiàn)。

 //繪制提示點(diǎn)
    private void drawTipsPoint(Canvas canvas) {
        //寬度為View寬度的一半
        float widthMiddleX = mPanelWidth / 2;
        //確定好相關(guān)坐標(biāo),找出第一個(gè)點(diǎn)的中心點(diǎn)
        float firstX = widthMiddleX - pieceWidthSmall / 4 - pieceWidthSmall / 2 - pieceWidthSmall;
        float firstY = panelHeight / 2 - pieceWidthSmall / 2 - pieceWidthSmall - pieceWidthSmall / 4 - 10;
        //畫(huà)點(diǎn),由于沒(méi)有選中,畫(huà)9個(gè)未選中點(diǎn)
        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)前的小手勢(shì)密碼·顯示第一次劃過(guò)的痕跡
        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來(lái)改變選中點(diǎn)的顏色
        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下面,很好理解,直接給代碼了:


    //繪制提示語(yǔ)
    private void drawTipsText(Canvas canvas) {
        float widthMiddleX = mPanelWidth / 2;
        mPaint.setStyle(Paint.Style.FILL);
        int widthStr1 = (int) mPaint.measureText("輸入手勢(shì)來(lái)解鎖");
        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("輸入手勢(shì)來(lái)解鎖", baseX, newY, mPaint);
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStrokeWidth(10);
    }

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

(1)在這里我們封裝的比較完善,我處理了驗(yàn)證超過(guò)驗(yàn)證次數(shù)會(huì)攔截手勢(shì)View,這里面算是后期完善,但是為大家梳理思路的話(huà)就顯得比較冗余,直接貼代碼,先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)開(kāi)始規(guī)范的很清楚,這個(gè)很好判斷

   if (event.getY() >= ((mLineHeight * (0 + 0.5) - pieceWidth / 2 + panelHeight))){
  //得到XY用于判斷 手指處于哪個(gè)點(diǎn)
            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)手指按下去的時(shí)候,我們要判斷按下去的點(diǎn),處于哪一個(gè)大手勢(shì)點(diǎn)范圍內(nèi),并把它加入List<Bean>中。通知onDraw重繪,如上文所說(shuō)的那樣,把點(diǎn)改為選中狀態(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)前手指處于哪個(gè)點(diǎn)范圍內(nèi),如果點(diǎn)沒(méi)存在listData,存進(jìn)去,第一個(gè)點(diǎn)
                            if (!listDatas.contains(new GestureBean(y, x))) {
                                listDatas.add(new GestureBean(y, x));
                            }
                        }
                    }
                    //重繪一次,第一個(gè)點(diǎn)顯示被選中了
                    invalidate();
                    break;

(4)MotionEvent.ACTION_MOVE: 手指在View上滑動(dòng),滑動(dòng)到哪個(gè)點(diǎn)就把,哪個(gè)點(diǎn)的坐標(biāo)add到List<Baen>中。在通知重繪

 case MotionEvent.ACTION_MOVE:
                    //手指移動(dòng)在大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) {
                            //滑倒的店處于哪個(gè)點(diǎn)范圍內(nèi),如果點(diǎn)沒(mé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),會(huì)從loadSharedPrefferenceData獲取到以前的listdatas做比較,判斷是否成功
2、注冊(cè)狀態(tài),會(huì)比較第一次的listdatas,來(lái)判斷兩次驗(yàn)證是否一致,從而,處理成功和失敗的邏輯。

 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, "手勢(shì)錯(cuò)誤,還可以再輸入" + tempCount + "次");
                            listDatas.clear();
                        }

                    }
                    //View處于注冊(cè)狀態(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, "點(diǎn)數(shù)不能小于" + minPointNums + "個(gè)");
                                invalidate();
                                return true;
                            }
                            listDatasCopy.addAll(listDatas);
                            listDatas.clear();
                            mError = false;
                            AlertUtil.t(mContext, "請(qǐng)?jiān)僖淮卫L制");
                        } else {
                            //兩次認(rèn)證成功
                            if (listDatas.equals(listDatasCopy)) {
                                saveToSharedPrefference(listDatas);
                                mError = false;
                                stateFlag = STATE_LOGIN;
                                postListener(true);
                                saveState();
                            } else {
                                mError = true;
                                AlertUtil.t(mContext, "與上次手勢(shì)繪制不一致,請(qǐng)重新設(shè)置");
                            }
                            listDatas.clear();
                            invalidate();
                            return true;
                        }
                    }
                    invalidate();
                    break;

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

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

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

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

(2)實(shí)例化接口,當(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í)時(shí)數(shù)據(jù)Action.UP

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

2、驗(yàn)證失敗倒計(jì)時(shí),通過(guò)Handler,Timer和TimerTask實(shí)現(xiàn)
(1)、內(nèi)部類(lèi)TimeTask

 //定義一個(gè)內(nèi)部TimerTask類(lèi)用于記錄,錯(cuò)誤倒計(jì)時(shí)
    static class InnerTimerTask extends TimerTask{
        Handler handler;

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

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

(2)實(shí)例化,上次錯(cuò)誤時(shí)間也是存儲(chǔ)在SharedPreference,waiTime為定義好的超時(shí)時(shí)間。代碼如下

 mTimer = new Timer();
        //計(jì)算上次失敗時(shí)間與現(xiàn)在的時(shí)間差
        try {
            long lastTime = PreferenceCache.getGestureTime();
            Date date = new Date();
            if (lastTime !=0 && (date.getTime()-lastTime)/1000<waitTime){
                //失敗時(shí)間未到,還處于鎖定狀態(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,"請(qǐng)繪制解鎖圖案");
               mError = false;
               invalidate();
               //將計(jì)時(shí)信息還原
               reSet();
               return;
           }
           mError = true;
           invalidate();
        }
    };

3、清理手勢(shì)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();
   }
   //用于更改手勢(shì)密碼,清除以前密碼
   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();
   }

四、簡(jiǎn)單使用:
1、以設(shè)置手勢(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è)置手勢(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è)置手勢(shì)密碼頁(yè)面):

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)用這個(gè)方法會(huì)造成第二次啟動(dòng)程序直接進(jìn)入手勢(shì)識(shí)別而不是手勢(shì)設(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é)
這個(gè)自定義手勢(shì)密碼,是我參照個(gè)別輪子+我本人的理解,仿照支付寶手勢(shì)密碼設(shè)計(jì)的,整體流暢我已帶大家分析了一波。該View還有很多需要完善的地方,我以后會(huì)慢慢完善,有什么指教或者疑問(wèn),請(qǐng)大家在下面留言。

Demo:
1、手勢(shì)密碼自定義View
2、指紋驗(yàn)證(由于仿支付寶安全的Demo,含指紋我就一塊做了,用的三方)

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

如果覺(jué)得對(duì)您有用,請(qǐng)給我一個(gè)贊,或者一個(gè)Star。


US2WT57LJJS5THY%NWL9VG5.jpg
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評(píng)論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,520評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 178,362評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,805評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,541評(píng)論 6 412
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,896評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評(píng)論 3 447
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 43,062評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,608評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,356評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,555評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,769評(píng)論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 35,175評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,489評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,289評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,516評(píng)論 2 379

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