Android从源码的角度彻底理解事件分发机制的解析(下)

记得在前面的文章中,我带大家一起从源码的角度分析了Android中View的事件分发机制,相信阅读过的朋友对View的事件分发已经有比较深刻的理解了。

还未阅读过的朋友,请先参考Android从源码的角度彻底理解事件分发机制的解析。

那么今天我们将继续上次未完成的话题,从源码的角度分析ViewGroup的事件分发。

首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别?

顾名思义,ViewGroup就是一组View的集合,它包含很多的子View和子VewGroup,是Android中所有布局的父类或间接父类,像LinearLayout、RelativeLayout等都是继承自ViewGroup的。但ViewGroup实际上也是一个View,只不过比起View,它多了可以包含子View和定义布局参数的功能。ViewGroup继承结构示意图如下所示:

可以看到,我们平时项目里经常用到的各种布局,全都属于ViewGroup的子类。

简单介绍完了ViewGroup,我们现在通过一个Demo来演示一下Android中VewGroup的事件分发流程吧。

首先我们来自定义一个布局,命名为MyLayout,继承自LinearLayout,如下所示:

public class MyLayout extends LinearLayout {
 public MyLayout(Context context, AttributeSet attrs) {
 super(context, attrs);
 }
}

然后,打开主布局文件activity_main.xml,在其中加入我们自定义的布局:

<com.example.viewgrouptouchevent.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 android:id="@+id/my_layout"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:orientation="vertical" >
 <Button
 android:id="@+id/button1"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:text="Button1" />
 <Button
 android:id="@+id/button2"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:text="Button2" /> 

</com.example.viewgrouptouchevent.MyLayout>

可以看到,我们在MyLayout中添加了两个按钮,接着在MainActivity中为这两个按钮和MyLayout都注册了监听事件:

myLayout.setOnTouchListener(new OnTouchListener() {
 @Override
 public boolean onTouch(View v, MotionEvent event) {
 Log.d("TAG", "myLayout on touch");
 return false;
 }
});
button1.setOnClickListener(new OnClickListener() {
 @Override
 public void onClick(View v) {
 Log.d("TAG", "You clicked button1");
 }
});
button2.setOnClickListener(new OnClickListener() {
 @Override
 public void onClick(View v) {
 Log.d("TAG", "You clicked button2");
 }
});

我们在MyLayout的onTouch方法,和Button1、Button2的onClick方法中都打印了一句话。现在运行一下项目,效果图如下所示:

分别点击一下Button1、Button2和空白区域,打印结果如下所示:

你会发现,当点击按钮的时候,MyLayout注册的onTouch方法并不会执行,只有点击空白区域的时候才会执行该方法。你可以先理解成Button的onClick方法将事件消费掉了,因此事件不会再继续向下传递。

那就说明Android中的touch事件是先传递到View,再传递到ViewGroup的?现在下结论还未免过早了,让我们再来做一个实验。

查阅文档可以看到,ViewGroup中有一个onInterceptTouchEvent方法,我们来看一下这个方法的源码:

/**
 * Implement this method to intercept all touch screen motion events. This
 * allows you to watch events as they are dispatched to your children, and
 * take ownership of the current gesture at any point.
 *
 * <p>Using this function takes some care, as it has a fairly complicated
 * interaction with {@link View#onTouchEvent(MotionEvent)
 * View.onTouchEvent(MotionEvent)}, and using it requires implementing
 * that method as well as this one in the correct way. Events will be
 * received in the following order:
 *
 * <ol>
 * <li> You will receive the down event here.
 * <li> The down event will be handled either by a child of this view
 * group, or given to your own onTouchEvent() method to handle; this means
 * you should implement onTouchEvent() to return true, so you will
 * continue to see the rest of the gesture (instead of looking for
 * a parent view to handle it). Also, by returning true from
 * onTouchEvent(), you will not receive any following
 * events in onInterceptTouchEvent() and all touch processing must
 * happen in onTouchEvent() like normal.
 * <li> For as long as you return false from this function, each following
 * event (up to and including the final up) will be delivered first here
 * and then to the target's onTouchEvent().
 * <li> If you return true from here, you will not receive any
 * following events: the target view will receive the same event but
 * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
 * events will be delivered to your onTouchEvent() method and no longer
 * appear here.
 * </ol>
 *
 * @param ev The motion event being dispatched down the hierarchy.
 * @return Return true to steal motion events from the children and have
 * them dispatched to this ViewGroup through onTouchEvent().
 * The current target will receive an ACTION_CANCEL event, and no further
 * messages will be delivered here.
 */
