View事件分发

NOTE: 笔记,碎片式内容

控件

App界面的开主要就是使用View,或者称为控件。View既绘制内容又响应输入,输入事件主要就是触摸事件。

ViewTree

控件基类为View,而ViewGroup是其子类。ViewGroup可以包含其它View作为其child。任何一个ViewGroup及其所有直接或间接的child形成一个ViewTree这样的树结构

RootView

显然每一个具体的ViewTree都会有一个root,它是一个ViewGroup,接下来称它为RootView。持有一个RootView就可以引用此ViewTree,最终访问到所有View。

以Activity为例,使用setContentView(View view)来指定要显示的内容,不过参数view并非是Activity最终显示到Window的ViewTree。通过追溯源码,最终参数view被添加到PhoneWindow.mDecor作为其childView。mDecor是FramLayout的子类对象:

// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;

private final class DecorView extends FrameLayout {...}

可见mDecor是Activity最终显示的ViewTree的root。

结构特点

ViewTree的特点有:

  • 只有一个RootView,它是ViewGroup。
  • ViewTree中的非叶子节点都是ViewGroup。
  • ViewTree中的叶子节点可以是View或ViewGroup。
  • 一个ViewGroup可以有0或多个直接childView。
  • 一个childView只能有一个直接ViewGroup。

直接或间接的parent和child关系是关于具体2个View而言的,而这个关系在界面中的反映就是View显示区域的包含关系,即child总是在parent的区域内。显示区域的包含关系和它们在ViewTree中的结构关系是对应的。

对于组成ViewTree的所有ViewGroup和View来说,View不需要知道其所在ViewGroup,但ViewGroup知道其所有childView。

路径

这里为ViewTree引入路径这一概念,它表示从RootView出发找到任一child时要经过的所有View的列表。 因为一个ViewGroup只能访问其直接child,而一个child只有唯一的parent,所以从RootView到达任一child的路径是唯一的,反之从任一child到达RootView的路线也是唯一的。

显然对任何child的路径总是存在的,虽然可以依靠额外的数据结构来保存各个View的关系,但树结构本身已经在做这样的事情了。

View系统的底层原理

View系统是framework层提供给应用开发者的一种方便开发界面的框架,类似其它编程平台中的控件系统那样。 Android底层使用WindowManagerService(简称WMS)、Surface、InputManagerService(简称IMS)这些服务组件和类型来管理界面显示和输入事件的。这里简单地对View系统的显示和输入事件的获取进行探索。

使用View和Window来显示界面

Window是像Activity、Dialog、PopupWindow这样的独立显示和交互的界面的抽象。 可以像下面这样将一个View显示到新窗口:

private void newFloatingWindow() {
    final WindowManager wm = (WindowManager)    getSystemService(Context.WINDOW_SERVICE);
    final Button button = new Button(this);
    button.setText("ClickToDismiss");
    LayoutParams lp = new LayoutParams();
    lp.height = LayoutParams.WRAP_CONTENT;
    lp.type = LayoutParams.TYPE_PHONE;
    lp.flags = LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_NOT_TOUCH_MODAL;

    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            wm.removeViewImmediate(button);
        }
    });

    wm.addView(button, lp);
}

上面使用WindowManager创建了一个Window并显示传递的view。 通过追溯源码: WindowManager->WindowManagerImpl->WindowManagerGlobal 可以看到最终addView()的执行是: ViewRootImpl.setView(View view, WindowManager.LayoutParams attrs, View panelParentView)

实际上,ViewRootImpl和WMS通信来完成所有实际工作:创建窗口,对View的绘制和事件分发。

NOTE: newFloatingWindow()的调用可以在非主线程中,仅要求线程Looper设置ok。这样onClick()回调也就在对应的子线程中。不过View对象为了性能其代码实现是非线程安全的,所以不允许其创建和修改在不同的线程中,所以,最方便的就是在主线程中创建View,之后其它线程可以转到主线程中去继续操作View。否则不同View在不同线程中操作是十分混乱的。要知道,main线程是唯一且必一直存在的。

ViewRootImpl

ViewRootImpl的知识比较多,这里对它进行一个感性的介绍,便于理解文章中对它的引用。

ViewRootImpl.mView字段就是要显示的窗口的ViewTree的RootView。 ViewRootImpl作为ViewTree的管理者,它和WMS通信完成各种“底层”操作。

