一. 前言

封装了手势解锁的基本操作,正常状态和选中状态的图片都可以替换,线条的颜色和宽度也可以改变,同时你可以设置同一个点能否选择多次。为了绘制的线条在点视图的下面,所以需要嵌套一个RelativeLayout,XLPictureUnlcok在嵌套的RelativeLayout中的宽高都是match_parent。

Github地址:XLPictureUnlock

二. 自定义View

1. 自定义用到的属性

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
//正常状态的图片资源
private int normal_image_id = R.drawable.normal;
//选中状态的图片资源
private int select_image_id = R.drawable.selected;

//点的大小
private int dot_size = 45;

//线条的颜色
private int line_color = Color.parseColor("#99CCFF");
//线条的宽度
private int line_width = 10;

//选中的点是否能够再次选择
private boolean can_select_again = false;

//路径的起点
private Point start_point;
//路径的终点
private Point end_point;
//画笔
private Paint paint;

//存储绘图的点
private List<ImageView> dots;
//存储点亮的点
private List<ImageView> light_dots;
//存储绘制的路径
private List<Path> paths;

//回调密码的监听者
private CallBackPasswordListener listener;

2. 在values文件夹下自定义属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="XLPictureUnlock">
<!--正常状态的图片资源-->
<attr name="normal_image_id" format="reference"/>
<!--选中状态的图片资源-->
<attr name="select_image_id" format="reference"/>
<!--点的大小-->
<attr name="dot_size" format="integer"/>
<!--线条的颜色-->
<attr name="line_color" format="color"/>
<!--线条的宽度-->
<attr name="line_width" format="integer"/>
<!--选中的点是否能够再次选择-->
<attr name="can_select_again" format="boolean"/>
</declare-styleable>
</resources>

3. 创建构造方法

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
/**
* 构造方法:Java代码初始化
* @param context
*/
public XLPictureUnlock(Context context) {
super(context);

//初始化操作
init();

}

/**
* 构造方法:Xml代码初始化
* @param context
* @param attrs
*/
public XLPictureUnlock(Context context, AttributeSet attrs) {
super(context, attrs);

//解析属性
if (attrs != null){
//1.获取所有属性值的集合
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.XLPictureUnlock);

//2.解析单个属性的值
normal_image_id = typedArray.getResourceId(R.styleable.XLPictureUnlock_normal_image_id,normal_image_id);
select_image_id = typedArray.getResourceId(R.styleable.XLPictureUnlock_select_image_id,select_image_id);
dot_size = PxUtil.dpToPx(typedArray.getInteger(R.styleable.XLPictureUnlock_dot_size,dot_size),getContext());
line_color = typedArray.getColor(R.styleable.XLPictureUnlock_line_color,line_color);
line_width = PxUtil.dpToPx(typedArray.getInteger(R.styleable.XLPictureUnlock_line_width,line_width),getContext());
can_select_again = typedArray.getBoolean(R.styleable.XLPictureUnlock_can_select_again,can_select_again);

//3.释放资源
typedArray.recycle();
}

//初始化操作
init();
}

