美文网首页Android知识进阶
Android事件分发机制详解

Android事件分发机制详解

作者: 彩虹_直至黑白 | 来源:发表于2022-02-28 16:05 被阅读0次

    前言

    Android事件分发机制是Android开发者必须了解的基础。

    目录

    一. 基础认知

    1.1 事件分发的由来

    安卓的View是树形结构的,View可能会重叠在一起,当我们点击的地方有多个View都可以响应的时候,这个点击事件应该给谁呢?为了解决这一个问题,就有了事件分发机制。

    1.2 事件分发的 "事件" 是指什么?

    点击事件 (Touch事件) 。具体介绍如下:


    特别说明:事件列,即指从手指接触屏幕至手指离开屏幕这个过程产生的一系列事件。一般情况下,事件列都是以DOWN事件开始、UP事件结束,中间有无数的MOVE事件。

    1.3 事件分发的本质

    将点击事件(MotionEvent)传递到某个具体的View & 处理的整个过程。’

    即 事件传递的过程 = 分发过程。

    1.4 事件在哪些对象之间进行传递?

    答:Activity、ViewGroup、View。


    1.5 事件分发的顺序

    即 事件传递的顺序:Activity -> ViewGroup -> View

    即:1个点击事件发生后,事件先传到Activity、再传到ViewGroup、最终再传到View

    1.6 事件分发过程由哪些方法协作完成?

    答:dispatchTouchEvent()onInterceptTouchEvent()onTouchEvent()

    二. 事件分发机制流程详细分析

    主要包括:Activity事件分发机制、ViewGroup事件分发机制、View事件分发机制

    2.1 Activity事件分发机制

    事件分发机制,首先会将点击事件传递到Activity中,具体是执行dispatchTouchEvent()进行事件分发。

    源码分析

    /**
      * 源码分析:Activity.dispatchTouchEvent()
      */ 
      public boolean dispatchTouchEvent(MotionEvent ev) {
    
        // 仅贴出核心代码
    
        // ->>分析1
        if (getWindow().superDispatchTouchEvent(ev)) {
    
            return true;
            // 若getWindow().superDispatchTouchEvent(ev)的返回true
            // 则Activity.dispatchTouchEvent()就返回true,则方法结束。即 :该点击事件停止往下传递 & 事件传递过程结束
            // 否则:继续往下调用Activity.onTouchEvent
    
        }
        // ->>分析3
        return onTouchEvent(ev);
      }
    
    /**
      * 分析1:getWindow().superDispatchTouchEvent(ev)
      * 说明:
      *     a. getWindow() = 获取Window类的对象
      *     b. Window类是抽象类,其唯一实现类 = PhoneWindow类
      *     c. Window类的superDispatchTouchEvent() = 1个抽象方法,由子类PhoneWindow类实现
      */
      @Override
      public boolean superDispatchTouchEvent(MotionEvent event) {
    
          return mDecor.superDispatchTouchEvent(event);
          // mDecor = 顶层View(DecorView)的实例对象
          // ->> 分析2
      }
    
    /**
      * 分析2:mDecor.superDispatchTouchEvent(event)
      * 定义:属于顶层View(DecorView)
      * 说明:
      *     a. DecorView类是PhoneWindow类的一个内部类
      *     b. DecorView继承自FrameLayout,是所有界面的父类
      *     c. FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup
      */
      public boolean superDispatchTouchEvent(MotionEvent event) {
    
          return super.dispatchTouchEvent(event);
          // 调用父类的方法 = ViewGroup的dispatchTouchEvent()
          // 即将事件传递到ViewGroup去处理,详细请看后续章节分析的ViewGroup的事件分发机制
    
      }
      // 回到最初的分析2入口处
    
    /**
      * 分析3:Activity.onTouchEvent()
      * 调用场景:当一个点击事件未被Activity下任何一个View接收/处理时,就会调用该方法
      */
      public boolean onTouchEvent(MotionEvent event) {
    
            // ->> 分析5
            if (mWindow.shouldCloseOnTouch(this, event)) {
                finish();
                return true;
            }
            
            return false;
            // 即 只有在点击事件在Window边界外才会返回true,一般情况都返回false,分析完毕
        }
    
    /**
      * 分析4:mWindow.shouldCloseOnTouch(this, event)
      * 作用:主要是对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界内等
      */
      public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    
      if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
              && isOutOfBounds(context, event) && peekDecorView() != null) {
    
            // 返回true:说明事件在边界外,即 消费事件
            return true;
        }
    
        // 返回false:在边界内,即未消费(默认)
        return false;
      } 
    

    源码总结

    当一个点击事件发生时,从Activity的事件分发开始(Activity.dispatchTouchEvent()),流程总结如下:

    核心方法总结

    主要包括:dispatchTouchEvent()、onTouchEvent() 总结如下:


    那么ViewGroupdispatchTouchEvent() 什么时候返回true / false? 请继续往下看ViewGroup事件的分发机制。

    2.2 ViewGroup事件分发机制

    从上面Activity的事件分发机制可知,在Activity.dispatchTouchEvent()实现了将事件从Activity->ViewGroup的传递,ViewGroup的事件分发机制从dispatchTouchEvent()开始。

    源码分析

    /**
      * 源码分析:ViewGroup.dispatchTouchEvent()
      */ 
      public boolean dispatchTouchEvent(MotionEvent ev) { 
    
      // 仅贴出关键代码
      ... 
    
      if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
      // 分析1:ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
        // 判断值1-disallowIntercept:是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
        // 判断值2-!onInterceptTouchEvent(ev) :对onInterceptTouchEvent()返回值取反
            // a. 若在onInterceptTouchEvent()中返回false,即不拦截事件,从而进入到条件判断的内部
            // b. 若在onInterceptTouchEvent()中返回true,即拦截事件,从而跳出了该条件判断
            // c. 关于onInterceptTouchEvent() ->>分析1
    
      // 分析2
        // 1. 通过for循环,遍历当前ViewGroup下的所有子View
        for (int i = count - 1; i >= 0; i--) {  
            final View child = children[i];  
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                    || child.getAnimation() != null) {  
                child.getHitRect(frame);  
    
                // 2. 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
                if (frame.contains(scrolledXInt, scrolledYInt)) {  
                    final float xc = scrolledXFloat - child.mLeft;  
                    final float yc = scrolledYFloat - child.mTop;  
                    ev.setLocation(xc, yc);  
                    child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
    
                    // 3. 条件判断的内部调用了该View的dispatchTouchEvent()
                    // 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面章节介绍的View事件分发机制)
                    if (child.dispatchTouchEvent(ev))  { 
    
                    // 调用子View的dispatchTouchEvent后是有返回值的
                    // 若该控件可点击,那么点击时dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立
                    // 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
                    // 即该子View把ViewGroup的点击事件消费掉了
    
                    mMotionTarget = child;  
                    return true; 
                          }  
                      }  
                  }  
              }  
          }  
        }  
    
      ...
    
      return super.dispatchTouchEvent(ev);
      // 若无任何View接收事件(如点击空白处)/ViewGroup本身拦截了事件(复写了onInterceptTouchEvent()返回true)
      // 会调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()
      // 因此会执行ViewGroup的onTouch() -> onTouchEvent() -> performClick() -> onClick(),即自己处理该事件,事件不会往下传递
      // 具体请参考View事件分发机制中的View.dispatchTouchEvent()
    
      ... 
    
    }
    
    /**
      * 分析1:ViewGroup.onInterceptTouchEvent()
      * 作用:是否拦截事件
      * 说明:
      *     a. 返回false:不拦截(默认)
      *     b. 返回true:拦截,即事件停止往下传递(需手动复写onInterceptTouchEvent()其返回true)
      */
      public boolean onInterceptTouchEvent(MotionEvent ev) {  
        
        // 默认不拦截
        return false;
    
      } 
      // 回到调用原处
    

    源码总结

    Android事件分发传递到Acitivity后,总是先传递到ViewGroup、再传递到View。流程总结如下:(假设已经经过了Acitivity事件分发传递并传递到ViewGroup)

    核心方法总结

    主要包括:dispatchTouchEvent()onTouchEvent()onInterceptTouchEvent()总结如下:

    实例分析

    1. 布局说明
    2. 测试代码

    布局文件:activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/my_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:focusableInTouchMode="true"
        android:orientation="vertical">
    
        <Button
            android:id="@+id/button1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="按钮1" />
    
        <Button
            android:id="@+id/button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="按钮2" />
    
    </LinearLayout>
    

    核心代码:MainActivity.java

    public class MainActivity extends AppCompatActivity {
    
      Button button1,button2;
      ViewGroup myLayout;
    
      @Override
      public void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_main);
    
          button1 = (Button)findViewById(R.id.button1);
          button2 = (Button)findViewById(R.id.button2);
          myLayout = (LinearLayout)findViewById(R.id.my_layout);
    
          // 1.为ViewGroup布局设置监听事件
          myLayout.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View v) {
                  Log.d("TAG", "点击了ViewGroup");
              }
          });
    
          // 2. 为按钮1设置监听事件
          button1.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View v) {
                  Log.d("TAG", "点击了button1");
              }
          });
    
          // 3. 为按钮2设置监听事件
          button2.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View v) {
                  Log.d("TAG", "点击了button2");
              }
          });
       }
    }
    
    3. 测试结果
    // 点击按钮1,输出如下
    点击了button1
    
    // 点击按钮2,输出如下
    点击了button2
    
    // 点击空白处,输出如下
    点击了ViewGroup
    
    4. 结果分析
    • 点击Button时,因为ViewGroup默认不拦截,所以事件会传递到子View Button,于是执行Button.onClick()
    • 此时ViewGroup. dispatchTouchEvent()会直接返回true,所以ViewGroup自身不会处理该事件,于是ViewGroupLayoutdispatchTouchEvent()不会执行,所以注册的onTouch()不会执行,即onTouchEvent()-> performClick()-> onClick()整个链路都不会执行,所以最后不会执行ViewGroup设置的onClick()里。
    • 点击空白区域时,ViewGroup. dispatchTouchEvent()里遍历所有子View希望找到被点击子View时找不到,所以ViewGroup自身会处理该事件,于是执行onTouchEvent() -> performClick() -> onClick(),最终执行ViewGroupLayout的设置的onClick()

    2.3 View事件分发机制

    从上面ViewGroup事件分发机制知道,View事件分发机制从dispatchTouchEvent()开始

    源码分析

    /**
      * 源码分析:View.dispatchTouchEvent()
      */
      public boolean dispatchTouchEvent(MotionEvent event) {  
    
           
            if ( (mViewFlags & ENABLED_MASK) == ENABLED && 
                  mOnTouchListener != null &&  
                  mOnTouchListener.onTouch(this, event)) {  
                return true;  
            } 
    
            return onTouchEvent(event);  
      }
      // 说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent()
      //   1. (mViewFlags & ENABLED_MASK) == ENABLED
      //   2. mOnTouchListener != null
      //   3. mOnTouchListener.onTouch(this, event)
      // 下面对这3个条件逐个分析
    
    /**
      * 条件1:(mViewFlags & ENABLED_MASK) == ENABLED
      * 说明:
      *    1. 该条件是判断当前点击的控件是否enable
      *    2. 由于很多View默认enable,故该条件恒定为true(除非手动设置为false)
      */
    
    /**
      * 条件2:mOnTouchListener != null
      * 说明:
      *   1. mOnTouchListener变量在View.setOnTouchListener()里赋值
      *   2. 即只要给控件注册了Touch事件,mOnTouchListener就一定被赋值(即不为空)
      */
      public void setOnTouchListener(OnTouchListener l) { 
    
        mOnTouchListener = l;  
    
    } 
    
    /**
      * 条件3:mOnTouchListener.onTouch(this, event)
      * 说明:
      *   1. 即回调控件注册Touch事件时的onTouch();
      *   2. 需手动复写设置,具体如下(以按钮Button为例)
      */
      button.setOnTouchListener(new OnTouchListener() {  
          @Override  
          public boolean onTouch(View v, MotionEvent event) {  
       
            return false;  
            // 若在onTouch()返回true,就会让上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束
            // 若在onTouch()返回false,就会使得上述三个条件不全部成立,从而使得View.dispatchTouchEvent()中跳出If,执行onTouchEvent(event)
            // onTouchEvent()源码分析 -> 分析1
          }  
      });
    
    /**
      * 分析1:onTouchEvent()
      */
      public boolean onTouchEvent(MotionEvent event) {  
    
        ... // 仅展示关键代码
    
        // 若该控件可点击,则进入switch判断中
        if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
    
            // 根据当前事件类型进行判断处理
            switch (event.getAction()) { 
    
                // a. 事件类型=抬起View(主要分析)
                case MotionEvent.ACTION_UP:  
                        performClick(); 
                        // ->>分析2
                        break;  
    
                // b. 事件类型=按下View
                case MotionEvent.ACTION_DOWN:  
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
                    break;  
    
                // c. 事件类型=结束事件
                case MotionEvent.ACTION_CANCEL:  
                    refreshDrawableState();  
                    removeTapCallback();  
                    break;
    
                // d. 事件类型=滑动View
                case MotionEvent.ACTION_MOVE:  
                    final int x = (int) event.getX();  
                    final int y = (int) event.getY();  
    
                    int slop = mTouchSlop;  
                    if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                            (y < 0 - slop) || (y >= getHeight() + slop)) {  
                        removeTapCallback();  
                        if ((mPrivateFlags & PRESSED) != 0) {  
                            removeLongPressCallback();  
                            mPrivateFlags &= ~PRESSED;  
                            refreshDrawableState();  
                        }  
                    }  
                    break;  
            }  
    
            // 若该控件可点击,就一定返回true
            return true;  
        }  
      // 若该控件不可点击,就一定返回false
      return false;  
    }
    
    /**
      * 分析2:performClick()
      */  
      public boolean performClick() {  
    
          if (mOnClickListener != null) {
              // 只要通过setOnClickListener()为控件View注册1个点击事件
              // 那么就会给mOnClickListener变量赋值(即不为空)
              // 则会往下回调onClick() & performClick()返回true
              playSoundEffect(SoundEffectConstants.CLICK);  
              mOnClickListener.onClick(this);  
              return true;  
          }  
          return false;  
      }  
    

    源码总结


    这里需要特别注意的是,onTouch()的执行 先于onClick()

    核心方法总结

    主要包括: dispatchTouchEvent()onTouchEvent()

    实例分析

    在本示例中,将分析2种情况:

    1. 注册Touch事件监听 且 在onTouch()返回false
    2. 注册Touch事件监听 且 在onTouch()返回true
    分析1:注册Touch事件监听 且 在onTouch()返回false
    代码示例
    // 1. 注册Touch事件监听setOnTouchListener 且 在onTouch()返回false
    button.setOnTouchListener(new View.OnTouchListener() {
    
          @Override
          public boolean onTouch(View v, MotionEvent event) {
              System.out.println("执行了onTouch(), 动作是:" + event.getAction());
              return false;
          }
    });
    
    // 2. 注册点击事件OnClickListener()
    button.setOnClickListener(new View.OnClickListener() {
    
          @Override
          public void onClick(View v) {
              System.out.println("执行了onClick()");
          }
    });
    
    
    
    测试结果
    执行了onTouch(), 动作是:0
    执行了onTouch(), 动作是:1
    执行了onClick()
    
    测试结果说明
    • 点击按钮会产生两个类型的事件-按下View与抬起View,所以会回调两次onTouch()
    • 因为onTouch()返回了false,所以事件无被消费,会继续往下传递,即调用View.onTouchEvent()
    • 调用View.onTouchEvent()时,对于抬起View事件,在调用performClick()时,因为设置了点击事件,所以会回调onClick()
    分析2:注册Touch事件监听 且 在onTouch()返回true
    代码示例
    // 1. 注册Touch事件监听setOnTouchListener 且 在onTouch()返回false
    button.setOnTouchListener(new View.OnTouchListener() {
    
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                System.out.println("执行了onTouch(), 动作是:" + event.getAction());
                return true;
            }
        });
    
    // 2. 注册点击事件OnClickListener()
    button.setOnClickListener(new View.OnClickListener() {
    
            @Override
            public void onClick(View v) {
                System.out.println("执行了onClick()");
            }
            
      });
    
    测试结果
    执行了onTouch(), 动作是:0
    执行了onTouch(), 动作是:1
    
    测试结果说明
    • 点击按钮会产生两个类型的事件-按下View与抬起View,所以会回调两次onTouch()
    • 因为onTouch()返回true,所以事件被消费,不会继续往下传递,View.dispatchTouchEvent()直接返回true
    • 所以最终不会调用View.onTouchEvent(),也不会调用onClick()

    三. 事件分发机制流程总结


    这个三个方法均有一个 boolean(布尔) 类型的返回值,通过返回 true 和 false 来控制事件传递的流程。
    PS: 从上表可以看到 ActivityView都是没有事件拦截的,这是因为:
    • Activity 作为原始的事件分发者,如果 Activity 拦截了事件会导致整个屏幕都无法响应事件,这肯定不是我们想要的效果。
    • View最为事件传递的最末端,要么消费掉事件,要么不处理进行回传,根本没必要进行事件拦截。

    View相关

    Question: 为什么 View 会有 dispatchTouchEvent ?

    A:View 可以注册很多事件监听器,例如:单击事件(onClick)、长按事件(onLongClick)、触摸事件(onTouch),并且View自身也有onTouchEvent方法,那么问题来了,这么多与事件相关的方法应该由谁管理?毋庸置疑就是 dispatchTouchEvent,所以 View 也会有事件分发。

    Question: 与 View 事件相关的各个方法调用顺序是怎样的?

    A:如果不去看源码,想一下让自己设计会怎样?

    • 单击事件(onClickListener) 需要两个事件(ACTION_DOWNACTION_UP)才能触发,如果先分配给onClick判断,等它判断完,用户手指已经离开屏幕,黄花菜都凉了,定然造成 View 无法响应其他事件,应该最后调用。(最后)
    • 长按事件(onLongClickListener) 同理,也是需要长时间等待才能出结果,肯定不能排到前面,但因为不需要ACTION_UP,应该排在onClick前面。(onLongClickListener> onClickListener)
    • 触摸事件(onTouchListener) , 如果用户注册了触摸事件,说明用户要自己处理触摸事件,这个应该排在最前面。(最前)、
    • View自身处理(onTouchEvent) 提供了一种默认的处理方式,如果用户已经处理好了,也就不需要了,所以应该排在onTouchListener后面。(onTouchListener > onTouchEvent)

    所以事件的调度顺序应该是 onTouchListener> onTouchEvent > onLongClickListener> onClickListener

    ViewGroup相关

    ViewGroup(通常是各种Layout) 的事件分发相对来说就要麻烦一些,因为 ViewGroup 不仅要考虑自身,还要考虑各种ChildView,一旦处理不好就容易引起各种事件冲突,正所谓养儿方知父母难啊。
    VIewGroup的事件分发流程又是如何的呢?

    我们了解到事件是通过ViewGroup一层一层传递的,最终传递给ViewViewGroup要比它的 ChildView 先拿到事件,并且有权决定是否告诉要告诉ChildView。在默认的情况下 ViewGroup事件分发流程是这样的。

    1. 判断自身是否需要(询问 onInterceptTouchEvent 是否拦截),如果需要,调用自己的 onTouchEvent
    2. 自身不需要或者不确定,则询问ChildView,一般来说是调用手指触摸位置的 ChildView
    3. 如果子 ChildView不需要则调用自身的onTouchEvent

    用伪代码应该是这样子的:

    // 点击事件产生后
    // 步骤1:调用dispatchTouchEvent()
    public boolean dispatchTouchEvent(MotionEvent ev) {
    
        boolean consume = false; //代表 是否会消费事件
    
        // 步骤2:判断是否拦截事件
        if (onInterceptTouchEvent(ev)) {
          // a. 若拦截,则将该事件交给当前View进行处理
          // 即调用onTouchEvent()去处理点击事件
          consume = onTouchEvent (ev) ;
    
        } else {
    
          // b. 若不拦截,则将该事件传递到下层
          // 即 下层元素的dispatchTouchEvent()就会被调用,重复上述过程
          // 直到点击事件被最终处理为止
          consume = child.dispatchTouchEvent (ev) ;
        }
    
        // 步骤3:最终返回通知 该事件是否被消费(接收 & 处理)
        return consume;
      }
    }
    

    安卓为了保证所有的事件都是被一个 View 消费的,对第一次的事件( ACTION_DOWN )进行了特殊判断,View 只有消费了 ACTION_DOWN 事件,才能接收到后续的事件(可点击控件会默认消费所有事件),并且会将后续所有事件传递过来,不会再传递给其他 View,除非上层 View 进行了拦截。如果上层 View 拦截了当前正在处理的事件,会收到一个 ACTION_CANCEL,表示当前事件已经结束,后续事件不会再传递过来。

    核心要点

    1. 事件分发原理: 责任链模式,事件层层传递,直到被消费。
    2. View 的 dispatchTouchEvent主要用于调度自身的监听器和 onTouchEvent
    3. View的事件的调度顺序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener
    4. 不论View自身是否注册点击事件,只要 View 是可点击的就会消费事件。
    5. 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。
    6. ViewGroup 中可能有多个 ChildView 时,将事件分配给包含点击位置的 ChildView。
    7. ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),由 ChildView 消费。
    8. 一次触摸流程中产生事件应被同一 View 消费,全部接收或者全部拒绝。
    9. 只要接受 ACTION_DOWN 就意味着接受所有的事件,拒绝 ACTION_DOWN 则不会收到后续内容。
    10. 如果当前正在处理的事件被上层 View 拦截,会收到一个 ACTION_CANCEL,后续事件不会再传递过来。

    参考与感谢

    Android事件分发机制详解:史上最全面、最易懂
    安卓自定义View进阶-事件分发机制详解

    相关文章

      网友评论

        本文标题:Android事件分发机制详解

        本文链接:https://www.haomeiwen.com/subject/cinelrtx.html