View事件分发---正文

简介事件分发起源事件分发---正片ViewGroup的事件分发第1部分:第2部分:第3部分View的事件分发总结滑动冲突

简介

本文开始梳理事件分发的细则。所谓事件分发,就是系统收到触摸事件后,封装成MotionEvent,然后将其交给需要消耗的View进行事件处理,MotionEvent一级级的传递给最终消耗事件View的过程就是事件分发过程。

事件分发起源

首先,我们知道当在我们的应用中触摸时,事件肯定会先传递给界面的Activity。然后从Activity开始一级级向下传递给需要消耗事件的控件。那么手指按下触摸到屏幕的时候起,系统将其封装成MotionEvent后,这个MotionEvent又是如何传递到我们的Activity的呢?

我们知道事件的发送均是通过dispatchTouchEvent()进行传递的,那么我们就在我们的主Activity上重写dispatchTouchEvent(),并打印出调用堆栈,看看它是如果传递过来的。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n8" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: &quot;Cascadia Code&quot;, Consolas, &quot;Noto Sans SC&quot;, &quot;Courier New&quot;, monospace; font-weight: normal; -webkit-font-smoothing: initial; font-size: 0.95rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; margin: 1rem 0px !important; padding: 0px; border-radius: 3px; width: auto; line-height: 1.43rem; overflow-wrap: normal; color: rgb(36, 42, 49); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">W/System.err( 3011): java.lang.Exception: welthy dispatchTouchEvent()

W/System.err( 3011):  at wxrecyclerview.wx.cn.wxrecylerview.MainActivity.dispatchTouchEvent(MainActivity.java:58)

W/System.err( 3011):  at android.support.v7.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:69)

W/System.err( 3011):  at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:2330)

W/System.err( 3011):  at android.view.View.dispatchPointerEvent(View.java:8666)

W/System.err( 3011):  at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4147)

W/System.err( 3011):  at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4013)

W/System.err( 3011):  at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3568)

W/System.err( 3011):  at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3621)

W/System.err( 3011):  at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3587)

W/System.err( 3011):  at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:3704)

W/System.err( 3011):  at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3595)

W/System.err( 3011):  at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:3761)

W/System.err( 3011):  at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3568)

W/System.err( 3011):  at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3621)

W/System.err( 3011):  at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3587)

W/System.err( 3011):  at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3595)

W/System.err( 3011):  at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3568)

W/System.err( 3011):  at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:5831)

W/System.err( 3011):  at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:5805)

W/System.err( 3011):  at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:5776)

W/System.err( 3011):  at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:5921)

W/System.err( 3011):  at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:185)

W/System.err( 3011):  at android.os.MessageQueue.nativePollOnce(Native Method)

W/System.err( 3011):  at android.os.MessageQueue.next(MessageQueue.java:143)

W/System.err( 3011):  at android.os.Looper.loop(Looper.java:122)

W/System.err( 3011):  at android.app.ActivityThread.main(ActivityThread.java:5305)

W/System.err( 3011):  at java.lang.reflect.Method.invoke(Native Method)

W/System.err( 3011):  at java.lang.reflect.Method.invoke(Method.java:372)

W/System.err( 3011):  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)

W/System.err( 3011):  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)</pre>

根据上述堆栈情况,我们来说说触摸事件是如何传递到Activity的。

当我们触摸屏幕时,系统会将该触摸事件作为一个节点写入指定的文件中(dev/input/event[x]下),然后Input系统的InputReadThread线程由于一直在Loop循环中,该循环中会通过EventHub去文件中取事件节点。然后InputReader会从EventHub中读取事件,并交给InputDispatcher。然后InputDispatcher就会将事件传递到需要的地方。如上述堆栈中的ViewRootImpl中。这部分流程均由C++完成,了解一个大概流程即可。

我们从ViewRootImplWindowInputEventReceiver.onInputEvent()开始。

//ViewRootImpl.java
@Override
public void onInputEvent(InputEvent event) {
   
}

如上,首先在注释1处,对输入事件进行预处理。我们先看看这个预处理是干啥:

