Android自定義View--實現(xiàn)九宮格解鎖圖案

源碼下載

項目中需求用到圖案解鎖的功能,就自己寫了類似的功能:
說下思路:

  • 1.實現(xiàn)一個子類繼承View
  • 2.覆蓋onDrow()函數(shù),渲染圖像
  • 3.覆蓋onTouchEvent()函數(shù)
  • 4.監(jiān)聽按下、移動,松開手指的動作
  • 5.重新在onDrow()中渲染對應(yīng)的的圖像

在描述功能之前,看一下效果圖,理解起來會起到事半功倍的作用

整體效果圖.jpg

說明

  • A、B、C、D、E、F、G、H、I代表九個坐標(biāo)點
  • 左圖中的圓由兩個同心圓組成.
  • 中圖鏈接起來的圓由四個同心圓組成,增加了兩個綠色的圓,最外層綠色的是空心圓,紅色連線是帶有寬度的直線.
  • 右圖線條由紅色條變成了綠色.

1.實現(xiàn)UnlockAppView類繼承View

實現(xiàn)左圖:九個點的坐標(biāo),圓的半徑及顏色。
空心圓:同圓心不同半徑,繪制顏色不同
坐標(biāo)如何確定:由屏幕的寬高決定,按照比例畫出的效果圖在各種屏幕中看起來協(xié)調(diào).
定義所需參數(shù):

//屏幕的寬度
private int width;
//屏幕的高度
private int height;
//大圓半徑
private float rH;
//小圓半徑
private int rM;
//A的坐標(biāo)
private float a1, b1;
//B的坐標(biāo)
private float a2, b2;
//C的坐標(biāo)
private float a3, b3;
//D的坐標(biāo)
private float a4, b4;
//E的坐標(biāo)
private float a5, b5;
//F的坐標(biāo)
private float a6, b6;
//G的坐標(biāo)
private float a7, b7;
//H的坐標(biāo)
private float a8, b8;
//I的坐標(biāo)
private float a9, b9;
//繪制大圓用到的畫筆
private Paint mPaint;
//繪制小圓用到的畫筆
private Paint mPaint0;

參數(shù)命名完成,接下來開始賦值:

DisplayMetrics metric = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metric);
//獲取屏幕的寬度
width = metric.widthPixels;
//獲取屏幕的高度
height = metric.heightPixels;
//以下計算是根據(jù)屏幕調(diào)試出來的合理大小,不必深究
//計算大圓的半徑,
rH = (width / 3) / 5;
//計算小圓的半徑,
rM = (width / 3) / 10;
//點A的橫坐標(biāo),及縱坐標(biāo)
a1 = (width / 3) / 2;
b1 = (width / 3) / 2 + (height - width) / 2;
//B點坐標(biāo)
a2 = (width / 3) + (width / 3) / 2;
b2 = b1;
//C點坐標(biāo)
a3 = (width / 3) * 2 + (width / 3) / 2;
b3 = b1;
//D點坐標(biāo)
a4 = a1;
b4 = (width / 3) + (width / 3) / 2 + (height - width) / 2;
//E點坐標(biāo)
a5 = a2;
b5 = b4;
//F點坐標(biāo)
a6 = a3;
b6 = b4;
//G點坐標(biāo)
a7 = a1;
b7 = (width / 3) * 2 + (width / 3) / 2 + (height - width) / 2;
//H點坐標(biāo)
a8 = a5;
b8 = b7;
//I點坐標(biāo)
a9 = a6;
b9 = b7;
//使位圖抗鋸齒
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//顏色,淺灰色
mPaint.setColor(Color.LTGRAY);
//使位圖抗鋸齒
mPaint0 = new Paint(Paint.ANTI_ALIAS_FLAG);
//顏色,白色
mPaint0 = new Paint(Paint.WHITE);

一切準(zhǔn)備就緒,重寫onDrow()函數(shù),重新渲染

