Android 事件分发机制源码解析-ViewGroup层

在上篇文章中咱们分析了view的事件分发机制**《Android 事件分发机制源码解析-view层》**,在本篇文章中咱们继续分析另外一层viewGroup的事件分发,viewGroup本质上是一组view的集合,它的里面包含了view和另外一组viewGroup,咱们日常使用的各类布局如LinearLayout、RelativeLayout、FrameLayout等等都是继承的viewGroup,对于viewgroup与view以前的关系,咱们能够用一张图来描述一下: java

在这里插入图片描述

ViewGroup和View组成了一棵树形结构,最顶层为Activity的ViewGroup,下面有若干的ViewGroup节点,每一个节点之下又有若干的ViewGroup节点或者View节点,依次类推。android

接下来咱们就开始进入正题分析viewGroup的事件分发。bash

分析工具

//Android源码环境
android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
}

//分析工具
Android Studio 2.2.3
Build #AI-145.3537739, built on December 2, 2016
JRE: 1.8.0_112-release-b05 x86_64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
复制代码

对于view事件分发,主要用到两个方法,而对于viewGroup来讲,比view多了一个方法,ide

onInterceptTouchEvent(MotionEvent ev)
复制代码

表示是否用于拦截当前事件,返回true表示拦截,若是拦截了事件,那么将不会分发给子View。好比说:ViewGroup拦截了这个事件,那么全部事件都由该ViewGroup处理,它内部的子View将不会得到事件的传递。(可是ViewGroup是默认不拦截事件的,这个下面会解释。)注意:View是没有这个方法的,也便是说,继承自View的一个子View不能重写该方法,也无需拦截事件,由于它下面没有View了,它要么处理事件要么不处理事件,因此最底层的子View不能拦截事件。工具

另外两个方法分别是:源码分析

//该方法用来进行事件的分发,即不管ViewGroup或者View的事件,都是从这个方法开始的。
public boolean dispatchTouchEvent(MotionEvent ev)
复制代码

布局

//这个方法表示对事件进行处理,在dispatchTouchEvent方法内部调用,若是返回true表示消耗当前事件,若是返回false表示不消耗当前事件。
public boolean onTouchEvent(MotionEvent ev)
复制代码

就是这三个方法决定着viewGroup层的事件分发,它们主要的做用能够经过如下的伪代码来表示:学习

public boolean dispatchTouchEvent(MotionEvent ev){
    boolean handle = false;
    if(onInterceptTouchEvent(ev)){
        handle = onTouchEvent(ev);
    }else{
        handle = child.dispatchTouchEvent(ev);
    }
    return handle;
}
复制代码

上面这段代码表示什么意思呢?若是一个事件传递到了ViewGroup处,首先会判断当前ViewGroup是否要拦截事件,即调用onInterceptTouchEvent()方法;若是返回true,则表示ViewGroup拦截事件,那么ViewGroup就会调用自身的onTouchEvent来处理事件;若是返回false,表示ViewGroup不拦截事件,此时事件会分发到它的子View处,即调用子View的dispatchTouchEvent方法,如此反复直到事件被消耗掉。接下来,咱们将从源码的角度来分析整个ViewGroup事件分发的流程是怎样的。动画

当一个点击事件产生后,它的传递过程将遵循以下顺序:ui

Activity -> Window -> View
复制代码

事件老是会传递给Activity,以后Activity再传递给Window,最后Window再传递给顶级的View,顶级的View在接收到事件后就会按照事件分发机制去分发事件。若是一个View的onTouchEvent返回了FALSE,那么它的父容器的onTouchEvent将会被调用,依次类推,若是全部都不处理这个事件的话,那么Activity将会处理这个事件。

源码分析

因为事件老是会先传递到Activity,因此咱们就从Activity里面的事件分发开始分析。首先来看下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);
    }
复制代码

咱们看到第二个if判断,getWindow返回的是Window的实现类PhoneWindow,因此这个判断的意思就是,Activity的事件会交给它所属的Window进行分发,若是它返回了TRUE,就表明整个事件就结束了,若是返回了FALSE的话就表明事件没有人处理,那么它终将会被Activity本身所处理,即会调用本身的onTouchEvent方法。