public List<InputEvent> processInputEventForCompatibility(InputEvent e) {
   if (mTargetSdkVersion < Build.VERSION_CODES.M && e instanceof MotionEvent) {
       mProcessedEvents.clear();
       MotionEvent motion = (MotionEvent) e;
       final int mask = MotionEvent.BUTTON_STYLUS_PRIMARY | MotionEvent.BUTTON_STYLUS_SECONDARY;
       final int buttonState = motion.getButtonState();
       final int compatButtonState = (buttonState & mask) >> 4;
       if (compatButtonState != 0) {
           motion.setButtonState(buttonState | compatButtonState);
       }
       mProcessedEvents.add(motion);
       return mProcessedEvents;
     }
     return null;
}

这里可以看到在Android M版本以下,这里会将输入事件InputEvent转换为MotionEvent类型,然后将加入到待处理事件集中。M以上的版本则不需这些操作。

回到onInputEvent()中,若返回得到的processedEvents是空的,则直接调用enqueueInputEvent(),将输入事件入队,在分析事件预处理时我们已经知道在M版本及其以上的版本会直接返回null,所以在M及其以上的版本这里直接将输入事件入队。以下的版本不多说,简单说是一个循环入队的过程。

然后看入队过程:

void enqueueInputEvent(InputEvent event,
 InputEventReceiver receiver, int flags, boolean processImmediately) {
 QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);

 // Always enqueue the input event in order, regardless of its time stamp.
 // We do this because the application or the IME may inject key events
 // in response to touch events and we want to ensure that the injected keys
 // are processed in the order they were received and we cannot trust that
 // the time stamp of injected events are monotonic.
       QueuedInputEvent last = mPendingInputEventTail;
       if (last == null) {
             mPendingInputEventHead = q;
             mPendingInputEventTail = q;
       } else {
             last.mNext = q;
             mPendingInputEventTail = q;
       }
       mPendingInputEventCount += 1;
       Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName, mPendingInputEventCount);

       if (processImmediately) {
             doProcessInputEvents();   //1
       } else {
             scheduleProcessInputEvents();  //2
       }
}

这里是一个链表操作,将事件插入事件队列。插入后,若要立即执行,则会进入注释1,直接处理事件;否则就进入注释2,事件按顺序入队。我们直接看处理事件流程:

void doProcessInputEvents() {
 // Deliver all pending input events in the queue.
       while (mPendingInputEventHead != null) {
             QueuedInputEvent q = mPendingInputEventHead;
             mPendingInputEventHead = q.mNext;
             if (mPendingInputEventHead == null) {
                     mPendingInputEventTail = null;
              }
             q.mNext = null;

             mPendingInputEventCount -= 1;
           Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName, mPendingInputEventCount);

           long eventTime = q.mEvent.getEventTimeNano();
          long oldestEventTime = eventTime;
         if (q.mEvent instanceof MotionEvent) {
                 MotionEvent me = (MotionEvent)q.mEvent;
                 if (me.getHistorySize() > 0) {
                         oldestEventTime = me.getHistoricalEventTimeNano(0);
                  }
           }
           mChoreographer.mFrameInfo.updateInputEventTime(eventTime,oldestEventTime);
           deliverInputEvent(q);   //1
         }

 // We are done processing all input events that we can process right now
 // so we can clear the pending flag immediately.
          if (mProcessInputEventsScheduled) {  //2
                 mProcessInputEventsScheduled = false;
                 mHandler.removeMessages(MSG_PROCESS_INPUT_EVENTS);
            }
}

这里是一个循环操作,会将事件队列中的所有事件均处理。从队列头开始循环遍历队列,拿到一个事件后会通过deliverInputEvent()传递出去。当整个队列循环遍历结束后,会移除对应的handler消息。

private void deliverInputEvent(QueuedInputEvent q) {
         Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",q.mEvent.getSequenceNumber());
         if (mInputEventConsistencyVerifier != null) {
                 mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
         }

         InputStage stage;
         if (q.shouldSendToSynthesizer()) {
                 stage = mSyntheticInputStage;
         } else {
                 stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
         }

         if (q.mEvent instanceof KeyEvent) {
                 mUnhandledKeyManager.preDispatch((KeyEvent) q.mEvent);
         }

         if (stage != null) {
                   handleWindowFocusChanged();
                   stage.deliver(q);  //1
         } else {
                 finishInputEvent(q);
         }
    }

 public final void deliver(QueuedInputEvent q) {
         if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {
                 forward(q);
         } else if (shouldDropInputEvent(q)) {
                 finish(q, false);
         } else {
                 apply(q, onProcess(q));  //2
         }
}

