Skip to content

Commit 4ec645d

Browse files
committed
Update
1 parent 4b537be commit 4ec645d

1 file changed

Lines changed: 63 additions & 90 deletions

File tree

CustomView/Advance/[17]touch-matrix-region.md

Lines changed: 63 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,31 @@
11
# 特殊控件的事件处理方案
22

3+
34
本文带大家了解 Android 特殊形状控件的事件处理方式,主要是利用了 Region 和 Matrix 的一些方法,超级实用的事件处理方案,相信看完本篇之后,任何奇葩控件的事件处理都会变得十分简单。
45

56
不得不说,Android 对事件体系封装的非常棒,即便对事件体系不太了解的人,只要简单的调用方法就能使用,而且具有防呆设计,能够保证事件流的完整性和统一性,最大可能性的避免了事件处理的混乱,着实令人佩服。
67

78
然而世界上并没有绝对完美的东西,**当【事件处理】遇上【自定义View】,一场好戏就开演了,玩的好叫坐镇军前,指挥千军万马而分毫不乱,玩的不好就是抓耳挠腮,眼见敌人前后包抄而无可奈何。**
89

10+
> #### 注意:
11+
>
12+
> 本文中所有的 自定义View 均继承自 CustomView ,这是一个自定义的超类,目的是简化 自定义View 部分常用操作,你可以在 [ViewSupport](https://github.com/GcsSloop/ViewSupport/wiki/CustomView) 中找到它以及关于它的简介。
13+
914
## 特殊形状控件
1015

1116
在通常的情况下,自定义 View 直接使用系统的事件体系处理就行,我们也不需要特殊处理,然而当一些特殊的控件出现的时候,麻烦就来了,举个栗子:
1217

1318
![](http://ww2.sinaimg.cn/large/005Xtdi2jw1f9t9u0tignj308c08cq3f.jpg)
1419

15-
这是一个在遥控器上非常常见的按键布局,注意中间上下左右选择的部分,看起来十分简单,然而当你真正准备在手机上实现的时候麻烦就出现了。因为所有的View默认都是矩形的,所以事件接收区域也是矩形的,如果直接使用系统提供的 View 来组合出一摸一样的布局也很简单,但点击区域该如何处理?显然这样有部分点击区域是在控件外面的,并且控件之间会产生重叠区域,点击的时候容易判断错误,例如
20+
这是一个在遥控器上非常常见的按键布局,注意中间上下左右选择的部分,看起来十分简单,然而当你真正准备在手机上实现的时候麻烦就出现了。因为所有的 View 默认都是矩形的,所以事件接收区域也是矩形的,如果直接使用系统提供的 View 来组合出一摸一样的布局也很简单,但点击区域该如何处理?显然有部分点击区域是在控件外面的,并且会产生重叠区域
1621

17-
> 红色方框表示用 View 组合的情况下,单个 View 的可点击区域。
22+
> 红色方框表示 View 的可点击区域。
1823
1924
![](http://ww3.sinaimg.cn/large/005Xtdi2jw1f9ta3eymeej308c08cwf1.jpg)
2025

21-
当我们面对这样比较奇特的控件的时候,有很多处理办法,比较投机的一种就是背景贴一个静态图,按钮做成透明的,设置小一点,放在对应的位置,这样可以保证不会误触,当然了如果想要点击效果可以在按钮按下的时候更新一下背景图,这样虽然也可以,但可点击区域会变小,体验效果会变差。设计方案变得非常复杂,而且逻辑也不容易处理,可以说是一种非常糟糕的设计
26+
当我们面对这样比较奇特的控件的时候,有很多处理办法,比较投机的一种就是背景贴一个静态图,按钮做成透明的,设置小一点,放在对应的位置,这样可以保证不会误触,当然了如果想要点击效果可以在按钮按下的时候更新一下背景图,这样虽然也可以,但是这样会导致可点击区域变小,体验效果变差,设计方案变得复杂,而且逻辑也不容易处理,是一种非常糟糕的设计
2227

23-
当然了,看了我这么多文章的小伙伴应该也猜到我接下来要说什么了,没错,就是自定义 View。当我们面对一些奇葩控件的时候,自定义 View 就变成了一种非常好用的处理方案。
28+
当然了,看了我这么多文章的小伙伴应该也猜到接下来要说什么了,没错,就是自定义 View。当我们面对一些奇葩控件的时候,自定义 View 就变成了一种非常好用的处理方案。
2429

2530
相信小伙伴们看过 [前面的文章][CustomViewIndex] 之后,对各种图形的绘制已经不成问题了,所以我们直接处理重点问题。
2631

@@ -34,44 +39,33 @@ Region 直接翻译的意思是 地域,区域。**在此处应该是区域的
3439

3540
**本文中我们重点要使用到的是 Region 中的 `contains` 方法,这个方法可以判断一个点是否包含在该区域内。**
3641

37-
下面是一个简单的示例程序:
42+
接下来是一个简单的示例,**判断手指是否是在圆形区域内按下**:
43+
44+
![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f9xtlae5wzj308c0ea3yn.jpg)
45+
46+
代码:
3847

3948
```java
40-
public class RegionClickView extends View {
41-
Paint mPaint;
42-
Region globalRegion;
43-
Region circleRegion1;
44-
Region circleRegion2;
45-
Path circlePath1;
46-
Path circlePath2;
49+
public class RegionClickView extends CustomView {
50+
Region circleRegion;
51+
Path circlePath;
4752

4853
public RegionClickView(Context context) {
4954
super(context);
50-
51-
mPaint = new Paint();
52-
mPaint.setStyle(Paint.Style.FILL);
53-
mPaint.setColor(Color.GRAY);
54-
55-
// 用Path创建两个圆
56-
circlePath1 = new Path();
57-
circlePath2 = new Path();
58-
circlePath1.addCircle(100, 100, 50, Path.Direction.CW);
59-
circlePath2.addCircle(400, 400, 50, Path.Direction.CW);
60-
61-
// 创建 Region
62-
circleRegion1 = new Region();
63-
circleRegion2 = new Region();
55+
mDeafultPaint.setColor(0xFF4E5268);
56+
circlePath = new Path();
57+
circleRegion = new Region();
6458
}
6559

6660
@Override
6761
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
6862
super.onSizeChanged(w, h, oldw, oldh);
69-
// 将剪裁边界设置为视图大小
70-
globalRegion = new Region(0, 0, w, h);
71-
63+
// ▼在屏幕中间添加一个圆
64+
circlePath.addCircle(w/2, h/2, 300, Path.Direction.CW);
65+
// ▼将剪裁边界设置为视图大小
66+
Region globalRegion = new Region(-w, -h, w, h);
7267
// ▼将 Path 添加到 Region 中
73-
circleRegion1.setPath(circlePath1, globalRegion);
74-
circleRegion2.setPath(circlePath2, globalRegion);
68+
circleRegion.setPath(circlePath, globalRegion);
7569
}
7670

7771
@Override
@@ -82,45 +76,39 @@ public class RegionClickView extends View {
8276
int y = (int) event.getY();
8377

8478
// ▼点击区域判断
85-
if (circleRegion1.contains(x,y)){
86-
Toast.makeText(this.getContext(),"圆1被点击",Toast.LENGTH_SHORT).show();
87-
}
88-
if (circleRegion2.contains(x,y)){
89-
Toast.makeText(this.getContext(),"圆2被点击",Toast.LENGTH_SHORT).show();
79+
if (circleRegion.contains(x,y)){
80+
Toast.makeText(this.getContext(),"圆被点击",Toast.LENGTH_SHORT).show();
9081
}
9182
break;
9283
}
93-
return super.onTouchEvent(event);
84+
return true;
9485
}
9586

9687
@Override
9788
protected void onDraw(Canvas canvas) {
98-
// ▼注意此处将全局变量转化为局部变量,方便 GC 回收 Canvas
99-
Path circle1 = circlePath1;
100-
Path circle2 = circlePath2;
101-
102-
// 绘制两个圆
103-
canvas.drawPath(circle1,mPaint);
104-
canvas.drawPath(circle2,mPaint);
89+
// 绘制圆
90+
canvas.drawPath(circle1,mDeafultPaint);
10591
}
10692
}
10793
```
10894

10995
> 代码中比较重要的内容都用 ▼ 符号标记出来了。
11096
111-
上述代码非常简单,就是创建了两个 Path,在 Path 中添加圆形,之后将 Path 设置到 Region 中,当手指在屏幕上按下的时候判断手指按下位置是否在 Region 区域内。
112-
113-
代码整体逻辑非常简单,大家测试一下就行了。
97+
上述代码非常简单,就是创建了个 Path 并在其中添加圆形,之后将 Path 设置到 Region 中,当手指在屏幕上按下的时候判断手指位置是否在 Region 区域内。
11498

11599

116100

117101
## 画布变换后坐标转换问题
118102

119-
还是上述的例子,绘制一个上下左右选择按键,这个控件是上下左右对称的,熟悉我代码风格的小伙伴都知道,如果遇上这种问题,我肯定是要将坐标系平移到这个控件中心的,这样数据比较好计算,然而进行画布变换操作会产生一个新问题:**手指触摸的坐标系和画布坐标系不统一,就可能引起手指触摸位置和绘制位置不统一。**
103+
还是本文一开始的例子,绘制一个上下左右选择按键,这个控件是上下左右对称的,熟悉我代码风格的小伙伴都知道,如果遇上这种问题,我肯定是要将坐标系平移到这个控件中心的,这样数据比较好计算,然而进行画布变换操作会产生一个新问题:**手指触摸的坐标系和画布坐标系不统一,就可能引起手指触摸位置和绘制位置不统一。**
120104

121105
举个栗子:
122106

123-
> 在手指按下位置绘制一个圆。
107+
> 画布移动后在手指按下位置绘制一个圆,可以看到,直接拿手指触摸位置的坐标来绘制会导致绘制位置不正确,**两者坐标是相同的,但是由于坐标系不同,导致实际显示位置不同。**
108+
109+
![](http://ww4.sinaimg.cn/large/005Xtdi2jw1f9tdc3p84uj308c0d4mx9.jpg)
110+
111+
代码:
124112

125113
```java
126114
public class CanvasVonvertTouchTest extends CustomView{
@@ -160,24 +148,20 @@ public class CanvasVonvertTouchTest extends CustomView{
160148
float x = down_x;
161149
float y = down_y;
162150

163-
// 绘制触摸坐标系,颜色为灰色,为了能够显示出坐标系,将坐标系位置稍微偏移了一点
164-
drawTouchCoordinateSpace(canvas);
165-
166-
canvas.translate(mViewWidth/2, mViewHeight/2); // 画布平移
167-
168-
// 绘制平移后的坐标系,颜色为红色
169-
drawTranslateCoordinateSpace(canvas);
151+
drawTouchCoordinateSpace(canvas); // 绘制触摸坐标系 灰色
152+
153+
// ▼注意画布平移
154+
canvas.translate(mViewWidth/2, mViewHeight/2);
155+
156+
drawTranslateCoordinateSpace(canvas); // 绘制平移后的坐标系,红色
170157

171-
// 如果没有就返回
172-
if (x == -1 && y == -1)
173-
return;
158+
if (x == -1 && y == -1) return; // 如果没有就返回
174159

175-
// 在触摸位置绘制一个小圆
176-
canvas.drawCircle(x,y,20,mDeafultPaint);
160+
canvas.drawCircle(x,y,20,mDeafultPaint); // 在触摸位置绘制一个小圆
177161
}
178162

179163
/**
180-
* 绘制触摸坐标系,颜色为灰色,为了能够显示出坐标系,将坐标系位置稍微偏移了一点
164+
* 绘制触摸坐标系,灰色,为了能够显示出坐标系,将坐标系位置稍微偏移了一点
181165
*/
182166
private void drawTouchCoordinateSpace(Canvas canvas) {
183167
canvas.save();
@@ -189,7 +173,7 @@ public class CanvasVonvertTouchTest extends CustomView{
189173
}
190174

191175
/**
192-
* 绘制平移后的坐标系,颜色为红色
176+
* 绘制平移后的坐标系,红色
193177
*/
194178
private void drawTranslateCoordinateSpace(Canvas canvas) {
195179
CanvasAidUtils.set2DAxisLength(500, 500, 700, 700);
@@ -200,12 +184,6 @@ public class CanvasVonvertTouchTest extends CustomView{
200184
}
201185
```
202186

203-
可以看到,直接拿手指触摸位置的坐标来绘制会导致绘制位置不正确,大概会像这样子:
204-
205-
![](http://ww4.sinaimg.cn/large/005Xtdi2jw1f9tdc3p84uj308c0d4mx9.jpg)
206-
207-
两者坐标是相同的,但是由于坐标系不同,导致实际显示位置不同。
208-
209187
**那么问题来了,我们在之前的文章中讲过,映射不同坐标系的坐标用 什么来着?**
210188
**是 Matrix。**
211189

@@ -214,7 +192,11 @@ public class CanvasVonvertTouchTest extends CustomView{
214192

215193
> **Matrix 是一个矩阵,主要功能是坐标映射,数值转换。**
216194
217-
那么接下来我们就对上面的示例进行简单的改造一下,让触摸位置和实际绘制绘制重合。
195+
那么接下来我们就对上面的示例进行简单的改造一下,让触摸位置和实际绘制绘制重合。小白点和黑色的圆没有完全重合是因为系统显示触摸位置的绘制逻辑和我使用的绘制逻辑不太相同导致的。
196+
197+
![](http://ww3.sinaimg.cn/large/005Xtdi2jw1f9te2mzxcvj308c0d40st.jpg)
198+
199+
代码:
218200

219201
**注意:比较重要的修改位置用▼标记出来了。**
220202

@@ -256,17 +238,13 @@ public class CanvasVonvertTouchTest extends CustomView{
256238
protected void onDraw(Canvas canvas) {
257239
float[] pts = {down_x, down_y};
258240

259-
// 绘制触摸坐标系,颜色为灰色,为了能够显示出坐标系,将坐标系位置稍微偏移了一点
260-
drawTouchCoordinateSpace(canvas);
261-
262-
canvas.translate(mViewWidth/2, mViewHeight/2); // 画布平移
263-
264-
// 绘制平移后的坐标系,颜色为红色
265-
drawTranslateCoordinateSpace(canvas);
266-
267-
// 如果没有就返回
268-
if (pts[0] == -1 && pts[1] == -1)
269-
return;
241+
drawTouchCoordinateSpace(canvas); // 绘制触摸坐标系,灰色
242+
// ▼注意画布平移
243+
canvas.translate(mViewWidth/2, mViewHeight/2);
244+
245+
drawTranslateCoordinateSpace(canvas); // 绘制平移后的坐标系,红色
246+
247+
if (pts[0] == -1 && pts[1] == -1) return; // 如果没有就返回
270248

271249
// ▼ 获得当前矩阵的逆矩阵
272250
Matrix invertMatrix = new Matrix();
@@ -301,15 +279,8 @@ public class CanvasVonvertTouchTest extends CustomView{
301279
CanvasAidUtils.draw2DCoordinateSpace(canvas);
302280
}
303281
}
304-
305282
```
306283

307-
**绘制结果:**
308-
309-
小白点和黑色的圆没有完全重合是因为系统显示触摸位置的绘制逻辑和我使用的绘制逻辑不太相同,两者的位置是一样的。
310-
311-
![](http://ww3.sinaimg.cn/large/005Xtdi2jw1f9te2mzxcvj308c0d40st.jpg)
312-
313284
其实核心部分就这两点:
314285

315286
```java
@@ -336,6 +307,10 @@ invertMatrix.mapPoints(pts);
336307

337308
在解决了上述两大难题之后,相信不论形状如何奇葩的自定义控件,基本上都难不倒大家了,最后用一个简单的示例作为结尾,还是文章开头所举的例子,核心内容就是上面讲的两个东西。
338309

310+
![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f9tinrk6ilj308c0eat92.jpg)
311+
312+
代码:
313+
339314
```java
340315
public class RemoteControlMenu extends CustomView {
341316
Path up_p, down_p, left_p, right_p, center_p;
@@ -545,8 +520,6 @@ public class RemoteControlMenu extends CustomView {
545520

546521
当手指在某一区域活动时,该区域会高亮显示,如果注册了监听器,点击某一区域会触发监听器回调。
547522

548-
![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f9tinrk6ilj308c0eat92.jpg)
549-
550523

551524

552525
## 总结

0 commit comments

Comments
 (0)