/**
* 初始化操作
*/
private void init() {
//初始化集合
dots = new ArrayList<>();
light_dots = new ArrayList<>();
paths = new ArrayList<>();

//画笔初始化
paint = new Paint();
paint.setColor(line_color);
paint.setStrokeWidth(line_width);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStyle(Paint.Style.STROKE);//一定要设置,不然绘制Path没效果
paint.setAntiAlias(true);

//关闭硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}

4. 绘制点

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
/**
* 加入点视图
* @param layout
*/
public void addDotView(final RelativeLayout layout){
this.post(new Runnable() {
@Override
public void run() {
//获取视图的宽高
int width = getWidth();
int height = getHeight();

//获取间距
int hor_padding = (width - dot_size * 3) / 4;
int ver_padding = (height - dot_size * 3) / 4;

//依次创建视图
for (int i = 0; i < 9; i++) {
//列数
int col = i % 3;
//行数
int row = i / 3;

//创建视图
@SuppressLint("DrawAllocation") ImageView dot = new ImageView(getContext());
//设置图片
dot.setImageResource(normal_image_id);
//设置图片填充方式
dot.setScaleType(ImageView.ScaleType.FIT_XY);
//设置位置
LayoutParams layoutParams = new LayoutParams(
dot_size,
dot_size
);
layoutParams.leftMargin = hor_padding + (hor_padding + dot_size) * col;
layoutParams.topMargin = ver_padding + (ver_padding + dot_size) * row;
//将视图加入布局
layout.addView(dot,layoutParams);
//设置布局的id
dot.setId(i+1);
//加入集合存储
dots.add(dot);
}
}
});
}

5. 触摸事件以及回调

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//触摸过程
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
//获取触摸点坐标
float x = event.getX();
float y = event.getY();

//存储获取的点
ImageView dot;

//触摸过程
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//获取触摸的点
dot = isDotsContainPoint(x, y);

//对选中的点,改变图片,设置为路径的起点
if (dot != null){
dot.setImageResource(select_image_id);
light_dots.add(dot);

start_point = new Point(
(int)(dot.getX()+dot.getPivotX()),
(int)(dot.getY()+dot.getPivotY())
);
}

break;
case MotionEvent.ACTION_MOVE:
//获取触摸的点
dot = isDotsContainPoint(x, y);

//如果触摸到了点
if (dot != null) {
//判断这个点有没有被点亮 或者 能不能重复点击
if (!isDotChangeLight(dot) || can_select_again) {

//1.点亮点
if (!isDotChangeLight(dot)){
dot.setImageResource(select_image_id);
light_dots.add(dot);
}else {
//防止在同一个点内一直移动,导致多次加入
if (light_dots.get(light_dots.size()-1).getId() != dot.getId()){
light_dots.add(dot);
}
}

//判断是不是第一个点
if (start_point != null){
//2.在两点之间绘制线条

//2.1确认路径
Path path = new Path();
path.moveTo(start_point.x,start_point.y);
path.lineTo(
(dot.getX()+dot.getPivotX()),
(dot.getY()+dot.getPivotY())
);
paths.add(path);
//2.3刷新
invalidate();

//2.2确认新的起点
start_point = new Point(
(int)(dot.getX()+dot.getPivotX()),
(int)(dot.getY()+dot.getPivotY())
);
end_point = start_point;

//2.3刷新
invalidate();
}else {
//2.设置为起始点
start_point = new Point(
(int)(dot.getX()+dot.getPivotX()),
(int)(dot.getY()+dot.getPivotY())
);
}

}
}else {
//1.绘制线条
end_point = new Point((int)x,(int)y);

//2.刷新
invalidate();
}

break;
case MotionEvent.ACTION_UP:
end_point = start_point;
//刷新
invalidate();

//所有选中的点回复正常样式
for (ImageView light_dot : light_dots) {
light_dot.setImageResource(normal_image_id);
}

//回调密码
if (listener != null){
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < light_dots.size(); i++) {
stringBuilder.append(light_dots.get(i).getId());
}

if (!stringBuilder.toString().equals("")){
listener.picturePassword(stringBuilder.toString());
}
}

//清空点亮的点
light_dots.clear();
//清空paths
paths.clear();
//起始点,终点,清空
start_point = null;
end_point = null;

break;
}

return true;
}

/**
* 回调接口
*/
public interface CallBackPasswordListener {
void picturePassword(String password);
}

6. 绘制过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//重绘过程
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);

//判断有没有路径可以画
if (paths.size() > 0){
for (Path path : paths) {
canvas.drawPath(path,paint);
}
}

//判断有没有终点
if (start_point != end_point && start_point != null && end_point != null){
canvas.drawLine(
start_point.x,start_point.y,
end_point.x,end_point.y,paint
);
}
}

