手势密码源码解析

参考自高仿支付宝解锁

构造

一个九宫格View + View的指示器标识已选单元

LockPatternView

构造函数以及成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  private float movingX, movingY;//记录移动x,y
//onTouchEvent中记录当前状态
private boolean isActionMove = false;
private boolean isActionDown = false;//default action down is false
private boolean isActionUp = true;//default action up is true

private int width, height;//onMeasure中获取View的宽和高
private int cellRadius, cellInnerRadius;//单个点的外圆半径以及内圆半径

private int cellBoxWidth, cellBoxHeight;//单个宽和高
//in stealth mode (default is false)
private boolean mInStealthMode = false;//隐藏模式
//haptic feed back (default is false)
private boolean mEnableHapticFeedback = false;//震动反馈
//set delay time
private long delayTime = 600L;//重绘延迟事件
//set lock
private boolean isLock = false;//锁定画布,禁止绘制
//set offset to the boundary
private int offset = 10;//外边距,相当于margin = 5
//draw view used paint
private Paint defaultPaint, selectPaint, errorPaint;

//draw triangle
private Path trianglePath;//三角形绘制
private Matrix triangleMatrix;
//Key object, which are used for drawing circle ,line ,and triangle
private Cell[][] mCells = new Cell[3][3];// 九宫格九个对象
private List<Cell> sCells = new ArrayList<Cell>();//记录选中单元
private OnPatternListener patterListener;// 实现Indicator与View的绑定

private static final double CONSTANT_COS_30 = Math.cos(Math.toRadians(30));// cos(/Π / 6)
//The runnable used for clear pattern mode
private Runnable mClearPatternRunnable = new Runnable() {
public void run() {
isLock = false;
}
};

主要类和成员方法

Cell类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public class Cell {

private int x;// the x position of circle's center point
private int y;// the y position of circle's center point
private int row;// the cell in which row
private int column;// the cell in which column
private int index;// the cell value(1-9)
private int status = STATE_NORMAL;//default status

//default status
public static final int STATE_NORMAL = 0;
//checked status
public static final int STATE_CHECK = 1;
//checked error status
public static final int STATE_CHECK_ERROR = 2;

public static final int STATE_PRESSED = 3;
...
}

手势密码状态
1
2
3
4
5
6
7
8
9
10
11
12
/**
* the display mode of the pattern
* 手势密码状态
*/
public enum DisplayMode {
//show default pattern (the default pattern is initialize status)
DEFAULT,
//show selected pattern normal
NORMAL,
//show selected pattern error
ERROR;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* initialize cell size (include circle radius, inner circle radius,
* cell box width, cell box height)
*/
private void initCellSize() {
this.cellRadius = (this.width - offset * 2) / 4 / 2;
this.cellInnerRadius = this.cellRadius / 3;
this.cellBoxWidth = (this.width - offset * 2) / 3;
this.cellBoxHeight = (this.height - offset * 2) / 3;
}
/**
* initialize nine cells
*/
private void init9Cells() {
//the distance between the center of two circles
int distance = this.cellBoxWidth + this.cellBoxWidth / 2 - this.cellRadius;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
mCells[i][j] = new Cell(distance * j + cellRadius + offset,
distance * i + cellRadius + offset, i, j, 3 * i + j + 1);
}
}
}

九宫格中的每一个点的半径相当于 (宽度 - 外边距 )/ 8,为什么处以8呢?因为水平上有三个圆,每个圆有两条半径,共有6条,再加上三个圆中间两个间距。故8条,其余同理

onMeasure方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//必须重写onMeasured获得正确的宽高,再重绘
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
getMeasuredHeight();
this.width = getMeasuredWidth();
this.height = getMeasuredHeight();
if (width != height) {
throw new IllegalArgumentException("the width must be equals height");
}
this.initCellSize();
this.set9CellsSize();
this.invalidate();
}
/**
* set nine cells size
*/
private void set9CellsSize() {
int distance = this.cellBoxWidth + this.cellBoxWidth / 2 - this.cellRadius;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
mCells[i][j].setX(distance * j + cellRadius + offset);
mCells[i][j].setY(distance * i + cellRadius + offset);
}
}
}