public boolean onInterceptTouchEvent(MotionEvent ev) {
 return false;
} 

如果不看源码你还真可能被这注释吓到了,这么长的英文注释看得头都大了。可是源码竟然如此简单!只有一行代码,返回了一个false!

好吧,既然是布尔型的返回,那么只有两种可能,我们在MyLayout中重写这个方法,然后返回一个true试试,代码如下所示:

public class MyLayout extends LinearLayout {
 public MyLayout(Context context, AttributeSet attrs) {
 super(context, attrs);
 }
 @Override
 public boolean onInterceptTouchEvent(MotionEvent ev) {
 return true;
 } 

}

现在再次运行项目,然后分别Button1、Button2和空白区域,打印结果如下所示:

你会发现,不管你点击哪里,永远都只会触发MyLayout的touch事件了,按钮的点击事件完全被屏蔽掉了!这是为什么呢?如果Android中的touch事件是先传递到View,再传递到ViewGroup的,那么MyLayout又怎么可能屏蔽掉Button的点击事件呢?

看来只有通过阅读源码,搞清楚Android中ViewGroup的事件分发机制,才能解决我们心中的疑惑了,不过这里我想先跟你透露一句,Android中touch事件的传递,绝对是先传递到ViewGroup,再传递到View的。记得在Android从源码的角度彻底理解事件分发机制的解析 中我有说明过,只要你触摸了任何控件,就一定会调用该控件的dispatchTouchEvent方法。这个说法没错,只不过还不完整而已。实际情况是,当你点击了某个控件,首先会去调用该控件所在布局的dispatchTouchEvent方法,然后在布局的dispatchTouchEvent方法中找到被点击的相应控件,再去调用该控件的dispatchTouchEvent方法。如果我们点击了MyLayout中的按钮,会先去调用MyLayout的dispatchTouchEvent方法,可是你会发现MyLayout中并没有这个方法。那就再到它的父类LinearLayout中找一找,发现也没有这个方法。那只好继续再找LinearLayout的父类ViewGroup,你终于在ViewGroup中看到了这个方法,按钮的dispatchTouchEvent方法就是在这里调用的。修改后的示意图如下所示:

那还等什么?快去看一看ViewGroup中的dispatchTouchEvent方法的源码吧!代码如下所示:

public boolean dispatchTouchEvent(MotionEvent ev) {
 final int action = ev.getAction();
 final float xf = ev.getX();
 final float yf = ev.getY();
 final float scrolledXFloat = xf + mScrollX;
 final float scrolledYFloat = yf + mScrollY;
 final Rect frame = mTempRect;
 boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
 if (action == MotionEvent.ACTION_DOWN) {
 if (mMotionTarget != null) {
  mMotionTarget = null;
 }
 if (disallowIntercept || !onInterceptTouchEvent(ev)) {
  ev.setAction(MotionEvent.ACTION_DOWN);
  final int scrolledXInt = (int) scrolledXFloat;
  final int scrolledYInt = (int) scrolledYFloat;
  final View[] children = mChildren;
  final int count = mChildrenCount;
  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);
   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;
   if (child.dispatchTouchEvent(ev)) {
    mMotionTarget = child;
    return true;
   }
   }
  }
  }
 }
 }
 boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
  (action == MotionEvent.ACTION_CANCEL);
 if (isUpOrCancel) {
 mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
 }
 final View target = mMotionTarget;
 if (target == null) {
 ev.setLocation(xf, yf);
 if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
  ev.setAction(MotionEvent.ACTION_CANCEL);
  mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
 }
 return super.dispatchTouchEvent(ev);
 }
 if (!disallowIntercept && onInterceptTouchEvent(ev)) {
 final float xc = scrolledXFloat - (float) target.mLeft;
 final float yc = scrolledYFloat - (float) target.mTop;
 mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
 ev.setAction(MotionEvent.ACTION_CANCEL);
 ev.setLocation(xc, yc);
 if (!target.dispatchTouchEvent(ev)) {
 }
 mMotionTarget = null;
 return true;
 }
 if (isUpOrCancel) {
 mMotionTarget = null;
 }
 final float xc = scrolledXFloat - (float) target.mLeft;
 final float yc = scrolledYFloat - (float) target.mTop;
 ev.setLocation(xc, yc);
 if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
 ev.setAction(MotionEvent.ACTION_CANCEL);
 target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
 mMotionTarget = null;
 }
 return target.dispatchTouchEvent(ev);
}

