自定義View之--九宮格圖形密碼鎖

前言:

很多金融和幾大商業銀行的APP,都使用了九宮格圖形密碼鎖來增強資金賬戶的安全。我也是金融公司的一員,在空餘的時候,寫下這個view,可以說是明智之舉。

效果預覽

這裏寫圖片描述

這樣一個邏輯差不多可以滿足基本的需求了。接下來就看代碼咯。

NineSquareView的成長

1、重寫構造方法和初始化屬性

    private Paint pointPaint;  //畫點的畫筆
    private Paint linePaint; // 畫線的畫筆
    private Path path;     //路徑
    private static int SQUAREWIDRH = 300; //默認正方形的邊長
    private float mSquarewidth = SQUAREWIDRH; //每個正方形的邊長 9個
    private float x, y; //手指在滑動的時候那個點的座標
    private float  startX, startY; //手指首次接觸View的那個點的座標
    private LinkedHashMap<String,Point> points = new LinkedHashMap<>(); //存放手指連接的點
    private OnFinishGestureListener finishGestureListener ; //當手指擡起時,觸發的監聽

    public NineSquareView(Context context) {
        this(context, null);
    }

    public NineSquareView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NineSquareView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        linePaint = new Paint();
        linePaint.setStyle(Paint.Style.STROKE);
        linePaint.setColor(Color.CYAN);
        linePaint.setStrokeWidth(5);
        linePaint.setAntiAlias(true);
        linePaint.setStrokeCap(Paint.Cap.ROUND);
        pointPaint = new Paint();
        pointPaint.setStyle(Paint.Style.FILL);
        pointPaint.setColor(Color.parseColor("#cbd0de"));
        pointPaint.setStrokeWidth(40);
        pointPaint.setAntiAlias(true);
        pointPaint.setStrokeCap(Paint.Cap.ROUND);
        path =new Path();
    }
   public interface OnFinishGestureListener {
       void onfinish(LinkedHashMap<String,Point> points);

    }

2、重寫onMeasure();

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int wideSize = MeasureSpec.getSize(widthMeasureSpec);
        int wideMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int width, height;
        if (wideMode == MeasureSpec.EXACTLY) { //精確值 或matchParent
            width = wideSize;
        } else {
            width = (int) (mSquarewidth * 3 + getPaddingLeft() + getPaddingRight());
            if (wideMode == MeasureSpec.AT_MOST) {
                width = Math.min(width, wideSize);
            }
        }
        if (heightMode == MeasureSpec.EXACTLY) { //精確值 或matchParent
            height = heightSize;
        } else {
            height = (int) (mSquarewidth * 3 + getPaddingTop() + getPaddingBottom());
            if (heightMode == MeasureSpec.AT_MOST) {
                height = Math.min(height, heightSize);
            }
        }
        setMeasuredDimension(width, height);
        mSquarewidth = (int) (Math.min(width - getPaddingLeft() - getPaddingRight(),
                height - getPaddingTop() - getPaddingBottom()) * 1.0f / 3);

    }

mSquarewidth始終是View的三分之一的寬度。對OnMeasure()方法還不是很懂的。可以去看看鴻神寫的博客Android 自定義View (二) 進階

3、重寫onTouchEvent();

@Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = ev.getX();
                startY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                x = ev.getX();
                y = ev.getY();
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                x = 0;
                y = 0;
                startX = 0;
                startY = 0;
                finishGestureListener.onfinish(points);
                points.clear();
                invalidate();
                break;
        }
        return true;
    }

在手指離開屏幕的時候,就是繪製完成的時候,所有數據清零。並觸發finishGestureListener,去處理當前用戶連接的points.
4.重寫onDraw();
最重要的,最精彩的部分來了。首先我們得把九個灰點畫出來。來個雙層for循環就搞定。

for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                pointPaint.setColor(Color.parseColor("#cbd0de"));
                canvas.drawPoint(mSquarewidth * (0.5f + i),mSquarewidth * (0.5f + j),pointPaint);
            }
        }