@Override
protected void onDraw(Canvas canvas) {
//每次繪制清空畫布
canvas.drawColor(Color.WHITE);

//渲染大圓,圓心(a1,b1)半徑rH,畫筆mPaint
canvas.drawCircle(a1, b1, rH, mPaint);
canvas.drawCircle(a2, b2, rH, mPaint);
canvas.drawCircle(a3, b3, rH, mPaint);
canvas.drawCircle(a4, b4, rH, mPaint);
canvas.drawCircle(a5, b5, rH, mPaint);
canvas.drawCircle(a6, b6, rH, mPaint);
canvas.drawCircle(a7, b7, rH, mPaint);
canvas.drawCircle(a8, b8, rH, mPaint);
canvas.drawCircle(a9, b9, rH, mPaint);
//渲染小圓
canvas.drawCircle(a1, b1, rM, mPaint0);
canvas.drawCircle(a2, b2, rM, mPaint0);
canvas.drawCircle(a3, b3, rM, mPaint0);
canvas.drawCircle(a4, b4, rM, mPaint0);
canvas.drawCircle(a5, b5, rM, mPaint0);
canvas.drawCircle(a6, b6, rM, mPaint0);
canvas.drawCircle(a7, b7, rM, mPaint0);
canvas.drawCircle(a8, b8, rM, mPaint0);
canvas.drawCircle(a9, b9, rM, mPaint0);
}

以上完成左圖的渲染。

實現(xiàn)中圖的效果

跟蹤手指劃過的痕跡
軌跡是否是否經(jīng)過圓的區(qū)域
說明,這里圓的區(qū)域用圓的外切正方形的區(qū)域代替。
矩形對象的contains()方法可判斷軌跡經(jīng)過園的區(qū)域。
代碼實例
rt1.contains(tX, tY)

定義園的外切正方形變量

//九個正方形區(qū)域
//左上角坐標(biāo)(a1 - rH, b1 - rH)及右下角坐標(biāo)(a1 + rH, b1 + rH)
private RectF rt1 = 
new RectF(a1 - rH, b1 - rH, a1 + rH, b1 + rH);
private RectF rt2 = 
new RectF(a2 - rH, b2 - rH, a2 + rH, b2 + rH);
private RectF rt3 = new RectF(a3 - rH, b3 - rH, a3 + rH, b3 + rH);
private RectF rt4 = new RectF(a4 - rH, b4 - rH, a4 + rH, b4 + rH);
private RectF rt5 = new RectF(a5 - rH, b5 - rH, a5 + rH, b5 + rH);
private RectF rt6 = new RectF(a6 - rH, b6 - rH, a6 + rH, b6 + rH);
private RectF rt7 = new RectF(a7 - rH, b7 - rH, a7 + rH, b7 + rH);
private RectF rt8 = new RectF(a8 - rH, b8 - rH, a8 + rH, b8 + rH);
private RectF rt9 = new RectF(a9 - rH, b9 - rH, a9 + rH, b9 + rH);

使用invalidate()方法,刷新整個畫布。
所以需要記錄軌跡經(jīng)過A、B、C、D、E、F、G、H、I九個點經(jīng)過的先后順序。

定義一個String變量passwordValue存儲經(jīng)過坐標(biāo)點的先后順序;
兩圓之間的紅色線段,passwordValue記錄著經(jīng)過的圓的先后順利,根據(jù)經(jīng)過圓的先后順利繪制線段;
例如:passwordValue ="ACDE"代表經(jīng)過的圓的順序圓A->圓C->圓D->圓E,繪制的線段AC、CD、DE;
線段是由起始坐標(biāo),終止坐標(biāo)表示,所以需要定義一個兩行兩列的二維數(shù)組用來存儲起始及終止坐標(biāo),第一行代表起始坐標(biāo),第二行代表終點坐標(biāo);
由于每次刷新整個畫布,需要把二維數(shù)據(jù)存儲在一個列表中,方便遍歷渲染;

圓與線段的渲染分開來講解,先來看看圓的渲染過程,獲取手指滑動坐標(biāo),重寫onTouchEvent方法