7. 辅助判断函数

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
/**
* 判断一个点是否已经被点亮
* @param dot
* @return
*/
private boolean isDotChangeLight(ImageView dot){
for (ImageView light_dot : light_dots) {
if (light_dot.getId() == dot.getId()){
return true;
}
}

return false;
}

/**
* 判断触摸点是否在一个点中
* @param x
* @param y
* @return
*/
private ImageView isDotsContainPoint(float x, float y){
//循环判断是否触摸点是否在哪一个点的范围内
for (ImageView dot : dots) {
//获取当前点的范围
RectF rectF = new RectF(dot.getLeft(), dot.getTop(), dot.getRight(), dot.getBottom());
//判断坐标是否在该点内
if (rectF.contains(x,y)){
return dot;
}
}

return null;
}

三. 具体的使用

首先需要添加依赖,添加依赖之后,在布局文件中这样调用。

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
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:background="@drawable/bg">

<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="请输入密码"
android:textSize="30sp"
android:textColor="#ffffff"
android:textAlignment="center"
android:layout_marginTop="80dp"
/>

<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
android:text="忘记密码?"
android:textColor="#ff99cc"
android:layout_centerHorizontal="true"
android:layout_below="@id/text"
/>

<RelativeLayout
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/text"
>
<swu.xl.pictureunlock_draw.XLPictureUnlock
android:id="@+id/unlock"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:can_select_again="true"
app:dot_size="65"
app:line_width="8"
/>
</RelativeLayout>

</RelativeLayout>
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
public class MainActivity extends AppCompatActivity {
//SharedPreferences的名字和存储数据的键名
private final String SHARE_NAME = "PictureUnlockSelf";
private final String PASSWORD_KEY = "password";

//密码
private String firPassword = "";
private String password = "";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

//读取SharedPreferences存储的数据,首先要获得SharedPreferences的对象
final SharedPreferences sharedPreferences = getSharedPreferences(SHARE_NAME, Context.MODE_PRIVATE);
//存储数据需要edit()
@SuppressLint("CommitPrefEdits") final SharedPreferences.Editor edit = sharedPreferences.edit();

//TextView
final TextView text = findViewById(R.id.text);
password = sharedPreferences.getString(PASSWORD_KEY, "");
if (password.length() == 0){
//设置文本
text.setText("请设置密码");
}else {
//设置文本
text.setText("请输入密码");
}

//Button
Button btn = findViewById(R.id.btn);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//清空密码
edit.putString(PASSWORD_KEY, "");
//异步提交
edit.apply();
firPassword = "";
password = "";

//设置文本
text.setText("请重新设置密码");
}
});

//XLPictureUnlock
XLPictureUnlock pictureUnlock = findViewById(R.id.unlock);
//找到添加点的布局
RelativeLayout layout = findViewById(R.id.layout);
//添加点
pictureUnlock.addDotView(layout);
//监听密码
pictureUnlock.setCallBackPasswordListener(new XLPictureUnlock.CallBackPasswordListener() {
@Override
public void picturePassword(String pwd) {
if (password.length() == 0){
if (firPassword.length() == 0){
//第一次设置密码
firPassword = pwd;

//设置文本
text.setText("请再次输入密码以确认");
}else {
//第二次设置密码
if (firPassword.equals(pwd)){
//密码设置成功
//设置文本
text.setText("密码设置成功");

//保存
edit.putString(PASSWORD_KEY, pwd);
//异步提交
edit.apply();

//清楚第一次密码
firPassword = "";

jump();
}else {
//设置文本
text.setText("两次密码不一致,请重新输入");
}
}
}else {
if (password.equals(pwd)){
//设置文本
text.setText("密码正确");

jump();
}else {
//设置文本
text.setText("密码错误");
}
}
}
});
}

/**
* 跳转界面
*/
private void jump(){
startActivity(new Intent(this,SecondActivity.class));
}
}