接下来咱们就来分析一下Window是如何将事件分给ViewGroup的。Window是个抽象类,因此咱们来看下的实现类PhoneWindow的dispatchTouchEvent的源码,看看里面是如何进行分发的。

//PhoneWindow.java
	@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
复制代码

咱们能够发现,代码很短,PhoneWindow直接将事件传递给了DecorView, 这个DecorView便是顶级view了,因此事件就已经传到了顶级view这里,通常状况下,顶级view是ViewGroup。因此从下面开始咱们进入到ViewGroup的事件分发阶段。

对于ViewGroup的事件分发过程,大概是这样的:若是顶级的ViewGroup拦截事件即onInterceptTouchEvent返回true的话,则事件会交给ViewGroup处理,若是ViewGroup的onTouchListener被设置的话,则onTouch将会被调用,不然的话onTouchEvent将会被调用,也就是说:二者都设置的话,onTouch将会屏蔽掉onTouchEvent,在onTouchEvent中,若是设置了onClickerListener的话,那么onClick将会被调用。若是顶级ViewGroup不拦截的话,那么事件将会被传递给它所在的点击事件的子view,这时候子view的dispatchTouchEvent将会被调用,从这开始就进入了view的事件分发过程(能够参考《Android 事件分发机制源码解析-view层》),这就是整个事件的分发过程。

首先,咱们来看下ViewGroup的事件分发过程,进入到dispatchTouchEvent方法里,因为这个方法比较长,因此咱们对重要代码进行分析,其余的省略,能够自行去看看。

//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
			// 处理初始状态
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
//...省略无关代码
 // 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;
            }
//...省略无关代码
}
复制代码

首先这里先判断事件是否为DOWN事件,若是是,则初始化,把mFirstTouchTarget置为null。因为一个完整的事件序列是以DOWN开始,以UP结束,因此若是是DOWN事件,那么说明是一个新的事件序列,因此须要初始化以前的状态。这里的mFirstTouchTarget很是重要,后面会说到当ViewGroup的子元素成功处理事件的时候,mFirstTouchTarget会指向子元素,接着咱们来看下ViewGroup何时会进行拦截呢?从上面那个if判断能够知道,在两种状况下,ViewGroup才会拦截事件,第一种是:事件类型是ACTION_DOWN,第二种就是mFirstTouchTarget != null,第一种好理解,可是第二种什么意思呢?咱们经过后面的代码分析能够知道这个mFirstTouchTarget的意思就是,若是ViewGroup里面的子元素view可以处理事件的话,那么这个mFirstTouchTarget就会指向这个子元素view。

在上面代码里面有个方法,onInterceptTouchEvent(),咱们进入里面查看一下

public boolean onInterceptTouchEvent(MotionEvent ev) { 
    return false; 
}
复制代码

默认返回false,也就是说,ViewGroup默认是不拦截事件的,若是想让ViewGroup拦截事件的话,须要重写该方法。接着往下看,里面有个变量FLAG_DISALLOW_INTERCEPT,这个标志位的做用是禁止ViewGroup拦截除了DOWN以外的事件,通常经过子View的requestDisallowInterceptTouchEvent来设置。因此,当ViewGroup要拦截事件的时候,那么后续的事件序列都将交给它处理,而不用再调用onInterceptTouchEvent()方法了,因此该方法并非每次事件都会调用的。

判断是否拦截后,咱们来看看ViewGroup不拦截的状况,ViewGroup不拦截的话那么它将会把事件交给它的子view来处理。

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;
                            }
//判断触摸点位置是否在子View的范围内或者子View是否在播放动画,若是均不符合则continue
                            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);
                            //若是子view消耗了事件
                            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();
                                //若是子View消耗掉了事件,那么mFirstTouchTarget就会指向子View。
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
复制代码