绘制三角形
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private void drawTriangle(Cell preCell, Cell nextCell, Canvas canvas, Paint paint) {
float distance = LockPatternUtil.getDistanceBetweenTwoPoints
(preCell.getX(), preCell.getY(), nextCell.getX(), nextCell.getY());
float x = this.cellInnerRadius * 2 / distance * (nextCell.getX() - preCell.getX()) + preCell.getX();//(2/3 * R)*cosθ + x
float y = this.cellInnerRadius * 2 / distance * (nextCell.getY() - preCell.getY()) + preCell.getY();

float angleX = LockPatternUtil.getAngleLineIntersectX(
preCell.getX(), preCell.getY(), nextCell.getX(), nextCell.getY(), distance);
float angleY = LockPatternUtil.getAngleLineIntersectY(
preCell.getX(), preCell.getY(), nextCell.getX(), nextCell.getY(), distance);
float x1, y1, x2, y2;
//slide right down
if (angleX >= 0 && angleX <= 90 && angleY >= 0 && angleY <= 90) {
x1 = x - (float) (cellInnerRadius * Math.cos(Math.toRadians(angleX - 30)));
y1 = y - (float) (cellInnerRadius * Math.sin(Math.toRadians(angleX - 30)));
x2 = x - (float) (cellInnerRadius * Math.sin(Math.toRadians(angleY - 30)));
y2 = y - (float) (cellInnerRadius * Math.cos(Math.toRadians(angleY - 30)));
}
//slide right up
else if (angleX >= 0 && angleX <= 90 && angleY > 90 && angleY <= 180) {
x1 = x - (float) (cellInnerRadius * Math.cos(Math.toRadians(angleX + 30)));
y1 = y + (float) (cellInnerRadius * Math.sin(Math.toRadians(angleX + 30)));
x2 = x - (float) (cellInnerRadius * Math.sin(Math.toRadians(180 - angleY + 30)));
y2 = y + (float) (cellInnerRadius * Math.cos(Math.toRadians(180 - angleY + 30)));
}
//slide left up
else if (angleX > 90 && angleX <= 180 && angleY >= 90 && angleY < 180) {
x1 = x + (float) (cellInnerRadius * Math.cos(Math.toRadians(180 - angleX - 30)));
y1 = y + (float) (cellInnerRadius * Math.sin(Math.toRadians(180 - angleX - 30)));
x2 = x + (float) (cellInnerRadius * Math.sin(Math.toRadians(180 - angleY - 30)));
y2 = y + (float) (cellInnerRadius * Math.cos(Math.toRadians(180 - angleY - 30)));
}
//slide left down
else {
x1 = x + (float) (cellInnerRadius * Math.cos(Math.toRadians(180 - angleX + 30)));
y1 = y - (float) (cellInnerRadius * Math.sin(Math.toRadians(180 - angleX + 30)));
x2 = x + (float) (cellInnerRadius * Math.sin(Math.toRadians(angleY + 30)));
y2 = y - (float) (cellInnerRadius * Math.cos(Math.toRadians(angleY + 30)));
}
trianglePath.reset();
trianglePath.moveTo(x, y);
trianglePath.lineTo(x1, y1);
trianglePath.lineTo(x2, y2);
trianglePath.close();
canvas.drawPath(trianglePath, paint);
}

  1. 先求出两点之间距离distance
  2. 定出三角形起始 x,y 。 x = float x = this.cellInnerRadius 2 / distance (nextCell.getX() - preCell.getX()) + preCell.getX();//(2/3 R) cosθ,θ为直线与x轴夹角。y同理
  3. 获得直线与x轴,y轴的夹角θ与δ,从而定出方向
  4. 根据方向,定出其余两个点
  5. 连接线段,绘制三角形

根据两点之间距离与半径关系判断是否选中某个点

1
2
3
4
5
6
7
8
9
10
11
private Cell checkSelectCell(float x, float y) {
for (int i = 0; i < mCells.length; i++) {
for (int j = 0; j < mCells[i].length; j++) {
Cell cell = mCells[i][j];
if (LockPatternUtil.checkInRound(cell.x, cell.y, this.cellRadius, x, y, -10)) {
return cell;
}
}
}
return null;
}

核心算法

在onTouchEvent中监听事件,进行对应处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
getParent().requestDisallowInterceptTouchEvent(true);
float ex = event.getX();
float ey = event.getY();
if (!isLock) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
handleActionDown(ex, ey);
break;
case MotionEvent.ACTION_MOVE:
handleActionMove(ex, ey);
break;
case MotionEvent.ACTION_UP:
handleActionUp();
break;
}
}
return true;
}

down

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void handleActionDown(float ex, float ey) {
isActionMove = false;
isActionDown = true;
isActionUp = false;

this.setPattern(DisplayMode.DEFAULT);

if (this.patterListener != null) {
this.patterListener.onPatternStart();
}

Cell cell = checkSelectCell(ex, ey);
if (cell != null) {
addSelectedCell(cell);
}
}
  1. 置标志位
  2. 设置当前模式为默认
  3. 回调监听器预处理
  4. 加入起始点
  5. 重绘

move

1
2
3
4
5
6
7
8
9
10
private void handleActionMove(float ex, float ey) {
isActionMove = true;
movingX = ex;
movingY = ey;
Cell cell = checkSelectCell(ex, ey);
if (cell != null) {
addSelectedCell(cell);
}
this.setPattern(DisplayMode.NORMAL);
}
  1. 置标志位
  2. 判断是否选中该点,是则加入该点
  3. 设置当前模式为常规状态并开始重绘

up

1
2
3
4
5
6
7
8
9
10
11
private void handleActionUp() {
isActionMove = false;
isActionUp = true;
isActionDown = false;

this.setPattern(DisplayMode.NORMAL);

if (this.patterListener != null) {
this.patterListener.onPatternComplete(sCells);
}
}

重绘