这里会获取输入事件的InputStage,然后调用InputStagedeliver()方法去分发事件,然后一般都会走到注释2处的apply()中,调用onProcess()去处理事件。

简单介绍下InputStage

//ViewRootImpl.java
/**
 * 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 {

源码解释如上。InputStage是处理输入事件责任链中的一个基类,每个处理输入事件的InputStage都要继承它,然后事件就会在这条链子中依次执行。责任链设计模式这里就不多说了。简单看下这一个个责任链中的每一环是在哪创建的:

//ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
         synchronized (this) { 
                 ......
                 mSyntheticInputStage = new SyntheticInputStage();
                 InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
                 InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage, "aq:native-post-ime:" + counterSuffix);
                 InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
                 InputStage imeStage = new ImeInputStage(earlyPostImeStage, "aq:ime:" + counterSuffix);
                 InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
                 InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage "aq:native-pre-ime:" + counterSuffix);

                 mFirstInputStage = nativePreImeStage;
                 mFirstPostImeInputStage = earlyPostImeStage;
                 ......
            }
}

ViewRootImplsetView()方法中有如上代码段,这里就设置了各种InputStage。这里我们以ViewPostImeInputStage()为例,来看看它是如何处理的,上文已说到,调用对应InputStageonProcess()去处理事件,所以我们看看它的onProcess()做了哪些事情:

 @Override
protected int onProcess(QueuedInputEvent q) {
         if (q.mEvent instanceof KeyEvent) {
                 return processKeyEvent(q);  //1
         } else {
                 final int source = q.mEvent.getSource();
                 if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                           return processPointerEvent(q);  //2
                 } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
                           return processTrackballEvent(q);  //3
                 } else {
                           return processGenericMotionEvent(q);  //4
                 }
         }
}

这里会区分不同的事件类型,如果是键盘事件,则进入注释1;如果是点触则进入注释2;如果是Trackball(一下忘记具体叫啥了),则进入注释3;否则就进入注释4。我们手指触摸事件自然是进入注释2:

private int processPointerEvent(QueuedInputEvent q) {
           final MotionEvent event = (MotionEvent)q.mEvent;

           mAttachInfo.mUnbufferedDispatchRequested = false;
           mAttachInfo.mHandlingPointerEvent = true;
           boolean handled = mView.dispatchPointerEvent(event);  //1
           maybeUpdatePointerIcon(event);
           maybeUpdateTooltip(event);
           mAttachInfo.mHandlingPointerEvent = false;
           if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
                     mUnbufferedInputDispatch = true;
                     if (mConsumeBatchedInputScheduled) {
                             scheduleConsumeBatchedInputImmediately();
                     }
           }
 return handled ? FINISH_HANDLED : FORWARD;
}

这里主要是经过注释1去分发事件。那么这里的mView是什么呢?这个mView会在setView()中设置,即我们初始化各个InputStage的那个方法。这里具体是传了什么过来,在之后的Activity章节会详细说明,这里只需知道是DecoreView即可。那么我们就要看看DecoreViewdispatchPointerEvent()干了些啥。但是在DecoreView中我们搜不到这个方法,那就一定在它的父类。一直往上找父类,最后会在View中找到dispatchPointerEvent()方法。

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

这里就会进入注释1,调用dispatchTouchEvent()。但其实我们是从DecoreViewdispatchPointerEvent()过来的,所以这里的dispatchTouchEvent()我们还要去DecoreView中先找找。(多态)

//DecoreView.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
           final Window.Callback cb = mWindow.getCallback();
           return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}

这里我们会看到先获取Windowcallback。我们的Activity就实现了这个Callback,所以这里调用cb.dispatchTouchEvent()就自然进入到了ActivitydispatchTouchEvent()方法中。这样触摸事件就传递到了Activity。我们再看看Activity中如何处理的:

//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
           if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                     onUserInteraction();
           }
           if (getWindow().superDispatchTouchEvent(ev)) {  //1
                     return true;
           }
                     return onTouchEvent(ev); //2
            }

首先在注释1先进行分发,若最后都没有消耗(返回false),则进入注释2,由Activity消耗触摸事件,这就是为啥事件都不消耗最后交由Activity处理的原因。那么我们看看注释1。getWindow()获取的就是我们ActivityPhoneWindow

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

没看错,这里又交给DecoreView了。

//DecoreView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
             return super.dispatchTouchEvent(event);
}

再将dispatchTouchEvent()交给DecoreView的父类。一直跟父类,就到了ViewGroup中。到了这里就是我们熟悉,也是网上一大堆的讲解触摸事件开始的地方了。

最后我们用一张图,一条事件传递链来表示触摸事件传递到ViewGroup的流程:

事件传递至Activity时序图.jpg

流程大致如上。

至此,遗留几个问题:

  • 触摸事件是如何定位到需要消耗事件的Activity的?

  • MotionEvent和Window,VIewRootImpl的关系是什么?

答:

1、对于第1个问题,我认为需要结合Input系统才会更清晰,这里仅结合当前的信息我认为,触摸事件一定是优先传递给当前可见且正处于onResume状态的Activity这个就不追究代码中的细节证据了,如果是优先传递给底部不处于onResume状态的Activity的话,那太不合理了。当前ViewRootImpl获取到输入事件后,就是不断的下发,最终下发到Activity所在的根VIewGroup。所以我们首先要知道ViewRootImpl是什么,贴一段源码中的注释:

/**
 * The top of a view hierarchy, implementing the needed protocol between View
 * and the WindowManager.  This is for the most part an internal implementation
 * detail of {@link WindowManagerGlobal}.
 *
 * {@hide}
 */