@Override
public boolean onTouchEvent(MotionEvent event)
//捕捉按下的動作
if (event.getAction() == MotionEvent.ACTION_DOWN) {

} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
    //X坐標(biāo)點
    float tX = event.getX();
    //Y坐標(biāo)點
    float tY = event.getY();
    //首次經(jīng)過圓A
    if (rt1.contains(tX, tY) && !passwordValue.contains("A")) {
         passwordValue += "A";
    } else if (rt2.contains(tX, tY) && !passwordValue.contains("B")) {//首次經(jīng)過圓B
         passwordValue += "B";
    } else if (rt3.contains(tX, tY) && !passwordValue.contains("C")) {//首次經(jīng)過圓C
         passwordValue += "C";
    } else if (rt4.contains(tX, tY) && !passwordValue.contains("D")) {//首次經(jīng)過圓D
         passwordValue += "D";
    } else if (rt5.contains(tX, tY) && !passwordValue.contains("E")) {//首次經(jīng)過圓E
         passwordValue += "E";
    } else if (rt6.contains(tX, tY) && !passwordValue.contains("F")) {//首次經(jīng)過圓F
         passwordValue += "F";
    } else if (rt7.contains(tX, tY) && !passwordValue.contains("G")) {//首次經(jīng)過圓G
        passwordValue += "G";
    } else if (rt8.contains(tX, tY) && !passwordValue.contains("H")) {//首次經(jīng)過圓H
        passwordValue += "H";
    } else if (rt9.contains(tX, tY) && !passwordValue.contains("I")) {//首次經(jīng)過圓I
        passwordValue += "I";
    }
    invalidate();// 刷新畫布,回調(diào)onDraw()方法
}  else if (event.getAction() == MotionEvent.ACTION_UP)  {
        
}

確定了圓的順序,刷新畫布,渲染軌跡坐標(biāo)經(jīng)過的圓的效果及紅色直線的效果

