【Android】事件分发机制

时间:2024-10-23 08:23:25

Android 的事件分发机制主要包括以下几个步骤:

  1. 事件生成:用户在设备上进行触摸、滑动等操作时,系统会生成相应的事件,如触摸事件(MotionEvent)。

  2. 事件发送:生成的事件会被发送到当前活动(Activity)或视图(View)树的根节点。

  3. 事件分发

    1. Activity:首先,事件会被传递给活动的 dispatchTouchEvent() 方法。这个方法决定如何将事件进一步分发。

    2. ViewGroup:如果当前活动包含 ViewGroup(如 LinearLayoutRelativeLayout 等),dispatchTouchEvent() 会先调用 ViewGrouponInterceptTouchEvent() 方法。如果返回 true,则 ViewGroup 会处理事件;如果返回 false,则将事件传递给子视图。

    3. View:对于普通的视图(View),会调用其 onTouchEvent() 方法来处理事件。

  4. 事件处理

    1. onTouchEvent():当视图接收到事件后,会根据事件的类型(如按下、移动、抬起等)在此方法中处理相应的逻辑。

    2. 事件处理过程可能涉及多个视图,尤其是在有嵌套的视图结构中。

  5. 事件消费:如果某个视图处理了事件(返回 true),后续的视图将不会再接收到这个事件。如果没有视图消费事件,事件将向上传递,直到达到活动。

  6. 最终结果:处理完成后,结果可能会影响用户界面的状态或行为。

注意事项:

  • onInterceptTouchEvent():在 ViewGroup 中使用,决定是否拦截子视图的事件。

  • 事件的传递顺序:从上到下(Activity → ViewGroup → View),处理顺序是从下到上(View → ViewGroup → Activity)。

好的,针对你提到的几个基础认知点,以下是更深入的解析:

事件分发的由来

由于 Android 中的视图是树形结构的,多个视图可能重叠并同时响应用户的触摸事件。

这种情况下,如何选择一个特定的视图来处理事件就变得尤为重要。事件分发机制的目的是在复杂的视图层次中,准确、高效地找到合适的视图来响应用户的交互。

事件的定义

触摸事件是用户与设备交互时产生的信号,通常由一系列连续的触摸状态构成。

事件列从 ACTION_DOWN 开始(手指触摸屏幕),

中间可能有多个 ACTION_MOVE 事件(手指移动),

最后以 ACTION_UP 结束(手指离开屏幕)。

这种事件序列使得应用能够理解用户的意图,比如滑动或点击。

事件分发的本质

事件分发的本质在于动态选择并传递触摸事件到合适的视图,同时根据用户的交互来决定事件的处理逻辑。以下是更深入的讲解:

  1. 决策过程:当触摸事件发生时,Activity 首先接收事件,执行 dispatchTouchEvent() 方法。在这个方法中,系统根据事件的类型和当前视图的状态(如是否可见、是否可点击)决定是否继续向下传递事件。

  2. 拦截机制ViewGroup 可以通过重写 onInterceptTouchEvent() 方法来决定是否拦截事件。如果返回 true,则该 ViewGroup 将直接处理事件,子视图将不再接收到此事件。这一机制允许 ViewGroup 在需要时优先处理特定的触摸交互,比如滚动或拖动。

  3. 事件传递链:如果事件没有被拦截,它将继续向下传递到子视图。每个视图都有自己的 onTouchEvent() 方法,用于处理具体的事件。如果一个视图能处理该事件(如点击),它会返回 true,表示事件已被消费,后续视图将不会再接收到该事件。这种机制使得触摸事件的处理能够精准到具体的视图。

  4. 状态管理:视图的状态(如按下、抬起、移动等)影响事件的处理。视图在 onTouchEvent() 中根据当前状态执行不同的逻辑,如更新视觉反馈、触发动画或改变内部状态。这种动态响应使得用户体验更加流畅和自然。

  5. 事件队列与优化:Android 还利用事件队列来优化事件处理。通过合并和优化事件,系统能够减少不必要的处理,提高性能。这在复杂交互或高频触摸操作中尤为重要。