@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})
public final class ViewRootImpl implements ViewParent,
 View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {

大概翻译一下就是:ViewRootImpl是一个顶层的View结构,它实现了View和WindowManager间需要的协议。是实现WindowManagerGlobal细节的重要部分。

所以我们可以把ViewRootImpl理解为View和WindowManager通信的桥梁,且一个View就会有对应的ViewRootImpl与WindowManager通信。

对于这个问题,我们可暂理解成,底层记录的触摸事件会直接派发给当前可视View所对应的ViewRootImpl,然后进行接下来的分发。至于如何给当前可视View对应的ViewRootImpl,需要结合之后Input系列文章的内容才更好理解。

2、MotionEvent就是框架层封装的触摸事件抽象,就代表一个触摸事件。ViewRootImpl就是将它经过层层分发,交给对应的Window的。

事件分发---正片

ViewGroup的事件分发

从上文的“起源”一节中,我们知道了触摸事件是如何传递到Activity的根ViewGroup的。那么现在开始,我们看看ViewGroup中又是如何进行事件分发的。这部分内容也是网上谈起事件分发讲的最多,也是开发者最需要关心的部分。

在讲这部分之前,首先我们需要知道ViewGroup的几个关键方法:

  • dispatchTouchEvent():分发触摸事件的方法,通常我们只需知道其内部实现即可。

  • onInterceptTouchEvent():这个通常需要重写,返回true则通常是要拦截触摸事件;否则就不拦截。

  • onTouchEvent():若当前ViewGroup拦截该事件的话,则处理该事件的操作就会在onTouchEvent()中,所以我们通常也需要重写这个方法,来处理当需要拦截时,我们如何处理该触摸事件。

由于这部分代码较多,我们分开来一部分一部分进行分析:

第1部分:

//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
 ......

           boolean handled = false;
           if (onFilterTouchEventForSecurity(ev)) {
                     final int action = ev.getAction();
                     final int actionMasked = action & MotionEvent.ACTION_MASK;   //1

                     // 初始化按下事件
                     if (actionMasked == MotionEvent.ACTION_DOWN) {   //2
                     // 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();
                     }

         // 检测是否拦截
         final boolean intercepted;  //3
         if (actionMasked == MotionEvent.ACTION_DOWN
 || mFirstTouchTarget != null) {  //4
                   final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  //5
                 if (!disallowIntercept) {  //6
                           intercepted = onInterceptTouchEvent(ev);
                           ev.setAction(action); // restore action in case it was changed
                   } else {  //7
                           intercepted = false;
                   }
           } else {  //8
           // There are no touch targets and this action is not an initial down
           // so this view group continues to intercept touches.
                     intercepted = true;
           }

 ......
           return handled;
 }

这第一部分也是其他问题提到最多的部分了,就是ViewGroup是否拦截事件的判断。

注释1,获取当前触摸事件的类型,这里采用的是位与的方式获取该标记类型,大家平常开发时也可以这么使用,位与操作毕竟效率最高。

注释2,若当前是按下的ACTION_DOWN触摸事件,则进行一些初始化场景的操作,如取消之前的触摸目标,重置触摸状态。

注释3,定义是否拦截变量,为true就代表当前ViewGroup需要拦截该触摸事件;否则就不拦截。

注释4,若当前是按下ACTION_DOWN事件,或者该ViewGroup的子View有要消耗该事件的View时(mFirstTouchTarget != null),则进入该条件判断。

注释5,判断当前的mGroupFlags是否含有FLAG_DISALLOW_INTERCEPT标记,若有则不拦截事件,即disallowIntercept会被赋值为true;否则为falsemGroupFlags是否含有该标记,可以动态的通过requestDisallowInterceptTouchEvent(boolean)方法设置,传入true则设置该标记,否则就去除该标记。这个标记位会在ACTION_DOWN中被重置,即注释2的重置操作。

注释6,若disallowIntereptfalse,则进入该条件内,我们可以看到,此时intercept为何值,需要通过onInterceptTouchEvent(ev)方法的返回值去设置。这也是上文提及的关键方法之一,也是我们最常需要重写的方法之一。

注释7,若注释6条件不满足,即mGroupFlags含有FLAG_DISALLOW_INTERCEPT标记时,直接设置interceptfalse,即ViewGroup不拦截该事件。所以我们在子View中若想父ViewGroup不拦截触摸事件时,也可以通过requestDisallowInterceptTouchEvent()方法去设置标记为让事件向下传递。

注释8,若当前不是ACTION_DOWN事件,且没有子View需要消耗事件时,就会设置intercepttrue,即当前ViewGroup拦截该事件。

从这里,我们就可以得出几个结论:

  • 若ViewGroup拦截了ACTION_DOWN事件,则后续事件都会交给当前ViewGroup,不会再向下分发。因为如果没有子View或子ViewGroup拦截事件,则mFirstTouchTarget就会一直等于null,那么当ACTION_MOVE来了之后,注释4的条件就不满足,直接接入注释8的条件,拦截事件。

  • 一条事件序列只能被一个View拦截且消耗。因为当一个View拦截了某次事件,同事件序列的剩余事件都会交给它。

第2部分:

//ViewGroup.java
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
 || actionMasked == MotionEvent.ACTION_CANCEL;   //1

// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {  //2

 // If the event is targeting accessibility focus we give it to the
 // view that has accessibility focus and if it does not handle it
 // we clear the flag and dispatch the event to all children as usual.
 // We are looking up the accessibility focused host to avoid keeping
 // state since these events are very rare.
 View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
 ? findChildWithAccessibilityFocus() : null;

 if (actionMasked == MotionEvent.ACTION_DOWN
 || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
 || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
 final int actionIndex = ev.getActionIndex(); // always 0 for down
 final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
 : TouchTarget.ALL_POINTER_IDS;

 // Clean up earlier touch targets for this pointer id in case they
 // have become out of sync.
 removePointersFromTouchTargets(idBitsToAssign);

 final int childrenCount = mChildrenCount;
 if (newTouchTarget == null && childrenCount != 0) {  //3
 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 = buildTouchDispatchChildList();  //4
 final boolean customOrder = preorderedList == null
 && isChildrenDrawingOrderEnabled();
 final View[] children = mChildren;
 for (int i = childrenCount - 1; i >= 0; i--) {  //5
 final int childIndex = getAndVerifyPreorderedIndex(
 childrenCount, i, customOrder);
 final View child = getAndVerifyPreorderedView(
 preorderedList, children, childIndex);

 ......
 if (!child.canReceivePointerEvents()
 || !isTransformedTouchPointInView(x, y, child, null)) {  //6
 ev.setTargetAccessibilityFocus(false);
 continue;
 }
 ......

 resetCancelNextUpFlag(child);
 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//7
 // 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;
 }

 ......
 }
 if (preorderedList != null) preorderedList.clear();
 }
 ......
 }
}
  • 注释1,获取当前是否取消触摸事件的标记。

  • 注释2,若即没有取消事件,也不拦截事件,则进入注释2.

  • 注释3,若没有新的触摸touchTarget,且有子View时,则进入注释3。

  • 注释4,获取子View序列。我们知道View的排列是个树形结构,这里从变量名我们可知,当前是通过前序遍历得到的树。

  • 注释5,开始遍历子View。

  • 注释6,判断子View是否能接收触摸事件。判断标准有2条:当前是否播放动画;触摸坐标是否在View区域内。
    注释6,拿到子View的index,并根据index拿到子View后,通过dispatchTransformedTouchEvent()将触摸事件传递给对应的子View。若子View想接收这个触摸事件的话,则进入注释6。并最后将该子View赋为newTouchTarget。结束循环。

