Android事件分发机制解密 一 ( ViewGroup分发)

1,045 阅读6分钟

曾几何时,当我还是一个Android小菜鸡的时候,我就被问过事件分发机制,当时我都是百度来的博客看看,回答的很浅显,当时是记住了过一段时间就忘。现在写一篇博客记录一下,下次在被问到就不怕了(得瑟.gif)。一般情况下理解了以下几个问题,就基本掌握了事件分发的总体面貌:

一.为什么要理解事件分发? 答:1.为了解决事件冲突(事件冲突就是:事件只有一个,多个控件想要处理且处理的控件不是我们想要给的控件就发生了冲突) 2.为了不被面试官问的一脸懵逼( 懵逼警告⚠️ )

在这里插入图片描述

二.事件是什么? 简单的来说:事件(MotionEvent)就是手指触摸(Touch)手机屏幕而产生的,具体的产生过程后面有机会再分析。本文主要分析事件(MotionEvent)分发流程,记住下表尤其是 ACTION_MOVE会多次触发,这个要记住了,后面分析源码经常会忘了这一点。 在这里插入图片描述

三.事件分发过程主要有哪些方法以及作用? 答:dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent() 在这里插入图片描述

四.事件分发中分发的流程是怎么样的?(发车了,坐稳) 在这里插入图片描述

所以如果我们没有对控件里面的方法进行重写或更改返回值,而直接用super调用父类的默认实现,那么整个事件流向应该是从Activity---->ViewGroup--->View 从上往下调用dispatchTouchEvent方法,一直到叶子节点(View)的时候,再由View--->ViewGroup--->Activity从下往上调用onTouchEvent方法。所以整个来说应该是一个U型的递归调用

带大家看一下源码,还原一下U型调用,最好自己去看一遍源码,加深印象(我分析的是Android29版本的源码,各个版本可能会有点不一样,原理都一样)

4.1 Activity->ViewGroup的事件传递机制

//Activity中
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 一般事件列开始都是DOWN事件 = 按下事件,故此处基本是true
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();//这个是个空方法,子类可以重写实现屏保功能
    }
    //这里调用的是Window的superDispatchTouchEvent()方法
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
        // 若getWindow().superDispatchTouchEvent(ev)的返回true
        // 则Activity.dispatchTouchEvent()就返回true,则方法结束。即 :该点击事件停止往下传递 & 事件传递过程结束(返回true是被消费了,false是没有被消费)
        // 如果没有被消费,则调用Activity.onTouchEvent
    }
    return onTouchEvent(ev);
}

//Window 是抽象类其唯一的子类是PhoneWindow
public abstract boolean superDispatchTouchEvent(MotionEvent event);

//PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
   //这里调用的是DecorView的superDispatchTouchEvent()方法
   return mDecor.superDispatchTouchEvent(event);
}
// DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
    //DecorView的父类是FrameLayout,FrameLayout的父类是ViewGroup,由于FrameLayout没有实现dispatchTouchEvent()方法,所以这里调用的是ViewGroup的dispatchTouchEvent()方法
    return super.dispatchTouchEvent(event);
}

4.2 ViewGroup事件的分发机制

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

