项目中需求用到图案解锁的功能,就自己写了类似的功能:
说下思路:
- 1.实现一个子类继承View
- 2.覆盖onDrow()函数,渲染图像
- 3.覆盖onTouchEvent()函数
- 4.监听按下、移动,松开手指的动作
- 5.重新在onDrow()中渲染对应的的图像
在描述功能之前,看一下效果图,理解起来会起到事半功倍的作用
说明
- A、B、C、D、E、F、G、H、I代表九个坐标点
- 左图中的圆由两个同心圆组成.
- 中图链接起来的圆由四个同心圆组成,增加了两个绿色的圆,最外层绿色的是空心圆,红色连线是带有宽度的直线.
- 右图线条由红色条变成了绿色.
1.实现UnlockAppView类继承View
实现左图:九个点的坐标,圆的半径及颜色。
空心圆:同圆心不同半径,绘制颜色不同
坐标如何确定:由屏幕的宽高决定,按照比例画出的效果图在各种屏幕中看起来协调.
定义所需参数:
//屏幕的宽度
private int width;
//屏幕的高度
private int height;
//大圆半径
private float rH;
//小圆半径
private int rM;
//A的坐标
private float a1, b1;
//B的坐标
private float a2, b2;
//C的坐标
private float a3, b3;
//D的坐标
private float a4, b4;
//E的坐标
private float a5, b5;
//F的坐标
private float a6, b6;
//G的坐标
private float a7, b7;
//H的坐标
private float a8, b8;
//I的坐标
private float a9, b9;
//绘制大圆用到的画笔
private Paint mPaint;
//绘制小圆用到的画笔
private Paint mPaint0;
参数命名完成,接下来开始赋值:
DisplayMetrics metric = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metric);
//获取屏幕的宽度
width = metric.widthPixels;
//获取屏幕的高度
height = metric.heightPixels;
//以下计算是根据屏幕调试出来的合理大小,不必深究
//计算大圆的半径,
rH = (width / 3) / 5;
//计算小圆的半径,
rM = (width / 3) / 10;
//点A的横坐标,及纵坐标
a1 = (width / 3) / 2;
b1 = (width / 3) / 2 + (height - width) / 2;
//B点坐标
a2 = (width / 3) + (width / 3) / 2;
b2 = b1;
//C点坐标
a3 = (width / 3) * 2 + (width / 3) / 2;
b3 = b1;
//D点坐标
a4 = a1;
b4 = (width / 3) + (width / 3) / 2 + (height - width) / 2;
//E点坐标
a5 = a2;
b5 = b4;
//F点坐标
a6 = a3;
b6 = b4;
//G点坐标
a7 = a1;
b7 = (width / 3) * 2 + (width / 3) / 2 + (height - width) / 2;
//H点坐标
a8 = a5;
b8 = b7;
//I点坐标
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);
一切准备就绪,重写onDrow()函数,重新渲染
@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);
}
以上完成左图的渲染。
实现中图的效果
跟踪手指划过的痕迹
轨迹是否是否经过圆的区域
说明,这里圆的区域用圆的外切正方形的区域代替。
矩形对象的contains()方法可判断轨迹经过园的区域。
代码实例
rt1.contains(tX, tY)
定义园的外切正方形变量
//九个正方形区域
//左上角坐标(a1 - rH, b1 - rH)及右下角坐标(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()方法,刷新整个画布。
所以需要记录轨迹经过A、B、C、D、E、F、G、H、I九个点经过的先后顺序。
定义一个String变量passwordValue存储经过坐标点的先后顺序;
两圆之间的红色线段,passwordValue记录着经过的圆的先后顺利,根据经过圆的先后顺利绘制线段;
例如:passwordValue ="ACDE"代表经过的圆的顺序圆A->圆C->圆D->圆E,绘制的线段AC、CD、DE;
线段是由起始坐标,终止坐标表示,所以需要定义一个两行两列的二维数组用来存储起始及终止坐标,第一行代表起始坐标,第二行代表终点坐标;
由于每次刷新整个画布,需要把二维数据存储在一个列表中,方便遍历渲染;
圆与线段的渲染分开来讲解,先来看看圆的渲染过程,获取手指滑动坐标,重写onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent event)
//捕捉按下的动作
if (event.getAction() == MotionEvent.ACTION_DOWN) {
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
//X坐标点
float tX = event.getX();
//Y坐标点
float tY = event.getY();
//首次经过圆A
if (rt1.contains(tX, tY) && !passwordValue.contains("A")) {
passwordValue += "A";
} else if (rt2.contains(tX, tY) && !passwordValue.contains("B")) {//首次经过圆B
passwordValue += "B";
} else if (rt3.contains(tX, tY) && !passwordValue.contains("C")) {//首次经过圆C
passwordValue += "C";
} else if (rt4.contains(tX, tY) && !passwordValue.contains("D")) {//首次经过圆D
passwordValue += "D";
} else if (rt5.contains(tX, tY) && !passwordValue.contains("E")) {//首次经过圆E
passwordValue += "E";
} else if (rt6.contains(tX, tY) && !passwordValue.contains("F")) {//首次经过圆F
passwordValue += "F";
} else if (rt7.contains(tX, tY) && !passwordValue.contains("G")) {//首次经过圆G
passwordValue += "G";
} else if (rt8.contains(tX, tY) && !passwordValue.contains("H")) {//首次经过圆H
passwordValue += "H";
} else if (rt9.contains(tX, tY) && !passwordValue.contains("I")) {//首次经过圆I
passwordValue += "I";
}
invalidate();// 刷新画布,回调onDraw()方法
} else if (event.getAction() == MotionEvent.ACTION_UP) {
}
确定了圆的顺序,刷新画布,渲染轨迹坐标经过的圆的效果及红色直线的效果
protected void onDraw(Canvas canvas) {
...
...
//轨迹经过圆A
if (passwordValue.contains("A")) {// (a1,b1)
canvas.drawCircle(a1, b1, rL, mPaintOKM);
canvas.drawCircle(a1, b1, rH, mPaintOKH);
}
//轨迹经过圆B
if (passwordValue.contains("B")) {// (a2,b2)
canvas.drawCircle(a2,b2, rL, mPaintOKM);
canvas.drawCircle(a2,b2, rH, mPaintOKH);
}
//轨迹经过圆C
if (passwordValue.contains("C")) {// (a3,b3)
canvas.drawCircle(a3,b3, rL, mPaintOKM);
canvas.drawCircle(a3,b3, rH, mPaintOKH);
}
//轨迹经过圆D
if (passwordValue.contains("D")) {// (a4,b4)
canvas.drawCircle(a4,b4, rL, mPaintOKM);
canvas.drawCircle(a4,b4, rH, mPaintOKH);
}
//轨迹经过圆E
if (passwordValue.contains("E")) {// (a5,b5)
canvas.drawCircle(a5,b5, rL, mPaintOKM);
canvas.drawCircle(a5,b5, rH, mPaintOKH);
}
//轨迹经过圆F
if (passwordValue.contains("F")) {// (a6,b6)
canvas.drawCircle(a6,b6, rL, mPaintOKM);
canvas.drawCircle(a6,b6, rH, mPaintOKH);
}
//轨迹经过圆G
if (passwordValue.contains("G")) {// (a7,b7)
canvas.drawCircle(a7,b7, rL, mPaintOKM);
canvas.drawCircle(a7,b7, rH, mPaintOKH);
}
//轨迹经过圆H
if (passwordValue.contains("H")) {// (a8,b8)
canvas.drawCircle(a8,b8, rL, mPaintOKM);
canvas.drawCircle(a8,b8, rH, mPaintOKH);
}
//轨迹经过圆I
if (passwordValue.contains("I")) {// (a9,b9)
canvas.drawCircle(a9,b9, rL, mPaintOKM);
canvas.drawCircle(a9,b9, rH, mPaintOKH);
}
线段的渲染过程,获取线段的端点坐标,重写onTouchEvent方法
//存储线段起始及终止坐标的二维数组
float[][] lineCoordinate = new float[2][2]
//存储二维数据的列表
List<Float[][]> listCoordinate = new ArrayList();
//经过圆的数量,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坐标点
float tX = event.getX();
//Y坐标点
float tY = event.getY();
//首次经过圆A
if (rt1.contains(tX, tY) && !passwordValue.contains("A")) {
passwordValue += "A";
//num经过的圆的数量
//num != 0代表不是第一个经过的圆,第一个经过的圆只能是线段的起始坐标不能是线段的终止坐标
if (num != 0) {
//线段的终止坐标
fts[1] = new float[]{a1, b1};
//存储线段起及始终止坐标的二维数组存储到列表中
listCoordinate.add(fts);
}
//初始化存储线段坐标的二维数组
fts = new float[2][2];
//线段的起始坐标
fts[0] = new float[]{a1, b1};
num += 1;
} else if (rt2.contains(tX, tY) && !passwordValue.contains("B")) {//首次经过圆B
passwordValue += "B";
if (num != 0) {
//线段的终止坐标
fts[1] = new float[]{a2, b2};
listCoordinate.add(fts);
}
//初始化存储线段坐标的二维数组
fts = new float[2][2];
//线段的起始坐标
fts[0] = new float[]{a2, b2};
num += 1;
} else if (rt3.contains(tX, tY) && !passwordValue.contains("C")) {//首次经过圆C
passwordValue += "C";
if (num != 0) {
//线段的终止坐标
fts[1] = new float[]{a3, b3};
listCoordinate.add(fts);
}
//初始化存储线段坐标的二维数组
fts = new float[2][2];
//线段的起始坐标
fts[0] = new float[]{a3, b3};
num += 1;
} else if (rt4.contains(tX, tY) && !passwordValue.contains("D")) {//首次经过圆D
passwordValue += "D";
if (num != 0) {
//线段的终止坐标
fts[1] = new float[]{a4, b4};
listCoordinate.add(fts);
}
//初始化存储线段坐标的二维数组
fts = new float[2][2];
//线段的起始坐标
fts[0] = new float[]{a4, b4};
num += 1;
} else if (rt5.contains(tX, tY) && !passwordValue.contains("E")) {//首次经过圆E
passwordValue += "E";
if (num != 0) {
//线段的终止坐标
fts[1] = new float[]{a5, b5};
listCoordinate.add(fts);
}
//初始化存储线段坐标的二维数组
fts = new float[2][2];
//线段的起始坐标
fts[0] = new float[]{a5, b5};
num += 1;
} else if (rt6.contains(tX, tY) && !passwordValue.contains("F")) {//首次经过圆F
passwordValue += "F";
if (num != 0) {
//线段的终止坐标
fts[1] = new float[]{a6, b6};
listCoordinate.add(fts);
}
//初始化存储线段坐标的二维数组
fts = new float[2][2];
//线段的起始坐标
fts[0] = new float[]{a6, b6};
num += 1;
} else if (rt7.contains(tX, tY) && !passwordValue.contains("G")) {//首次经过圆G
passwordValue += "G";
if (num != 0) {
//线段的终止坐标
fts[1] = new float[]{a7, b7};
listCoordinate.add(fts);
}
//初始化存储线段坐标的二维数组
fts = new float[2][2];
//线段的起始坐标
fts[0] = new float[]{a7, b7};
num += 1;
} else if (rt8.contains(tX, tY) && !passwordValue.contains("H")) {//首次经过圆H
passwordValue += "H";
if (num != 0) {
//线段的终止坐标
fts[1] = new float[]{a8, b8};
listCoordinate.add(fts);
}
//初始化存储线段坐标的二维数组
fts = new float[2][2];
//线段的起始坐标
fts[0] = new float[]{a8, b8};
num += 1;
} else if (rt9.contains(tX, tY) && !passwordValue.contains("I")) {//首次经过圆I
passwordValue += "I";
if (num != 0) {
//线段的终止坐标
fts[1] = new float[]{a9, b9};
listCoordinate.add(fts);
}
//初始化存储线段坐标的二维数组
fts = new float[2][2];
//线段的起始坐标
fts[0] = new float[]{a9, b9};
num += 1;
}
invalidate();// 刷新画布,回调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)
}
...
...
右图跟中图的渲染过程一样,区别在于经过的圆的数量大于等于4,画笔的颜色设置成绿色
至此,以上左中右图的渲染实现过程完毕,但还有两个中间状态
右图跟左图的渲染过程一样,讲解左图的实现过程,我们称该状态线段为不完整线段,以区分之前的线段。
手指滑动未到达圆所在的区域时,线段的起始坐标是轨迹经过的最后一个圆的圆心坐标,我们只需记录终点坐标就可实现以上图中的状态。
//存储不完整线段起始终止坐标的二维数组
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();
//不完整线段终点坐标赋值
lineCrdinateImperfect[1] [0] = tX;
lineCrdinateImperfect[1] [1] = tY;
//首次经过圆A
if (rt1.contains(tX, tY) && !passwordValue.contains("A")) {
passwordValue += "A";
//不完整线段起始坐标赋值
lineCrdinateImperfect[0] [0] = a1;
lineCrdinateImperfect[0] [1] = b1;
} else if (rt2.contains(tX, tY) && !passwordValue.contains("B")) {//首次经过圆B
passwordValue += "B";
//不完整线段起始坐标赋值
lineCrdinateImperfect[0] [0] = a2;
lineCrdinateImperfect[0] [1] = b2;
} else if (rt3.contains(tX, tY) && !passwordValue.contains("C")) {//首次经过圆C
passwordValue += "C";
//不完整线段起始坐标赋值
lineCrdinateImperfect[0] [0] = a3;
lineCrdinateImperfect[0] [1] = b3;
} else if (rt4.contains(tX, tY) && !passwordValue.contains("D")) {//首次经过圆D
passwordValue += "D";
//不完整线段起始坐标赋值
lineCrdinateImperfect[0] [0] = a4;
lineCrdinateImperfect[0] [1] = b4;
} else if (rt5.contains(tX, tY) && !passwordValue.contains("E")) {//首次经过圆E
passwordValue += "E";
//不完整线段起始坐标赋值
lineCrdinateImperfect[0] [0] = a5;
lineCrdinateImperfect[0] [1] = b5;
} else if (rt6.contains(tX, tY) && !passwordValue.contains("F")) {//首次经过圆F
passwordValue += "F";
//不完整线段起始坐标赋值
lineCrdinateImperfect[0] [0] = a6;
lineCrdinateImperfect[0] [1] = b6;
} else if (rt7.contains(tX, tY) && !passwordValue.contains("G")) {//首次经过圆G
passwordValue += "G";
//不完整线段起始坐标赋值
lineCrdinateImperfect[0] [0] = a7;
lineCrdinateImperfect[0] [1] = b7;
} else if (rt8.contains(tX, tY) && !passwordValue.contains("H")) {//首次经过圆H
passwordValue += "H";
//不完整线段起始坐标赋值
lineCrdinateImperfect[0] [0] = a8;
lineCrdinateImperfect[0] [1] = b8;
} else if (rt9.contains(tX, tY) && !passwordValue.contains("I")) {//首次经过圆I
passwordValue += "I";
//不完整线段起始坐标赋值
lineCrdinateImperfect[0] [0] = a9;
lineCrdinateImperfect[0] [1] = b9;
}
invalidate();// 刷新画布,回调onDraw()方法
} else if (event.getAction() == MotionEvent.ACTION_UP) {
}
刷新画布,渲染不完整线段
@Override
protected void onDraw(Canvas canvas) {
...
...
//不完整线段坐标赋值
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)出发,绕过一个圆(B),到达圆另一个圆(C),这样会忽略中间的圆(B),经过的圆的顺序A->C,这样不合理,明明经过了中间圆(B),轨迹应该是A->B->C才对。
解决思路:计算两圆心坐标中点坐标是否为其他圆的圆心坐标。