自定义下拉刷新组件之前,必须要了解什么是事件分发机制。
按照顺序阅读,看完郭霖 事件分发机制上,郭霖 事件分发机制下,和鸿洋 事件分发机制上,鸿洋 事件分发机制下以后结合Android开发艺术探索和源码,然后看事件分发机制总结这篇,基本能够掌握事件分发机制了。
事件分发机制的内容对于初学者而言,是反复看,实践过后再理解便能掌握其原理。下面我们简要介绍下事件分发机制,具体内容,请按顺序反复阅读上面提及的几篇文章。
事件分发机制
什么是事件分发机制?
事件分发机制就是,当一个点击事件发生后,系统将这个事件按照Activity,ViewGroup,View传递给具体的View去处理。
事件分发机制涉及以下三个方法
- dispatchTouchEvent 分发点击事件
- onInterceptTouchEvent 拦截点击事件
- onTouchEvent 处理点击事件
记住View没有onInterceptTouchEvent这个方法。
小例子
我们通过一个小例子来大致了解下事件分发机制的流程,这里我们使用系统默认的返回方式。
步骤:
- 创建TestLinearLayout 继承 Linearlayout, 重写 dispatchTouchEvent, onInterceptTouchEvent,onTouchEvent
- 创建TestButton 继承 Button,重写onInterceptTouchEvent,onTouchEvent
- 创建布局
- 重写Activity的dispatchTouchEvent,onTouchEvent
TestLinearLayout1
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
55public class TestLinearLayout extends LinearLayout {
public String TAG = "TestLinearLayout";
public TestLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "onTouchEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "onTouchEvent: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "onTouchEvent: ACTION_UP");
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "onInterceptTouchEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "onInterceptTouchEvent: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "onInterceptTouchEvent: ACTION_UP");
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "dispatchTouchEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "dispatchTouchEvent: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "dispatchTouchEvent: ACTION_UP");
break;
}
return super.dispatchTouchEvent(ev);
}
}
TestButton中的内容和TestLinearLayout完全一样,只是少了OnInterceptTouchEvent不贴了。
布局
1 | <?xml version="1.0" encoding="utf-8"?> |
在MainActivity中按类似的方式重写dispatchTouchEvent和OnTouchEvent。
点击空白处打印如下
通过这张图片我们可以看到事件确实是从Activity传递到ViewGroup,由于Activity没有OnInterceptTouchEvent,所以调用完dispatchTouchEvent后,调用ViewGroup的dispatchTouchEvent。
我们还可以发现ViewGroup的onTouchEvent 比Activity先调用。
点击Button打印如下
在这里我们可以清晰的看到
Activity 调用dispatchTouchEvent
ViewGroup 调用dispatchTouchEvent
ViewGroup 调用onInterceptTouchEvent
View 调用dispatchTouchEvent
VIew 调用onTouchEvent
此时我们看到了当View调用了onTouchEvent后,ViewGroup和MainActivity中的onTouchEvent不被调用。
这是为什么呢?
由于Button是可点击的,一旦触发了Button的onTouchEvent,返回true,表示消费了该事件,就不向上传递onTouchEvent事件。
如果在ViewGroup中的OnTouchEvent也返回true,那么Activity中的OnTouchEvent也不会调用。
上述内容可以被表达为:
领导下发了一个事件给 经理,经理把事件 交给你处理,如果你有能力处理 就消费掉这个事件。 没有能力处理,就退回给经理,经理也没能力处理 就退回给领导。
拦截事件可以表达为:
领导下发事件给 经理, 经理觉得太简单了 直接拦截处理。
源码解析
Activity源码
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
我们主要关注 getWindow().superDispatchTouchEvent(ev)) ,这句话接调用的内容
1 | @Override |
再来看看mDecor调用的内容
1 | public boolean superDispatchTouchEvent(MotionEvent event) { |
这里的super.dispatchTouchEvent()调用的就是ViewGroup的dispatchTouchEvent。
所以在 Activity调用完dispatchTouchEvent之后会调用ViewGroup的相同的方法。
ViewGroup的源码 伪代码
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
通过这段代码,我们可以很清楚的看到,ViewGroup调用dispatchTouchEvent后,
调用onInterceptTouchEvent,判断是否拦截,如果返回true,表示拦截。那么接下来就调用ViewGroup自身的onTouchEvent来处理时间,不会传递给View。
如果返回false,表示不拦截,接下来就调用View的dispatchTouchEvent方法,触发View相关的事件。
View的源码
1 | public boolean dispatchTouchEvent(MotionEvent event) { |
这段源码涉及到了 setOnTouchListener事件。当我们给View设置 setOnTouchListener
1 | public boolean onTouch(View v, MotionEvent event) { |
默认返回false。 结合上面的代码,如果我们设置了setOnTouchListener,
即mOnTouchListener!=null
(mViewFlags & ENABLED_MASK) == ENABLED表示View可使用
mOnTouchListener.onTouch(this, event) 中将默认的false 改为true, 就直接返回true,不会执行onTouchEvent。
否则,执行 onTouchEvent。
接下来我们看onTouchEvent,源码很长,我们截取一部分看,
1 | public boolean onTouchEvent(MotionEvent event) { |
这段代码中我们可以发现 只要进入 if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) ,返回值就为true,表明了只要View为可点击的,那么onTouchEvent 一定会 返回true,也就是一定会 消费事件。这也就解释了上面 点击Button 触发onTouchEvent 不调用 ViewGroup和Activity onTouchEvent的原因了。
注意事项
我们知道MotionEvent 主要有以下三个事件
- ACTION_DOWN
- ACTION_MOVE
- ACTION_UP
我们每次点击屏幕,最先触发的就是ACTION_DOWN事件。在ViewGroup的拦截事件中,
面对ACTION_DOWN ,ViewGroup总会调用自己的onInterceptTouchEvent来询问是否拦截事件。
一旦ViewGroup在ACTION_DOWN 拦截,onInterceptTouchEvent 返回true,表明要自己处理所有点击事件,也就是不会传递给View,后续的ACTION_MOVE,ACTION_UP 同样不能。
系统还给View提供了改变拦截状态的方法
requestDisallowInterceptTouchEvent,来修改处ACTION_DOWN以外的拦截事件。
综上两点,我们在处理滑动冲突的时候,在ViewGroup的ACTION_DOWN中 onIntercept 设置为false。下面就来介绍滑动冲突的相关内容。
滑动冲突
这篇文章介绍了滑动冲突解决
滑动冲突分为三种
- 内布局横向滑动,外布局纵向滑动,或者相反。也就是内外滑动的方向不同
- 内外布局同时横向滑动,或者纵向滑动。也就是内外滑动方向相同
- 前两者的多重组合
滑动冲突小例子
滑动冲突的解决方案有两种,
外部拦截法
父View根据需要对事件进行拦截。逻辑处理放在父View的onInterceptTouchEvent方法中。我们只需要重写父View的onInterceptTouchEvent方法,并根据逻辑需要做相应的拦截即可。内部拦截法
父View不拦截任何事件,所有事件都传递给子View,子View根据需要决定是自己消费事件还是给父View处理。这需要子View使用requestDisallowInterceptTouchEvent方法才能正常工作。
为了更容易理解滑动冲突,这里的小例子 创建一个 BadViewPager继承自ViewPager,改写onInterceptTouchEvent
父类不拦截
1 | public class BadViewPager extends ViewPager{ |
布局文件
1 | <?xml version="1.0" encoding="utf-8"?> |
list_view_layout的布局
1 | <?xml version="1.0" encoding="utf-8"?> |
主代码如下,在这里给ViewPager 添加了三个ListView布局。
1 | public class ScrollerActivity extends AppCompatActivity { |
我们在父布局中设置不拦截,所以ListView 消费点击触摸事件,效果如图所示,只有ListView可以滑动,左右不能滑动。
父类拦截
1 | public class BadViewPager extends ViewPager{ |
效果如图,我们能够左右滑动,ListView不能上下滑动
外部拦截方法
套路伪代码
1 | public boolean onInterceptTouchEvent(MotionEvent event) { |
注意
根据我们已经了解到的事件分发机制,在这里
ACTION_DOWN 一定返回false,不要拦截它,否则后续ACTION_MOVE 与 ACTION_UP事件都将默认交给父View去处理。
当我们的滑动方向呈一定角度,如图
当横向滑动的距离大于纵向距离,我们就认为是ViewPager的滑动
1 | case MotionEvent.ACTION_MOVE: |
重写后的代码如下
1 | public class BadViewPager extends ViewPager { |
在ACTION_DOWN中调用了 super.onInterceptTouchEvent(ev);
目的是为了初始化ViewPager的成员变量mActivePointerId。mActivePointerId默认值为-1,只有初始化后,MOVE时才能控制界面滑动,否则ViewPager会认为这个事件已经被子View给消费了,然后break掉,接下来的滑动操作也就不执行了。
在ViewPager中的OnTouchEvent的代码如下
1 |
|
效果如图
内部拦截方法
套路伪代码
子View的dispatchTouchEvent方法的伪代码:
1 | public boolean dispatchTouchEvent(MotionEvent event) { |
父类重写onInterceptTouchEvent,不拦截ACTION_DOWN
1 | public boolean onInterceptTouchEvent(MotionEvent event) { |
实际代码如下,自定义子类继承ListView。
1 | public class SubListView extends ListView { |
修改布局list_view_layout
1 | <?xml version="1.0" encoding="utf-8"?> |
重写BadViewPager的onInterceptTouchEvent
1 | public class BadViewPager extends ViewPager { |
效果如图
外部拦截方法比内部拦截更加简单,建议使用外部拦截方法来解决滑动冲突的问题。
懂得了什么是事件分发机制和如何解决滑动冲突,下一篇,我们开始自定义下拉刷新组件。