//ViewGroup的dispatchTouchEvent()方法代码比较多,这里说一些重点的片段
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
   ...//仅贴出关键代码
   
   //这里是方法的核心,每个事件都会走到这个if里面
   if (onFilterTouchEventForSecurity(ev)) {
        //重点分析1:ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
         final boolean intercepted;
            //判断条件1:拦截对象针对于ACTION_DOWN 
            //判断条件2:mFirstTouchTarget赋值在addTouchTarget()方法中,先给出结论:当事件被ViewGroup的子View消费了mFirstTouchTarget的值才不为null,具体分析见文末链接
            //也就是如果是down事件或者之前事件被子View消费了后续事件都可以进入if条件代码块内
         if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
             //判断条件3:disallowIntercept = 是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
             final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
             if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                intercepted = true;
            }
       //判断是否被取消或者拦截
       if (!canceled && !intercepted) {
            // 重点分析2
            // 如果当前ViewGroup有子View,则通过for循环,遍历了当前ViewGroup下的所有子View
       	    for (int i = childrenCount - 1; i >= 0; i--) {
                // 判断条件1:子View是否显示
                // 判断条件2:当前遍历的View是不是正在点击的View,从而找到当前被点击的View
                // 如果不是则跳过
                if (!canViewReceivePointerEvents(child)|| !isTransformedTouchPointInView(x, y, child, null)) {
                      ev.setTargetAccessibilityFocus(false);
                      continue;
                }
                // dispatchTransformedTouchEvent的内部调用了该View的dispatchTouchEvent()
                // 即 实现了点击事件从ViewGroup到子View的传递(具体请看下文的View事件分发机制)
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                     //这里对mFirstTouchTarget进行了赋值
                     newTouchTarget = addTouchTarget(child, idBitsToAssign);
                     alreadyDispatchedToNewTouchTarget = true;
                     //如果事件被消费了则跳出循环
                     break;
                }
            }
       }
       //重点分析3 点击事件没有任何子View消费则仍然调用dispatchTransformedTouchEvent()方法,但是chrild传入的是null,会调用View的dispatchTouchEvent()将当前ViewGroup本身作为View来进行处理
      if (mFirstTouchTarget == null) {    
         handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
      } else {
          //...    
      }
   }
}
/**
 * 分析1:ViewGroup.onInterceptTouchEvent()
 * 作用:是否拦截事件
 * 说明:
 *     a. 返回true = 拦截,即事件停止往下传递(需手动设置,即复写onInterceptTouchEvent(),从而让其返回true)
 *     b. 返回false = 不拦截(默认)
  */
  public boolean onInterceptTouchEvent(MotionEvent ev) {  
  
    return false;
  } 
/**
 * 分析2:ViewGroup.dispatchTransformedTouchEvent()
 * 作用:都是调用View的dispatchTouchEvent()方法
 * 说明:
 *      a.如果当前ViewGroup有子View可以处理,则调用当前child的dispatchTouchEvent()
 *      b.如果当前ViewGroup没有子View则把当前ViewGroup当做View处理也调用View的dispatchTouchEvent()
  */  
 private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
   if (child == null) {
       handled = super.dispatchTouchEvent(transformedEvent);
   } else {
     final float offsetX = mScrollX - child.mLeft;
     final float offsetY = mScrollY - child.mTop;
     transformedEvent.offsetLocation(offsetX, offsetY);
     if (! child.hasIdentityMatrix()) {
          transformedEvent.transform(child.getInverseMatrix());
     }
     handled = child.dispatchTouchEvent(transformedEvent);
    }
     // Done.
     transformedEvent.recycle();
     return handled;
  }
  /**
   * 分析3:ViewGroup.requestDisallowInterceptTouchEvent()
   * 作用:子View可以改变父ViewGroup的拦截,前提是父ViewGroup必须把down事件下发到处理事件的子View
   * 说明:
   *      a.disallowIntercept==true 则禁止ViewGroup拦截事件
   *      b.disallowIntercept==false 则允许ViewGroup拦截事件
   */
   public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
       //约束当前ViewGroup的拦截
       if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
        //约束父ViewGroup的拦截(如果有父容器)
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
   }
 

五.总结

  • 结论:Android事件分发总是先传递到ViewGroup、再传递到View
  • 当点击了某个控件时
    down--确定事件给谁 1.先看ViewGroup是否拦截后自己处理(即不分发下去) 2.分发下去:排序 遍历分发 领取事件的View 处理事件 3.没子View领取,再看下自己是否处理事件 move--处理事件 1.先看ViewGroup是否拦截后自己处理(即不分发下去)(子View可以请求不拦截Move事件) 2.分发下去:直接由down事件确定的view处理 在这里插入图片描述

View的事件处理下一篇文章会讲解,如果觉得这篇文章有用,欢迎点赞加收藏✨。

相关参考文章链接: 郭婶的事件分发机制完全解析 Android事件分发机制详解:史上最全面、最易懂 Android事件分发mFirstTouchTarget的思考