自定义View系列:简易九宫格手势锁控件

前言

项目里想添加一个九宫格手势锁的控件,在增加便捷性的同时来增加安全性,虽然是重复造轮子,但是本着自我娱乐的精神就手撸了一个简易九宫格手势锁的自定义view控件。


1、Demo 演示



该控件实现的功能比较简单,一个九宫格手势解锁控件,你可以通过控件的几个属性来对控件的 UI 和功能进行控制:

  • 九宫格圆圈的颜色和宽度 (mLockCircleStrokeColor/ mLockCircleStrokeWidth
  • 手势路径上小圆点的颜色 (mLockPointStrokeColor
  • 手势路径的颜色以及路径的宽 (mLockPathStrokeColor/ mLockPathStrokeWidth
  • 定义锁的最小和最大长度 (mMinLockLength/ mMaxLockLength
  • 锁是否可重复(手势划过的圆圈是否可重复)(mRepeatAllowed
  • 是否显示锁(手势划过的)路径 (mShowPath
  • 解(绘制)锁成功或失败时的小圆点和路径的颜色 (mSuccessColor/ mErrorColor

下面是简单演示的动图:

image


2、实现流程



自定义手势锁控件点实现类直接继承了 View 类,实现上分为了三大块,首先自定义 attr、复写 onDraw(Canvas canvas) 实现界面点绘制、在 onTouchEvent(MotionEvent event) 中处理事件。

2.1、定义 attr


首先,定义下九宫格手势锁控件的相关属性,大概就下面几个,参照之前的说明,很容易理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
<declare-styleable name="com.robin.gesturelockview">
<attr name="mMinLockLength" format="integer" />
<attr name="mMaxLockLength" format="integer" />
<attr name="mRepeatAllowed" format="boolean" />
<attr name="mShowPath" format="boolean" />
<attr name="mLockCircleStrokeWidth" format="dimension" />
<attr name="mLockCircleStrokeColor" format="color" />
<attr name="mLockPointStrokeColor" format="color" />
<attr name="mLockPathStrokeColor" format="color" />
<attr name="mLockPathStrokeWidth" format="dimension" />
<attr name="mErrorColor" format="color" />
<attr name="mSuccessColor" format="color" />
</declare-styleable>


2.2、界面绘制



OK,进入正题。首先读取相关的属性,这个比较简单,跳过,主要说一下,就是默认的参数情况是:

  • 最小最长锁长度是4
  • 不允许重复锁
  • 不显示手势路径
  • 九宫格圆圈宽度是4px
  • 手势路径宽度是4px
  • 解(绘制)锁成功颜色是蓝色,失败则是红色
  • 其他情况颜色均为黑色

那么首先,我们要读取自定义view的长宽数值,然后来初始化我们的画笔了,主要就是四个画笔,一个来绘制九宫格圆圈的 mLockCirclePaint,并且需要一个 mLockInnerCirclePaint 来配合绘制空心圆圈,mLockPointPaint 负责绘制手势路径上的小圆点,mLockPathPaint 则负责绘制手势路径。

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
if (mWidth == 0) {
mWidth = getWidth();
mHeight = getHeight();
}
/**
*
*/
mLockInnerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLockInnerCirclePaint.setColor(Color.WHITE);
mLockCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLockCirclePaint.setColor(mLockCircleStrokeColor);
mLockPointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLockPointPaint.setColor(mLockPointStrokeColor);
mLockPathPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLockPathPaint.setColor(mLockPathStrokeColor);
mLockPathPaint.setStrokeWidth(mLockPathStrokeWidth);
if (status == Status.FAIL) {
mLockPointPaint.setColor(mErrorColor);
mLockPathPaint.setColor(mErrorColor);
}
if (status == Status.SUCCESS) {
mLockPointPaint.setColor(mSuccessColor);
mLockPathPaint.setColor(mSuccessColor);
}

这里,简单了定义几个绘制的状态,分为四个,看名字也非常容易理解,即初始阶段、解(绘制)锁中、成功和失败。通过状态值来改变相关画笔的颜色值。

1
2
3
4
5
6
enum Status {
SUCCESS,
FAIL,
ORIGIN,
LOCKING
}

第一步,绘制九宫格空心圆圈,首先根据自定义 View 的宽度来计算空心圆圈的半径 mCirclrRadius,然后找左上角第一个空心圆圈距离自定义 View 的 marginLeft 和 marginTop 值,然后计算下空心圆圈之间的距离 marginBetweenCircles。 接下来,定位左上角第一个空心圆圈的圆点位置 (mStartCx, mStartCy),很好理解吧,marginLeft 和 marginTop 分别加上半径。根据左上角的圆圈的圆心为相对坐标点 (cx, cy),分别找个 9 个空心圆圈的圆心位置并绘制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mCirclrRadius = mWidth / 12;
marginLeft = (mWidth - mCirclrRadius * 6) / 3;
marginTop = (mHeight - mCirclrRadius * 6 - marginLeft) / 2;
marginBetweenCircles = marginLeft / 2;
mStartCx = marginLeft + mCirclrRadius;
mStartCy = marginTop + mCirclrRadius;
for (int i = 0; i < 9; i++) {
int cx = mStartCx + (mCirclrRadius * 2 + marginBetweenCircles) * (i % 3);
int cy = mStartCy + (mCirclrRadius * 2 + marginBetweenCircles) * (i / 3);
canvas.drawCircle(cx, cy, mCirclrRadius, mLockCirclePaint);
canvas.drawCircle(cx, cy, mCirclrRadius - mLockCircleStrokeWidth, mLockInnerCirclePaint);
}

第二步,在绘制完九宫格之后,我们来绘制手势划过九宫格空心圆圈时的小圆点和手势路径。这里所有可能经过的格子的圆心点都已经有了,我们可以很轻松的完成这一步骤。路径显示控制条件为 mShowPath,绘制的点 mLockList 为记录的手势划过的格子序列(我们这里是 9 个格子,就用了 0-8 来表示)。需要注意的是,在对 mLockList 循环的过程中,路径线段需要两个点,我们从 i=1 开始。先画路径,然后画小圆点,没有在一个循环中同时绘制路径和小圆点,这是因为这样小圆点会完全覆盖路径线段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if (mShowPath) {
int lastcx = mStartCx;
int lastcy = mStartCy;
//
for (int i = 0; i < mLockList.size(); i++) {
int idx = mLockList.get(i);
int cx = mStartCx + (mCirclrRadius * 2 + marginBetweenCircles) * (idx % 3);
int cy = mStartCy + (mCirclrRadius * 2 + marginBetweenCircles) * (idx / 3);
if (i > 0) {
canvas.drawLine(lastcx, lastcy, cx, cy, mLockPathPaint);
}
lastcx = cx;
lastcy = cy;
}
//
for (Integer idx : mLockList) {
int cx = mStartCx + (mCirclrRadius * 2 + marginBetweenCircles) * (idx % 3);
int cy = mStartCy + (mCirclrRadius * 2 + marginBetweenCircles) * (idx / 3);
canvas.drawCircle(cx, cy, mCirclrRadius / 3, mLockPointPaint);
}
}


2.3、事件处理



OK,完成了绘制代码之后,要进行事件处理了。首先,为了方便使用,我们定义了一个回调点接口,来方便对解(绘制)锁成功/失败之后进行自己的处理。主要就两个方法:

  • 成功回调 onNext,并返回锁(格子)路径序列,方便你去做处理,比如解锁或者存储;
  • 失败回调 onError,除了返回锁路径,还返回 errCode:1 为锁过长、2 为锁过短、3 为锁重复
1
2
3
4
5
6
7
8
9
10
11
12
public interface GestureLockListener {
/**
* @param lockArray
*/
public void onNext(List<Integer> lockArray);
/**
* @param lockArray
* @param errCode
*/
public void onError(List<Integer> lockArray, int errCode);
}

事件的处理,大致的处理思路是,对于每一次解(绘制)锁来说,当该过程没有结束(解锁时成功或失败视为结束状态)。判断每个 event 是否在某一个九宫格上,根据此条件进行如下处理过程:

  • 如果是在其中一个九宫格上,分为三种情况: 1)按下事件,则纪录锁并刷新界面;2)移动事件,判断是否超过最大长度(若超过,则回调接口并刷新界面)、根据是否允许锁重复进行处理,无异常情况则纪录锁并刷新界面;3)抬起事件,重复锁的情况只会在移动过程中出现,这里不用再处理,只处理锁的长度是否在规定的最大和最小长度之间即可;
  • 不在的情况,只处理当手指离开 view 的情况,判断解锁是否成功或失败,调用接口方法并刷新界面

相关的代码如下:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
@Override
public boolean onTouchEvent(MotionEvent event) {
if (status == Status.ORIGIN || status == Status.LOCKING) {
int idx = getPoint(event);
if (event.getAction() == MotionEvent.ACTION_DOWN)
status = Status.LOCKING;
if (idx != -1) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLockList.add(idx);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
if (mLockList.size() > 0 && mLockList.get(mLockList.size() - 1) == idx) {
//do nothing
} else {
if (mMaxLockLength == mLockList.size()) {
if (mGestureLockListener != null) {
status = Status.FAIL;
invalidate();
mGestureLockListener.onError(mLockList, OUT_OF_MAX_LOCK_LENGTH);
}
return true;
}
if (mRepeatAllowed) {
mLockList.add(idx);
invalidate();
} else {
HashSet<Integer> set = new HashSet<>();
set.addAll(mLockList);
if (set.contains(idx)) {
if (mGestureLockListener != null) {
status = Status.FAIL;
invalidate();
mGestureLockListener.onError(mLockList, REPEAT_NOT_ALLOWED);
}
return true;
}
mLockList.add(idx);
invalidate();
}
}
break;
case MotionEvent.ACTION_UP:
if (mLockList.size() > 0 && mLockList.get(mLockList.size() - 1) == idx) {
//
} else {
mLockList.add(idx);
}
int code = -1;
if (mLockList.size() < mMinLockLength) {
code = LOWER_THAN_MIN_LOCK_LENGTH;
}
if (mMaxLockLength < mLockList.size()) {
code = OUT_OF_MAX_LOCK_LENGTH;
}
if (mGestureLockListener != null) {
if (code != -1) {
status = Status.FAIL;
mGestureLockListener.onError(mLockList, code);
} else {
status = Status.SUCCESS;
mGestureLockListener.onNext(mLockList);
}
}
invalidate();
break;
}
} else {
if (event.getAction() == MotionEvent.ACTION_UP) {
if (mLockList.size() < mMinLockLength && mGestureLockListener != null) {
status = Status.FAIL;
invalidate();
mGestureLockListener.onError(mLockList, LOWER_THAN_MIN_LOCK_LENGTH);
return true;
}
if (mGestureLockListener != null) {
status = Status.SUCCESS;
invalidate();
mGestureLockListener.onNext(mLockList);
}
}
}
}
return true;
}
/***
* @param event
*/
private int getPoint(MotionEvent event) {
int idx = -1;
int dist = mCirclrRadius * mCirclrRadius;
for (int i = 0; i < 9; i++) {
int cx = mStartCx + (mCirclrRadius * 2 + marginBetweenCircles) * (i % 3);
int cy = mStartCy + (mCirclrRadius * 2 + marginBetweenCircles) * (i / 3);
if (dist > Math.pow(event.getX() - cx, 2.0) + Math.pow(event.getY() - cy, 2.0)) {
idx = i;
break;
}
}
return idx;
}

此外,还提供一个方法来重置所有的状态,代码较为简单,如下:

1
2
3
4
5
6
7
8
/**
*
*/
public void reset() {
mLockList.clear();
status = Status.ORIGIN;
invalidate();
}


3、View 使用



该自定义控件已经上传到 GitHub,你可以 down 下来源码自己改造(项目地址 GestureLockView),也可以直接通过下面的两步加到你的工程中去。

第一步,在你的项目的 build.gradle 中加上 jitpack

1
2
3
4
5
6
allprojects {
repositories {
jcenter()
maven { url 'https://jitpack.io' } // add this
}
}

第二步,在你需要的 Module 的依赖中加入这个:

1
2
3
dependencies {
compile 'com.github.whtacm:GestureLockView:v1.0.1'
}

OK, sync 下一你的项目,就可以使用了,那么今天这个简单的九宫格手势锁自定义 View 就到这里了。