Android 事件分发机制详解(下)

简介: Android 事件分发机制详解(下)

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

源码总结


image.png

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

核心方法总结

主要包括: dispatchTouchEvent()onTouchEvent()


image.png

实例分析

在本示例中,将分析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()


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

image.png


这个三个方法均有一个 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,后续事件不会再传递过来。
相关文章
|
30天前
|
消息中间件 存储 Java
Android消息处理机制(Handler+Looper+Message+MessageQueue)
Android消息处理机制(Handler+Looper+Message+MessageQueue)
38 2
|
14天前
|
Android开发
Android面试高频知识点(1) 图解Android事件分发机制
Android面试高频知识点(1) 图解Android事件分发机制
|
14天前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
|
17天前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
41 1
|
17天前
|
Android开发
Android面试高频知识点(1) 图解 Android 事件分发机制
Android面试高频知识点(1) 图解 Android 事件分发机制
34 1
|
18天前
|
存储 安全 数据安全/隐私保护
探索安卓与iOS的隐私保护机制####
【10月更文挑战第15天】 本文深入剖析了安卓和iOS两大操作系统在隐私保护方面的策略与技术实现,旨在揭示两者如何通过不同的技术手段来保障用户数据的安全与隐私。文章将逐一探讨各自的隐私控制功能、加密措施以及用户权限管理,为读者提供一个全面而深入的理解。 ####
33 1
|
21天前
|
消息中间件 存储 Java
Android消息处理机制(Handler+Looper+Message+MessageQueue)
Android消息处理机制(Handler+Looper+Message+MessageQueue)
43 2
|
22天前
|
XML 前端开发 Android开发
Android面试高频知识点(1) 图解Android事件分发机制
Android面试高频知识点(1) 图解Android事件分发机制
Android面试高频知识点(1) 图解Android事件分发机制
|
30天前
|
Android开发
Android 事件分发机制详细解读
Android 事件分发机制详细解读
37 4
|
2月前
|
存储 缓存 Android开发
Android RecyclerView 缓存机制深度解析与面试题
本文首发于公众号“AntDream”,详细解析了 `RecyclerView` 的缓存机制,包括多级缓存的原理与流程,并提供了常见面试题及答案。通过本文,你将深入了解 `RecyclerView` 的高性能秘诀,提升列表和网格的开发技能。
64 8