最后还要清空数据集等操作。那么接下来先看看dispatchTransformedTouchEvent()是如何处理子View与对应的触摸事件的:

//ViewGroup.java
/**
 * 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) {
 final boolean handled;

 // Canceling motions is a special case.  We don't need to perform any transformations
 // or filtering.  The important part is the action, not the contents.
 final int oldAction = event.getAction();
 if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {  //1
 event.setAction(MotionEvent.ACTION_CANCEL);
 if (child == null) {
 handled = super.dispatchTouchEvent(event);
 } else {
 handled = child.dispatchTouchEvent(event);
 }
 event.setAction(oldAction);
 return handled;
 }

 ......

 // Perform any necessary transformations and dispatch.
 if (child == null) {  //2
 handled = super.dispatchTouchEvent(transformedEvent);
 } else {  //3
 final float offsetX = mScrollX - child.mLeft;
 final float offsetY = mScrollY - child.mTop;
 transformedEvent.offsetLocation(offsetX, offsetY);
 if (! child.hasIdentityMatrix()) {
 transformedEvent.transform(child.getInverseMatrix());
 }

 handled = child.dispatchTouchEvent(transformedEvent);
 }

 // Done.
 transformedEvent.recycle();
 return handled;
}

这里可以看到,无论当前是否是对ACTION_CANCEL事件进行处理,还是其他事件进行处理都可以分为:

若child是null,则触摸事件交给父类处理;否则由child自己通过dispatchTouchEvent()处理触摸事件。

如果子元素的dispatchTouchEvent()返回了true,即子View要消耗事件了,则当前方法会返回true。最后通过:

newTouchTarget = addTouchTarget(child, idBitsToAssign);

去设置新的touchTarget。我们看下addTouchTarget()里面是如何操作的:

//ViewGroup.java
/**
 * Adds a touch target for specified child to the beginning of the list.
 * Assumes the target child is not already present.
 */
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
 final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
 target.next = mFirstTouchTarget;
 mFirstTouchTarget = target;
 return target;
}