上面代码比较清楚,首先就是遍历ViewGroup的全部子元素,而后判断子元素是否可以接收到点击事件(一、是否正在播放动画,二、点击事件的坐标是否在子元素的区域内),这里面有个方法private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits)实际上就是调用子元素的dispatchTouchEvent方法的,方法内部有个判断,判断child是否为null,若是不为null的话,直接调用子元素的dispatchTouchEvent方法,这样事件就交给子元素处理。完成了ViewGroup到子View的事件传递,当事件处理完毕,就会返回一个布尔值handled,该值表示子View是否消耗了事件。怎样判断一个子View是否消耗了事件呢?若是说子View的onTouchEvent()返回true,那么就是消耗了事件。

if (child == null) {
             handled = super.dispatchTouchEvent(event);
         } else {
             handled = child.dispatchTouchEvent(event);
         }
复制代码

若是子元素的dispatchTouchEvent返回了true的话,那么这个变量mFirstTouchTarget就会被赋值

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
复制代码

上面这段代码就是给mFirstTouchTarget赋值的,能够看出来,这个mFirstTouchTarget实际上是一个单链表结构,这个值直接影响ViewGroup是否对事件的拦截,若是为null的话,那么ViewGroup将会默认拦截同一序列中全部的点击事件,若是遍历全部的元素后发现事件没有被处理的话,那么只有两种状况,一是ViewGroup没有子元素,二是子元素处理了点击事件,可是在dispatchTouchEvent事件中返回了false,也就是在onTouchEvent事件中返回了false,在这种状况下,ViewGroup只能本身处理事件了。即继续往下分析代码能够看出来:

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;
           }
       }
复制代码

在上面,若是mFirstTouchTarget==null的话,就说明子view不处理该事件,那么该事件将交给ViewGroup来处理。而若是在上面已经找到一个子View来消耗事件了,那么这里的mFirstTouchTarget不为空,接着会往下执行。接着有一个if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget)判断,这里就是区分了ACTION_DOWN事件和别的事件,由于在在上面咱们知道,若是子View消耗了ACTION_DOWN事件,那么alreadyDispatchedToNewTouchTarget和newTouchTarget已经有值了,因此就直接置handled为true并返回;那么若是alreadyDispatchedToNewTouchTarget和newTouchTarget值为null,那么就不是ACTION_DOWN事件,便是ACTION_MOVE、ACTION_UP等别的事件的话,就会调用下面代码,把这些事件分发给子View。

if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {
           handled = true;
}
复制代码

上面这段代码处理除了ACTION_DOWN事件以外的其余事件,若是ViewGroup拦截了事件或者全部子View均不消耗事件那么在这里交由ViewGroup处理事件;若是有子View已经消耗了ACTION_DOWN事件,那么在这里继续把其余事件分发给子View处理。

以上基本就是ViewGroup的事件分发过程。看到这里估计上面的也忘了,因此咱们就用流程图来总结一下这个ViewGroup的事件分发过程。

总结

ViewGroup默认不拦截任何事件,因此事件能正常分发到子View处(若是子View符合条件的话),这时候子view的dispatchTouchEvent将会被调用,从这开始就进入了view的事件分发过程。若是没有合适的子View或者子View不消耗ACTION_DOWN事件,那么接着事件会交由ViewGroup处理,而且同一事件序列以后的事件不会再分发给子View了。若是ViewGroup的onTouchEvent也返回false,即ViewGroup也不消耗事件的话,那么最后事件会交由Activity处理。

若是顶级的ViewGroup拦截事件即onInterceptTouchEvent返回true的话,则事件会交给ViewGroup处理,若是ViewGroup的onTouchListener被设置的话,则onTouch将会被调用,不然的话onTouchEvent将会被调用,也就是说:二者都设置的话,onTouch将会屏蔽掉onTouchEvent,在onTouchEvent中,若是设置了onClickerListener的话,那么onClick将会被调用。这就是ViewGroup的事件分发过程。

关于做者

专一于 Android 开发多年,喜欢写 blog 记录总结学习经验,blog 同步更新于本人的公众号,欢迎你们关注,一块儿交流学习~

在这里插入图片描述
相关文章
相关标签/搜索