Android事件分发机制(二)

这篇文章继续讨论Android事件分发机制,首先我们来探讨一下,什么是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 boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}

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事件分发机制完全解析到此全部结束,结合上下两篇,相信大家对事件分发的理解已经非常深刻了。

时间: 2024-08-10 19:16:51

Android事件分发机制(二)的相关文章

图解 Android 事件分发机制

首发原文:http://mp.weixin.qq.com/s?__biz=MzI0MjE3OTYwMg==&mid=2649548149&idx=1&sn=709149df682c7d3a6e453c9ef0626a1f&chksm=f1180e08c66f871eb2e7e39e057a5b090214fd71adcd98aa36b3d7fcecf77ad5d08138c50131#rd 在Android开发中,事件分发机制是一块Android比较重要的知识体系,了解并熟

Android事件分发机制实例验证

Android事件分发机制实例验证 欢迎转载,转载请注明原文出处http://blog.csdn.net/lavor_zl/article/details/51198634,谢谢. 事件分发机制的学习主要来自下面两篇博文: Android事件传递机制 Android 编程下 Touch 事件的分发和消费机制 这两篇博文写的非常好,但是看完了这两篇博文还是有一些不理解的地方,缺少一丝明悟.于是亲自写下几种情况的代码,来看事件分发的结果,从而验证事件分发机制.验证完后瞬间有了一种明悟,感觉豁然开朗.

浅谈Android 事件分发机制(一)

在上一篇文章中,浅谈Android 事件分发机制(一),简要分析了一下事件分发机制的原理,总结一下就是事件层层传递,直到被消费,原理看似简单,但是在实际使用过程中,场景各不相同,复杂程度也就因产品而异,这篇文章就通过给view加移动来模拟事件分发. 触摸事件 这里涉及到几个与手指触摸相关的常见事件: 坐标系对于单指触控移动来说,一次简单的交互流程是这样的:手指落下(ACTION_DOWN) -> 移动(ACTION_MOVE) -> 离开(ACTION_UP) 坐标系 Android坐标系以手

【朝花夕拾】Android自定义View篇之(六)Android事件分发机制(中)从源码分析事件分发逻辑及经常遇到的一些“诡异”现象

前言 转载请注明,转自[https://www.cnblogs.com/andy-songwei/p/11039252.html]谢谢! 在上一篇文章[[朝花夕拾]Android自定义View篇之(五)Android事件分发机制(上)Touch三个重要方法的处理逻辑][下文简称(五),请先阅读完(五)再阅读本文],我们通过示例和log来分析了Android的事件分发机制.这些,我们只是看到了现象,如果要进一步了解事件分发机制,这是不够的,我们还需要透过现象看本质,去研究研究源码.本文将从源码(基

Android事件分发机制详解:史上最全面、最易懂

前言 Android事件分发机制是每个Android开发者必须了解的基础知识 网上有大量关于Android事件分发机制的文章,但存在一些问题:内容不全.思路不清晰.无源码分析.简单问题复杂化等等 今天,我将全面总结Android的事件分发机制,我能保证这是市面上的最全面.最清晰.最易懂的 本文秉着"结论先行.详细分析在后"的原则,即先让大家感性认识,再通过理性分析从而理解问题: 所以,请各位读者先记住结论,再往下继续看分析: 文章较长,阅读需要较长时间,建议收藏等充足时间再进行阅读 目

Android事件分发机制

转载请注明出处:http://blog.csdn.net/chziroy/article/details/44401615 要理解Android事件分发机制,首先得了解几个概念,也算是总结,如果暂时看不懂也无妨,本文会讲解这几个问题. 1,点击屏幕,首先事件的传递从Activity的dispatchTouchEvent()方法开始. 2,关于Android事件分发机制,相关方法的方法有三个:onTouchEvent(),dispatchTouchEvent(),还有onInterceptTouc

Android事件分发机制的学习

最近被Android事件分发机制折磨的很烦躁,网上各种博客资料看完觉得还是得自己写一篇,一方面加深理解,另一方面希望能帮助到也同样在学习相关知识的童鞋们. 话不多说,直接开整. 当用户的手指点击到屏幕,便是整个事件的开始. 首先获取到该事件的是view层的控制者Activity,具体怎么获得我们不得而知,在此也不追究,而继续我们的主题.Activity获得事件后便执行它自身的方法: public boolean dispatchTouchEvent(MotionEvent ev) { if (e

Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/9097463 其实我一直准备写一篇关于Android事件分发机制的文章,从我的第一篇博客开始,就零零散散在好多地方使用到了Android事件分发的知识.也有好多朋友问过我各种问题,比如:onTouch和onTouchEvent有什么区别,又该如何使用?为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?为什么图片轮播器里的图片使用Button而不用Ima

android 事件分发机制(图文详解)

在Android开发中,事件分发机制是一块Android比较重要的知识体系,了解并熟悉整套的分发机制有助于更好的分析各种点击滑动失效问题,更好去扩展控件的事件功能和开发自定义控件,同时事件分发机制也是Android面试必问考点之一,如果你能把下面的一些事件分发图当场画出来肯定加分不少.废话不多说,总结一句:事件分发机制很重要. Android 事件分发流 关于Android 事件分发机制网上的博文很多,但是很多都是写个Demo然后贴一下输出的Log或者拿源码分析,然后一堆的注释和说明,如果用心的