作用包括:

  1. 执行ViewTree的绘制。主动发起或是响应requestLayout()或invalidate()而执行performTraversals()/scheduleTraversals()来对ViewTree执行遍历操作,即测量、布局和绘制。
  2. 分发InputEvent给ViewTree。

在“将Root添加到Window通知WMS显示”时,执行performTraversals()中会调用View.dispatchAttachedToWindow(AttachInfo info, int visibility将AttachInfo指定给mView,而ViewGroup会遍历childViews递归此调用。总之,每个ViewTree的View也会持有其添加到的Window的信息,其中就包含了关联的ViewRootImpl对象。

NOTE: 一般想知道一些方法的调用时序的话,可以在可重写的方法中打印其StackTrace信息查看方法的调用栈。除了那些跨进程IPC调用,或者Handler方式的async调用。

示例ViewTree:MyTree

这里给出一个构成界面的ViewTree的示例,它将作为后续讨论的例子。为了描述方便,将此ViewTree称作“MyTree”。

界面效果:

图1:示例界面

对应的ViewTree结构:

图2:ViewTree结构

ViewTree事件来源

ViewRootImpl接收来自WMS的InputEvent事件,然后调用ViewRootImpl.mView(也就是构成界面的ViewTree的RootView)的View.dispatchPointerEvent(InputEvent event)来向ViewTree传递一个MotionEvent event对象。 所以这就是ViewTree事件来源。

InputEvent主要是KeyEvent和MotionEvent,本文仅讨论后者。

ViewRootImpl从获得WMS的InputEvent,到分发给mView这里有一个过程,分两个部分。

  • InputEvent的接收 ViewRootImpl使用一个InputEventReceiver对象获得WMS发送的事件,在onInputEvent(InputEvent event)回调中,它执行enqueueInputEvent(event, this, 0, true)将事件添加到一个链表,这样对事件的deliver是保证顺序的
  • 分发InputEvent 过程稍微复杂,因为使用了InputStage组成的一个"input pipeline"来处理InputEvent事件。 其中一个阶段就是将MotionEvent传递给mView。
/**
 * Base class for implementing a stage in the chain of responsibility
 * for processing input events.
 * <p>
 * Events are delivered to the stage by the {@link #deliver} method.  The stage
 * then has the choice of finishing the event or forwarding it to the next stage.
 * </p>
 */
abstract class InputStage {...}

ViewRootImpl对事件的分发过程是在主线程中的(它的创建线程和其使用MessageQueue接收事件决定的),而且每次会分发其收到的所有消息。 所以在App的消息循环模型中,响应用户操作后对UI的改动,全部会一次性得到执行。之后在下一次主线程下一次Message处理中响应invalidate()/requestLayout()操作进行ViewTree遍历。

对于一个ViewTree而言,只需要关心输入事件是从RootView那里传入的事实即可。

触摸操作和触摸点

用户第一个手指按下和最终所有手指完全离开屏幕的过程为一次触摸操作,每次操作都可归类为不同触摸模式(touch pattern),被定义为不同的手势。

每个触屏的手指——或者称触摸点被称作一个pointer,即一次触摸过程涉及一或多个pointer。

这里声明以下概念:

  • 任意一个pointer的按下定义为down事件;
  • 任意一个pointer的移动定义为move事件;
  • 任意一个pointer的抬起定义为up事件;

第一个down事件,意味着触摸操作的开始,最后一个up事件意味着触摸操作的结束。开始和结束时的pointer可以不是同一个。

事件序列

一次手势操作过程中每个触摸点都在其down->move->up过程中产生一系列事件,每个触摸点产生的所有事件为一个独立的事件序列

  • 事件? 事件这一概念在代码中是一个用来携带数据的类型,它描述发生了什么。类似消息这样的概念,是数据对象而非业务对象。

View.dispatchTouchEvent

在代码中,ViewRootImpl调用RootView的View.dispatchPointerEvent(MotionEvent event)将事件传递给RootView。

public final boolean dispatchPointerEvent(MotionEvent event) {
    if (event.isTouchEvent()) {
        return dispatchTouchEvent(event);
    } else {
        return dispatchGenericMotionEvent(event);
    }
}

对于触摸事件event.isTouchEvent()为true,所以执行dispatchTouchEvent(event),方法原型:

/**
  * Pass the touch screen motion event down to the target view, or this
  * view if it is the target.
  *
  * @param event The motion event to be dispatched.
  * @return True if the event was handled by the view, false otherwise.
  */
  public boolean dispatchTouchEvent(MotionEvent event)

方法返回一个boolean值,表示是否此View对象是否处理了传递的事件。

传递触摸事件到一个view对象,就是调用其View.dispatchTouchEvent(MotionEvent event)。

对方法View.dispatchTouchEvent()的调用一方面传递事件给view,其返回结果又表明了此view是否处理了事件。

事件传递

View和ViewGroup两个类对dispatchTouchEvent()方法提供了不同的实现。

ViewGroup的实现是,因为其含有child,它会根据一定的规则选择调用child.dispatchTouchEvent()将事件传递给child,或者不。当ViewGroup.dispatchTouchEvent()中执行了对child.dispatchTouchEvent()的调用时,那么事件就经由此ViewGroup到达了child。若child依然是ViewGroup,那么可能继续传递事件给其child。 所以,ViewGroup的dispatchTouchEvent()方法使得多个View对象形成了dispatchTouchEvent()方法的调用栈。这样事件参数得到传递,而且,返回值也会在方法调用不断返回时向上返回。

View不包含child,所以不会有调用child.dispatchTouchEvent()的操作,它作为dispatchTouchEvent()传递调用的终点。

基于它们的实现,事件参数从RootView的dispatchTouchEvent()方法的调用开始,会沿着ViewTree的一个路径不断传递给下一个child——也就是调用child的dispatchTouchEvent()。

以上就是View和ViewGroup的dispatchTouchEvent()方法使得ViewTree产生事件传递的原理。

事件序列传递给View的规则

作为事件序列的第一个事件down,dispatchTouchEvent()对它殊性处理,dispatchTouchEvent()传递调用时,任何view若返回true,则表示它处理了down事件,那么后续事件会继续传递给它。如果某个view返回false,那么调用的传递在它这里终止,后续事件也不会再传递给它。

实际上也只在传递down事件时,ViewGroup才会采取一定规则来决定是否传递事件给child。 并且它使用TouchTarget类来保存可能的传递目标,作为后续事件传递的依据,后续的事件不再应用down事件那样的规则。这反映的是事件序列的连续性原则,一个view处理了down事件那么它一定收到后续事件,否则不再传递事件给它。可见down事件传递完成后会确定下后续事件传递的路径。

NOTE: 一个View收到并处理某个触摸点的down事件后,那么即便之后触摸点移动到View之外,或在View的范围之外离开屏幕,此View也会收到相应的move、up事件,不过收到的事件中触摸点的(x,y)坐标是在View的区域外。

有关down事件的传递细节和TouchTarget等概念,下面源码分析时再详细探索。

MotionEvent

上面对事件的描述都是概念上的,代码中,触摸事件由MotionEvent表示,它包含了当前事件类型和所有触摸点的数据,产生事件时触摸点坐标等。

事件拆分

ViewTree中,事件是经过parent到达child的。由于parent和child的一对多关系和显示区域包含关系,一个ViewGroup可以先后收到两个手指的按下操作,而这两个触摸点可以落在不同的child中,并且在不同的child来看都是第一个手指的按下。

可见child和parent所“应该”处理的触摸点是不同的,那么传递给它们的事件数据也应该不一样。

ViewGroup.setMotionEventSplittingEnabled(boolean split)可以用来设置一个ViewGroup对象是否启用事件拆分,方法原型:

/**
 * Enable or disable the splitting of MotionEvents to multiple children during touch event
 * dispatch. This behavior is enabled by default for applications that target an
 * SDK version of {@link Build.VERSION_CODES#HONEYCOMB} or newer.
 *
 * <p>When this option is enabled MotionEvents may be split and dispatched to different child
 * views depending on where each pointer initially went down. This allows for user interactions
 * such as scrolling two panes of content independently, chording of buttons, and performing
 * independent gestures on different pieces of content.
 *
 * @param split <code>true</code> to allow MotionEvents to be split and dispatched to multiple
 *              child views. <code>false</code> to only allow one child view to be the target of
 *              any MotionEvent received by this ViewGroup.
 * @attr ref android.R.styleable#ViewGroup_splitMotionEvents
 */
public void setMotionEventSplittingEnabled(boolean split);

若不开启拆分,那么第一个触摸点落在哪个child中,之后所有触摸点的事件都发送给此view。若开启,每个触摸点落在哪个view中,其事件序列就发送给此child。而且因为RootView收到的事件总是包含了所有触摸到数据,所以非第一个触摸点操作时,第一个触摸点收到“拆分后得到的move事件”。

因为ViewGroup处理的pointer的数量肯定是大于等于所有child处理的pointer的数量的,特别的,传递给RootView的事件肯定包含所有触摸点的数据。但child只处理它感兴趣的触摸点的事件——就是down事件发生在自身显示范围内的那些pointer。

事件拆分可以让ViewGroup将要分发的事件根据其pointer按下时所属的child进行拆分,然后把拆分后的事件分别发送给不同child。child收到的事件只包含它所处理的pointer的数据,而不含不相干的pointer的事件数据。

最初的MotionEvent中携带所有触摸点数据是为了便于一些view同时根据多个触摸点进行手势判断。而事件拆分目的是让不同的view可以同时处理不同的事件序列——从原事件序列中分离出来的,以允许不同内容区域同时处理自己的手势。

事件类型

action表示事件的动作类型,即上面描述的down、move、up等,不过MotionEvent类提供了更详细的划分。

MotionEvent.getAction()返回一个int值,它包含了两部分信息:action和产生此事件的触摸点的pointerIndex。

/**
 * Return the kind of action being performed.
 * Consider using {@link #getActionMasked} and {@link #getActionIndex} to retrieve
 * the separate masked action and pointer index.
 * @return The action, such as {@link #ACTION_DOWN} or
 * the combination of {@link #ACTION_POINTER_DOWN} with a shifted pointer index.
 */
public final int getAction();

实际的动作类型应该通过getActionMasked()来获得。

当一个View处理多个触摸点的事件序列时,触摸点产生不同事件过程是:

  1. 用户第一个手指按下,产生ACTION_DOWN事件。
  2. 其它手指按下,触发ACTION_POINTER_DOWN。
  3. 任何手指的移动,触发ACTION_MOVE。
  4. 非最后一个手指离开,触发ACTION_POINTER_UP。
  5. 最好一个手指离开,触发ACTION_UP。
  6. 收到ACTION_CANCEL,例如View被移除、弹框、界面切换等引起的View突然不可见。此时收到cancel事件,终止一次手势。

pointerIndex和pointerId

一个MotionEvent对象中记录了当前View所处理的所有触摸点(1或多个)的数据

在MotionEvent中,pointerId是触摸点的唯一标识,每根手指按下至离开期间其pointerId是不变的,所以可以用来在一次事件序列中用来连续访问某个触摸点的数据。

pointerIndex是当前触摸点在数据集合中的索引,需要先根据pointerId得到其pointerIndex,再根据pointerIndex来调用“以它为参数的各种方法”来获取MotionEvent中此触摸点的各种属性值,如x,y坐标等。

NOTE: 出于性能的考虑,多个move事件会被batch到一个MotionEvent对象,可以使用getHistorical**()等方法来访问最近的其它move事件的数据。

源码分析

经过上面的“理论描述”,可以获得View系统事件处理的一个整体认识。接下来分析View、ViewGroup中如何实现这些设计的。

源码:View.dispatchTouchEvent

View.dispatchTouchEvent()中不涉及事件传递,它只能自己处理事件。

/**
 * Pass the touch screen motion event down to the target view, or this
 * view if it is the target.
 *
 * @param event The motion event to be dispatched.
 * @return True if the event was handled by the view, false otherwise.
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    ...

    boolean result = false;    
    final int actionMasked = event.getActionMasked();
    ...

    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;
}

操作如下:

  1. 调用OnTouchListener.onTouch(),传递事件给外部监听者。
  2. 若监听器未处理,则将事件交给自身的onTouch()去处理。

note:对OnTouchListener调用需要view的enabled=true,即为激活状态。而onTouchEvent()的调用受enabled状态的影响。

源码:ViewGroup.dispatchTouchEvent

首先需要理解TouchTarget的概念。

TouchTarget

当一个触摸点的down事件被某个child处理时,ViewGroup使用一个TouchTarget对象来保存child和pointer的对应关系。此pointer的后续事件就直接根据发给此TouchTarget中的child处理,因为down事件决定了整个事件序列的接收者。 因为TouchTarget记录了接收后续触摸点事件的child,而后事件将传递给它们,所以可以称它为派发目标。

TouchTarget是ViewGroup的静态内部类:

private static final class TouchTarget {
  // The touched child view.
  public View child;

  // The combined bit mask of pointer ids for all pointers captured by the target.
  public int pointerIdBits;

  // The next target in the target list.
  public TouchTarget next;

  ...
}

字段pointerIdBits存储了一个child处理的所有触摸点的id信息,使用了bit mask技巧。比如id = n (pointer ids are always in the range 0..31 )那么pointerIdBits = 1 << n

因为ViewGroup中可以是多个child接收不同的pointer的事件序列,所以它将TouchTarget设计为一个链表节点的结构,它使用字段mFirstTouchTarget来引用一个TouchTarget链表来记录一次触屏操作中的所有派发目标。

// First touch target in the linked list of touch targets.
private TouchTarget mFirstTouchTarget;

ACTION_CANCEL

一般的,一个触摸点的序列遵循down-move-up这样的序列,但如果在down或者move之后,突然发生界面切换或者类似view被移除,不可见等情况,那么此时触摸点不会收的“正常”情况下的up事件,取而代之的是来自parent的一个ACTION_CANCEL类型的事件。 此时child应该以“取消”的形式终止对一次事件序列的处理,如返回之前状态等。

整体过程

方法的整体操作过程如下:

  • ACTION_DOWN产生时重置状态,准备迎接新触屏操作的处理。主要就是清除上次事件派发用到的派发目标。
  • 在down事件时确定pointer的派发目标。
  • 根据派发目标,派发事件给child。
  • 在up事件时移除对应view处理的触摸点。

初始化操作

ACTION_DOWN意味着一次新触摸操作的的事件序列的开始,即第一个手指按下。 这时就需要重置View的触摸状态,清除上一次跟踪的触摸点的TouchTarget列表。

// 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();
}

拦截事件

ViewGroup的设计思路是优先传递事件给child去处理,但child的设计是不考虑其parent——不现实, 所以为了避免child返回true优先拿走parent期望去先处理的事件序列,可以重写onInterceptTouchEvent()来根据自身状态(也可以包含child的状态判断)选择拦截事件序列。注意onInterceptTouchEvent()只能用返回值通知dispatchTouchEvent()传递过程需要拦截的意思,但对事件的处理是onTouchEvent()中或者OnTouchListener——和View中的处理一样。

// 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;
}

onInterceptTouchEvent()的默认实现返回false——即不拦截,而子类根据需要在一些状态下时拦截DOWN事件。

同时,ViewGroup提供了方法requestDisallowInterceptTouchEvent(boolean disallowIntercept)供childView申请parent不要拦截某些事件。ViewGroup会传递此方法到上级parent,使得整个路径上的parent收到通知,不去拦截发送给child的一个事件序列。 一般child在onInterceptTouchEvent或onTouchEvent中已经确定要处理一个事件序列时(往往是在ACTION_MOVE中判断出了自己关注的手势)就调用此方法确保parent不打断正在处理的事件序列。

处理down事件:确定派发目标

在ACTION_DOWN或ACTION_POINTER_DOWN产生时,显然一个新的触摸点按下了,此时ViewGroup需要确定接收此down事件的child,并且将pointerId关联给child。

TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
    ...
    if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)) {
        final int actionIndex = ev.getActionIndex(); // always 0 for down
        final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                : TouchTarget.ALL_POINTER_IDS;

        ...

        final int childrenCount = mChildrenCount;
        if (newTouchTarget == null && childrenCount != 0) {
            final float x = ev.getX(actionIndex);
            final float y = ev.getY(actionIndex);
            // Find a child that can receive the event.
            // Scan children from front to back.
            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;
                final View child = (preorderedList == null)
                        ? children[childIndex] : preorderedList.get(childIndex);

                ...

                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;
                }

                // The accessibility focus didn't handle the event, so clear
                // the flag and do a normal dispatch to all children.
                ev.setTargetAccessibilityFocus(false);
            }
            if (preorderedList != null) preorderedList.clear();
        }

        if (newTouchTarget == null && mFirstTouchTarget != null) {
            // Did not find a child to receive the event.
            // Assign the pointer to the least recently added target.
            newTouchTarget = mFirstTouchTarget;
            while (newTouchTarget.next != null) {
                newTouchTarget = newTouchTarget.next;
            }
            newTouchTarget.pointerIdBits |= idBitsToAssign;
        }
    }
}

上面的方法主要工作:

  1. 根据x,y位置,根据绘制顺序“后绘制的在上”的假设对children执行倒序遍历,找到显示区域包含事件且可以接收事件的第一个child,因为处理的是down事件,它将作为此pointer的TouchTarget。
  2. 遍历过程中,若child已经在mFirstTouchTarget所记录的链表中,那么将pointerId增加给它。此时事件未派发,等待后面根据TouchTarget进行派发。
  3. 调用dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)将down事件派发给child,若child处理了事件,那么它作为此pointer的TouchTarget,被添加到mFirstTouchTarget链表。
  4. 如果没找到newTouchTarget,ViewGroup会选择将pointer绑定到最近处理触摸点的那个child——还是不自己处理。

NOTE:

  • 方法dispatchTransformedTouchEvent()在检查child是否处理事件的过程中同时已经完成了事件的派发,所以变量alreadyDispatchedToNewTouchTarget用来记录当前event是否已经派发。
  • split变量表示是否对事件拆分,根据前面的理论知识,不拆分那么整个触屏操作过程所有的触摸点的所有事件只会发给第一个接收ACTION_DOWN的view。拆分的话,每个触摸点的事件都是一个单独的事件序列,发送给不同的处理它们的child。
  • 无论事件拆分与否,若触摸点没有找到合适的child去处理,而已经有child在处理之前的触摸点,那么ViewGroup还是选择将事件交给已经处理事件的child,因为有理由相信它在处理多点触摸事件,而后续触摸点是整个手势的一部分。

 dispatchTransformedTouchEvent

/**
 * Transforms a motion event into the coordinate space of a particular child view,
 * filters out irrelevant pointer ids, and overrides its action if necessary.
 * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
 */
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits);

