在从Android 6.0源码的角度剖析UI界面架构一文中,我们了解到Activity是Android的可视化界面,是用户与Android系统交互的窗口,也就是说每个Activity都对应着一个窗体,但窗体是一个抽象的概念,它的具体表现形式是视图。在Android中,窗体对应着Window类,视图对应着View类。
Window
是一个抽象类,它的具体实现是PhoneWindow
类,该类将DecorView
作为窗体的顶层视图并封装了相关操作窗体的方法,而这个DecorView就是View的子类,它将被作为整个窗体的最顶层视图,后续所有我们希望展示的界面(比如setContentView()操作
)将被加载到DecorView中,准确来说,是DecorView的内容区域。
在Android系统中,一个事件用MotionEvent
来表示,事件的传递分为分发
、拦截
、处理
三个过程,它们分别对应于方法dispatchTouchEvent
、onInterceptTouchEvent
和onTouchEvent
。根据从Android 6.0源码的角度剖析UI界面架构所述,当一个点击事件到来时,事件的传递总是会遵循以下顺序:Activity->Window->DecorView
,即事件首先会被传递到Activity,由Activity的dispatchTouchEvent进行派发事件,而具体的工作则由Activity内部的Window来完成,Window会将事件传递DecorView。DecorView是窗体的最顶层视图,它继承于FrameLayout,是setContentView的底层容器。换句话说,一个事件从传递到Activity
开始,会经过最顶层视图DecorView
,直到具体的View
,当然,期间肯定会经历过多个ViewGroup,但这里我们不考虑,因为多个ViewGroup的传递过程跟在DecorView中是一样的,毕竟DecorView就是一个ViewGroup的子类。Activity视图结构如下:
Activity是Android的可视化界面,是用户与Android系统交互的窗口,当用户操作Android应用产生的事件时,该事件首先会传递到Activity,并由Activity的dispatchTouchEvent
进行事件分发,该方法源码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
// (1)判断down事件
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// (2)调用PhoneWindow的superDispatchTouchEvent继续传递事件
// public Window getWindow() {
// return mWindow;
// }
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
// (3)由Activity消耗事件
return onTouchEvent(ev);
}
从上述源码可知,dispatchTouchEvent()方法首先会判断事件是否是down事件,如果是则调用Activity的onUserInteraction()
方法,这个方法是个空方法,在实际开发中,我们却可以通过重写该方法获知是否用户与设备进行交互;然后,调用Window的superDispatchTouchEvent()
方法将事件传递到顶层视图DecorView,具体的传递过程我们下一小节详细剖析,这里只需要知道从这里开始事件将会被逐级向下传递,如果没有任何一个View或者ViewGroup消耗该事件,getWindow().superDispatchTouchEvent(ev)将返回false,那么事件将会直接交给Activty来处理,即调用Activity的onTouchEvent
消耗事件。onTouchEvent()源码如下:
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
接下来,我们具体分析Window是如何将事件传递给ViewGroup的,即最顶层视图DecorView。通过之前的分析可知,Window是一个抽象类,它的具体实现是PhoneWindow,为此,我们找到PhoneWindow的superDispatchTouchEvent,该方法将直接调用DecorView的superDispatchTouchEvent方法,并在这个方法中将事件交给DecorView的dispatchTouchEvent,切确的说是ViewGroup的dispatchTouchEvent。具体源码如下:
// PhoneWindow.DecorView,窗体顶层视图
private DecorView mDecor;
// PhoneWindow.superDispatchTouchEvent()
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
// PhoneWindow.DecorView.superDispatchTouchEvent()
public boolean superDispatchTouchEvent(MotionEvent event) {
// surper即ViewGroup
return super.dispatchTouchEvent(event);
}
在上一节说到,当事件由Window传递到顶层视图DecorView时,实际上是将事件传递给了ViewGroup,因为DecorView本身继承于FrameLayout,而FrameLayout又是ViewGroup的子类。因此,分析DecorView中事件的分发处理过程,就需要从ViewGroup的dispatchTouchEvent方法入手。ViewGroup.dispatchTouchEvent
方法源码如下,考虑到该部分逻辑较为复杂,下面我们将分段剖析。
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
// 1. 初始化状态,当为ACTION_DOWN事件时
// 清除之前的所有状态,开始一次新的触摸事件
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 2. 检查是否需要拦截事件
// 只针对ACTION_DOWN事件,或mFirstTouchTarget!=null情况
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags &
FLAG_DISALLOW_INTERCEPT) != 0;
// 当disallowIntercept为false时,调用onInterceptTouchEvent
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
// 如果down事件被拦截,同一事件序列的其他事件也会被ViewGroup拦截
intercepted = true;
}
从上述代码可知,ViewGroup的dispatchTouchEvent首先会去判断当前触摸事件是否为down
事件(ACTION_DOWN),如果是则清理之前的所有状态,并重置FLAG_DISALLOW_INTERCEPT
标志,以便开始新的触摸事件序列。然后,通过判断down
事件或mFirstTouchTarget != null
决定是否对事件进行拦截,即调用ViewGroup的onInterceptTouchEvent
方法,从该方法的源码可知它默认返回false,也就是说,ViewGroup默认是不对任何事件进行拦截。对于mFirstTouchTarget
对象,后面我们会谈到,这里只需要知道当ViewGroup不拦截事件并交给子元素处理时mFirstTouchTarget!=null,如果拦截则mFirstTouchTarget=null
。由此可知,假如ViewGroup在ACTION_DOWN到来时,对事件进行了拦截,那么与down
事件的同一事件序列中的其他事件,比如move
事件、up
事件,都会被ViewGroup拦截且onInterceptTouchEvent也不会再被调用,即intercepted = true
。当然,假如ViewGroup没有拦截down事件,那么同一事件序列中的其他事件是否会被拦截,将由FLAG_DISALLOW_INTERCEPT
标志决定,该标志由ViewGroup的requestDisallowInterceptTouchEvent
方法控制,通常用于子View中,一旦这个标志被设置,即mGroupFlags&FLAG_DISALLOW_INTERCEPTViewGroup!=0
,ViewGroup将无法拦截除ACTION_DOWN的其他事件。为什么是除ACTION_DOWN情况,那是因为down事件到来的时候会直接重置FLAG_DISALLOW_INTERCEPT
标志。
// ViewGroup.resetTouchState()方法
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
// 使mGroupFlags&FLAG_DISALLOW_INTERCEPT=0
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
// ViewGroup.onInterceptTouchEvent()方法
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
// ViewGroup.requestDisallowInterceptTouchEvent()方法
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
// mGroupFlags&FLAG_DISALLOW_INTERCEPT!=0为真
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
同一个事件序列
是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。
if (!canceled && !intercepted) {
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
// 获取触摸事件相对于父容器坐标
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 遍历所有child,找到能够接收当前触摸事件event的child
final ArrayList<View> preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
// 获取一个child,即子view
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
// 判断子view是否拥有焦点
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
// (1)判断子view是否能够接收点击事件
// 点击事件的坐标是否处于子view区域内
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
// (2)进入下一级事件分发
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();
// (3)mFirstTouchTarget赋值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
}
}
从上述代码可知,假如ViewGroup没有拦截当前事件且canceled不为真,那么ViewGroup的dispatchTouchEvent
接下来会去遍历ViewGroup的所有子View,以寻找哪个子View能够接收该事件。那么如何找到这个子View呢?
首先,当前遍历到的子View是否具有焦点和是否能够接收点击事件,其中,后者主要根据子View是否正在播放动画
和点击事件的坐标是否处于子View区域内
来确定,如果不满足这两个条件,则进入下一个子View判断。相关源码如下:
// ViewGroup.canViewReceivePointerEvents()方法
private static boolean canViewReceivePointerEvents(View child) {
// View处于可见状态或正在播放动画
return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null;
}
// ViewGroup.isTransformedTouchPointInView()方法
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
// 坐标x,y是否位于子view区域
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
接下来,调用ViewGroup的dispatchTransformedTouchEvent
方法处理事件分发。该方法会判断child是否为空,当然走到这里child肯定不为null,因此最终会调用child的dispatchTouchEvent()
方法,这样事件就交给子元素(子view)来处理了,从而完成了一轮事件分发。假如这个子view本身是一个ViewGroup,那么接下来的事件分发过程与上述DecorView的一致;假如这个子View是一个View,那么接下来当前事件会交给View的dispatchTouchEvent来处理,这部分我们下一节详讲。当然,如果child.dispatchTouchEvent()返回false,那么将进入下一个子view的判断,直至找到那个能够接收该事件的子view。
//ViewGroup.dispatchTransformedTouchEvent()方法
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean 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());
}
// 调用child的dispatchTouchEvent分发事件
// 如果child是ViewGroup,则分发过程跟上面一样
// 如果child是View,则进入View的dispatchTouchEvent
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
最后,如果ViewGroup.dispatchTransformedTouchEvent()
方法返回true,说明事件已经被分发至子View,从而会跳出循环,mFirstTouchTarget将会被赋值。从ViewGroup的addTouchTarget源码可知,mFirstTouchTarget实际上是一个单链表,它能否被赋值将影响ViewGroup对事件拦截的策略,即当mFirstTouchTarget=null时,ViewGroup将拦截同一事件序列的所有点击事件。addTouchTarget()源码如下:
// ViewGroup.addTouchTarget()方法
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
由前面的分析可知,当mFirstTouchTarget=null
的时候,if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))返回的是false,可能说明两种情况:(1)当前ViewGroup没有子元素
;(2)所有的子元素处理了当前点击事件,但是dispatchTouchEvent返回false,子元素的这个方法返回false通常是其onTouchEvent返回false,即子元素没有消耗事件。
这两种情况,我们从调用dispatchTransformedTouchEvent时,传入child=null可以看出。既然没有任何一个子元素消耗点击事件,那么就只有ViewGroup自身来消耗了,从ViewGroup的dispatchTransformedTouchEvent方法可知,当child=null时,最终会调用View的dispatchTouchEvent方法。为什么是View?因为,ViewGroup本身也是继承于View,View是视图基类。
//ViewGroup.dispatchTransformedTouchEvent()方法
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
//...
// Perform any necessary transformations and dispatch.
// 调用view的dispatchTouchEvent方法
// ViewGroup继承于View
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
}
return handled;
}
View对点击事件的分发过程较为简单,即当前View是否需要点击事件,如果消耗,则返回true,否则,返回false
。View的dispatchTouchEvent方法主要处理二件事情:
(1) 判断View是否能够处理当前点击事件的能力;
(2) 判断View是否注册了触摸事件监听器(setOnTouchEventListener)且是否可点击,如果两个条件均满足,则会触发onTouch方法,同时View.dispatchTouchEvent()返回true,而点击事件即被当前View消耗掉。否则,就会触发onTouchEvent()方法来消耗点击事件。
// View.dispatchTouchEvent()部分源码
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
// View.onTouchEvent()方法
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
// 即使view被disable,仍然能够消耗事件
// 只是没有对点击事件进行反馈而已
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP &&
(mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
// 检测clickable或long_clickable标志
// 调用OnClickLisenter的onClick方法
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) {
// 请求获取焦点
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
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();
}
}
}
break;
}
return true;
}
return false;
}
在View的onTouchEvent方法中,我们可以看到它首先会判断当前View是否被disable了,如果被disable了,将不再继续下面的逻辑,当然,如果View的clickable或long_clickable是有效的,它照样能够消耗点击事件,只是没有对点击事件进行反馈而已。如果View是enable状态,则先会去检测clickable或long_clickable标志,如果没有被禁用,当前View就会去处理当前事件,onTouchEvent返回true,表示已经消耗了该事件。当点击事件是up事件的时候,说明已经完成了一次同一事件序列,就会触发onClickListener的onClick
方法,当然,前提是我们有为View注册点击事件监听器(OnClickListenter
)。performClick()的源码如下:
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
// 判断是否注册了点击事件监听器
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
// 调用onClick方法
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
至此,通过对View的onTouchEvent()方法的研究,我们知道View的onTouchEvent是最终用来决定是否处理点击事件的,并且受View的clickable和long_clickable状态的影响,如果它们被禁用,则onTouchEvent会返回false,View不会消耗该点击事件。另外,如果我们注册了触摸事件(OnTouchListener)和点击事件(OnClickListener)监听器的话,消耗事件的顺序是:onTouch->onTouchEvent->onClick。,其中,如果onTouch被回调,onTouchEvent将永远不会被触发,自然onClick也永远不会被回调。
View.dispatchTouchEvent()源码如下:
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
// (1)判断是否具有处理当前点击事件的能力
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
// (2) 判断是否需要onTouch消耗事件
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// (3) 判断是否需要消耗onTouchEvent事件
if (!result && onTouchEvent(event)) {
result = true;
}
}
//....
return result;
}
(1) View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEnvent方法就会被调用。
(2)View的onTouchEvent默认都会消耗点击事件(返回true),除非它是不可点击的,即clickable和longClickable同时返回false。
(3)View的enbale属性不影响onTouchEvent的默认返回值,即使是disable状态,只要它的clickable或longClickable返回true,那么onTouchEvent就会返回true,即表示消耗该点击事件。
文章的最后,我们再对View事件分发机制作个小结:在Android中,事件的传递总是会遵循以下顺序:`Activity->Window->DecorView,其中,DecorView是窗体最顶层的实体,我们设计的界面就“装载”在该容器中。事件的分发总是自顶向下,事件的处理消耗总是自低向上,对于同一事件序列来说,当其中的一个事件被拦截了,这个事件序列的剩余事件将不会再继续向下传递给其他视图,都会被拦截的视图来处理。
对于第二点,我记得某本书上有这么一个例子,就是领导分配任务问题。假设你是最底层码农,你上面有一个项目组长,一个项目经理,一个老总,某天老总安排了一个任务给项目经理(为什么不是直接给你,因为你太渺小了,认不得啊~),那么通常情况项目经理觉得太简单了会转给项目组长,然后项目组长不想做最终转给你,由你来处理这个任务,你做完任务后(直接返回true
),再报给项目组长,项目组长报给项目经理,项目经理就交给老总,这样,一次任务的分发和处理就完成了,当然所有功劳都是领导的。假如当你接到任务时,本来就觉得领导总是不认可自己,就直接跟项目组长说,做不了(返回false
),那么任务就会又转回给项目组长,项目组长没办法就自己处理了(返回true
)。此时,项目组长心里也肯定不爽,所以,当下次有相同的任务派下来时,项目组长就不会再派给你了,要么自己处理(返回true
),要么继续向项目经理汇报做不了(返回false
),那么一次任务的分发和处理也完成了。