一:事件分发基础知识讲解
1.事件分发的”事件“是指什么?
即 用户触摸屏幕时产生的点击事件(Touch事件)。这个点击事件(Touch事件)会被封装成MotionEvent对象。从手指接触屏幕至手指离开屏幕这个过程产生的一系列事件。一般情况下,事件列都是以DOWN事件开始、UP事件结束,中间有无数的MOVE事件。
MotionEvent 事件类型:
MotionEvent.ACTION_DOWN 手指 初次接触到屏幕 时触发
MotionEvent.ACTION_MOVE 手指 在屏幕上滑动时触发,会多次触发
MotionEvent.ACTION_UP 手指 离开屏幕 时触发
MotionEvent.ACTION_CANCEL 事件 被上层拦截 时触发
2.事件分发的本质
将点击事件(MotionEvent)传递到某个具体的View。及我们说所的事件分发。
3.事件在哪些对象之间进行传递?事件分发的流程顺序?
事件传递的对象:Activity、ViewGroup、View (所有的控件 都必须具备传递事件的能力)
事件传递的顺序:1个点击事件发生后,事件先传到Activity-->再传到ViewGroup-->再到View
4. 事件分发过程由哪些方法协作完成?(重点)
① dispatchTouchEvent() : 当前事件传递的对象是否把某个点击事件分发传递给他的孩子。
dispatchTouchEvent()=true : 当前事件传递的对象 当前点击事件分发给下一级ViewGroup,让下一级ViewGroup 再来分发这个点击事件
dispatchTouchEvent()=false : 当前点击事件没有往下分发
(表示当前点击事件分发结束了,但是还没有处理,那就需要在 viewGroup.OnTouchEvent()方法来处理这个点击事件).
②onTouchEvent() : (在dispatchTouchEvent()方法内部执行)
当 前事件传递的对象 对 该点击事件 是否处理
true 处理该点击事件
false 不处理该点击事件
③ onInterceptTouchEvent() : (在ViewGroup的dispatchTouchEvent()方法内部执行)
当前事件传递的对象 对 该点击事件是否进行拦截
onInterceptTouchEvent() =false (默认) 不拦截当前的点击事件,那么该事件会继续向下级子View 传递分发点击事件
onInterceptTouchEvent() =true (需手动复写设置) 拦截当前的点击事件,即当前点击事件不允许往下传递
dispatchTouchEvent()、 onTouchEvent() 属于消费事件、终结事件传递(返回true)
而onInterceptTouchEvent 并不能消费事件,它相当于是一个分叉口起到分流导流的作用,对后续的ACTION_MOVE和ACTION_UP事件接收起到非常大的作用
接收了ACTION_DOWN事件的函数不一定能收到后续事件(ACTION_MOVE、ACTION_UP)
onTouch()和onTouchEvent()的区别:
该2个方法都是在View.dispatchTouchEvent()中调用
但onTouch()优先于onTouchEvent执行;若手动复写在onTouch()中返回true(即 将事件消费掉),
将不会再执行onTouchEvent()
二:事件分发机制流程详细分析
流程1:Activity对点击事件的分发流程
Android事件分发机制首先会将点击事件传递到Activity中,通过dispatchTouchEvent()方法进行点击事件分发。
dispatchTouchEvent()如何分发 这个点击事件? 这个点击事件分发给谁呢?
(1)Activity对点击事件的分发流程 简单点总结:
Activity.dispatchTouchEvent()
true 当前点击事件分发给下一级ViewGroup,让下一级ViewGroup 再来分发这个点击事件
false 当前点击事件没有往下分发(表示当前点击事件分发结束了),
(当前点击事件虽然结束了分发,但是还没有处理)那就需要在Acvity.dispatchTouchEvent() 方法内部的执行 Activity.OnTouchEvent()方法来处理这个点击事件,
Activity.OnTouchEvent() =true 被Activity 消费处理了该点击事件(点击事件在Windows边界外)
Activity.OnTouchEvent() =false Activity 没有处理消费该事件 (点击事件在Windows边界内)
(2)源码分析:这里略
Window类是抽象类,其唯一实现类 = PhoneWindow类
DecorView顶层View.DecorView类是PhoneWindow类的一个内部类.
DecorView继承自FrameLayout,是所有界面的父类.
FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup
调用父类ViewGroup.dispatchTouchEvent()方法去处理这个点击事件
所以Activity的事件分发(Activity.dispatchTouchEvent(),最终还是有 ViewGroup.dispatchTouchEvent()方法去处理这个点击事件ViewGroup.dispatchTouchEvent() =false 就是ViewGroup,不会把点击事件往下分发, 那么就执行Activity.onTouchEvent()方法, 有Activity处理这个点击事件。
流程2: ViewGroup对点击事件的分发流程
ViewGroup每次事件分发时,都需先调用viewGroup.onInterceptTouchEvent()询问是否拦截当前点击事件
通过ViewGroup 源码分析:
根据源码我们知道:viewgroup.dispatchTouchEvent()事件分发 返回true 或false 的需要判断条件是:
(1)判断条件1:boolean disallowIntercept:是否禁用事件拦截的功能
① disallowIntercept = true 禁用事件拦截的功能, 那么viewGroup.onInterceptTouchEvent(ev)=false,
所以不拦截当前的点击事件,那么该事件会继续向下级子View 传递分发点击事件。
②disallowIntercept = false(默认是false) 不禁用事件拦截的功能 那么 就执行boolean intercepted = onInterceptTouchEvent(ev)方法 根据这个方法返回true/false 来判断是否拦截当前点击事件
boolean disallowIntercept:是否禁用事件拦截的功能(默认是false),
可通过调用requestDisallowInterceptTouchEvent(boolean disallowIntercept)修改boolean值
(2)判断条件2: 判断 ViewGroup类里的onInterceptTouchEvent(ev)方法 返回true/false来判断是否拦截当前点击事件(这里很重要)
①boolean intercepted = onInterceptTouchEvent(ev)=false(默认),即不拦截当前点击事件,从而进入到条件判断的内部,当前事件分发传递给孩子View(viewGroup->view)去处理。
那么这里ViewGroup如何把当前事件传递给孩子View的呢?
通过for循环,遍历当前ViewGroup下的所有子View找到那个被点击的子View,从而找到当前被点击的View,内部调用了childView.dispatchTouchEvent(ev)方法 = true ,就是实现了点击事件从ViewGroup到子View的传递,子view就处理了此点击事件;如果找不到 viewGroup 会自己处理消费该点击事件::onTouch() -> onTouchEvent() -> performClick() -> onClick()。
②boolean intercepted = onInterceptTouchEvent()=true(手动复写修改true),即拦截事件,从而跳出了该if条件判断,父容器停止当前事件往下传递,那么父容器viewGroup自己处理当前点击事件:
super.dispatchTouchEvent(ev) = true-->viewonTouch() -> onTouchEvent() -> performClick() -> onClick()
源码分析:
/**
* 源码分析:ViewGroup.dispatchTouchEvent()
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
// 仅贴出关键代码
...
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// 分析1:ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
// 判断值1-disallowIntercept:是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
// 判断值2-!onInterceptTouchEvent(ev) :对onInterceptTouchEvent()返回值取反
// a. 若在onInterceptTouchEvent()中返回false,即不拦截事件,从而进入到条件判断的内部
// b. 若在onInterceptTouchEvent()中返回true,即拦截事件,从而跳出了该条件判断
// c. 关于onInterceptTouchEvent() ->>分析1
// 分析2
// 1. 通过for循环,遍历当前ViewGroup下的所有子View
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
// 2. 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
// 3. 条件判断的内部调用了该View的dispatchTouchEvent()
// 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面章节介绍的View事件分发机制)
if (child.dispatchTouchEvent(ev)) {
// 调用子View的dispatchTouchEvent后是有返回值的
// 若该控件可点击,那么点击时dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立
// 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
// 即该子View把ViewGroup的点击事件消费掉了
mMotionTarget = child;
return true;
}
}
}
}
}
}
...
return super.dispatchTouchEvent(ev);
// 若无任何View接收事件(如点击空白处)/ViewGroup本身拦截了事件(复写了onInterceptTouchEvent()返回true)
// 会调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()
// 因此会执行ViewGroup的onTouch() -> onTouchEvent() -> performClick() -> onClick(),即自己处理该事件,事件不会往下传递
// 具体请参考View事件分发机制中的View.dispatchTouchEvent()
...
}
/**
* 分析1:ViewGroup.onInterceptTouchEvent()
* 作用:是否拦截事件
* 说明:
* a. 返回false:不拦截(默认)
* b. 返回true:拦截,即事件停止往下传递(需手动复写onInterceptTouchEvent()其返回true)
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 默认不拦截
return false;
}
// 回到调用原处
流程3:View的事件分发机制
View的事件分发机制view.dispatchTouchEvent()返回true/false的核心点在于:
view是否注册了Touch事件监听(mOnTouchListener.onTouch(this, event))
(1) 没有注册Touch事件监听setOnTouchListener, 怎么处理事件?
调用View.onTouchEvent() -> performClick() ->(如果设置了点击事件,就回调) onClick()。
(2)注册Touch事件监听setOnTouchListener 判断onTouch()的返回值
① 返回 onTouch()=false
事件无被消费,View.dispatchTouchEvent()=false 跳出If,
事件会继续往下传递,去处理事件, 即调用View.onTouchEvent() -> performClick() ->(如果设置了点击事件,就回调) onClick()。
②返回onTouch()=true,
view.dispatchTouchEvent()=true,事件被消费,不会继续往下传递 ,事件分发结束(已经分发到头了),
View.dispatchTouchEvent()直接返回true;
所以最终不会调用View.onTouchEvent(),也不会调用onClick()。
这里需要特别注意的是,onTouch()的执行 先于onClick()
三:工作流程总结事件分发流程_Activity_ViewGroup_View_流程图:
1个点击事件发生后,事件先传到Activity、再传到ViewGroup、最终再传到View。
四:事件分发的案例场景:
操作情景:
- 用户先触摸到屏幕上
View C
上的某个点(图中黄区)
Action_DOWN
事件在此处产生
2.用户移动手指
3.最后离开屏幕
一般的事件传递的几种情况
场景1:默认情况的事件分发
即不对控件里的方法(dispatchTouchEvent()
、onTouchEvent()
、onInterceptTouchEvent()
)进行重写 或 更改返回值。这3个方法的默认实现:调用下层的方法 & 逐层返回
默认情况的事件分发事件传递情况:(呈U
型)
Activity A:dispatchTouchEvent() ->> ViewGroup B:dispatchTouchEvent() ->> View C:dispatchTouchEvent() -->>View C:onTouchEvent() ->> ViewGroup B:onTouchEvent() ->> Activity A:onTouchEvent()
场景2:处理事件分发
设View C
希望处理该点击事件,即:设置View C
为可点击的(Clickable)
或 复写其onTouchEvent()
返回true
DOWN
事件被传递给C的onTouchEvent
方法,该方法返回true
,表示处理该事件
因为View C
正在处理该事件,那么DOWN
事件将不再往上传递给ViewGroup B 和 Activity A
的onTouchEvent()
;
其他事件(Move、Up)
也将传递给View C
的onTouchEvent()处理。
场景3:拦截DOWN事件
假设ViewGroup B
希望处理该点击事件,即ViewGroup B
复写了onInterceptTouchEvent()
返回true
拦击点击事件不允许往下传递、onTouchEvent()
返回true ViewGroup B 自己处理该点击事件
DOWN
事件被传递给ViewGroup B
的onInterceptTouchEvent()
=true
,表示拦截该事件,即自己处理该事件(事件不再往下传递)
其他事件(Move、Up)
将直接传递给ViewGroup B
的onTouchEvent()
场景4:拦截DOWN的后续事件
ViewGroup B
无拦截DOWN
事件(DOWN
事件传递到View C
的onTouchEvent()
=true,
还是View C
来处理DOWN
事件),但ViewGroup B
拦截了接下来的MOVE
事件,viewGroup B
的onInterceptTouchEvent()
=true
拦截该MOVE
事件,但该事件并没有传递给ViewGroup B,
这个MOVE
事件将会被系统变成一个CANCEL
事件传递给View C
的onTouchEvent()来进行处理
只有后续再来了一个MOVE
事件,该MOVE
事件才会直接传递给ViewGroup B
的onTouchEvent()=true来进行处理,ViewGroup B就处理了后续的MOVE
事件了.
View C
再也不会收到该事件列产生的后续事件