每個灰色的點都畫在正方形的中央。可接下來有個問題就要思考了,我們的手指去繪製的時候,要判斷手指觸碰的點是不是正好是那些個灰點。判斷兩個座標是否相等?NONONO,我們畫的點比我們的手指要細些。手指要精確的觸碰到那個灰點,估計有點困難。照這樣下去,你的app早就被用戶卸載了。
我們可以給一個範圍,這個範圍是用戶觸碰的點離最近的那個灰點的距離。比如mSquarewidth * 0.3f,如果手指觸摸在這個範圍內,就說明用戶想要繪製這個點。這個範圍不能超過mSquarewidth * 0.5f,然後,我們把這個點加入到集合中。

  for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (Math.abs(startX - mSquarewidth * (0.5f + i)) < mSquarewidth * 0.3f &&
                        Math.abs(startY - mSquarewidth * (0.5f + j)) < mSquarewidth * 0.3f) {
                    path.moveTo(mSquarewidth * (0.5f + i), mSquarewidth * (0.5f + j));
                    path.lineTo(x, y);
                    canvas.drawPath(path,linePaint);
                    path.reset();
                    Point point =new Point(mSquarewidth * (0.5f + i),mSquarewidth * (0.5f + j));
                    points.put(i+":"+j,point);
                    System.out.println(points.size());
                    System.out.println(i+"//"+j);
                }
            }
        }

這樣寫完後,運行寫代碼。結果就是,只能加入手指點下去的第一個點,想連接下一個點,怎麼辦?繼續思考,寫代碼。剛纔,我們已經連接到了第一個點,想要連接到第二個點,我們必須滑動我們的手指,滑動的時候,座標變爲了x,y.而且時時刻刻在變動。再來一次範圍判斷,是不是就可以連接到第二個點了?答案是正確的!

for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (Math.abs(x - mSquarewidth * (0.5f + i)) < mSquarewidth * 0.3f &&
                        Math.abs(y - mSquarewidth * (0.5f + j)) <mSquarewidth * 0.3f
                        ) {
                    Iterator<Point> iterator2 = collection.iterator();
                    while(iterator2.hasNext()){
                        Point point = iterator2.next();
                       if(mSquarewidth * (0.5f + i)==point.getX() && mSquarewidth * (0.5f + j)==point.getY()){
                              return;
                       }
                    }
                    startX = mSquarewidth * (0.5f + i);
                    startY = mSquarewidth * (0.5f + j);

                }
            }
        }

但要排除下,我們已經連接過的點。並把這連接好的第二個點設爲起始點。這樣就可以循環的連接點了。在一開始的效果預覽中可以看到,連接過的點,會變一種顏色,而且還會有一個小圓環,點與點之間會有一根線連接着,不會消失。這也好辦。

 Collection<Point> collection = points.values();
        Iterator<Point> iterator = collection.iterator();
        if(iterator.hasNext()){
            Point point = iterator.next();
            drawCyanPoint(canvas,point);
            System.out.println("moveTo:"+point.getX()+"===="+point.getY());
            path.moveTo(point.getX(),point.getY());
        }
        while (iterator.hasNext()) {
            Point point = iterator.next();
            drawCyanPoint(canvas,point);
            System.out.println("lineTo:"+point.getX()+"===="+point.getY());
            path.lineTo(point.getX(),point.getY());
        }
        canvas.drawPath(path,linePaint);
        path.reset();