这个方法代码比较长,我们只挑重点看。首先在第13行可以看到一个条件判断,如果disallowIntercept和!onInterceptTouchEvent(ev)两者有一个为true,就会进入到这个条件判断中。disallowIntercept是指是否禁用掉事件拦截的功能,默认是false,也可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改。那么当第一个值为false的时候就会完全依赖第二个值来决定是否可以进入到条件判断的内部,第二个值是什么呢?竟然就是对onInterceptTouchEvent方法的返回值取反!也就是说如果我们在onInterceptTouchEvent方法中返回false,就会让第二个值为true,从而进入到条件判断的内部,如果我们在onInterceptTouchEvent方法中返回true,就会让第二个值为false,从而跳出了这个条件判断。

这个时候你就可以思考一下了,由于我们刚刚在MyLayout中重写了onInterceptTouchEvent方法,让这个方法返回true,导致所有按钮的点击事件都被屏蔽了,那我们就完全有理由相信,按钮点击事件的处理就是在第13行条件判断的内部进行的!

那我们重点来看下条件判断的内部是怎么实现的。在第19行通过一个for循环,遍历了当前ViewGroup下的所有子View,然后在第24行判断当前遍历的View是不是正在点击的View,如果是的话就会进入到该条件判断的内部,然后在第29行调用了该View的dispatchTouchEvent,之后的流程就和 Android从源码的角度彻底理解事件分发机制的解析中讲解的是一样的了。我们也因此证实了,按钮点击事件的处理确实就是在这里进行的。

然后需要注意一下,调用子View的dispatchTouchEvent后是有返回值的。我们已经知道,如果一个控件是可点击的,那么点击该控件时,dispatchTouchEvent的返回值必定是true。因此会导致第29行的条件判断成立,于是在第31行给ViewGroup的dispatchTouchEvent方法直接返回了true。这样就导致后面的代码无法执行到了,也是印证了我们前面的Demo打印的结果,如果按钮的点击事件得到执行,就会把MyLayout的touch事件拦截掉。

那如果我们点击的不是按钮,而是空白区域呢?这种情况就一定不会在第31行返回true了,而是会继续执行后面的代码。那我们继续往后看,在第44行,如果target等于null,就会进入到该条件判断内部,这里一般情况下target都会是null,因此会在第50行调用super.dispatchTouchEvent(ev)。这句代码会调用到哪里呢?当然是View中的dispatchTouchEvent方法了,因为ViewGroup的父类就是View。之后的处理逻辑又和前面所说的是一样的了,也因此MyLayout中注册的onTouch方法会得到执行。之后的代码在一般情况下是走不到的了,我们也就不再继续往下分析。

再看一下整个ViewGroup事件分发过程的流程图吧,相信可以帮助大家更好地去理解:

现在整个ViewGroup的事件分发流程的分析也就到此结束了,我们最后再来简单梳理一下吧。

1. Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View的。

2. 在ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。

3. 子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。