parent在传递事件给child前将坐标转换为child坐标空间下的,即对x,y进行偏移。

若child=null,则意味着ViewGroup自己处理事件,那么它以父类View.dispatchTouchEvent()的方式处理事件。

参数desiredPointerIdBits中使用位标记的方式记录了此child处理的那些pointer,所有参数event在真正传递给child时会调用MotionEvent.split()来获得仅包含这些pointerId的那些数据。也就是拆分后的子序列的事件。

派发事件

只有down事件会产生一个确定派发目标的过程。之后,pointer已经和某个child通过TouchTarget进行关联,后续事件只需要根据mFirstTouchTarget链表找到接收当前事件的child,然后分发给它即可。

// 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;
    }
}

若mFirstTouchTarget=null说明没有child处理事件,那么ViewGroup自己处理事件。 传递给dispatchTransformedTouchEvent()的参数child==null。 否则,就循环mFirstTouchTarget链表,因为event中是包含了所有pointer的数据的,在 dispatchTransformedTouchEvent()中,会根据target.pointerIdBits对事件进行拆分,只发送包含对应pointerId的那些事件数据给target.child。

处理up/cancel事件

每个pointer的ACTION_UP和ACTION_CANCEL事件意味着其事件序列的终止。 此时在传递事件给child之后,应该从mFirstTouchTarget链表中移除包含这些pointerId的那些派发目标。

// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
        || actionMasked == MotionEvent.ACTION_UP
        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
    resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
    final int actionIndex = ev.getActionIndex();
    final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
    removePointersFromTouchTargets(idBitsToRemove);
}

自己处理事件

在mFirstTouchTarget链表为空时,ViewGroup自己处理事件。 它通过传递给dispatchTransformedTouchEvent()的child参数为null来表示这一点。

// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
        TouchTarget.ALL_POINTER_IDS);

之后在上面的调用方法中:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
      ...
}

因为ViewGroup的父类就是View,所以super.dispatchTouchEvent(transformedEvent)其实就是执行了 View.dispatchTouchEvent(),这时ViewGroup以普通View的方式自己处理事件。

流程总结

设计理论

  • MotionEvent
  • dispatchTouchEvent
  • MotionEvent.split
  • TouchTarget
  • onInterceptTouchEvent
  • disallowIntercept
  • OnTouchListener和onTouchEvent

流程

  • View 通知OnTouchListener去处理; 不处理? 自己的onTouchEvent()处理。 dispatchTouchEvent()返回true?继续处理后续事件; false?不再收到后续事件。
  • ViewGroup child让你拦截吗,onInterceptTouchEvent()自己拦截吗? 不拦截?——找TouchTarget;传递给child。 找不到child?拦截?——自己处理。 dispatchTouchEvent()返回true?继续处理后续事件; false?不再收到后续事件。

