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功能的逻辑处理等。总的来说,本文还只不过是登堂入室的开始而已,其中如果有不妥或者不正确的地方,欢迎来拍砖。

  • 相关阅读:
    创建你自己的依赖注入容器Ioc Container(转) dodo
    LINQ to XML 介绍(转) dodo
    使用jquery修复ie6/7不支持focus的bug dodo
    ASP.NET MVC 2强类型HTML辅助方法 dodo
    Ioc容器Autofac介绍 dodo
    serverU上传中文文件乱码 dodo
    LINQ语法二 dodo
    DIV+CSS解决IE6,IE7,IE8,FF兼容问题 dodo
    依赖注入容器Autofac与MVC集成 dodo
    mvc VIEW部分介绍 dodo
  • 原文地址:https://www.cnblogs.com/andy-songwei/p/11039252.html
Copyright © 2011-2022 走看看