事件分发
参考:
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制详解:史上最全面、最易懂
- 图解 Android 事件分发机制
- Android全面解析之Window机制
文章目录
本文贴出的代码有删减!
一、流程概览
(1)总图
从触摸某一个View
的那一刻开始,事件分发流程如下:
说明:
- onTouchEvent就是直接处理触摸事件时调用的方法,跟它比较相似的OnTouchListener.onTouch则是在View接收到触摸事件之前调用的,即优先级高于onTouchEvent。
- onInterceptTouchEvent是指是否要拦截当前的事件,值得注意的是只有ViewGroup才有这个方法,Activity和View都没有。这个方法的返回值如果是TRUE,那么就表示当前ViewGroup要自己处理这个事件,不再将事件传给后续的子View。默认返回FALSE。
- dispatchTouchEvent就是事件分发,如果返回TRUE,那就是当前事件被消费了,不会再往下分发。onInterceptTouchEvent在就是在该方法内执行的。通过先确定当前事件没有有被CANCEL或INTERCEPT,才会继续dispatch。
(2)一个事例
假设有一个Activity
,它的布局是一个LinearLayout
,中间有一个Button
。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="180dp"
android:layout_height="180dp"
android:text="hello"
android:textSize="22sp"
android:gravity="center" />
</LinearLayout>
从点击Button
开始的事件分发流程如下:
从点击Buttton
开始的事件消费情况如下:
二、dispatchTouchEvent
在流程概览的图中可以看到,有三个地方使用了dispatchTouchEvent
,分别是:Activity
、ViewGroup
、View
。从系统源码中可以看出,这三个地方的对于这个方法的实现完全不一样,含义也不一样。
1. Activity.dispatchTouchEvent
Activity.dispatchTouchEvent流程图
Activity.dispatchTouchEvent中涉及到的部分变量的类图
Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
public Window getWindow() {
return mWindow;
}
(1)onUserInteraction和onUserLeaveHint
onUserInteraction
和onUserLeaveHint
是成对使用的回调方法,需要用户自己实现。可以用于处理或取消状态栏的通知列表。
onUserInteraction
在key、touch、trackball event分发给Activity
的时候调用。
onUserLeaveHint
在Activity
回到后台之前,执行onPause
之前调用。
(2)PhoneWindow
PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
private DecorView mDecor;
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
}
}
Window
是个abstract class
,它唯一的实现类是android.policy.PhoneWindow
。
PhoneWindow
是一个顶级窗口
,它持有的mDecor
作为该Window
的view
,是顶级View
。
DecorView
继承于FrameLayout
,也就是说它是个ViewGroup
。FrameLayout
中没有重写dispatchTouchEvent
,所以DecorView
执行super.dispatchTouchEvent
时,就是在执行ViewGroup.dispatchTouchEvent
。
PhoneWindow
执行superDispatchTouchEvent
实现了从Activity.dispatchTouchEvent
过渡到ViewGroup.dispatchTouchEvent
。也就是说把事件从Activity
传递到ViewGroup
。从代码上看,该过程是必然会发生的,没有被拦截的情况出现。
(3)返回值
可以见到Activity.dispatchTouchEvent
是有返回值的,但是这里不管返回TRUE还是FALSE,都代表了消费了事件。当返回FALSE时,必然是执行了onTouchEvent
。当返回TRUE时,一定分发了事件,但可能执行了onTouchEvent
。
2. ViewGroup.dispatchTouchEvent
ViewGroup
事件分发流程图:
ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
if (!canceled && !intercepted) {
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {}
}
}
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
handled = child.dispatchTouchEvent(transformedEvent);
}
}
ViewGroup
在事件分发的时候跟Activity
最大的不同是,要先判断是否该事件是否被CANCELED
或INTERCEPTED
,然后才决定是否要继续往下dispatch
。
如果要继续往下分发,那么必须找到触摸的那个子view
,实现的方式就是最普通、最直接的遍历整个子View数组。如果找到的子view
的边界符合触摸事件的坐标条件且可以接收事件,那么就可以分发了。
在继续往下分发的过程中,如果符合条件的子view
是空的,那么事件会调用super.dispatchTouchEevnt
处理。否则,分发给子view
。
onInterceptTouchEvent
ViewGroup.java
// First touch target in the linked list of touch targets.
private TouchTarget mFirstTouchTarget;
/**
* Implement this method to intercept all touch screen motion events. This
* allows you to watch events as they are dispatched to your children, and
* take ownership of the current gesture at any point.
*
* <p>Using this function takes some care, as it has a fairly complicated
* interaction with {@link View#onTouchEvent(MotionEvent)
* View.onTouchEvent(MotionEvent)}, and using it requires implementing
* that method as well as this one in the correct way. Events will be
* received in the following order:
*
* <ol>
* <li> You will receive the down event here.
* <li> The down event will be handled either by a child of this view
* group, or given to your own onTouchEvent() method to handle; this means
* you should implement onTouchEvent() to return true, so you will
* continue to see the rest of the gesture (instead of looking for
* a parent view to handle it). Also, by returning true from
* onTouchEvent(), you will not receive any following
* events in onInterceptTouchEvent() and all touch processing must
* happen in onTouchEvent() like normal.
* <li> For as long as you return false from this function, each following
* event (up to and including the final up) will be delivered first here
* and then to the target's onTouchEvent().
* <li> If you return true from here, you will not receive any
* following events: the target view will receive the same event but
* with the action {@link MotionEvent#ACTION_CANCEL}, and all further
* events will be delivered to your onTouchEvent() method and no longer
* appear here.
* </ol>
*
* @param ev The motion event being dispatched down the hierarchy.
* @return Return true to steal motion events from the children and have
* them dispatched to this ViewGroup through onTouchEvent().
* The current target will receive an ACTION_CANCEL event, and no further
* messages will be delivered here.
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
public boolean dispatchTouchEvent(MotionEvent ev) {
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
resetTouchState();
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild, target.child,
target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
}
/**
* Resets all touch state in preparation for a new cycle.
*/
private void resetTouchState() {
clearTouchTargets();
}
/**
* Clears all touch targets.
*/
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
mFirstTouchTarget = null;
}
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
}
只有ViewGroup
有onInterceptTouchEvent
这个方法,且返回值默认是FALSE。源码中全靠注释,代码只有一句return false;
。
从ViewGroup.dispatchTouchEvent
中可以看出,事件处理的周期从ACTION_DOWN开始,于下一个ACTION_DOWN的前一个事件结束。onInterceptTouchEvent
的返回值表示是否要拦截这个事件。以一个周期为例,如果ViewGroup.onInterceptTouchEvent
返回TRUE,那么这个触摸事件就会直接派发给ViewGroup
自己。ACTION_DOWN事件只派发给ViewGroup
,其他子view
收不到任何事件。在这个周期结束之前,ViewGroup
会持续拦截所有事件,但是只有事件为ACTION_DOWN时,其他子view
才收不到;如果事件为ACTION_MOVE等事件,其他子view
会收到事件ACTION_CANCEL。
当onInterceptTouchEvent
返回TRUE时,ViewGroup
会调用super.dispatchTouchEvent
,也就是会调用View.dispatchTouchEvent
。
3. View.dispatchTouchEvent
View.dispatchTouchEvent
流程图:
View.dispatchTouchEvent
类图:
View.java
public boolean dispatchTouchEvent(MotionEvent event) {
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
从源码中可以看出,OnTouchListener.onTouch
的优先级会高于onTouchEvent
。
onTouchEvent
是处理触摸事件时调用的,OnTouchListener.onTouch
可以拦截这个触摸事件,跟ViewGroup.onInterceptTouchEvent
有点像,只是它的条件比较多,而且不一定会执行。
(1)ViewGroup调用super.dispatchTouchEvent
如果此时的View.onTouchEvent
是由ViewGroup
的super.dispatchTouchEvent
触发的,那么当View.onTouchEvent
返回TRUE时,这意味着触摸事件已经被消费;如果View.onTouchEvent
返回FALSE时,这意味着触摸事件没有被消费,那么ViewGroup.dispatchTouchEvent
就会返回FALSE,进而触发Activity.onTouchEvent
。
(2)View调用dispatchTouchEvent
如果此时的View.dispatchTouchEvent
是View
调用的话,那么就证明触摸事件已经分发到对应的子view
上了。如果View.onTouchEvent
返回TRUE,那么该事件被消费;如果View.onTouchEvent
返回FALSE,那么会回到ViewGroup.dispatchTouchEvent
中,要么继续遍历找到符合条件的子view
派发事件,要么调用ViewGroup.onTouchEvent
进行处理。如果是后者,如果ViewGroup.onTouchEvent
返回TRUE,那么事件被消费;如果ViewGroup.onTouchEvent
返回FALSE,那么就会回到Activity.dispatchTouchEvent
,然后调用Activity.onTouchEvent
。
三、总结
事件分发的过程就是先从Activity.dispatchTouchEvent
开始,然后到ViewGroup.dispatchTouchEvent
。这时要先判断ViewGroup.onInterceptTouchEvent
是否有拦截事件,如果有,事件交由ViewGroup.onTouchEvent
处理;如果没有,事件交由View.dispatchTouchEvent
处理。在View.dispatchTouchEvent
中,如果不考虑OnTouchListener.onTouchEvent
的话,就会调用View.onTouchEvent
。
在调用各种onTouchEvent
的过程中,如果返回值为TRUE,就证明事件被消费;如果返回值为FALSE,除了Activity
会视作事件被消费,其他的ViewGroup
和View
都会视为事件未被消费,然后将事件交由Activity
或父view
的onTouchEvent
进行处理。
在调用各种dispatchTouchEvent
的过程中,不考虑Activity
,如果返回值为TRUE,意味着该事件被当前view
或者子view
消费了;如果返回值为FALSE,意味着事件未被消费,需要交由Activity
或父view
的onTouchEvent
进行处理。