每次设置模式后自动重绘,如果设置隐藏路径,即不重绘。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void setPattern(DisplayMode mode) {
switch (mode) {
case DEFAULT:
for (Cell cell : sCells) {
cell.setStatus(Cell.STATE_NORMAL);
}
sCells.clear();
break;
case NORMAL:
break;
case ERROR:
for (Cell cell : sCells) {
cell.setStatus(Cell.STATE_CHECK_ERROR);
}
break;
}
this.handleStealthMode();
}

/**
* handle the stealth mode (if true: do not post invalidate; false: post invalidate)
*/
private void handleStealthMode() {
if (!mInStealthMode) {
this.postInvalidate();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
this.drawToCanvas(canvas);
}

private void drawToCanvas(Canvas canvas) {

for (int i = 0; i < mCells.length; i++) {
for (int j = 0; j < mCells[i].length; j++) {
switch (mCells[i][j].getStatus()) {
case Cell.STATE_CHECK: {
selectPaint.setStyle(Style.FILL);
selectPaint.setColor(getResources().getColor(R.color.color_red_fde7ec));
canvas.drawCircle(mCells[i][j].getX(), mCells[i][j].getY(),
this.cellRadius, this.selectPaint);
canvas.drawCircle(mCells[i][j].getX(), mCells[i][j].getY(),
this.cellRadius, this.defaultPaint);
selectPaint.setColor(getResources().getColor(R.color.pink));
canvas.drawCircle(mCells[i][j].getX(), mCells[i][j].getY(),
this.cellInnerRadius / 3, this.selectPaint);
}
break;
case Cell.STATE_NORMAL: {
canvas.drawCircle(mCells[i][j].getX(), mCells[i][j].getY(),
this.cellRadius, this.defaultPaint);
canvas.drawCircle(mCells[i][j].getX(), mCells[i][j].getY(),
this.cellInnerRadius / 3, this.selectPaint);
}
break;
case Cell.STATE_CHECK_ERROR: {
errorPaint.setStyle(Style.STROKE);
canvas.drawCircle(mCells[i][j].getX(), mCells[i][j].getY(),
this.cellRadius, this.errorPaint);
errorPaint.setStyle(Style.FILL);
canvas.drawCircle(mCells[i][j].getX(), mCells[i][j].getY(),
this.cellInnerRadius / 3, this.errorPaint);
}
break;
default:
break;
}
}
}

if (sCells.size() > 0) {
//temporary cell: at the beginning the cell is the first of sCells
Cell tempCell = sCells.get(0);

for (int i = 1; i < sCells.size(); i++) {
Cell cell = sCells.get(i);
if (cell.getStatus() == Cell.STATE_CHECK) {
drawLineIncludeCircle(tempCell, cell, canvas, selectPaint);
drawTriangle(tempCell, cell, canvas, selectPaint);
} else if (cell.getStatus() == Cell.STATE_CHECK_ERROR) {
drawLineIncludeCircle(tempCell, cell, canvas, errorPaint);
drawTriangle(tempCell, cell, canvas, selectPaint);
}
tempCell = cell;
}

if (isActionMove && !isActionUp) {
this.drawLineFollowFinger(tempCell, canvas, selectPaint);
}
}
}

  1. 根据mCells状态绘制点
  2. 按照sCells依次重绘线和三角形
  3. 若正在滑动,取出最后一个连接上的点与正常滑动的点划线

LockPatternIndicator

刷新状态,更新UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void setIndicator(List<LockPatternView.Cell> cells) {
for(LockPatternView.Cell cell : cells) {
for(int i = 0; i < mIndicatorCells.length; i++) {
for(int j = 0; j < mIndicatorCells[i].length; j++) {
if (cell.getIndex() == mIndicatorCells[i][j].getIndex()) {

mIndicatorCells[i][j].setStatus(IndicatorCell.STATE_CHECK);
}
}
}
}
this.postInvalidate();
}


private void drawToCanvas(Canvas canvas) {
for(int i = 0; i < mIndicatorCells.length; i++) {
for(int j = 0; j < mIndicatorCells[i].length; j++) {
if(mIndicatorCells[i][j].getStatus() == IndicatorCell.STATE_NORMAL) {
canvas.drawCircle(mIndicatorCells[i][j].getX(), mIndicatorCells[i][j].getY(), radius, defaultPaint);
} else if(mIndicatorCells[i][j].getStatus() == IndicatorCell.STATE_CHECK) {
canvas.drawCircle(mIndicatorCells[i][j].getX(), mIndicatorCells[i][j].getY(), radius, selectPaint);
}
}
}
}

  1. 根据传入的Cells设置自己的mIndicatorCells状态
  2. 重绘
文章目录
  1. 1. 构造
  2. 2. LockPatternView
    1. 2.1. 构造函数以及成员变量
    2. 2.2. 主要类和成员方法
    3. 2.3. 核心算法
      1. 2.3.1. down
      2. 2.3.2. move
      3. 2.3.3. up
      4. 2.3.4. 重绘
  3. 3. LockPatternIndicator
|