View点击事件分发原理,源码解读
- 前言
- 1. 原理总结
- 2.1 时序图总结
- 2.2 流程图总结
- 2. 源码解读
- 2.1 Activity到ViewGroup
- 2.2 ViewGroup
- 事件中断
- 逆序搜索
- 自己处理点击事件
- ViewGroup总结
- 2.3 View
- OnTouchListener
- onTouchEvent
- 3. 附录:时序图uml代码
前言
两年前我曾经写过一篇点击事件的原理博客,在今年重新翻看的时候发现文章的结构不好,且没有总结,让人不容易理解,所以重新整理了一下再写一次。
1. 原理总结
注意:正文中虽然说的都是点击事件,实际上他并不是指我们常用语境中的onClick或者onLongClick,而是任意类型的事件,只是用点击事件来形容比较让人容易理解,实际上视图的事件分发是包括按下,抬起,移动这三个部分的。
MotionEvent.ACTION_DOWN | 按下View(所有事件的开始) |
MotionEvent.ACTION_UP | 抬起View(与DOWN对应) |
MotionEvent.ACTION_MOVE | 移动View |
MotionEvent.ACTION_CANCEL | 结束事件(非人为原因) |
再我看完点击事件分发的原理之后,我会用三个词来形容点击事件的全部原理:
-
View树:
首先我们需要知道的是,在Android中我们所写的视图代码,无论是xml还是通过代码手动添加的,其数据结构展现出来的就是一个树,一个一对多的存储关系的集合。
在代码中我们表现这个树状数据结构的方式:在ViewGroup中设置了一个子View的List,通过让最上层的父节点DevorView(他也是一个ViewGroup)—持有好几个ViewGroup,里面的ViewGroup中每个又持有多个ViewGroup,层层嵌套到最底层的View为止。
-
深度搜索优先dfs:
在用户触发任意一个点击事件的时候,我们是通过深度搜索优先的方式去寻找可以消费该点击事件的视图,从树的最深层开始处理点击事件。
这个代表了什么意思呢?如果一个ViewGroup和它的子View同时都设置了OnClickListener,那么我们在点击它们之间重合的部分时,只会触发子View的点击事件而不会触发父View的点击事件。
在代码表现这个深度搜索的方式:处理分发事件的时候,会先用for循环把所有的子View遍历,尝试调用子View的方法来处理该点击事件,只有确认所有子View都不能处理该点击事件之后,才会调用自己的点击事件处理方法。
-
逆序遍历:
触发点击事件搜索子View的时候,总有个搜索的顺序,这个顺序是逆序,也就是从最后一个添加的子View开始查找和处理点击事件。
其原理和添加VIew是相关联的,我们知道,在一个ViewGroup的两个子View中,如果这两个子View有重合的部分,那么一般而言总是后添加的视图会覆盖掉前面添加的视图的部分。
点击事件也是同理,当用户点击他们重合的部分时,一般而言用户总是希望点击到用户本身可以看见的那个视图。所以我们的点击事件分发就和添加视图的顺序相反,从最后添加的视图开始遍历。
2.1 时序图总结
为了方便我们看完源码之后以后复习方便,我先将点击事件分发的全部流程放到这里,在看的过程中有需要可以翻回来看:
2.2 流程图总结
由于时序图一般而言没有办法很好的展示我们深度优先搜索的思想,所以我额外又补充了一张流程图,这个流程图也画出了点击事件分发的原理:
2. 源码解读
2.1 Activity到ViewGroup
任何的事件源头都是从我们底层的SurfaceFlinger进程来的,他直接管理着用户可以看到的窗口,但是我们这里不用去深究那么底层的原理。只要知道从底层来的点击事件第一个触发的是Activity的DispatchTouchEvent就足够了。
public class Activity extends ContextThemeWrapperimplements LayoutInflater.Factory2,Window.Callback, KeyEvent.Callback,OnCreateContextMenuListener, ComponentCallbacks2,Window.OnWindowDismissedCallback,ContentCaptureManager.ContentCaptureClient {/*** 调用以处理触摸屏事件。您可以重写此方法,在将所有触摸屏事件发送到窗口之前拦截它们。* 请确保为应该正常处理的触摸屏事件调用此实现。* * @param ev 点击事件本体* @return boolean 如果事件被消费了会return true*/public boolean dispatchTouchEvent(MotionEvent ev) {if (ev.getAction() == MotionEvent.ACTION_DOWN) {onUserInteraction();}// 重点是这行if (getWindow().superDispatchTouchEvent(ev)) {return true;}return onTouchEvent(ev);}
}
点击事件就这样从Activity手上分出去了,接下来看看Window类是如何处理的,顺便一提,Window本身是一个抽象类,作为他承载的实体一般而言是PhoneWindow类。
public class PhoneWindow extends Window implements MenuBuilder.Callback {private DecorView mDecor;@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) {return mDecor.superDispatchTouchEvent(event);}
}
事件就这样直接转到了DecorView。
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {final Window.Callback cb = mWindow.getCallback();return cb != null && !mWindow.isDestroyed() && mFeatureId < 0? cb.dispatchTouchEvent(ev) : /* 重点看这部分 */super.dispatchTouchEvent(ev);}
}
DecorView作为最上层的特殊View,他在处理事件的时候会有特殊的窗口判断,但是一般而言是不会触发的,我们不去理他,重点看super.dispatchTouchEvent(ev),这个就代表了点击事件的真正起点。
2.2 ViewGroup
接下来就进入到我们真正的主角,ViewGroup了,我会将他的源码切成好几段一点点的说明。
事件中断
在正式开始点击事件之前,ViewGroup会通过onInterceptTouchEvent这个方法对点击事件做一个中断判断,如果被中断了就不会处理后续的流程了
onInterceptTouchEvent这个方法一般而言都是会返回false,也就是不中断。如果你有业务上的需求需要中断的话,可以返回true。这样事件就不会往下面的View分发,只会由自己进行处理。
public abstract class ViewGroup extends View implements ViewParent, ViewManager {@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {boolean handled = false;// 检查事件是否被中断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);} else {intercepted = false;}} else {intercepted = true;}// 如果被拦截,就会跳过分发的流程if (!canceled && !intercepted) {// ...正式分发点击事件}// 自己处理点击事件return handled;}public boolean onInterceptTouchEvent(MotionEvent ev) {if (ev.isFromSource(InputDevice.SOURCE_MOUSE)&& ev.getAction() == MotionEvent.ACTION_DOWN&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)&& isOnScrollbarThumb(ev.getXDispatchLocation(0), ev.getYDispatchLocation(0))) {return true;}return false;}
}
逆序搜索
开始正式处理点击事件,会先逆序遍历所有的子View,然后进行一些判断
- 该子View能否点击。canReceivePointerEvents
- 用户点击的位置是否和该View重合。isTransformedTouchPointInView
两个判断条件都符合后,就会尝试在该子View中处理点击事件
注意,子View也有可能是一个ViewGroup,所以调用子View的dispatchTouchEvent后,有可能会实际上调用的还是ViewGroup.dispatchTouchEvent。
public abstract class ViewGroup extends View implements ViewParent, ViewManager {@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {boolean handled = false;// 检查事件是否被中断final boolean intercepted;TouchTarget newTouchTarget = null;if (!canceled && !intercepted) {// ...正式处理点击事件final int childrenCount = mChildrenCount;for (int i = childrenCount - 1; i >= 0; i--) {// 不用关注他的原理,我们只需要他掏出了一个View即可。final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);// 对View做合法性判断,如果合法就可以继续点击if (!child.canReceivePointerEvents()|| !isTransformedTouchPointInView(x, y, child, null)) {ev.setTargetAccessibilityFocus(false);continue;}newTouchTarget = getTouchTarget(child);// 转换为点击事件,注意child这个入参我们是有传值的if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//...处理了一些逻辑break; //然后直接跳出循环}}// 下面又开始处理其他逻辑}return handled;}/*** 分发转换为点击事件*/ private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {event.setAction(MotionEvent.ACTION_CANCEL);// 注意child这个入参,我们此时传入的child不为空,所以走下面if (child == null) {handled = super.dispatchTouchEvent(event);} else {handled = child.dispatchTouchEvent(event);}event.setAction(oldAction);return handled;}}
}
自己处理点击事件
在遍历了所有子View都没有处理掉该事件之后,ViewGroup会尝试自己来处理该事件。
public abstract class ViewGroup extends View implements ViewParent, ViewManager {@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {boolean handled = false;TouchTarget newTouchTarget = null;if (!canceled && !intercepted) {// ...正式处理点击事件TouchTarget newTouchTarget = null;for (int i = childrenCount - 1; i >= 0; i--) {// 刚才的for循环,用来表示代码的相对位置}// 如果点击事件之前被子View给处理了,// 那么代码到这里之后newTouchTarget就不为空,或者mFirstTouchTarget不为空if (newTouchTarget == null && mFirstTouchTarget != null) {newTouchTarget = mFirstTouchTarget;while (newTouchTarget.next != null) {newTouchTarget = newTouchTarget.next;}newTouchTarget.pointerIdBits |= idBitsToAssign;}}// 这里和上面是连着的,如果mFirstTouchTarget== null其实就代表着点击事件没有被子View给处理if (mFirstTouchTarget == null) {handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);} else {// 处理其他逻辑}return handled;}/*** 分发转换为点击事件*/ private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {event.setAction(MotionEvent.ACTION_CANCEL);// 注意child这个入参,我们此时传入的child为空,所以走上面if (child == null) {handled = super.dispatchTouchEvent(event);} else {handled = child.dispatchTouchEvent(event);}event.setAction(oldAction);return handled;}}
}
ViewGroup总结
要不然就是通过某个子View层层遍历,走到最深层的某个View的dispatchTouchEvent
要不然就是没有子View,调用自己的super,dispatchTouchEvent,还是View的dispatchTouchEvent
总而言之,代码就会通过这两种方式走到View这个类里面,
ViewGroup的dispatchTouchEvent这个方法的功能也很明显了:找一个可以处理该点击事件的View(可能是自己),将点击事件(TouchEvent)分发(Dispatch)给它(View)。
2.3 View
OnTouchListener
点击事件到View之后,入口还是DispatchTouchEvent,他会先检查是否有TouchListener,有的话先执行它。
这里有两个点,第一个点就是在View里面,OnClickListener和OnTouchListener是完全不同的东西。
第二个点就是OnTouchListener这个方法的return是有开发者自己控制的,换句话说,开发者可以自行控制事件是否要停在onTouch这里
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {public boolean dispatchTouchEvent(MotionEvent event) {boolean result = false;// 这个if一般而言都是通过的if (onFilterTouchEventForSecurity(event)) {if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {result = true;}ListenerInfo li = mListenerInfo;if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED// 重点看这里&& li.mOnTouchListener.onTouch(this, event)) {result = true;}// 如果事件没有被onTouch处理掉,就会进入事件处理流程if (!result && onTouchEvent(event)) {result = true;}}return result;}public interface OnTouchListener {boolean onTouch(View v, MotionEvent event);}
}
onTouchEvent
这里就是单个事件真正的处理方法,但是对于我们而言我们反而不需要太关注这个方法的处理逻辑。
第一是本文主要关注事件时如何分发到这里的。
第二是该方法无非就是对我们常用的一些逻辑,如focus,onClickListener,onLongClick等内容进行判断,有的话这个方法就会return true,没有的话就return false
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {public boolean onTouchEvent(MotionEvent event) {final int action = event.getAction();if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {switch (action) {case MotionEvent.ACTION_UP:// 一堆判断之后会走到这里,我们就不看这些判断了performClickInternal();break;case MotionEvent.ACTION_DOWN:break;case MotionEvent.ACTION_CANCEL:break;case MotionEvent.ACTION_MOVE:break;}return true;}return false;}private boolean performClickInternal() {notifyAutofillManagerOnClick();return performClick();}public boolean performClick() {notifyAutofillManagerOnClick();final boolean result;final ListenerInfo li = mListenerInfo;if (li != null && li.mOnClickListener != null) {playSoundEffect(SoundEffectConstants.CLICK);// 这里就是我们设置的点击事件了,OnClickListenerli.mOnClickListener.onClick(this);result = true;} else {result = false;}sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);notifyEnterOrExitForAutoFillIfNeeded(true);return result;}
}
3. 附录:时序图uml代码
@startumlparticipant Activity
participant PhoneWindow
participant DecorView
participant ViewGroup as vg
participant View as v
participant TouchListener as tl
participant "子View\nViewGroup" as cvgactivate Activity
Activity -> PhoneWindow : dispatchTouchEvent
activate PhoneWindowPhoneWindow -> DecorView : dispatchTouchEventactivate DecorViewDecorView -> vg : dispatchTouchEvent\n进入View树处理点击事件activate vg vg -> vg : onInterceptTouchEvent\n判断事件是否被拦截activate vg vg --> vg : return booleandeactivate vgalt truevg --> vg : return\n不进行任何点击事件的处理\n流程结束endloop 逆序遍历子Viewvg -> cvg : isTransformedTouchPointInView\n判断是否可以点击activate cvgcvg --> vg : return boolean\ntrue代表可以点击deactivate cvgalt 不能点击vg -> vg : continue\n搜索下一个子View else 可以点击vg -> vg : dispatchTransformedTouchEvent\n将分发事件转变为点击事件activate vgvg -> cvg : dispatchTouchEvent\n重复该ViewGroup的行为,继续往下分发activate cvgbreak 点击事件被消费cvg --> vg : return boolean\n告知点击事件是否被消费enddeactivate cvgdeactivate vgendendalt 点击事件没被消费vg -> vg : dispatchTransformedTouchEvent\n将分发事件转变为点击事件activate vgvg -> v :dispatchTouchEventactivate valt 该View有TouchListenerv -> tl : onTouchactivate tltl --> v : return boolean\n告知是否继续往下处理deactivate tlend alt return truev --> vg : return true\n告知点击事件已经被处理else return falsev -> v :onTouchedactivate vv --> v : return boolean\n告知是否处理了点击事件deactivate vv --> vg : return boolean\n告知是否处理了点击事件enddeactivate vdeactivate vgendvg --> DecorView : return boolean\n告知是否处理了点击事件deactivate vgDecorView --> PhoneWindow : return boolean\n告知是否处理了点击事件deactivate DecorViewPhoneWindow --> Activity : return boolean\n告知是否处理了点击事件
deactivate PhoneWindow
deactivate Activity@enduml