事件传递的对象

事件在 Android 中主要在 ActivityViewGroupView 之间传递。

Activity 作为应用的入口,首先接收事件;ViewGroup 可能根据需要拦截事件,而具体的 View 则负责执行实际的事件处理。这种多层次的传递机制保证了事件处理的灵活性和精确性。

事件分发顺序

  1. Activity:当用户触摸屏幕时,事件首先被发送到当前的 ActivityActivity 会调用其 dispatchTouchEvent() 方法,决定事件的后续处理。

  2. ViewGroup:如果 ActivitydispatchTouchEvent() 方法未拦截事件,事件将传递到 ViewGroupViewGroup 会执行 onInterceptTouchEvent() 方法,判断是否拦截该事件。如果返回 true,则事件会在 ViewGroup 中处理;如果返回 false,事件会继续传递到子视图。

  3. View:最终,事件将传递到具体的 View,在 View 中调用 onTouchEvent() 方法来处理事件。视图可以根据事件的类型(如点击、滑动等)执行相应的逻辑。

事件分发过程中的方法

  • dispatchTouchEvent()

    • 作用:负责分发触摸事件。

    • 调用时刻:当 ActivityViewGroup 收到触摸事件时首先调用。它决定事件是否继续传递给子视图或直接处理。

  • onInterceptTouchEvent()

    • 作用:用于判断 ViewGroup 是否拦截事件。

    • 调用时刻:在 ViewGroupdispatchTouchEvent() 内部调用。通常用于处理复杂的触摸交互,比如滑动或拖动。

  • onTouchEvent()

    • 作用:处理具体的触摸事件。

    • 调用时刻:在 dispatchTouchEvent() 内部调用。用于执行视图的响应逻辑,比如状态更新、动画触发等。

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.dispatchTouchEvent() 的实现和事件分发的过程。以下是对每个分析点的进一步阐释:

分析1:dispatchTouchEvent()

  • 核心逻辑:当触摸事件发生时,Activity.dispatchTouchEvent() 首先调用 getWindow().superDispatchTouchEvent(ev)。如果返回 true,表示该事件已经被处理,事件传递过程结束;如果返回 false,则继续调用 onTouchEvent(ev) 进行后续处理。

分析2:superDispatchTouchEvent()

  • 窗口与视图getWindow() 获取 Window 对象,通常是 PhoneWindow 的实例。superDispatchTouchEvent(ev) 将事件传递给 DecorView,它是应用界面的顶层视图,负责接收和分发事件。

  • 层级关系DecorView 继承自 FrameLayout,是所有界面的根视图,最终调用的是 ViewGroupdispatchTouchEvent() 方法,进入视图分发的下一阶段。

分析3:onTouchEvent()

  • 回调机制:当触摸事件没有被任何子视图处理时,Activity 将调用 onTouchEvent()。这通常用于处理未消费的事件,例如判断用户点击是否在窗口边界之外。

  • 返回值逻辑onTouchEvent() 的返回值主要依赖于 mWindow.shouldCloseOnTouch(this, event),该方法判断点击是否在窗口外。

分析4:shouldCloseOnTouch()

  • 边界判断:此方法的作用是判断用户点击是否在窗口的边界外,特别是在 ACTION_DOWN 事件时。如果点击发生在边界外且设置为关闭窗口,则返回 true,表示事件已被消费;否则,返回 false,表示事件未被消费。

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;
  } 
  // 回到调用原处

这段源码分析清楚地描述了 ViewGroup.dispatchTouchEvent() 方法的实现过程,以及如何判断和处理触摸事件。以下是对每个分析点的进一步解释:

分析1:dispatchTouchEvent()

  • 事件拦截判断:在每次事件分发时,ViewGroup 会首先调用 onInterceptTouchEvent(ev) 来询问是否拦截事件。这个决策是基于两个条件:

    • disallowIntercept:如果为 true,表示不允许拦截。

    • !onInterceptTouchEvent(ev):如果返回 false,表示不拦截事件,继续执行事件传递逻辑。

