項目中需求用到圖案解鎖的功能,就自己寫了類似的功能:
說下思路:
- 1.實現(xiàn)一個子類繼承View
- 2.覆蓋onDrow()函數(shù),渲染圖像
- 3.覆蓋onTouchEvent()函數(shù)
- 4.監(jiān)聽按下、移動,松開手指的動作
- 5.重新在onDrow()中渲染對應(yīng)的的圖像
在描述功能之前,看一下效果圖,理解起來會起到事半功倍的作用

說明
- 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)

右圖跟左圖的渲染過程一樣,講解左圖的實現(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)。