这里我们可以看到,为mFirstTouchTarget赋值了,把当前的触摸target赋值给它。此时mFirstTouchTarget就不为null了。

第3部分

//ViewGroup.java
// 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);  //1
} 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;
 }
}

这部分理解的不是很多,也不是很重要。我们只要知道,若第2部分未找到需要消耗触摸事件的子View的话,那么mFirstTouchTarget就还是null,那就进入了注释1,且此时传入的第3个参数是null,在第2部分我们知道这个时候,若为null的话,则会调用super.dispatchTouchEvent()方法,ViewGroup的父类就是View,所以这里就走到了View的dispatchTouchEvent()中。

其实若不为null,进入child.dispatchTouchEvent()的话也是进入到View.dispatchTouchEvent()中,只是对于View所代表的含义和身份都不一样。

View的事件分发

承接上节第3部分,此时事件分发进入到了View的dispatchTouchEvent()中,我们继续看这里的实现:

//View.java
/**
     * 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();  //1
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Defensive cleanup for new gesture
        stopNestedScroll();   //2
    }

    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {  //3
            result = true;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {  //4
            result = true;
        }

        if (!result && onTouchEvent(event)) {  //5
            result = true;
        }
    }

    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }

    // Clean up after nested scrolls if this is the end of a gesture;
    // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
    // of the gesture.
    if (actionMasked == MotionEvent.ACTION_UP ||
        actionMasked == MotionEvent.ACTION_CANCEL ||
        (actionMasked == MotionEvent.ACTION_DOWN && !result)) {   //6
        stopNestedScroll();
    }

    return result;
}

View的这部分事件分发代码相对ViewGroup少的多,也相对简单。

首先也是定义了一个标记位,代表是否处理该事件。

  • 注释1,获取当前事件类型。

  • 注释2,若当前是ACTION_DOWN按下事件,则停止嵌套滑动操作。

  • 注释3,如果当前正在滑动ScrollBar之类的,则设置result为true,即消耗事件。

  • 注释4,这里会判断是否设置了onTouchListener,若设置的话还要看其onTouch()方法是否返回了true,若也返回了true,则直接设置result为true。

  • 注释5,若result不为true,且当前View的onTouchEvent()返回了true,则设置result为true,代表消耗当前事件。

  • 注释6,若当前触摸事件是UP||CANCEL||DOWN时,且当前result还为false,则停止嵌套滑动操作。

最后返回result结果。

这里我们也看下onTouchEvnt()中的默认实现:

//View.java
public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();

    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;   //1
    }
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {   //2
            return true;
        }
    }

    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {  //3
        ......
                case MotionEvent.ACTION_UP:
                    ......
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

                        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)) {
                                    performClickInternal();  //4
                                }
                            }
                        }
                        ......
                    }
                    break
             ......
        return true;
    }

    return false;
}

这里可以看到,若当前View是可点击,或者可以长按的则clickable为true,若此View被禁止的话,则进入注释1,返回clickable。由此可得,若该View被禁止,但其可以被点击或长按的话,则仍会消耗事件。

若设置了touchDelegate的话,且其onTouchEvent()也返回true的话,则也会返回true,消耗此事件。这个touchDelegate也是可以动态设置的,基本不使用,就不再深究了。

最后,若可点击或者含有TOOLTIP标记,则会对各个触摸事件类型进行处理。这里就不深究了。

这里要提及下对ACTION_UP的处理,因为这里最后会执行到performClickInternal()中,这个既是我们的点击事件。最终调用的就是我们最常使用的setOnClickListener设置的点击事件监听器:

//View.java
public boolean performClick() {
    .......
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);   //1
        result = true;
    } else {
        result = false;
    }
    ......
    return result;
}

这里的注释1,就是调用到我们的onClick()方法。

这里我们也能得出几个小结论:

  • 若通过setOnTouchListener设置了监听的话,且在重写onTouch()方法并返回true的话,则不会执行到onTouchEvent()方法去。也代表消耗当前事件。

  • onTouchEvent()返回true,则代表消耗当前事件。

  • 若当前View是可点击或者可长按的,则即使它被禁止了,仍会消耗触摸事件。

总结

至此,ViewGroup和View的触摸事件分发和处理就讲完了,最后总结一下:

用一个刚哥在艺术探索中提到的一个伪代码来代表这里事件分发的核心思想:

public boolean dispatchTouchEvent(MotionEvent ev){
    boolean consume = false;
    if(onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev);
    }else{
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

即首先ViewGroup判断是否消耗触摸事件,若不消耗的话,则循环遍历子View,找到需要消耗的子View,若找到的话,则事件交给它即完事儿。若没找到,则ViewGroup自己判断是否要消耗,若自己也不消耗就交给Activity去消耗,Activity一定会消耗触摸事件。在子View需要消耗事件的时候,还会判断才有什么方式(onTouch()还是onTouchEvent()等)消耗触摸事件。

滑动冲突

最后简单说一下滑动冲突处理的一般规则。

对于滑动冲突基本都发生在滑动嵌套的情况下,那么滑动嵌套基本就分为三类:

  • 内外滑动方向不一致;

  • 内外滑动方向一致;

  • 内外有多层滑动嵌套。

对于第①种:我们可以根据水平滑动距离和垂直距离差判断滑动方向,根据滑动方向将事件交给对应的滑动控件。

对于第②种:基本都是根据业务要求,如内部滑到临界处后将事件交给外部控件等。

对于第③种:需要结合①和②的方式判断。

以上是针对滑动冲突的场景处理的基本原则。由于触摸事件是从外至内的传输,所以触摸事件一定会先传到父控件,故针对滑动冲突的处理方式就有2种:

  • 外部拦截

  • 内部拦截

外部拦截就是在父控件就判断是否要下发触摸事件。

内部拦截就是父控件不拦截事件,将事件下发给子控件,子控件判断是否要处理该事件,若不处理则事件根据系统规则会返回到父控件。

注意点:根据之前事件分发的详解,当一个事件交给某个View处理的话,同一事件序列的其他事件就都会交给它处理。所以ACTION_DOWN事件,父控件均不能拦截,否则其他事件下发不到子View。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容