事件传递过程

  • 遍历子视图:如果未拦截,ViewGroup 将通过 for 循环遍历其所有子视图。

  • 可见性检查:在循环中,首先检查子视图的可见性。只有可见或有动画效果的视图会继续处理。

  • 命中检测:使用 getHitRect(frame) 获取子视图的矩形区域,判断用户点击的坐标是否在子视图内。如果点击在子视图内,将事件位置调整为相对于子视图的坐标,并调用该子视图的 dispatchTouchEvent(ev) 方法处理事件。

分析2:子视图的事件处理

  • 返回值逻辑:如果子视图处理了事件(返回 true),ViewGroup.dispatchTouchEvent() 也会返回 true,表示事件被消费,事件不会继续向下传递。

分析3:无视图处理的情况

  • 调用父类方法:如果没有任何子视图处理事件,或者 ViewGroup 自身拦截了事件,将调用其父类 ViewdispatchTouchEvent() 方法。这可能导致 View 自身处理事件,执行点击逻辑(如调用 performClick())。

onInterceptTouchEvent()

  • 默认行为onInterceptTouchEvent() 默认返回 false,即不拦截事件。开发者可以重写此方法以自定义拦截逻辑,返回 true 时会停止事件的进一步传递。

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

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;  
  }

这段源码分析清晰地描述了 View.dispatchTouchEvent()View.onTouchEvent() 方法的工作原理,以及它们如何处理触摸事件。以下是对每个关键点的进一步解析:

dispatchTouchEvent()

  • 返回条件dispatchTouchEvent() 会在三个条件同时满足时返回 true,表示事件已被消费:

    • 条件1(mViewFlags & ENABLED_MASK) == ENABLED,判断视图是否启用。一般情况下,这个条件为真,除非手动设置为禁用。

    • 条件2mOnTouchListener != null,确保视图注册了触摸事件监听器。

    • 条件3mOnTouchListener.onTouch(this, event),调用监听器的 onTouch() 方法并检查返回值。如果返回 true,事件被消费,方法结束;如果返回 false,将调用 onTouchEvent(event) 继续处理。

onTouchEvent()

  • 事件处理逻辑onTouchEvent() 负责处理实际的触摸事件,首先检查视图的可点击性。如果视图可点击,会进入 switch 语句根据事件类型进行处理:

    • ACTION_UP:抬起手指时调用 performClick(),触发点击事件。

    • ACTION_DOWN:按下时可能设置延迟检查点击状态。

    • ACTION_CANCEL:处理取消事件,重置状态。

    • ACTION_MOVE:处理移动事件,检查是否超过触摸阈值。

performClick()

  • 点击事件触发performClick() 方法会检查是否注册了点击事件监听器 mOnClickListener。如果存在,调用 onClick() 方法执行点击逻辑,并返回 true 表示点击事件已成功处理。

点击按钮会产生两个类型的事件-按下View与抬起View,所以会回调两次 onTouch() ;

因为 onTouch() 返回了false,所以事件无被消费,会继续往下传递,即调用 View.onTouchEvent() ;

调用 View.onTouchEvent() 时,对于抬起View事件,在调用 performClick() 时,因为设置了点击事件,所以会回调 onClick() 。

点击按钮会产生两个类型的事件-按下View与抬起View,所以会回调两次 onTouch() ;

因为 onTouch() 返回true,所以事件被消费,不会继续往下传递, View.dispatchTouchEvent() 直接返回true;

所以最终不会调用 View.onTouchEvent() ,也不会调用 onClick() 。

事件分发机制流程总结

从上表可以看到 Activity和 View都是没有事件拦截的,这是因为:

Activity 作为原始的事件分发者,如果 Activity 拦截了事件会导致整个屏幕都无法响应事件,这肯定不是我们想要的效果。

View作为事件传递的最末端,要么消费掉事件,要么不处理进行回传,根本没必要进行事件拦截。

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