View事件分发

NOTE: 笔记,碎片式内容

控件

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

ViewTree

控件基类为View,而ViewGroup是其子类。ViewGroup能够包含其它View做为其child。任何一个ViewGroup及其全部直接或间接的child造成一个ViewTree这样的树结构java

RootView

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

以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中的结构关系是对应的。app

对于组成ViewTree的全部ViewGroup和View来讲,View不须要知道其所在ViewGroup,但ViewGroup知道其全部childView。框架

路径

这里为ViewTree引入路径这一律念,它表示从RootView出发找到任一child时要通过的全部View的列表。
由于一个ViewGroup只能访问其直接child,而一个child只有惟一的parent,因此从RootView到达任一child的路径是惟一的,反之从任一child到达RootView的路线也是惟一的。async

显然对任何child的路径老是存在的,虽然能够依靠额外的数据结构来保存各个View的关系,但树结构自己已经在作这样的事情了。ide

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”。

界面效果:

view-tree-ui

图1:示例界面

对应的ViewTree结构:

view-tree-structure

图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中。
    明确各个方法的职责。

资料

  • MotionEvent和手势识别
    http://www.cnblogs.com/everhad/p/6075716.html

(本文使用Atom编写)

相关文章
相关标签/搜索