zoukankan      html  css  js  c++  java
  • 【朝花夕拾】Android自定义View篇之(六)Android事件分发机制(中)从源码分析事件分发机制

    前言

           转载请注明,转自【https://www.cnblogs.com/andy-songwei/p/11039252.html】谢谢!

           在上一篇文章【【朝花夕拾】Android自定义View篇之(五)Android事件分发机制(上)Touch三个重要方法的处理逻辑】【下文简称(五),请先阅读完(五)再阅读本文】,我们通过示例和log来分析了Android的事件分发机制。这些,我们只是看到了现象,如果要进一步了解事件分发机制,这是不够的,我们还需要透过现象看本质,去研究研究源码。本文将从源码(基于Android API-26)出发,去分析我们上一篇文章中看到的现象,以及其它一些和事件相关的常见问题,如:事件是如何传到View中的、requestDisallowInterceptTouchEvent为什么失效、view设置了focusable=“false”,为什么还能触发点击事件、Touch事件和Click事件谁先谁后等!

          本文的主要内容如下:

    一、事件的前世今生

           前文中研究事件传递是从Activity的dispatchTouchEvent开始的,但是事件的起源肯定不是Activity。因为触摸事件是触摸的硬件,所以很明显事件一定是从底层传过来的。但是,事件是如何传递到View的呢?这一节我们简单了解一下事件传递到Activity的经过。

           首先,我们在ViewInner类的dispatchTouchEvent方法中打印调用栈,看看这个方法的调用流程。

    1 //=============ViewInner.java============
    2 @Override
    3 public boolean dispatchTouchEvent(MotionEvent event) {
    4      Log.i("songzheweiwang",Log.getStackTraceString(new Throwable()));
    5      return super.dispatchTouchEvent(event);
    6 }

    点击ViewInner区域,得到如下log:

     1 java.lang.Throwable
     2     at com.example.demos.customviewdemo.ViewInner.dispatchTouchEvent(ViewInner.java:24)
     3     at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3043)
     4     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2727)
     5     at com.example.demos.customviewdemo.ViewGroupMiddle.dispatchTouchEvent(ViewGroupMiddle.java:23)
     6     at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3043)
     7     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2727)
     8     at com.example.demos.customviewdemo.ViewGroupOuter.dispatchTouchEvent(ViewGroupOuter.java:23)
     9     at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3043)
    10     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2727)
    11     at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3043)
    12     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2727)
    13     at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3043)
    14     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2727)
    15     at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3043)
    16     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2727)
    17     at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3043)
    18     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2727)
    19     at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3043)
    20     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2727)
    21     at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:526)
    22     at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1830)
    23     at android.app.Activity.dispatchTouchEvent(Activity.java:3410)
    24     at com.example.demos.customviewdemo.EventDemoActivity.dispatchTouchEvent(EventDemoActivity.java:37)
    25     at android.support.v7.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:68)
    26     at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:475)
    27     at android.view.View.dispatchPointerEvent(View.java:12768)
    28     at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:5455)
    29     at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:5258)
    30     at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4766)
    31     at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4819)
    32     at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4785)
    33     at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4934)
    34     at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4793)
    35     at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4991)
    36     at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4766)
    37     at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4819)
    38     at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4785)
    39     at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4793)
    40     at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4766)
    41     at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7490)
    42     at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:7448)
    43     at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:7409)
    44     at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:7593)
    45     at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:199)
    46     at android.os.MessageQueue.nativePollOnce(Native Method)
    47     at android.os.MessageQueue.next(MessageQueue.java:326)
    48     at android.os.Looper.loop(Looper.java:165)
    49     at android.app.ActivityThread.main(ActivityThread.java:7477)
    50     at java.lang.reflect.Method.invoke(Native Method)
    51     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:500)
    52     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:865)

           实际上,Android输入事件的源头是位于/dev/input/下的设备节点,而输入事件的终点是由WMS管理的某个窗口,最终由窗口中的View处理。最初的输入事件为内核生成的原始事件,而最终交付给窗口的则是KeyEvent(键盘)或MotionEvent(鼠标和触摸屏)对象。上述log中(从下往上看),用三种颜色标注了3个阶段:红色部分表示底层活动,这里我们看到了不少熟悉的身影,ZygoteInit,ActivityThread,Native Method等;绿色部分第45行的InputEventReceiver.dispatchInputEvent()方法,事件通过该方法由Native层进入到Java层,并传递到Activity中,这里我们也看到了不少熟悉的身影,ViewRootImpl,DecorView等;从第24行开始,就是我们前面熟悉的,从Acitivty开始调用dispatchTouchEvent一层层来分发事件。可能读者看到从第20~9行会有疑惑,为什么中间还有这么多过程?如果了解Android的View层次结构的话就会知道,在DecorView和开发者定义的布局(如ViewGroupOuter)之间,隔着很多层ViewGroup,也需要一层层传递,这一点我在本系列第一篇【【朝花夕拾】Android自定义View篇之(一)View绘制流程】【后面简称(一)】的第二节做过讲解,不明白的可以先去看看。

           本文的重点是从Activity开始的事件分发,至于前面从底层到Activity的事件流程,咱们这里做一定的了解即可,有兴趣的可以自行研究。

       

    二、事件从Activity到View体系

           在上一篇文章的代码示例中,我们的Boss——EventDemoActivity类中有如下代码

     1 //=============Boss:EventDemoActivity.java============
     2  @Override
     3  public boolean dispatchTouchEvent(MotionEvent ev) {
     4      Log.i("songzheweiwang", "[EventDemoActivity-->dispatchTouchEvent]ev=" + EventUtil.parseAction(ev.getAction()));
     5      return super.dispatchTouchEvent(ev);
     6  }
     7 
     8  @Override
     9  public boolean onTouchEvent(MotionEvent event) {
    10      Log.i("songzheweiwang", "[EventDemoActivity-->onTouchEvent]event=" + EventUtil.parseAction(event.getAction()));
    11      return super.onTouchEvent(event);
    12  }

          Activity是事件的起点,dispatchTouchEvent又是整个逻辑中最早分发事件的地方。但是Activity并不是View体系中的一员,那它是怎样把事件分发到View体系中的呢?追踪super.dispatchTouchEvent方法,进入到Activity.java的方法中:

     1 //==========================Activity.java=======================
     2 /**
     3  * Called to process touch screen events.  You can override this to
     4  * intercept all touch screen events before they are dispatched to the
     5  * window.  Be sure to call this implementation for touch screen events
     6  * that should be handled normally.
     7  *
     8  * @param ev The touch screen event.
     9  *
    10  * @return boolean Return true if this event was consumed.
    11  */
    12 public boolean dispatchTouchEvent(MotionEvent ev) {
    13     if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    14         onUserInteraction();
    15     }
    16     if (getWindow().superDispatchTouchEvent(ev)) {
    17         return true;
    18     }
    19     return onTouchEvent(ev);
    20 }

           如注释中所说,在事件分发到window之前,可以重写这个方法来拦截所有的屏幕事件,如果事件被消费了,这个方法会返回true。这个方法咱们只关注第15~18行。第15行中如果getWindow().superDispatchTouchEvent值为true,表示后续的View体系把事件消费了,那么执行第16行,直接返回true了,后面该Activity的onTouchEvent方法就不再执行了。如果其值为false,则表示事件没有被消费,那该Activity就调用自己的onTouchEvent来消费该事件。这一点,在上一篇文章中多份log都体现了这个结论。当然我们还是需要继续看看getWindow().superDispatchTouchEvent是如何实现的。

           其主要代码如下:

     1 //=================Activity.java=================
     2 ......
     3 private Window mWindow;
     4 ......
     5 public Window getWindow() {
     6    return mWindow;
     7 }
     8 ......
     9 
    10 //================Window.java================
    11 /**
    12  * ......
    13  * <p>The only existing implementation of this abstract class is
    14  * android.view.PhoneWindow, which you should instantiate when needing a
    15  * Window.
    16  */
    17 public abstract class Window {
    18   ......
    19     public abstract boolean superDispatchTouchEvent(MotionEvent event);
    20   ......
    21 }

    superDispatchTouchEvent在Window.java中是个抽象方法,需要在实现类中来实现。如注释所说,PhoneWindow是该抽象类唯一存在的实现类,所以我们追踪到PhoneWindow中。

     1 //=============PhoneWindow.java==========
     2 ......
     3 // This is the top-level view of the window, containing the window decor.
     4 private DecorView mDecor;
     5 ......
     6 @Override
     7 public boolean superDispatchTouchEvent(MotionEvent event) {
     8     return mDecor.superDispatchTouchEvent(event);
     9 }
    10 
    11 //===========DecorView.java==========
    12 ......
    13 public boolean superDispatchTouchEvent(MotionEvent event) {
    14     return super.dispatchTouchEvent(event);
    15 }
    16 ....
    17 
    18 //========ViewGroup.java=======
    19 @Override
    20 public boolean dispatchTouchEvent(MotionEvent ev) {
    21 ......
    22 }

    在本系列第一篇文章【(一)】中,对DecorView做过讲解,如果有看过这篇文章,那么此时看到如上代码,就不会陌生了。DecorView是整个View体系的根view,它是一个ViewGroup,所以事件流程就进入到ViewGroup中的dispatchTouchEvent中了。通过上述流程,Activity就通过dispatchTouchEvent将事件分发到View体系中来了。

           如果View体系没有消费掉事件,就会调用Activity自己的onTouchEvent方法,看看其源码。

     1 /**
     2  * Called when a touch screen event was not handled by any of the views
     3  * under it.  This is most useful to process touch events that happen
     4  * outside of your window bounds, where there is no view to receive it.
     5  *
     6  * @param event The touch screen event being processed.
     7  *
     8  * @return Return true if you have consumed the event, false if you haven't.
     9  * The default implementation always returns false.
    10  */
    11 public boolean onTouchEvent(MotionEvent event) {
    12     if (mWindow.shouldCloseOnTouch(this, event)) {
    13         finish();
    14         return true;
    15     }
    16     return false;
    17 }

           这里注释说得非常明确了:当它下面的所有view都没有处理掉屏幕触摸事件时,Activity中的这个方法会调用。当触摸事件发生在window边界以外的地方时,这个方法尤为有用,因为这些地方,没有view来接收这个事件。【(五)】的第三点中有个模型图,当触摸白色的Boss区域时,下面三个区域中的view接收不到触摸事件,所以最后只有Activity中的onTouchEvent响应了,【(五)】的第六节中log可以明确这一点。如果您消费了这个事件,返回true;消费不了,则返回false,默认的实现,总是返回false。这一点好理解,Boss嘛,只安排底下的员工干活,自己绝不会动手,就算他们干不了,把事件返回到Boss这里,自己也不会处理的。这里可以改动EventDemoActivity.java中重写的onTouchEvent方法来验证一下:

    1 @Override
    2 public boolean onTouchEvent(MotionEvent event) {
    3     boolean b = super.onTouchEvent(event);
    4     Log.i("songzheweiwang", "[EventDemoActivity-->onTouchEvent]event=" + EventUtil.parseAction(event.getAction())+";b="+b);
    5     return b;
    6 }

    最终发现,打印的log中,b值总是false。

           到这里,Activity中的两个重要方法就讲完了,后面再来看看事件从Activity分发到View体系后,事件流程是如何在其中一层层分发和处理的。

    三、叶子View的事件处理逻辑

           控件分为两种:一种是容器类控件,即继承自ViewGroup,如LinearLayout等;一种是叶子View,View的派生类,如TextView,Button等。与之对应,处理事件分发也分为两种情形:一种是ViewGroup处理事件分发;另一种是View处理事件逻辑。ViewGroup会递归遍历子View,如果是叶子View也会调用到View中的dispatchTouchEvent方法。所以,这里咱们先从叶子View处理事件开始分析。

      1、叶子View处理事件常见的若干场景

        (1)onTouch、onTouchEvent和onClick方法执行顺序问题

           我们工作中经常需要为控件设置监听Touch事件和Click事件。这里对【(五)】中的实例代码做一点改动,为ViewInner控件添加触摸事件和点击事件,如下所示:

     1 //===========EventDemoActivity========== 
     2 ViewInner mViewInner = findViewById(R.id.viewInner);
     3 mViewInner.setOnClickListener(new View.OnClickListener() {
     4     @Override
     5     public void onClick(View v) {
     6         Log.i("songzheweiwang-2", "[EventDemoActivity-->onclick]");
     7     }
     8 });
     9 mViewInner.setOnTouchListener(new View.OnTouchListener() {
    10     @Override
    11     public boolean onTouch(View v, MotionEvent event) {
    12         Log.i("songzheweiwang-2", "[EventDemoActivity-->onTouch]event="+EventUtil.parseAction(event.getAction()));
    13         return false;
    14     }
    15 });

    点击ViewInner控件后得到如下log:

    1 06-14 01:19:59.246 25518-25518/com.example.demos I/songzheweiwang-2: [EventDemoActivity-->onTouch]event=ACTION_DOWN
    2 06-14 01:19:59.246 25518-25518/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_DOWN
    3 06-14 01:19:59.277 25518-25518/com.example.demos I/songzheweiwang-2: [EventDemoActivity-->onTouch]event=ACTION_UP
    4 06-14 01:19:59.277 25518-25518/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_UP
    5 06-14 01:19:59.278 25518-25518/com.example.demos I/songzheweiwang-2: [EventDemoActivity-->onclick]

    我们发现,从执行顺序上看,onTouch > onTouchEvent > onClick,并且onTouch方法执行了两次,onClick在ACTION_UP之后才被执行。

        (2)onTouch返回true的场景

           另外,我们发现,oTouch是一个boolean型方法,默认返回的是false。如果将其返回值该为true,结果又怎样呢?

    1 06-14 01:59:00.487 25760-25760/com.example.demos I/songzheweiwang-2: [EventDemoActivity-->onTouch]event=ACTION_DOWN
    2 06-14 01:59:00.492 25760-25760/com.example.demos I/songzheweiwang-2: [EventDemoActivity-->onTouch]event=ACTION_UP

    打印log发现,onTouchEvent和onClick方法都没有执行。

        (3)控件设置clickable=“false”的场景

           在【(五)】中我们讲过,如果控件的clickable属性为false,那么它是无法消费触摸事件的。这里加上Touch和Click事件后,情形如何呢?

    1 06-14 02:15:50.279 26161-26161/com.example.demos I/songzheweiwang-2: [EventDemoActivity-->onTouch]event=ACTION_DOWN
    2 06-14 02:15:50.279 26161-26161/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_DOWN
    3 06-14 02:15:50.328 26161-26161/com.example.demos I/songzheweiwang-2: [EventDemoActivity-->onTouch]event=ACTION_UP
    4 06-14 02:15:50.328 26161-26161/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_UP
    5 06-14 02:15:50.330 26161-26161/com.example.demos I/songzheweiwang-2: [EventDemoActivity-->onclick]

    这里发现和clickable为true时打印的log一样,也就是说,此时clickable=“false”失效了,这是什么原因呢?

        (4)控件设置setEnable(false)时的场景

           默认情况下,控件都是enable的,如果给控件执行setEnable(false),结果会怎样呢?

    1 06-14 02:19:56.369 26359-26359/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_DOWN
    2 06-14 02:19:56.401 26359-26359/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_UP

    发现onTouch和onClick都没有执行。为什么?

           上面4种场景是工作中经常会碰到的与事件相关的场景,下面我们从源码的角度来分析其原因。

    2、View.dispatchTouchEvent方法

           View类中用于处理事件分发逻辑的两个主要方法是dispatchTouchEvent和onTouchEvent,前者是事件分发到该控件时最先执行的方法,先分析这个方法。

     1 //=========================View.java==================
     2 /**
     3  * Pass the touch screen motion event down to the target view, or this
     4  * view if it is the target.
     5  * ......
     6  * @return True if the event was handled by the view, false otherwise.
     7  */
     8 public boolean dispatchTouchEvent(MotionEvent event) {
     9     ......
    10     boolean result = false;
    11     ......
    12     if (onFilterTouchEventForSecurity(event)) {
    13         ......
    14         //noinspection SimplifiableIfStatement
    15         ListenerInfo li = mListenerInfo;
    16         if (li != null && li.mOnTouchListener != null
    17                 && (mViewFlags & ENABLED_MASK) == ENABLED
    18                 && li.mOnTouchListener.onTouch(this, event)) {
    19             result = true;
    20         }
    21         if (!result && onTouchEvent(event)) {
    22             result = true;
    23         }
    24     }
    25     ......
    26     return result;
    27 }

    先看注释:将屏幕动作事件传递给目标view,如果自己就是这个目标则将事件传给自己。如果这个事件被该目标view处理了,会返回true,否则会返回false。当ViewInner被触摸时,会执行到这里。

           第11行有个if语句判断,它的作用是为了安全判断当前TouchEvent是否被过滤掉了,比如该控件不在最顶部,被其它控件遮住了等情形。如果被过滤了,该方法就会返回false,事件没有被消费。我们前面的几种场景下,该函数返回的都是true。第16~18行中有多个判断条件,我们来看看这些条件:

     1 //============View.java===========
     2 static class ListenerInfo {
     3     ......
     4     private OnTouchListener mOnTouchListener;
     5     ......
     6 }
     7 
     8 /**
     9  * Register a callback to be invoked when a touch event is sent to this view.
    10  * @param l the touch listener to attach to this view
    11  */
    12 public void setOnTouchListener(OnTouchListener l) {
    13     getListenerInfo().mOnTouchListener = l;
    14 }

           前面在EventDemoActivity类中ViewInner调用了该方法,并设置了OnTouchListener,所以li.mOnTouchListener的值这里不是null。(mViewFlags & ENABLED_MASK) == ENABLED表示该view是enable的,默认情况下控件都是enable;li.mOnTouchListener.onTouch(this, event),就是我们EventDemoActivity中ViewInner的onTouch方法的返回值,默认是返回falsle。所以会跳出该if判断,进入到第21行。由于前面的if条件为false,所以这里result的值还是false,后面那个条件就开始执行onTouchEvent方法了。如果事件被处理了,onTouchEvent会返回true,返回的result值也就为true了,这个事件就完美处理掉了;如果没有处理掉,onTouchEvent就返回false,那返回值result自然就是false了,事件没有被处理掉。所以此时,onTouchEvent方法都是执行了的。如果在onTouch方法中返回了true,那么第16行的判断条件就是true了,此时result被赋值为true,那么第21行的onTouchEvent方法就没有机会执行了,这就是咱们前面说的第(2)种场景“onTouch返回true的场景”了。这种情形下,dispatchTouchEvent返回true,事件也被处理掉了。这里给一个结论:当控件是enable的时候,如果onTouch事件返回true,onTouchEvent和onClick方法均不会执行,且事件仍会被消费。下面,我们再看看onTouchEvent方法的源码。

      3、View.onTouchEvent方法

           方法很长,这里选取比较关键的代码进行分析。

     1 /**
     2  * Implement this method to handle touch screen motion events.
     3  * ......
     4  * @return True if the event was handled, false otherwise.
     5  */
     6 public boolean onTouchEvent(MotionEvent event) {
     7     ......
     8     final int action = event.getAction();
     9     final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
    10             || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
    11             || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    12     if ((viewFlags & ENABLED_MASK) == DISABLED) {
    13         if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
    14             setPressed(false);
    15         }
    16         mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    17         // A disabled view that is clickable still consumes the touch
    18         // events, it just doesn't respond to them.
    19         return clickable;
    20     }
    21     ......
    22     if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    23         switch (action) {
    24             case MotionEvent.ACTION_UP:
    25                 ......
    26                 // Use a Runnable and post this rather than calling
    27                 // performClick directly. This lets other visual state
    28                 // of the view update before click actions start.
    29                 if (mPerformClick == null) {
    30                     mPerformClick = new PerformClick();
    31                 }
    32                 if (!post(mPerformClick)) {
    33                     performClick();
    34                 }
    35                 ......
    36                 break;
    37             case MotionEvent.ACTION_DOWN:
    38                 ......
    39                 break;
    40             case MotionEvent.ACTION_CANCEL:
    41                 ......
    42                 break;
    43             case MotionEvent.ACTION_MOVE:
    44                 ......
    45                 break;
    46         }
    47         return true;
    48     }
    49     return false;
    50 }

     看看注释:实现该方法来处理屏幕触摸事件,如果处理了就返回true,否则返回false。这里需要对比一下dispatchTouchEvent的注释,它的关键字是“pass”,而onTouchEvent的关键字是“handle”,这里可以区别两者的作用。

           第9行中的clickable值,前说过,有的控件默认是true,有的则是false。控件的flag为可以一般点击和长按,都表示是可点击的。第12行中,如果控件为disable的,即调用了setEnable(false)时,这里就return了,该方法后面的流程就结束了。如果该控件是可点击的,即clickable的值为true,那onTouchEvent返回的就是true,事件仍然被消费了;反之,控件不可点击,那返回的就是false,事件没有被消费,传递到上一级的onTouchEvent方法中,这一点我们在【(五)】中有讲过。第17、18行的注释也明确说明了:一个可以点击的disable的控件,仍然可以消费触摸事件,只不过它不能回应这些控件。

           第22行的第二个条件,tooltip的说明如下:

    1 //=========================View.java====================
    2 1 /**
    3 2  * <p>Indicates this view can display a tooltip on hover or long press.</p>
    4 3  * {@hide}
    5 4  */
    6 5 static final int TOOLTIP = 0x40000000;

    这是一种比较特殊的用途,这里不继续深究,我们平时的使用以及这里demo中没有使用该功能。所以咱们这里只考虑第一个条件。

           能够走到第22行,说明该控件是enable的。如果该控件是不可点击的,那么这个if语句就跳过去了,返回false,此时前面的dispatchTouchEvent方法返回的也一定是false了,说明该控件没有消费掉事件。相反,如果是可点击的,可以看到,后面一定是会返回true的,那么事件就能被消费了。到这里,结合第19行和dispatchTouchEvent方法,我们可以得到一个结论:如果控件是clickable且没有被过滤的,那么这个控件一定是可以消费掉事件的。在ACTION_UP的时候,会走到第29行。这里我们需要关注一下PerformClick类,其源码如下:

    1 private final class PerformClick implements Runnable {
    2     @Override
    3     public void run() {
    4         performClick();
    5     }
    6 }

    而第32行的post方法,本质上就是一个Handler发的post,所以第32、33行是一定要执行performClick()方法的。我们着重看一看这个方法:

     1 //====================View.java================
     2 /**
     3  * Call this view's OnClickListener, if it is defined.  Performs all normal
     4  * actions associated with clicking: reporting accessibility event, playing
     5  * a sound, etc.
     6  *
     7  * @return True there was an assigned OnClickListener that was called, false
     8  *         otherwise is returned.
     9  */
    10 public boolean performClick() {
    11     final boolean result;
    12     final ListenerInfo li = mListenerInfo;
    13     if (li != null && li.mOnClickListener != null) {
    14         playSoundEffect(SoundEffectConstants.CLICK);
    15         li.mOnClickListener.onClick(this);
    16         result = true;
    17     } else {
    18         result = false;
    19     }
    20     ......  
    21     return result;
    22 }

    这个方法就是用来执行点击相关的工作的,第13行播放点击音效,第14行响应点击事件等。

     1 //===============View.java=============
     2 static class ListenerInfo {
     3     ......
     4     public OnClickListener mOnClickListener;
     5     ......
     6 }
     7 ......
     8 /**
     9  * Register a callback to be invoked when this view is clicked. If this view is not
    10  * clickable, it becomes clickable.
    11  *
    12  * @param l The callback that will run
    13  *
    14  * @see #setClickable(boolean)
    15  */
    16 public void setOnClickListener(@Nullable OnClickListener l) {
    17     if (!isClickable()) {
    18         setClickable(true);
    19     }
    20     getListenerInfo().mOnClickListener = l;
    21 }

    我们在EventDemoActivity中ViewInner通过该方法设置了OnClickListener,所以EventDemoActivity中的onClick方法在这里就被调用了,此时点击事件就产生了。我们可以看到,点击事件是发生在onTouchEvent方法的ACTION_UP事件中的。这里结合前面讲的onTouch方法,就可以解释场景(1)“onTouch、onTouchEvent和onClick方法执行顺序问题”了:onTouch > onTouchEvent > onClick,并且要在ACTION_UP后才会有onClick。在setOnClickListener方法中第17、18行,我们还可以看到,如果这个控件本身不是clickable的,也会先让它变成clickable的。所以,只要控件调用了setOnClickListener方法,那么之前设置的clickable=“false”就会失效。这里就解释了场景(3)“控件设置clickable=“false”的场景”。此时,我们返回去看看onTouchEvent方法的第19行,就会发现,即使控件是enable的,由于我们设置了点击事件监听,所以这里是返回true的,事件仍然被消费了,再结合前面dispatchTouchEvent的第17行,就能够解释场景(4)“控件设置setEnable(false)时的场景”下,没有执行onTouch和onClick方法,但onTouchEvent方法仍然消费了事件。

            这里对关键的源码进行了详细的讲解,且对前面提到的几种常见的现象,从源码角度进行了解释,希望通过这些来加深对View事件分发的认识和理解。关于View中事件的分发,要讲的就是这些了。

    四、ViewGroup事件分发处理解析

           ViewGroup中处理事件逻辑的方法有两个,分别是dispatchTouchEvent和onInterceptTouchEvent。当一个ViewGroup控件收到上一级传递来的事件时,也是先经过dispatchTouchEvent处理。咱们先看看该方法的源码,该方法很长,这里仅截取了其中关键流程的代码:

     1 //=========================ViewGroup.java========================
     2 @Override
     3 public boolean dispatchTouchEvent(MotionEvent ev) {
     4     ......
     5     boolean handled = false;
     6     //该判断条件在View的dispatchTouchEvent部分讲过,这个方法用于判断控件是否被遮挡。
     7     if (onFilterTouchEventForSecurity(ev)) {
     8         final int action = ev.getAction();
     9         final int actionMasked = action & MotionEvent.ACTION_MASK;
    10         // Handle an initial down.
    11         if (actionMasked == MotionEvent.ACTION_DOWN) {
    12             // Throw away all previous state when starting a new touch gesture.
    13             // The framework may have dropped the up or cancel event for the previous gesture
    14             // due to an app switch, ANR, or some other state change.
    15             /**当开始一个新的触摸时,这两句会清除掉以前的状态,
    16              * 清除FLAG_DISALLOW_INTERCEPT设置,
    17              * 代码中还会执行mFirstTouchTarget = null
    18              */
    19             cancelAndClearTouchTargets(ev);
    20             resetTouchState();
    21         }
    22         // Check for interception.
    23         final boolean intercepted;
    24         /**事件由子View去处理时mFirstTouchTarget会赋值并指向子View。
    25          * mFirstTouchTarget != null表示事件由子View来处理;
    26          * mFirstTouchTarget = null表示事件由自己处理
    27          */
    28         if (actionMasked == MotionEvent.ACTION_DOWN
    29                 || mFirstTouchTarget != null) {
    30             /**FLAG_DISALLOW_INTERCEPT是子View通过requestDisallowInterceptTouchEvent(true)来设置的
    31              * disallowIntercept等同于表示是否调用了该方法。
    32              */
    33             final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    34             if (!disallowIntercept) {
    35                 //子View通过onInterceptTouchEvent来判断是否需要拦截。
    36                 intercepted = onInterceptTouchEvent(ev);
    37                 ev.setAction(action); // restore action in case it was changed
    38             } else {
    39                 intercepted = false;
    40             }
    41         } else {
    42             // There are no touch targets and this action is not an initial down
    43             // so this view group continues to intercept touches.
    44             //没有找到处理事件的子View,并且这个action不是ACTION_DOWN,所以该View开始拦截触摸事件。
    45             intercepted = true;
    46         }
    47         ......
    48         //如果事件没有取消且没有被拦截
    49         if (!canceled && !intercepted) {
    50             ......
    51             //寻找能够接收该事件的View
    52             for (int i = childrenCount - 1; i >= 0; i--) {
    53                 ......
    54                 final View child = getAndVerifyPreorderedView(
    55                         preorderedList, children, childIndex);
    56                 ......
    57                 /**这两个判断条件分别为1、View可见并且没有播放动画;
    58                  * 2、点击事件的坐标在View的范围内。
    59                  * 如果其中一个条件不满足,就进行下一次循环。
    60                  */
    61                 if (!canViewReceivePointerEvents(child)
    62                         || !isTransformedTouchPointInView(x, y, child, null)) {
    63                     ev.setTargetAccessibilityFocus(false);
    64                     continue;
    65                 }
    66                 ......
    67                 //true表示找到了子View来处理事件,false表示没有子View来处理。
    68                 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
    69                     // Child wants to receive touch within its bounds.
    70                     ......
    71                     //addTouchTarget方法中,会给mFirstTouchTarget赋值。
    72                     newTouchTarget = addTouchTarget(child, idBitsToAssign);
    73                     ......
    74                     break;
    75                 }
    76                ......
    77             }
    78         }
    79         //没有找到可以处理事件的子View,需要自身来处理。
    80         if (mFirstTouchTarget == null) {
    81             // No touch targets so treat this as an ordinary view.
    82             handled = dispatchTransformedTouchEvent(ev, canceled, null,
    83                     TouchTarget.ALL_POINTER_IDS);
    84         } else {
    85             ......
    86             //这里有一部分源码暂时没有读懂
    87         }
    88         ......
    89     }
    90     ......
    91     return handled;
    92 }

    代码中梳理出了关键的流程,并对几处要点做了一些注释说明,咱们这里再解读一下这段代码:

        (1)第11行中,当一个新的事件开始时,即ACTION_DOWN触发时,会进行初始化,清理掉之前的一些状态,这一点很容易理解。

        (2)第24~27行对字段mFirstTouchTarget进行了简单的说明,这里不再赘述了,后面几个关键点需要用到这个字段来进行判断。

        (3)第28行,如果是ACTION_DOWN事件或者其它事件下找到了处理事件的子View时,会进入到该方法中。

        (4)第30~33行,主要是FLAG_DISALLOW_INTERCEPT相关。这里需要简单介绍一下requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法,该方法用于子View请求父View,不让父View拦截,即让父View的onInterceptTouchEvent方法返回true时,失效。在没有调用该方法时,默认其参数为false,即requestDisallowInterceptTouchEvent(false)效果等同于没有调用该方法。FLAG_DISALLOW_INTERCEPT的标志位就是通过该方法来进行设置的。该方法对除ACTION_DOWN之外的事件有效,对ACTION_DOWN事件无效,在第12~20行中讲过,ACTION_DOWN时,会清除之前状态,FLAG_DISALLOW_INTERCEPT标志位也被恢复。这里简单演示一下该方法的使用:

           首先在ViewGroupMiddle类中修改onInterceptTouchEvent为以下代码,ViewInner中先不做修改,且为了做对比,要让ViewInner添加clickable=“true”,能够消费事件:

     1 //===========ViewGroupMiddle.java===========
     2 @Override
     3 public boolean onInterceptTouchEvent(MotionEvent ev) {
     4     Log.i("songzheweiwang-2", "[ViewGroupMiddle-->onInterceptTouchEvent]ev=" + EventUtil.parseAction(ev.getAction()));
     5     switch (ev.getAction()) {
     6         case MotionEvent.ACTION_DOWN:
     7             return false;
     8         default:
     9             return true;
    10     }
    11 }

    查看log为:

    06-12 00:21:27.800 6906-6906/com.example.demos I/songzheweiwang-2: [ViewGroupMiddle-->dispatchTouchEvent]ev=ACTION_DOWN
    06-12 00:21:27.800 6906-6906/com.example.demos I/songzheweiwang-2: [ViewGroupMiddle-->onInterceptTouchEvent]ev=ACTION_DOWN
    06-12 00:21:27.800 6906-6906/com.example.demos I/songzheweiwang-2: [ViewInner-->dispatchTouchEvent]event=ACTION_DOWN
    06-12 00:21:27.800 6906-6906/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_DOWN
    06-12 00:21:27.832 6906-6906/com.example.demos I/songzheweiwang-2: [ViewGroupMiddle-->dispatchTouchEvent]ev=ACTION_UP
    06-12 00:21:27.832 6906-6906/com.example.demos I/songzheweiwang-2: [ViewGroupMiddle-->onInterceptTouchEvent]ev=ACTION_UP
    06-12 00:21:27.832 6906-6906/com.example.demos I/songzheweiwang-2: [ViewInner-->dispatchTouchEvent]event=ACTION_CANCEL
    06-12 00:21:27.832 6906-6906/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_CANCEL

    ACTION_DOWN没有被拦截,正常传递到了ViewInner中,而其他事件却被拦截了。这里还可以看到,一个完整的事件,如果只有ACTION_DOWN,却没有ACTION_UP,最终会触发ACTION_CANCEL。

           现在在ViewInner类的onTouchEvent方法中调用getParent().requestDisallowInterceptTouchEvent(true)方法,如下所示:

    1 //=============ViewInner.java=============
    2 @Override
    3 public boolean onTouchEvent(MotionEvent event) {
    4     Log.i("songzheweiwang-2","[ViewInner-->onTouchEvent]event="+EventUtil.parseAction(event.getAction()));
    5     getParent().requestDisallowInterceptTouchEvent(true);
    6    return super.onTouchEvent(event);
    7 }

    之所以要在ViewGroup类中不拦截ACTION_DOWN事件,是因为如果这里拦截了,ViewInner中的方法就不会执行了,第5行的代码也自然起不到效果。得到的log如下:

    1 06-12 00:37:24.505 7509-7509/com.example.demos I/songzheweiwang-2: [ViewGroupMiddle-->dispatchTouchEvent]ev=ACTION_DOWN
    2 06-12 00:37:24.505 7509-7509/com.example.demos I/songzheweiwang-2: [ViewGroupMiddle-->onInterceptTouchEvent]ev=ACTION_DOWN
    3 06-12 00:37:24.505 7509-7509/com.example.demos I/songzheweiwang-2: [ViewInner-->dispatchTouchEvent]event=ACTION_DOWN
    4 06-12 00:37:24.505 7509-7509/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_DOWN
    5 06-12 00:37:24.524 7509-7509/com.example.demos I/songzheweiwang-2: [ViewGroupMiddle-->dispatchTouchEvent]ev=ACTION_UP
    6 06-12 00:37:24.524 7509-7509/com.example.demos I/songzheweiwang-2: [ViewInner-->dispatchTouchEvent]event=ACTION_UP
    7 06-12 00:37:24.524 7509-7509/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_UP

    可以看到,ACTION_UP事件在ViewInner中正常执行了。所以在第33行的disallowIntercept变量值,在调用该方法且当前事件不是ACTION_DOWN时(因为会清除掉该方法的效果),其值为true,反之则为false。在这里,可以看到,由于调用了该方法,所以在ACTION_UP事件中,ViewGroupMiddle中的onInterceptTouchEvent方法没有执行了。

            该方法在日常解决事件冲突时经常会用到,所以这里花了一点篇幅来讲解了一下该方法。

        (5)第36行中,调用了onInterceptTouchEvent方法。这个方法我们在【(五)】中提到无数遍了,这里总算看到了它的身影。自然,这里一定要去看看其源码。

     1 //=================ViewGroup.java=============
     2 public boolean onInterceptTouchEvent(MotionEvent ev) {
     3     if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
     4             && ev.getAction() == MotionEvent.ACTION_DOWN
     5             && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
     6             && isOnScrollbarThumb(ev.getX(), ev.getY())) {
     7         return true;
     8     }
     9     return false;
    10 }

    What?这么简单?if语句看起来是和鼠标有关系的,貌似和咱们的示例及平时使用没有半毛钱的关系,所以除去这个判断语句,其实就只有第8行的代码了。这里,在【(五)】中讲过,对于重写的onInterceptTouchEvent方法,返回false和返回默认的super.onInterceptTouchEvent,结果是一样的。

        (6)第28~46行就确定了是否需要对事件进行拦截了。这里也可以明确看到,onInterceptTouchEvent方法,不一定每次都会被调用了。这里我们在【(五)】中也多次看到这种情形。

        (7)第49行中,我们关注后面这个条件,在没有被拦截的情况下才会进入这个if语句内部。它的主要作用就是递归遍历其子view,找到可以接收该事件的view。这里重点需要关注的是第68行的dispatchTransformedTouchEvent方法。这个方法我们在第一节的log中就多次看到过,在ViewGroup中它和dispatchTouchEvent方法总是成对出现的。它的源码如下:

     1 //===============ViewGroup.java============== 
     2 private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
     3             View child, int desiredPointerIdBits) {
     4      final boolean handled;
     5      ......
     6      //transformedEvent是对event的加工处理
     7      if (child == null) {
     8             handled = super.dispatchTouchEvent(transformedEvent);
     9         } else {
    10            ......
    11             handled = child.dispatchTouchEvent(transformedEvent);
    12      }
    13      return handle;
    14 }

    这里传入的child参数是有值的,不是null,所以会调用第10行,进行递归。当遍历到叶子View的时候,就跳转到View类中的diapatchTouchEvent方法中了,我们前面已经讲解过了。所以,如果找到了处理事件的view了,handle就是true,该方法返回true,就会跳出for循环,不再需要便利了,否则继续便利。这里面有个addTouchTarget方法,其中会对mFirstTouchTarge进行赋值,说明找到了处理事件的子View,如果没找到,这个值会一直为null。

        (8)第79~83行,既然前面没有找到可以处理该事件的子view,那么就只能自己来接手该事件了。第82行,第三个参数传入的是nul,所以该方法体中就会执行第7行。ViewGroup的父类是View,所以会走到View类中,走View处理事件的逻辑了,这里也需要注意,不要混淆父类和父控件了。而如果子View能够处理该事件,那后面当前ViewGroup控件就和这个事件没啥关系了。现在,我们也就能够解释【(五)】中的很多现象了,如:ViewInner无法消费掉事件时,ViewGroupMiddle会回调自身的onTouchEvent方法;如果ViewInner消费了当前的事件,那么本次事件就不会再返回到上级控件了。

        (9)最后该方法的返回值仍然是,如果返回true,那么表示事件被消费了,否则返回false。

           ViewGroup类中重点需要关注的方法就讲完了,其实主要就是dispatchTouchEvent方法的逻辑。现在我们应该可以理解,在【(五)】在各种现象和结论了吧。

    五、参考文章

           【Android View 事件分发机制

           【一文读懂Android View事件分发机制

           【Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

          【Android事件分发机制完全解析,带你从源码的角度彻底理解(下)

    结语

           Android的View事件传递机制的源码分析,到这里就讲完了,不知道本文中提到的一些场景,是不是也曾经困扰过读者呢?当然,笔者这里只是抓取了主要的关键流程和方法进行了讲解,真实的源码逻辑远远比这要复杂,因为平时会碰到各种不同的情况,比如长按,tip功能的逻辑处理等。总的来说,本文还只不过是登堂入室的开始而已,其中如果有不妥或者不正确的地方,欢迎来拍砖。

  • 相关阅读:
    ASP.NET Web API 控制器执行过程(一)
    ASP.NET Web API 控制器创建过程(二)
    ASP.NET Web API 控制器创建过程(一)
    ASP.NET Web API WebHost宿主环境中管道、路由
    ASP.NET Web API Selfhost宿主环境中管道、路由
    ASP.NET Web API 管道模型
    ASP.NET Web API 路由对象介绍
    ASP.NET Web API 开篇示例介绍
    ASP.NET MVC 视图(五)
    ASP.NET MVC 视图(四)
  • 原文地址:https://www.cnblogs.com/andy-songwei/p/11039252.html
Copyright © 2011-2022 走看看