补充

  • 不要重写dispatchTouchEvent 可以看到,从View系统的设计原则上看,View和ViewGroup对dispatchTouchEvent()的不同实现形成了View事件的传递机制。 如果需要在ViewGroup中拦截处理事件,那么应该配合使用onInterceptTouchEvent()和requestDisallowInterceptTouchEvent()。
  • ACTION_MOVE中的getAction() 此时action中不包含pointerIndex信息,其实只有ACTION_POINTER_UP和 ACTION_POINTER_DOWN的action才需要保护pointerIndex信息,因为此时pointerCount>1。
  • 拦截和不拦截 在正常的事件传递行为中补充了parent的优先处理和child的优先处理的动作。 向上传递child的反对拦截的请求。 在onTouchEvent中做处理,而不是在onInterceptTouchEvent中。 明确各个方法的职责。

资料

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏青蛙要fly的专栏

项目需求讨论-APP中提交信息及编辑信息界面及功能

好久好久没写文章了,这次我们来讨论下一些具有填写很多资料的界面,或者详情编辑界面等如何做起来更方便。 (PS:我写的可能不好,希望大家不好喷,哈哈,可以留言)

13720
来自专栏QQ音乐技术团队的专栏

[Android] Toast问题深度剖析(二)

题记 Toast 作为 Android 系统中最常用的类之一,由于其方便的api设计和简洁的交互体验,被我们所广泛采用。但是,伴随着我们开发的深入,Toast ...

