Android(安卓)事件分发源码分析
1、前言
事件分发可以说是 Android 众多知识点中最为基础且最重要的之一了,吃透这块知识点不仅能让你在解决滑动冲突时由仍有余,也能为你在面试过程中轻松过关斩将。
2、MontionEvent + 3个重要方法
MontionEvent
事件分发的起源点来自哪?
答:用户对手机屏幕进行指尖的触摸、亦或是轻轻的抚摸、又或是一顿狂风暴雨般的猛击。
Android 将我们对手机这一系列操作封装到了 MontionEvent 对象中
常见的几种:
事件类型 | 具体操作 |
---|---|
MontionEvent .ACTION_DOWN | 手指触摸屏幕(内部对应点击某个View) |
MontionEvent .ACTION_MOVE | 手指滑动屏幕(内部对应滑动某个View) |
MontionEvent .ACTION_UP | 手指离开屏幕的一瞬间(与DOWN对应) |
3个重要方法
接着我们先来看看事件分发中主要的三个方法,先看看结果再来分析过程,这段简单了解即可,
方法 | 作用 | 调用 | view中是否存在 | ViewGroup中是否存在 |
---|---|---|---|---|
dispatchTouchEvent | 分发点击事件 | 当点击事件传递到当前View时 | 存在 | 存在 |
onInterceptTouchEvent | 拦截点击事件 | ViewGroup 内部的 dispatchTouchEvent() 内部调用 | 不存在 | 存在 |
onTouchEvent | 处理点击事件 | dispatchTouchEvent()内部调用 | 存在 | 存在 |
三个方法都是 boolean 类型返回值的方法,true 即表示自己消费,false 则继续分发,一段伪代码简单描述上述的三个方法的关系:
//摘自艺术探索public boolean dispatchTouchEvent(MotionEvent ev) { boolean consume = false; if (onInterceptTouchEvent(ev)) { consume = onTouchEvent(ev); } else { consume = child.dispatchTouchEvent(ev); } return consume; }
3、Activity 分发
现在我们从源码的角度简单的走一遍分发的流程,当我们的 Activity 接收到用户的触摸屏操作时,便会调用 Activity 的 dispatchTouchEvent(), 如下
public boolean dispatchTouchEvent(MotionEvent ev) { //可以忽略这一部分代码 if (ev.getAction() == MotionEvent.ACTION_DOWN) { //当此activity在栈顶时,点击按home、back、menu等键都会触发此方法 onUserInteraction(); } //1、点进去会走到Window 类中的 superDispatchTouchEvent //2、然后 Window 的实现类只有PhoneWindow,类注释中有明确说明 //3、如下有贴出PhoneWindow 的superDispatchTouchEvent //和 DecorView 的superDispatchTouchEvent,先去看这俩方法的注释再回来 if (getWindow().superDispatchTouchEvent(ev)) { //4、欢迎回来.若superDispatchTouchEvent返回true, //即ViewGroup或者View的dispatchTouchEvent返回true,则事件结束 return true; } //5、如果ViewGroup或者View都没有消费事件则会返回false,<--面试重点 //那么事件将会交给Activity的onTouchEvent处理。 return onTouchEvent(ev); } //走进#PhoneWindow@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) { //mDecor 指的是 DecorView 的实例 //DecorView 其实就是 activity 窗口的根视图 return mDecor.superDispatchTouchEvent(event); }//走进#DecorViewpublic boolean superDispatchTouchEvent(MotionEvent event) { //这个 dispatchTouchEvent 其实就是调用的 ViewGroup 中的 dispatchTouchEvent //这个 ViewGroup 就相当于每个界面的顶层View(根View) //好了,可以回去了... return super.dispatchTouchEvent(event); }
该说的都在码里了,我干了,你随意...
这部分只是 Activity 中的分发,接着我们走进 ViewGroup 瞧瞧
4、ViewGroup 分发
接着我们来分析 ViewGroup 中的 dispatchTouchEvent(),因为代码比较多所以我们分段进行分析:
// 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; }
上面的代码主要是描述当前 View 是否拦截点击事件这个逻辑。
第一个 if 判断中:
第一个判断很好理解,根据用户操作手指进行判断;
第二个mFirstTouchTarget: 这个对象结合后面的代码逻辑可知,在 ViewGroup 的子 view 处理成功时,mFirstTouchTarget 会被赋值并指向子 View ,所以反推可知此时的 mFirstTouchTarget == null;那么当事件来到 ACTION_MOVE 和 ACTION_UP 的时候该 if 语句将会不成立,这就导致 ViewGroup 的 onInterceptTouchEvent() 不会被调用,并且 ViewGroup 同一事件序列中的其他事件都会交给它处理;
disallowIntercept默认为false,在代码中我们可以通过requestDisallowInterceptTouchEvent() 来设置,一旦设置之后,ViewGroup将无法拦截除了 ACTION_DOWN 以外的点击事件,为什么说除 ACTION_DOWN 以外呢?因为在 ViewGroup 分发的时候,如果是 ACTION_DOWN 将会重置 FLAG_DISALLOW_INTERCEPT 这个标记位,
以下是事件为 ACTION_DOWN 时重置 FLAG_DISALLOW_INTERCEPT 标记位的源码:
// 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(); } private void resetTouchState() { clearTouchTargets(); resetCancelNextUpFlag(this); mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; mNestedScrollAxes = SCROLL_AXIS_NONE; }
根据源码分析可知:
当 ViewGroup 进行拦截事件时,只有事件为 ACTION_DOWN 时才会调用 onInterceptTouchEvent(),并且 FLAG_DISALLOW_INTERCEPT 这个标记位也可以来控制 ViewGroup 是否进行拦截,在处理滑动冲突的时候可以利用这一特性来解决。
以上分析的是 ViewGroup 拦截事件的源码,接着来看看当 ViewGroup 不对事件进行拦截时的情况会是如何:
final View[] children = mChildren;for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); // If there is a view that has accessibility focus we want it // to get the event first and if not handled we will perform a // normal dispatch. We may do a double iteration but this is // safer given the timeframe. if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } 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); 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; }
//dispatchTransformedTouchEvent 内部调用if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); }
总结一下:
首先会遍历 ViewGroup 所有的子 View ,然后判断子 View 是否有接收到点击事件,如果有则事件便会传递给它,dispatchTransformedTouchEvent 实际上就是调用了子 View 的 child.dispatchTouchEvent 方法,这样事件就从 ViewGroup 传递到了 View上,交由 View 去进行一轮新的分发,这里暂不讨论子 View 具体如何分发,如果 child.dispatchTouchEvent(event) 返回了 true ,那么 if 语句成立 ,mFirstTouchTarget便会被赋值,并且跳出 for 循环,如下所示:
newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true;
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; }
还有一种情况便是 child.dispatchTouchEvent(event) 返回了 false ,那么便会接着遍历分发(在还有下一个 View 的情况下),如果遍历完所有的子 View 都没有被处理,要么就是 ViewGroup 没有子 View ,要么就是子 View 内部自己做了处理,在 dispatchTouchEvent 或者 onTouchEvent 方法中返回了 false ;如果是这样的话那么 ViewGroup 便会自己去处理点击事件,如下所示:
// 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); }
上述代码说的很清楚,当mFirstTouchTarget为null时,同样会调用 dispatchTransformedTouchEvent 方法,但是注意第三个参数(View child),传的是个 null,再看一遍:
if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); }
这个判断主要是用来处理当 ACTION_MOVE 和 ACTION_UP 事件到来时,改 if 语句就会不成立,这样就是导致 ViewGroup 的 onInterceptTouchEvent() 不会再被调用,并且同一事件序列中的其他事件都会默认交给它处理。
5、View 分发
View 的事件分发相比 ViewGroup 就相对简单一些了,先来看看它的 dispatchTouchEvent 方法:
boolean result = false; ...... if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } ...... return result;
从源码中可以看出,会先去判断当前 View 是否有设置 onTouchListener 监听事件,如果有的话,事件会交给 onTouchListener 中的 onTouch 处理,则不会传递到 onTouchEvent 方法,从这里可以说明, onTouchListener 的优先级要高于 onTouchEvent 方法,这样处理的目的就是为了方便用户自己去处理点击事件 。
接着看看 onTouchEvent 的实现,首先来看看 View 处于不可用状态 (指 TextView 、ImageView等)下点击事件的处理过程,如下:
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return clickable; }
很显然,不可用状态下的 View 同样会消耗掉点击事件,即便它看起来是不可用的。
接着往下走,如果 View 有设置代理,那么还会执行 TouchDelegate 的 onTouchEvent 方法,这个机制跟 onTouchListener类似
if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } }
接着往下走,来看看 onTouchEvent 对事件的具体处理
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { 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(); } } } ... break; ... return true; }
从上面 if 语句就可以看出只要 View 的 CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE(代理),有一个为 true ,便会消费掉事件,不管它的状态是不是可用状态,当 ACTION_UP 事件发生时,会触发 performClick 方法,如果 View 设置了 OnClickListener 监听事件,则会调用它的 onClick 方法
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); notifyEnterOrExitForAutoFillIfNeeded(true); return result;
6、总结
关于这个知识点可以从三个步骤进行理解,也是事件分发的流程
Activity --> ViewGroup -->View
Activity
事件分发首先从 Activity 开始进行分发,在 Activity 的 dispatchTouchEvent() 中会将事件分发到 window 上,而 window 是个抽象类,所以会把具体工作交给它的唯一实现类 PhoneWindow 上,而 PhoneWindow 又会将事件传递到 DecorView 上,而 DecorView 其实就相当于当前界面的底层容器,接着这个事件会来到容器的顶层 View ,一般来说这个顶层 View 就是 ViewGroup。
ViewGroup
事件首先会来到 dispatchTouchEvent 方法,如果 onInterceptTouchEvent 方法返回 true 表示对事件进行了拦截,那么该事件便会交给 ViewGroup 的 onTouchEvent 方法进行消费;
如果 onInterceptTouchEvent 方法返回 false 即表示没有对事件进行拦截,那么 ViewGroup 则会去遍历子View ,如果子 View 没有做特殊处理的情况下,事件便会顺利的传递到子 View ;如果子 View 在 dispatchTouchEvent 或者 onTouchEvent 方法中返回了 false ,那么该事件还是会交给 ViewGroup 处理,最终会传递到 Activity 的 onTouchEvent 方法进行消费掉;
View
View 的分发同样是从 dispatchTouchEvent 方法开始,但是 View 没有 onInterceptTouchEvent 方法,当 View 事件传递到 onTouchEvent 之前会先判断是否有设置 onTouchListener 监听,如果有的话事件便交给 onTouchListener 的 onTouch 处理,如果没有设置,则会交给 onTouchEvent 进行处理,当 View 有设置 onClickListener 监听时,事件最终便会传递到 onClickListener 的 onClick 方法。
所以在 View 中的优先级如下:
onTouchListener > onTouchEvent > onClickListener
参考:艺术探索
更多相关文章
- 1.4掌握日志工具的使用——Android第一行代码(第二版)笔记
- Android(安卓)4.4 meminfo 实现分析
- Android中的线程与线程池
- Android怎样播放多张图片形成一个动画
- Android:Activity与Fragment通信(99%)完美解决方案
- 一行代码搞定Android屏幕适配
- 第一行代码 Android读书笔记(四)
- [置顶] Android触摸事件分发
- DataBinding使用教程详解