Android ——事件分发机制

173 阅读9分钟

一、事件分发机制

用户在使用app时,会经常频繁的触摸屏幕,点击页面交互,打开需要的功能。用户触摸点击的动作,就涉及到app中相关UI的触摸和点击事件。事件在UI中传递和处理的过程,就是事件分发流程。 发生一次点击事件时,事件会按照Activity->ViewGroup->View的顺序,进行事件传递。

  • Activity:控制UI页面的生命周期,是事件分发的入口。
  • ViewGroup:View的特殊子类,是一组View的集合,是Android中所有布局的父类。
  • View:所有UI组件的基类,常见的Button、TextView等控件都继承自View。

1.1 MotionEvent

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

1.2 事件分发过程由哪些方法协作完成? 在整个事件分发,并响应事件的过程中,有三个重要的方法:

  • dispatchTouchEvent:分发(传递)点击事件,当点击事件能够传递给当前View时,该方法就会被调用。
  • onInterceptTouchEvent:判断是否拦截某个事件,该方法仅在ViewGroup中存在。一般情况下会在ViewGroup的dispatchTouchEvent方法中调用该方法。
  • onTouchEvent:处理点击事件,在dispatchTouchEvent内部调用。 [图片] Activity的事件分发流程 Activity 中包含两个事件分发与处理的方法,分别是:
  • boolean dispatchTouchEvent(MotionEvent ev):事件分发
  • boolean onTouchEvent(MotionEvent event):事件消费 当一个事件发生时,首先会将点击事件传递到Activity中,执行dispatchTouchEvent进行事件分发。经过window、decorView依次传递后,页面上的 ViewGroup会接收到该事件。ViewGroup如果消费了该事件,则分发结束,未消费则继续调用Activity的onTouchEvent 方法处理事件,简略流程图如下:
/**
  * 源码分析: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;
  } 

ViewGroup的事件分发流程 ViewGroup 中包含三个事件分发与处理的方法,分别是:

  • dispatchTouchEvent(MotionEvent ev):事件分发
  • onIntercepTouchEvent(MotionEvent ev):事件拦截
  • onTouchEvent(MotionEvent ev):事件消费 ViewGroup的事件分发机制从dispatchTouchEvent开始,接着调用onInterceptTouchEvent方法判断是否需要拦截事件。 如果需要拦截,则表示当前 ViewGroup 希望处理该事件,或者不希望子 View 处理该事件,此时将直接调用 onTouchEvent方法处理事件。onTouchEvent方法如果消费了该事件,则分发结束,未消费则调用Activity的onTouchEvent方法处理事件。 如果不需要拦截,则会遍历寻找被点击的子View,将该事件传递给子 View 的 dispatchTouchEvent 方法。如果未找到子View,事件将会继续传递给ViewGroup的onTouchEvent方法,与事件被拦截的效果一致。 主要的事件分发链路图如下所示:
源码分析
/**
  * 源码分析: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)

View的事件分发流程

View 中包含如下两个事件分发与处理的方法,分别是:

  • dispatchTouchEvent(MotionEvent event)
  • onTouchEvent(MotionEvent event) View通过dispatchTouchEvent方法接收到从ViewGroup传递过来的事件后,直接调用 onTouchEvent方法处理事件。如果消费了该事件,则分发结束,未消费则调用ViewGroup的onTouchEvent方法处理事件。所以 View 中的事件处理流程很简单, 此处说明一个细节,当View把事件消费后,如果View的onTouch方法返回true,View的dispatchTouchEvent方法会直接返回true,不会再调用View的onClick方法。只有当onTouch方法返回false时,才会有onClick事件处理。 场景:拦截DOWN的后续事件 结论
  • 若 ViewGroup 拦截了一个半路的事件(如MOVE),该事件将会被系统变成一个CANCEL事件 & 传递给之前处理该事件的子View;
  • 该事件不会再传递给ViewGroup 的onTouchEvent()
  • 只有再到来的事件才会传递到ViewGroup的onTouchEvent() 场景描述 ViewGroup B 无拦截DOWN事件(还是View C来处理DOWN事件),但它拦截了接下来的MOVE事件 即 DOWN事件传递到View C的onTouchEvent(),返回了true 实例
  • 在后续到来的MOVE事件,ViewGroup B 的onInterceptTouchEvent()返回true拦截该MOVE事件,但该事件并没有传递给ViewGroup B ;这个MOVE事件将会被系统变成一个CANCEL事件传递给View C的onTouchEvent()
  • 后续又来了一个MOVE事件,该MOVE事件才会直接传递给ViewGroup B 的onTouchEvent() 后续事件将直接传递给ViewGroup B 的onTouchEvent()处理,而不会再传递给ViewGroup B 的onInterceptTouchEvent(),因该方法一旦返回一次true,就再也不会被调用了。
  • View C再也不会收到该事件列产生的后续事件

1.3 特殊说明

  • 若给控件注册了Touch事件,每次点击都会触发一系列action事件(ACTION_DOWN,ACTION_MOVE,ACTION_UP等)
  • 当dispatchTouchEvent()事件分发时,只有前一个事件(如ACTION_DOWN)返回true,才会收到后一个事件(ACTION_MOVE和ACTION_UP) 即如果在执行ACTION_DOWN时返回false,后面一系列的ACTION_MOVE、ACTION_UP事件都不会执行

二、Activity解析

2.1 主要的几种Activity

暂时无法在Transsioner文档外展示此内容 在 Android 应用程序开发中,Activity 是一个关键组件,用于处理用户界面和用户交互。当开发 Android 应用程序时,通常会使用 Activity 类作为应用程序中每个屏幕的基础。 ComponentActivity 是一个继承自 FrgmentActivity 的类,它提供了与 AndroidX 组件兼容的 Activity 基础功能,比如支持 Fragment,支持生命周期,支持在旋转设备时保存 Activity 状态等等。 AppCompatActivity 是一个 Android 支持库中的类,用于在旧版 Android 平台上实现 Material Design 风格的应用程序。它扩展自 FragmentActivity,是 Activity 的子类,为 Android 应用程序提供了兼容性支持,使得应用程序可以在不同版本的 Android 平台上运行。 因此,ComponentActivity 和 AppCompatActivity 都是 Activity 的子类,但 ComponentActivity 是 AndroidX 组件兼容的 Activity 基础类,而 AppCompatActivity 则是提供兼容性支持的 Activity 类。 一般来说,如果应用程序需要支持 Material Design,并且需要在旧版 Android 平台上运行,那么就应该使用 AppCompatActivity。而如果你的应用程序已经使用了 AndroidX 组件,并且不需要在旧版 Android 平台上运行,那么就应该使用 ComponentActivity。