1.8K90
来自专栏郭霖

Android ListView异步加载图片乱序问题,原因分析及解决方案

在Android所有系统自带的控件当中,ListView这个控件算是用法比较复杂的了,关键是用法复杂也就算了,它还经常会出现一些稀奇古怪的问题,让人非常头疼。比...

421100
来自专栏developerHaoz 的安卓之旅

Android Volley 源码解析(三),图片加载的实现

在上一篇文章中,我们一起深入探究了 Volley 的缓存机制,通过源码分析对缓存的工作原理进行了了解,这篇文章将带大家一起探究「Volley 图片加载的实现」,...

10320
来自专栏潇涧技术专栏

Android Universal Image Loader

最近在阅读Coding的安卓客户端源码,因为该源码的图片加载库使用的是universal-image-loader,我以前也使用过,但是没总结过,所以这次好好研...

8420
来自专栏软件开发 -- 分享 互助 成长

WIFI环境下Android手机和电脑通信

前面已经写过一篇java实现最基础的socket网络通信,这篇和之前那篇大同小异,只是将客户端代码移植到手机中,然后获取本机IP的方法略有不同。 先讲一下本篇中...

39950
来自专栏Android干货

Android横屏下Fragment界面重叠问题

31950
来自专栏Vamei实验室

安卓第四夜 概念漫游(下)

在安卓第三夜 概念漫游(上)中,我介绍了安卓最基本的功能单元和Intent的连接方式。在这个骨架之上,我们可以进一步增加一些与开发密切相关的重要概念。 Cont...

213100
来自专栏Android干货园

【PageLayout】非常简单的一键切换加载-空数据-错误页,支持自定义

Android中经常使用一个空白页和网络错误页用来提高用户体验,给用户一个较好的感官,如果获取到的数据为空,那么会显示一个空白数据页,如果在获取数据的过程中网络...

12930
来自专栏学海无涯

Android开发之浮动Activity

场景 在使用App时,曾经看到这样一个场景,如下图所示,点击顶部菜单按钮,有一个类似的对话框的列表显示出来,让用户选择其中的一个快递选项,然后选中的快递信息就会...

35070

扫码关注云+社区

领取腾讯云代金券