protected void onDraw(Canvas canvas) {
...
...
//軌跡經(jīng)過圓A
if (passwordValue.contains("A")) {// (a1,b1)
     canvas.drawCircle(a1, b1, rL, mPaintOKM);
     canvas.drawCircle(a1, b1, rH, mPaintOKH);
}
//軌跡經(jīng)過圓B
if (passwordValue.contains("B")) {// (a2,b2)
     canvas.drawCircle(a2,b2, rL, mPaintOKM);
     canvas.drawCircle(a2,b2, rH, mPaintOKH);
}
//軌跡經(jīng)過圓C
if (passwordValue.contains("C")) {// (a3,b3)
     canvas.drawCircle(a3,b3, rL, mPaintOKM);
     canvas.drawCircle(a3,b3, rH, mPaintOKH);
}
//軌跡經(jīng)過圓D
if (passwordValue.contains("D")) {// (a4,b4)
     canvas.drawCircle(a4,b4, rL, mPaintOKM);
     canvas.drawCircle(a4,b4, rH, mPaintOKH);
}
//軌跡經(jīng)過圓E
if (passwordValue.contains("E")) {// (a5,b5)
     canvas.drawCircle(a5,b5, rL, mPaintOKM);
     canvas.drawCircle(a5,b5, rH, mPaintOKH);
}
//軌跡經(jīng)過圓F
if (passwordValue.contains("F")) {// (a6,b6)
     canvas.drawCircle(a6,b6, rL, mPaintOKM);
     canvas.drawCircle(a6,b6, rH, mPaintOKH);
}
//軌跡經(jīng)過圓G
if (passwordValue.contains("G")) {// (a7,b7)
     canvas.drawCircle(a7,b7, rL, mPaintOKM);
     canvas.drawCircle(a7,b7, rH, mPaintOKH);
}
//軌跡經(jīng)過圓H
if (passwordValue.contains("H")) {// (a8,b8)
     canvas.drawCircle(a8,b8, rL, mPaintOKM);
     canvas.drawCircle(a8,b8, rH, mPaintOKH);
}
//軌跡經(jīng)過圓I
if (passwordValue.contains("I")) {// (a9,b9)
     canvas.drawCircle(a9,b9, rL, mPaintOKM);
     canvas.drawCircle(a9,b9, rH, mPaintOKH);
}

線段的渲染過程,獲取線段的端點坐標(biāo),重寫onTouchEvent方法

//存儲線段起始及終止坐標(biāo)的二維數(shù)組
float[][] lineCoordinate = new float[2][2]
//存儲二維數(shù)據(jù)的列表
List<Float[][]> listCoordinate = new ArrayList();
//經(jīng)過圓的數(shù)量,num < 4 線段顏色為紅色 num >= 4線段顏色為綠色
int num = 0;

@Override
public boolean onTouchEvent(MotionEvent event) 
if (event.getAction() == MotionEvent.ACTION_DOWN) {

} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
   //X坐標(biāo)點
    float tX = event.getX();
    //Y坐標(biāo)點
    float tY = event.getY();
    //首次經(jīng)過圓A
    if (rt1.contains(tX, tY) && !passwordValue.contains("A")) {
         passwordValue += "A";
         //num經(jīng)過的圓的數(shù)量
         //num != 0代表不是第一個經(jīng)過的圓,第一個經(jīng)過的圓只能是線段的起始坐標(biāo)不能是線段的終止坐標(biāo)
         if (num != 0) {      
            //線段的終止坐標(biāo)  
             fts[1] = new float[]{a1, b1};    
            //存儲線段起及始終止坐標(biāo)的二維數(shù)組存儲到列表中
            listCoordinate.add(fts);
         }
        //初始化存儲線段坐標(biāo)的二維數(shù)組
         fts = new float[2][2];
        //線段的起始坐標(biāo)
        fts[0] = new float[]{a1, b1};
         num += 1;
    } else if (rt2.contains(tX, tY) && !passwordValue.contains("B")) {//首次經(jīng)過圓B
         passwordValue += "B";
         if (num != 0) {      
            //線段的終止坐標(biāo)  
             fts[1] = new float[]{a2, b2};    
             listCoordinate.add(fts);
         }
        //初始化存儲線段坐標(biāo)的二維數(shù)組
         fts = new float[2][2];
        //線段的起始坐標(biāo)
        fts[0] = new float[]{a2, b2};
        num += 1;
    } else if (rt3.contains(tX, tY) && !passwordValue.contains("C")) {//首次經(jīng)過圓C
         passwordValue += "C";
         if (num != 0) {      
            //線段的終止坐標(biāo)  
             fts[1] = new float[]{a3, b3};    
             listCoordinate.add(fts);
         }
        //初始化存儲線段坐標(biāo)的二維數(shù)組
         fts = new float[2][2];
        //線段的起始坐標(biāo)
        fts[0] = new float[]{a3, b3};
        num += 1;
    } else if (rt4.contains(tX, tY) && !passwordValue.contains("D")) {//首次經(jīng)過圓D
         passwordValue += "D";
         if (num != 0) {      
            //線段的終止坐標(biāo)  
             fts[1] = new float[]{a4, b4};    
             listCoordinate.add(fts);
         }
        //初始化存儲線段坐標(biāo)的二維數(shù)組
         fts = new float[2][2];
        //線段的起始坐標(biāo)
        fts[0] = new float[]{a4, b4};
        num += 1;
    } else if (rt5.contains(tX, tY) && !passwordValue.contains("E")) {//首次經(jīng)過圓E
         passwordValue += "E";
         if (num != 0) {      
            //線段的終止坐標(biāo)  
             fts[1] = new float[]{a5, b5};    
             listCoordinate.add(fts);
         }
        //初始化存儲線段坐標(biāo)的二維數(shù)組
         fts = new float[2][2];
        //線段的起始坐標(biāo)
        fts[0] = new float[]{a5, b5};
        num += 1;
    } else if (rt6.contains(tX, tY) && !passwordValue.contains("F")) {//首次經(jīng)過圓F
         passwordValue += "F";
         if (num != 0) {      
            //線段的終止坐標(biāo)  
             fts[1] = new float[]{a6, b6};    
             listCoordinate.add(fts);
         }
        //初始化存儲線段坐標(biāo)的二維數(shù)組
         fts = new float[2][2];
        //線段的起始坐標(biāo)
        fts[0] = new float[]{a6, b6};
        num += 1;
    } else if (rt7.contains(tX, tY) && !passwordValue.contains("G")) {//首次經(jīng)過圓G
        passwordValue += "G";
         if (num != 0) {      
            //線段的終止坐標(biāo)  
             fts[1] = new float[]{a7, b7};    
             listCoordinate.add(fts);
         }
        //初始化存儲線段坐標(biāo)的二維數(shù)組
         fts = new float[2][2];
        //線段的起始坐標(biāo)
        fts[0] = new float[]{a7, b7};
        num += 1;
    } else if (rt8.contains(tX, tY) && !passwordValue.contains("H")) {//首次經(jīng)過圓H
        passwordValue += "H";
         if (num != 0) {      
            //線段的終止坐標(biāo)  
             fts[1] = new float[]{a8, b8};    
             listCoordinate.add(fts);
         }
        //初始化存儲線段坐標(biāo)的二維數(shù)組
         fts = new float[2][2];
        //線段的起始坐標(biāo)
        fts[0] = new float[]{a8, b8};
        num += 1;
    } else if (rt9.contains(tX, tY) && !passwordValue.contains("I")) {//首次經(jīng)過圓I
        passwordValue += "I";
         if (num != 0) {      
            //線段的終止坐標(biāo)  
             fts[1] = new float[]{a9, b9};    
             listCoordinate.add(fts);
         }
        //初始化存儲線段坐標(biāo)的二維數(shù)組
         fts = new float[2][2];
        //線段的起始坐標(biāo)
        fts[0] = new float[]{a9, b9};
        num += 1;
    }
    invalidate();// 刷新畫布,回調(diào)onDraw()方法
} else if (event.getAction() == MotionEvent.ACTION_UP) {

}