在畫了灰點後,可以把map中的points連接起來。改變畫筆的顏色,畫上圓圈,這個圓圈的半徑最好是你設置的那個範圍的大小。我的是mSquarewidth * 0.3f。

 //繪製手指劃到的那個點,點外加上一層圈。
    public void drawCyanPoint(Canvas canvas, Point point){
        String s =getKey(point);
        String [] strings =  s.split(":");
        int i= Integer.parseInt(strings[0]);
        int j=Integer.parseInt(strings[1]);
        pointPaint.setColor(Color.CYAN);
        canvas.drawPoint(mSquarewidth * (0.5f + i),mSquarewidth * (0.5f + j),pointPaint);
        canvas.drawCircle(mSquarewidth * (0.5f + i),mSquarewidth * (0.5f + j),mSquarewidth * 0.3f,linePaint);
    }

    //根據value取key值
    public  String getKey(Point value)
    {
        String key = "";
        Set<Map.Entry<String, Point>> set = points.entrySet();
        for(Map.Entry<String, Point> entry : set){
            if(entry.getValue().equals(value)){
                key = entry.getKey();
                break;
            }
        }
        return key;
    }

NinePointView的成長

這個View就是在繪製玩手勢後的一個簡單顯示繪製的點的位置。

這裏寫圖片描述

這個就比較簡單了,很多都是 copy NineSquaredView的代碼,就不細說了。

PswActivity的成長。

Activity中的就是邏輯和UI了。PswActivity包含設置密碼鎖和解鎖並跳轉到其他界面。大致邏輯我們都懂的,就不細說了。唯一要說的就是比較兩次設置的密碼是否一致,以及設置密碼與解鎖密碼是否一致。我們要比較兩次的密碼是否一致,其實就是比較兩次繪製時的繪製點的個數,位置是否一致。

public boolean isEquals(LinkedHashMap<String, Point> pointsOne,LinkedHashMap<String, Point> pointsTwo) {
        Iterator<String> iterator = pointsOne.keySet().iterator();
        Iterator<String> iterator2 = pointsTwo.keySet().iterator();
        if (pointsOne.size() != pointsTwo.size()) {
            return false;
        }
        while (iterator.hasNext()) {
            String s = iterator.next();
            String s2 = iterator2.next();
            if (!s.equals(s2)) {
                return false;
            }
        }
        return true;
    }

因爲LinkedHashMap是有序的,所以才能這樣一個一個對應的去比較。我們設置密碼後,密碼是需要存放在本地的,SharedPreferences來幫忙了。等到下一次打開APP的時候,才能與解鎖密碼作比較。可尋遍了SharedPreferences中的put相關方法,就是沒有能把LinkedHashMap放進去的。剛還思考着呢,Stream來幫忙了。通過寫流和讀流,這樣操作更加安全。

public  String map2String(LinkedHashMap<String, Point> hashmap) {
        // 實例化一個ByteArrayOutputStream對象,用來裝載壓縮後的字節文件。
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        String sceneListString = null;
        // 然後將得到的字符數據裝載到ObjectOutputStream
        ObjectOutputStream objectOutputStream = null;
        try {
            objectOutputStream = new ObjectOutputStream(
                    byteArrayOutputStream);
            // writeObject 方法負責寫入特定類的對象的狀態,以便相應的 readObject 方法可以還原它
            objectOutputStream.writeObject(hashmap);
            // 最後,用Base64.encode將字節文件轉換成Base64編碼保存在String中
            sceneListString = new String(Base64.encode(
                    byteArrayOutputStream.toByteArray(), Base64.DEFAULT),"utf8");
            // 關閉objectOutputStream
            objectOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sceneListString;
    }

    public LinkedHashMap<String, Point> getHashMap() {
        String liststr = preferences.getString(PREFERENCENAME, null);
        try {
            return string2Map(liststr);
        } catch (StreamCorruptedException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public  LinkedHashMap<String, Point> string2Map(
            String SceneListString) throws
            IOException, ClassNotFoundException {
        byte[] mobileBytes = Base64.decode(SceneListString.getBytes(),
                Base64.DEFAULT);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(
                mobileBytes);
        ObjectInputStream objectInputStream = new ObjectInputStream(
                byteArrayInputStream);
        LinkedHashMap<String, Point> SceneList = (LinkedHashMap<String, Point>) objectInputStream
                .readObject();
        objectInputStream.close();
        return SceneList;
    }

所有代碼鏈接:

https://github.com/Demidong/ClockView.git

That all,歡迎評論和交流!

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