前言
在Android中,View主要负责界面的绘制和事件的分发、处理,它是所有控件Widgets的基类。通过源码分析View的事件分发,我们可以更加深刻地理解Android系统中View的工作原理。不仅如此,在日常的开发中,当我们遇到View事件冲突、滑动冲突时,处理起来将会游刃有余。
基础知识
当我们的手指触摸手机屏幕时,手机中的应用会对我们的触摸动作做出响应,确切地说是应用里的控件Widgets响应了触摸事件。在Android中,使用MotionEvent来描述触摸事件,我们可以通过getAction()方法来获取当前的事件类型。通常,一次手势动作会产生一系列的事件,下面列举了4个主要事件:
- ACTION_DOWN事件 当手指第一次触摸到屏幕时将产生此事件。ACTION_DOWN事件表示一系列事件的开始。
- ACTION_UP事件 当手指离开屏幕时将产生此事件。与ACTION_DOWN事件对应,ACTION_UP事件表示一系列事件的结束。
- ACTION_MOVE事件 当手指有在屏幕上滑动时将产生此事件。
- ACTION_CANCEL事件 表示当前的手势被中止了。如果一个View收到了ACTION_CANCEL事件,那么它不会再收到其它任何事件,包括ACTION_UP事件。
通过getX(), getY()方法可以获取到当前事件在屏幕上的坐标。注意,这个坐标是相对于父容器左上角的坐标。通过getRawX(), getRawY()方法可以获取到当前事件在屏幕上的原始坐标。通过前后两个ACTION_MOVE事件的坐标我们就可以知道当前手势动作的方向了。
在具体分析之前,先提一下View的事件分发的3个核心方法:
- dispatchTouchEvent()方法 主要负责事件的分发。
- onInterceptTouchEvent()方法 主要负责事件的拦截,ViewGroup专有。
- onTouchEvent()方法 主要负责事件的处理。
dispatchTouchEvent()和onTouchEvent()方法都有返回值,如果返回值为true,表示当前事件被处理了或者被消费了。另外再提一个ViewGroup的requestDisallowInterceptTouchEvent()方法,子控件通过调用这个方法可以控制是否允许父容器拦截事件,它具体影响了父容器的FLAG_DISALLOW_INTERCEPT标志位。
下面我们开始具体的源码分析。
Activity的事件分发
在Android中,底层的触摸事件最开始是传递到Activity中的,从Activity的dispatchTouchEvent()方法开始分发事件。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
从上面的代码可以知道,Activity将事件交给Window来负责分发到具体的页面布局中。如果Window的superDispatchTouchEvent()方法返回了true,即事件被消费了,那么直接退出。反之,如果没有任何一个View消费事件,那么最终Activity的onTouchEvent()方法将被调用,即Activity自己来处理事件。
Activity的Window是个抽象类,它的具体实现类是PhoneWindow。下面来看PhoneWindow的superDispatchTouchEvent()方法。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
PhoneWindow的superDispatchTouchEvent()方法比较简单,它直接将事件传递给了DecorView。DecorView是Android系统中所有Activity页面布局的顶级父容器。平常我们在Activity的onCreate()方法中调用setContentView()方法来设置页面布局,其实页面布局是被添加到DecorView这个父容器中。下面来看DecorView的superDispatchTouchEvent()方法。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView的superDispatchTouchEvent()方法比较简单,它将事件传递给了父类的dispatchTouchEvent()方法。在Android中,所有的父容器都是继承自ViewGroup,而ViewGroup继承自View。ViewGroup重写了View的dispatchTouchEvent()方法,所以事件开始从ViewGroup中进行分发。
ViewGroup的事件分发
ViewGroup的dispatchTouchEvent()方法比较复杂,我们分段来分析。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
...
}
...
return handled;
}
前面说过,ACTION_DOWN事件表示一次手势动作产生的一系列事件的起始事件。在dispatchTouchEvent()方法的开始,如果是ACTION_DOWN事件,那么ViewGroup会做一些复位、重置操作。
private void cancelAndClearTouchTargets(MotionEvent event) {
if (mFirstTouchTarget != null) {
...
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
clearTouchTargets();
...
}
}
ViewGroup使用mFirstTouchTarget变量来存储消费了事件的子控件。mFirstTouchTarget变量将所有消费了事件的子控件以链表的形式存储在一起。但是,通常要么没有子控件消费事件,要么只有一个子控件消费了事件。在cancelAndClearTouchTargets()方法中,如果之前有子控件消费了事件,那么ViewGroup将通过dispatchTransformedTouchEvent()方法向它们分发ACTION_CANCEL中止事件以便开始一轮新的事件传递。接着在clearTouchTargets()方法中将mFirstTouchTarget变量重置为null。
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
resetTouchState()方法中复位了一些标志位,包括了不允许父容器拦截事件的标志位FLAG_DISALLOW_INTERCEPT。接着往下看dispatchTouchEvent()方法。
@Override
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;
...
}
这段代码的主要作用是检查ViewGroup是否拦截了事件、是否中止了事件传递。当ACTION_DOWN事件发生或者mFirstTouchTarget变量不为null,即之前有子控件消费了事件时,检查ViewGroup是否拦截事件。如果子控件没有调用ViewGroup的requestDisallowInterceptTouchEvent()方法来设置FLAG_DISALLOW_INTERCEPT标志位,那么ViewGroup将调用onInterceptTouchEvent()方法来决定是否拦截事件。
ViewGroup的onInterceptTouchEvent()方法默认返回false,即父容器默认是不拦截事件的。在平常的开发中,我们可以根据需要重写ViewGroup的onInterceptTouchEvent()方法来决定是否拦截事件。
如果ViewGroup拦截了ACTION_DOWN事件,那么mFirstTouchTarget变量将为null。根据上面的代码可以知道,当后续的ACTION_MOVE、ACTION_UP等其它事件到来时,intercepted直接为true,ViewGroup拦截事件。这种情况下所有的事件都将由ViewGroup自己处理,子控件一个事件也接收不到。所以,在平常的开发中一般不会让ViewGroup拦截ACTION_DOWN事件。
如果ViewGroup不拦截ACTION_DOWN事件,但是没有子控件消费ACTION_DOWN事件,那么mFirstTouchTarget变量将为null。同上,当后续的ACTION_MOVE、ACTION_UP等其它事件到来时,intercepted直接为true,ViewGroup拦截事件。这种情况下子控件只接收到一个ACTION_DOWN事件,不会接收到后续的其它事件。
接着往下看dispatchTouchEvent()方法。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
...
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
...
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
...
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
...
}
if (preorderedList != null) preorderedList.clear();
}
...
}
}
...
}
这段代码的主要作用是将事件分发到可以处理事件的子控件。当事件没有被中止和拦截时,如果是ACTION_DOWN事件,那么ViewGroup开始遍历子控件进行事件分发。ViewGroup主要通过两个方法来判断子控件是否可以接收事件,canViewReceivePointerEvents()方法判断子控件的可见性和是否有动画,isTransformedTouchPointInView()方法判断事件是否落在子控件的布局区域中。
当子控件满足条件时,ViewGroup将调用dispatchTransformedTouchEvent()方法将事件传递给子控件。如果dispatchTransformedTouchEvent()方法返回了true,即子控件消费了事件,那么将调用addTouchTarget()方法将子控件设置给mFirstTouchTarget变量,然后退出循环。如果没有一个子控件消费了事件,那么mFirstTouchTarget变量仍然为null。
接着往下看dispatchTouchEvent()方法。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// 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;
}
}
...
}
当ViewGroup一开始就拦截了ACTION_DOWN事件或者没有子控件消费ACTION_DOWN事件时,mFirstTouchTarget变量为null,ViewGroup将通过dispatchTransformedTouchEvent()方法将事件传递给自己处理。反之,如果有子控件消费了ACTION_DOWN事件,并且后续事件没有被ViewGroup拦截,那么ViewGroup将直接通过mFirstTouchTarget变量进行事件分发。
如果有子控件消费了ACTION_DOWN事件,即mFirstTouchTarget变量不为null,但是后续事件被ViewGroup拦截了,此时cancelChild为true,ViewGroup将通过dispatchTransformedTouchEvent()方法向子控件分发ACTION_CANCEL事件,之后mFirstTouchTarget将被置为null。当后续事件到来时,ViewGroup将自己处理拦截的事件了。
接着看下dispatchTransformedTouchEvent()方法。
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;
}
...
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
当参数cancel为true或者是ACTION_CANCEL事件时,ViewGroup通过dispatchTransformedTouchEvent()方法传递ACTION_CANCEL事件给子控件或者ViewGroup自身。反之,将传递其它事件。
如果参数child为null,那么将调用ViewGroup父类的dispatchTouchEvent()方法,即ViewGroup自己处理事件。如果child不为null,那么将调用child,即View的dispatchTouchEvent()方法,即子控件处理事件。此时,事件分发由ViewGroup传入了View。
View的事件分发
因为ViewGroup也继承自View,所以要特别说明一下这部分提到的View特指子控件,不包括ViewGroup。View的事件处理相对来说就比较简单了,来看下View的dispatchTouchEvent()方法。
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
...
//noinspection SimplifiableIfStatement
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;
}
}
...
return result;
}
当View是enabled状态并且设置了OnTouchListener时,View将先调用OnTouchListener的onTouch()方法。如果onTouch()方法返回了true,那么将不再调用View的onTouchEvent()方法。可见,View的OnTouchListener的优先级高于onTouchEvent()方法。
如果View没有设置OnTouchListener,那么onTouchEvent()方法将被调用。最后我们来看下onTouchEvent()方法。
public boolean onTouchEvent(MotionEvent event) {
...
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
...
}
mIgnoreNextUpEvent = false;
break;
...
}
return true;
}
return false;
}
当View是disabled状态时,只要View是clickable的,onTouchEvent()方法将返回true。如果View是enabled状态并且是clickable的,onTouchEvent()方法默认也返回true。这说明,默认情况下只要有事件传递到了View并且View是clickable的,那么事件就会被消费。
阅读View的源码可以发现,默认情况下View不是clickable的,即默认情况下View没有消费事件。ViewGroup继承自View,但是ViewGroup没有重写View的onTouchEvent()方法,所以默认情况下ViewGroup也没有消费事件。
通过View的setClickable()、setLongClickable()和setContextClickable()方法可以设置相应的clickable状态。特别要提一下的是,平常我们通过View的setOnClickListener()方法设置监听器时其实也设置了View的clickable状态。
最后,在ACTION_UP事件时View将调用performClick()方法。
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
在performClick()方法中,如果View设置了OnClickListener,那么将调用OnClickListener的onClick()方法。
到这里View的事件分发源码分析就结束了。
总结
通过对View的事件分发的源码进行分析,我们可以总结出以下一些结论:
- 触摸事件的传递顺序是:Activity -> Window -> DecorView -> 具体的页面布局容器 -> 具体的子控件。如果没有View消费事件,那么事件将逐级返回,最终Activity的onTouchEvent()方法会被调用。
- ViewGroup的onInterceptTouchEvent()方法默认返回false,即父容器默认是不拦截事件的。
- 如果父容器拦截了ACTION_DOWN事件,那么它的子控件一个事件也接收不到。
- 如果一个View没有消费ACTION_DOWN事件,那么后续的ACTION_MOVE、ACTION_UP等其它事件它都接收不到了。
- 如果一个View消费了ACTION_DOWN事件,并且后续事件没有被父容器拦截,那么父容器会将后续事件直接传递给此View。
- 如果一个View消费了ACTION_DOWN事件,但是后续事件被父容器拦截了,那么这个View只会再收到一个ACTION_CANCEL事件。
- 默认情况下,ViewGroup和View都是不消费事件的。
- OnTouchListener的onTouch()方法优先级高于onTouchEvent()方法。
例子
这里举了两个简单的例子。例子代码地址:https://github.com/chongyucaiyan/ViewDemo
第一个例子主要用来了解正常情况下触摸事件的传递顺序。
如上图所示,布局很简单,垂直方向的LinearLayout布局里放置了一个TextView和一个Button。代码里主要是在View的事件分发核心方法里加了日志打印,PhoneWindow和DecorView没办法加日志就没加了。首先,在TextView上触发一次手势,打印的日志如下图所示:
如上图所示,触摸事件从Demo01Activity传递到MyLinearLayout01父容器,最后传递到MyTextView01子控件。同时我们可以看到,默认情况下,父容器不拦截事件,父容器和子控件不消费事件。下面简要分析一下。
ACTION_DOWN事件发生后,事件先被传递到Demo01Activity的dispatchTouchEvent()方法。接着事件被传递到MyLinearLayout01的dispatchTouchEvent()方法。MyLinearLayout01的onInterceptTouchEvent()方法返回false,即父容器默认不拦截事件。接着事件被传递到MyTextView01的dispatchTouchEvent()方法。此时,MyTextView01调用onTouchEvent()方法来处理事件。MyTextView01的onTouchEvent()方法返回false,即子控件默认不消费事件。这时事件返回,MyLinearLayout01调用onTouchEvent()方法来处理事件。MyLinearLayout01的onTouchEvent()方法返回false,即父容器默认不消费事件。事件接着返回,最终Demo01Activity的onTouchEvent()方法被调用。
因为MyTextView01和MyLinearLayout01都没有消费ACTION_DOWN事件,所以后续的ACTION_MOVE、ACTION_UP事件它们都接收不到了。
然后,在Button上触发一次手势,打印的日志如下图所示:
如上图所示,默认情况下MyButton01消费了事件,代码里并没有给MyButton01设置OnClickListener。这是因为Android应用默认使用的theme之中设置了Button的clickable属性为true,造成Button默认消费事件。
MyButton01消费了ACTION_DOWN事件,所以MyLinearLayout01和Demo01Activity的onTouchEvent()方法都不会被调用了。并且MyButton01可以正常接收到后续的ACTION_MOVE、ACTION_UP事件。
第二个例子主要用来了解拦截情况下触摸事件的传递情况。
布局更简单,FrameLayout里放置了一个Button。代码里在MyFrameLayout02中重写了onInterceptTouchEvent()方法,对ACTION_MOVE事件进行拦截。
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
intercepted = true;
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
intercepted = super.onInterceptTouchEvent(event);
break;
}
Log.i(TAG, "onInterceptTouchEvent(), " + Utils.getActionString(event) + ", intercepted = " + intercepted);
return intercepted;
}
然后,在Button上触发一次手势,打印的日志如下图所示:
如上图所示,ACTION_DOWN事件被MyButton02正常消费,当ACTION_MOVE事件发生时,MyFrameLayout02对事件进行拦截。此时,MyButton02只再接收到一个ACTION_CANCEL事件,其它事件都接收不到了。
参考
- Android 7.1.1 (API level 25)
- https://developer.android.com/reference/android/view/View.html