好了,Android事件分发机制完全解析到此全部结束,结合上下两篇,相信大家对事件分发的理解已经非常深刻了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android事件分发机制(下) View的事件处理

    综述 在上篇文章Android中的事件分发机制(上)--ViewGroup的事件分发中,对ViewGroup的事件分发进行了详细的分析.在文章的最后ViewGroup的dispatchTouchEvent方法调用dispatchTransformedTouchEvent方法成功将事件传递给ViewGroup的子View.并交由子View进行处理.那么现在就来分析一下子View接收到事件以后是如何处理的. View的事件处理 对于这里描述的View,它是ViewGroup的父类,并不包含任何的子元

  • 30分钟搞清楚Android Touch事件分发机制

    Touch事件分发中只有两个主角:ViewGroup和View.Activity的Touch事件事实上是调用它内部的ViewGroup的Touch事件,可以直接当成ViewGroup处理. View在ViewGroup内,ViewGroup也可以在其他ViewGroup内,这时候把内部的ViewGroup当成View来分析. ViewGroup的相关事件有三个:onInterceptTouchEvent.dispatchTouchEvent.onTouchEvent.View的相关事件只有两个:

  • 谈谈对Android View事件分发机制的理解

    最近因为项目中用到类似一个LinearLayout中水平布局中,有一个TextView和Button,然后对该LinearLayout布局设置点击事件,点击TextView能够触发该点击事件,然而奇怪的是点击Button却不能触发.然后google到了解决办法(重写Button,然后重写其中的ontouchEvent方法,且返回值为false),但是不知道原因,这两天看了几位大神的博客,然后自己总结下. public class MyButton extends Button { private

  • Android View的事件分发机制

    一.Android View框架提供了3个对事件的主要操作概念. 1.事件的分发机制,dispatchTouchEvent.主要是parent根据触摸事件的产生位置,以及child是否愿意负责处理该系列事件等状态,向其child分发事件的机制. 2.事件的拦截机制,onInterceptTouchEvent.主要是parent根据它内部的状态.或者child的状态,来把事件拦截下来,阻止其进一步传递到child的机制. 3.事件的处理机制,onTouchEvent.主要是事件序列的接受者(可以是

  • Android事件分发机制(上) ViewGroup的事件分发

    综述 Android中的事件分发机制也就是View与ViewGroup的对事件的分发与处理.在ViewGroup的内部包含了许多View,而ViewGroup继承自View,所以ViewGroup本身也是一个View.对于事件可以通过ViewGroup下发到它的子View并交由子View进行处理,而ViewGroup本身也能够对事件做出处理.下面就来详细分析一下ViewGroup对时间的分发处理. MotionEvent 当手指接触到屏幕以后,所产生的一系列的事件中,都是由以下三种事件类型组成.

  • Android从源码的角度彻底理解事件分发机制的解析(上)

    其实我一直准备写一篇关于Android事件分发机制的文章,从我的第一篇博客开始,就零零散散在好多地方使用到了Android事件分发的知识.也有好多朋友问过我各种问题,比如:onTouch和onTouchEvent有什么区别,又该如何使用?为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?为什么图片轮播器里的图片使用Button而不用ImageView?等等--对于这些问题,我并没有给出非常详细的回答,因为我知道如果想要彻底搞明白这些问题,掌握Android事件分发机

  • Android View 事件分发机制详解

    Android开发,触控无处不在.对于一些 不咋看源码的同学来说,多少对这块都会有一些疑惑.View事件的分发机制,不仅在做业务需求中会碰到这些问题,在一些面试笔试题中也常有人问,可谓是老生常谈了.我以前也看过很多人写的这方面的文章,不是说的太啰嗦就是太模糊,还有一些在细节上写的也有争议,故再次重新整理一下这块内容,十分钟让你搞明白View事件的分发机制. 说白了这些触控的事件分发机制就是弄清楚三个方法,dispatchTouchEvent(),OnInterceptTouchEvent(),o

  • android事件分发机制的实现原理

    android中的事件处理,以及解决滑动冲突问题都离不开事件分发机制,android中的事件流,即MotionEvent都会经历一个从分发,拦截到处理的一个过程.即dispatchTouchEvent(),onInterceptEvent()到onTouchEvent()的一个过程,在dispatchTouchEvent()负责了事件的分发过程,在dispatchTouchEvent()中会调用onInterceptEvent()与onTouchEvent(),如果onInterceptEven

  • Android View事件分发机制详解

    准备了一阵子,一直想写一篇事件分发的文章总结一下,这个知识点实在是太重要了. 一个应用的布局是丰富的,有TextView,ImageView,Button等,这些子View的外层还有ViewGroup,如RelativeLayout,LinearLayout.作为一个开发者,我们会思考,当点击一个按钮,Android系统是怎样确定我点的就是按钮而不是TextView的?然后还正确的响应了按钮的点击事件.内部经过了一系列什么过程呢? 先铺垫一些知识能更加清晰的理解事件分发机制: 1. 通过setC

  • Android事件分发机制的详解

    Android事件分发机制 我们只考虑最重要的四个触摸事件,即:DOWN,MOVE,UP和CANCEL.一个手势(gesture)是一个事件列,以一个DOWN事件开始(当用户触摸屏幕时产生),后跟0个或多个MOVE事件(当用户四处移动手指时产生),最后跟一个单独的UP或CANCEL事件(当用户手指离开屏幕或者系统告诉你手势(gesture)由于其他原因结束时产生).当我们说到"手势剩余部分"时指的是手势后续的MOVE事件和最后的UP或CANCEL事件. 在这里我也不考虑多点触摸手势(我

随机推荐