刷新畫布,渲染紅色線段,

//初始化渲染紅色線段畫筆
//Paint.ANTI_ALIAS_FLAG使圖像抗鋸齒
mPaintCancelM = new Paint(Paint.ANTI_ALIAS_FLAG)
//顏色紅色
mPaintCancelM.setColor(Color.RED);
//畫筆的寬度
mPaintCancelM.setStrokeWidth(rM);

protected void onDraw(Canvas canvas) {
...
...
for (int i = 0; i < listCoordinate.size(); i++) {
      float[][] lineCoordinate = listCoordinate.get(i);
      float startX = lineCoordinate[0][0];
      float startY = lineCoordinate[0][1];
      float stopX = lineCoordinate[1][0];
      float stopY = lineCoordinate[1][1];
     //渲染紅色線段
     canvas.drawLine(startX, startY, stopX, stopY, mPaintCancelM)
}
...
...

右圖跟中圖的渲染過程一樣,區(qū)別在于經(jīng)過的圓的數(shù)量大于等于4,畫筆的顏色設(shè)置成綠色

至此,以上左中右圖的渲染實現(xiàn)過程完畢,但還有兩個中間狀態(tài)

不完整的線段效果圖.jpg

右圖跟左圖的渲染過程一樣,講解左圖的實現(xiàn)過程,我們稱該狀態(tài)線段為不完整線段,以區(qū)分之前的線段。

手指滑動未到達(dá)圓所在的區(qū)域時,線段的起始坐標(biāo)是軌跡經(jīng)過的最后一個圓的圓心坐標(biāo),我們只需記錄終點坐標(biāo)就可實現(xiàn)以上圖中的狀態(tài)。

//存儲不完整線段起始終止坐標(biāo)的二維數(shù)組
float[][] lineCrdinateImperfect = new float[2][2]
lineCrdinateImperfect[0] = new float[2];
lineCrdinateImperfect[1] = new float[2];
@Override
public boolean onTouchEvent(MotionEvent event) {    
if (event.getAction() == MotionEvent.ACTION_DOWN) {

} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
       float tX = event.getX();
       float tY = event.getY();
       //不完整線段終點坐標(biāo)賦值
       lineCrdinateImperfect[1] [0] = tX;
       lineCrdinateImperfect[1] [1] = tY;
   //首次經(jīng)過圓A
    if (rt1.contains(tX, tY) && !passwordValue.contains("A")) {
         passwordValue += "A";
       //不完整線段起始坐標(biāo)賦值
       lineCrdinateImperfect[0] [0] = a1;
       lineCrdinateImperfect[0] [1] = b1;
    } else if (rt2.contains(tX, tY) && !passwordValue.contains("B")) {//首次經(jīng)過圓B
         passwordValue += "B";
       //不完整線段起始坐標(biāo)賦值
       lineCrdinateImperfect[0] [0] = a2;
       lineCrdinateImperfect[0] [1] = b2;
    } else if (rt3.contains(tX, tY) && !passwordValue.contains("C")) {//首次經(jīng)過圓C
         passwordValue += "C";
       //不完整線段起始坐標(biāo)賦值
       lineCrdinateImperfect[0] [0] = a3;
       lineCrdinateImperfect[0] [1] = b3;
    } else if (rt4.contains(tX, tY) && !passwordValue.contains("D")) {//首次經(jīng)過圓D
         passwordValue += "D";
       //不完整線段起始坐標(biāo)賦值
       lineCrdinateImperfect[0] [0] = a4;
       lineCrdinateImperfect[0] [1] = b4;
    } else if (rt5.contains(tX, tY) && !passwordValue.contains("E")) {//首次經(jīng)過圓E
         passwordValue += "E";
       //不完整線段起始坐標(biāo)賦值
       lineCrdinateImperfect[0] [0] = a5;
       lineCrdinateImperfect[0] [1] = b5;
    } else if (rt6.contains(tX, tY) && !passwordValue.contains("F")) {//首次經(jīng)過圓F
         passwordValue += "F";
       //不完整線段起始坐標(biāo)賦值
       lineCrdinateImperfect[0] [0] = a6;
       lineCrdinateImperfect[0] [1] = b6;
    } else if (rt7.contains(tX, tY) && !passwordValue.contains("G")) {//首次經(jīng)過圓G
        passwordValue += "G";
       //不完整線段起始坐標(biāo)賦值
       lineCrdinateImperfect[0] [0] = a7;
       lineCrdinateImperfect[0] [1] = b7;
    } else if (rt8.contains(tX, tY) && !passwordValue.contains("H")) {//首次經(jīng)過圓H
        passwordValue += "H";
       //不完整線段起始坐標(biāo)賦值
       lineCrdinateImperfect[0] [0] = a8;
       lineCrdinateImperfect[0] [1] = b8;
    } else if (rt9.contains(tX, tY) && !passwordValue.contains("I")) {//首次經(jīng)過圓I
        passwordValue += "I";
       //不完整線段起始坐標(biāo)賦值
       lineCrdinateImperfect[0] [0] = a9;
       lineCrdinateImperfect[0] [1] = b9;
    }
    invalidate();// 刷新畫布,回調(diào)onDraw()方法
} else if (event.getAction() == MotionEvent.ACTION_UP) {

}

刷新畫布,渲染不完整線段

@Override
protected void onDraw(Canvas canvas) {
...
...
      //不完整線段坐標(biāo)賦值
      float startXImperfect = lineCrdinateImperfect[0][0];
      float startYImperfect = lineCrdinateImperfect[0][1];
      float stopXImperfect= lineCrdinateImperfect[1][0];
      float stopYImperfect = lineCrdinateImperfect[1][1];
      //渲染不完整直線
      canvas.drawLine(startXImperfect, startYImperfect, stopXImperfect, stopYImperfect, mPaintCancelM);
...
...
}

注意:從一個圓(A)出發(fā),繞過一個圓(B),到達(dá)圓另一個圓(C),這樣會忽略中間的圓(B),經(jīng)過的圓的順序A->C,這樣不合理,明明經(jīng)過了中間圓(B),軌跡應(yīng)該是A->B->C才對。
解決思路:計算兩圓心坐標(biāo)中點坐標(biāo)是否為其他圓的圓心坐標(biāo)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容