View事件传递之父View和子View之间的那点事
Android
事件传递流程在网上可以找到很多资料,FrameWork
层输入事件和消费事件,可以参考:
- [Touch事件派发过程详解] 1
这篇blog阐述了底层是如何处理屏幕输,并往上传递的。Touch
事件传递到Activity
的DecorView
时,往下走就是ViewGroup
和子View
之间的事件传递,可以参考郭神的这两篇博客
郭神的两篇博客清楚明白地说明了View
之间事件传递的大方向,但是具体的一些晦暗的细节阐述较少,本文主要是总结这两篇博客的同时,侧重于两点:
- 事件分发过程中一些细节到底如何实现的?
- 子
view
到底如何和父View
抢事件,父View
又是如何拦截事件不发送给子View
,以及如果我们需要处理这种混乱的关系才能让两者和谐相处?。
MotionEvent抽象
要明白View
的事件传递,很有必要先说一下Touch
事件是如何在Android
系统中抽象的,这主要使用的就是MotionEvent
。这个类经历了几次重大的修改,一次是在2.x版本支持多点触摸,一次是4.x将大部分代码甩给native
层处理。
一次简单的事件
我们先举个栗子来说明一次完整的事件,用户触屏 滑动 到手机离开屏幕,这认为是一次完整动作序列(movement traces
)。一个动作序列中包含很多动作Action
,比如在用户按下时,会封装一个MotionEvent
,分发给视图树,我们可以通过motionevent.getAction
拿到这个动作是ACTION_DOWN
。同样,在手指抬起时,我们可以接收到Action
类型是Action_UP
的MotionEvent
。对于滑动(MOVE
)这个操作,Android
为了从效率出发,会将多个MOVE
动作打包到一个MotionEvent
中。通过getX getY
可以获取当前的坐标,如果要访问打包的缓存数据,可以通过getHistorical**()
函数来获取。
加入多点触摸
对于单点的操作来看,MotionEvent
显得比较简单,但是考虑引入多点触摸呢?我们定义一个接触点为(Pointer
)。我们从onTouch
接受到一个MotionEvent
,怎么拿到多个触碰点的信息?为了解开笔者刚开始学习这部分知识时的困惑,我们首先树立起一种概念:一个MotionEvent
只允许有一个Action
(动作),而且这个Action
会包含触发这次Action
的触碰点信息,对于MOVE
操作来说,一定是当前所有触碰点都在动。只有ACTION_POINTER_DOWN
这类事件事件会在Action
里面指定是哪一个POINTER
按下。
在MotionEvent
的底层实现中,是通过一个16位来存储Action
和Pointer
信息(PointerIndex
)。低8位表示Action
,理论上可以表示255种动作类型;高8位表示触发这个Action
的PointerIndex
,理论上Android
最多可以支持255点同时触摸,但是在上层代码使用的时候,默认多点最多存在32个,不然事件在分发的时候会有问题。
MotionEvent
中多个手指的操作API
大部分都是通过pointerindex
来进行的,如:获取不同Pointer
的触碰位置,getX(int pointerIndex)
;获取PointerId
等等。大部分情况下,pointerid == pointeridex
。
ACTION_DOWN
OR ACTION_POINTER_DOWN
:
这两个按下操作的区别是ACTION_DOWN
是一个系列动作的开始,而ACTION_POINTER_DOWN
是在一个系列动作中间有另外一个触碰点触碰到屏幕。
这部分详细的描述,请参考:
android触控,先了解MotionEvent
到这里,铺垫终于结束了,我们开始直奔主题。
View的事件传递
Android
的Touch
事件传递到Activity
顶层的DecorView
(一个FrameLayout
)之后,会通过ViewGroup
一层层往视图树的上面传递,最终将事件传递给实际接收的View
。下面给出一些重要的方法。如果你对这个流程比较熟悉的话,可以跳过这里,直接进入第二部分。
dispatchTouchEvent
事件传递到一个ViewGroup
上面时,会调用dispatchTouchEvent
。代码有删减
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Attention 1 :在按下时候清除一些状态
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
//注意这个方法
resetTouchState();
}
// Attention 2:检查是否需要拦截
final boolean intercepted;
//如果刚刚按下 或者 已经有子View来处理
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
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 {
// 不是一个动作序列的开始 同时也没有子View来处理,直接拦截
intercepted = true;
}
//事件没有取消 同时没有被当前ViewGroup拦截,去找是否有子View接盘
if (!canceled && !intercepted) {
//如果这是一系列动作的开始 或者有一个新的Pointer按下 我们需要去找能够处理这个Pointer的子View
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
//上面说的触碰点32的限制就是这里导致
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
//对当前ViewGroup的所有子View进行排序,在上层的放在开始
final ArrayList<View> preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
// canViewReceivePointerEvents visible的View都可以接受事件
// isTransformedTouchPointInView 计算是否落在点击区域上
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//能够处理这个Pointer的View是否已经处理之前的Pointer,那么把
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
} }
//Attention 3 : 直接发给子View
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
// 前面已经找到了接收事件的子View,如果为NULL,表示没有子View来接手,当前ViewGroup需要来处理
if (mFirstTouchTarget == null) {
// ViewGroup处理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
if(alreadyDispatchedToNewTouchTarget) {
//ignore some code
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
}
return handled;
}
上面代码中的Attention
在后面部分将会涉及,重点注意。
这里需要指出一点的是,一系列动作中的不同Pointer
可以分配给不同的View
去响应。ViewGroup会维护一个PointerId
和处理View
的列表TouchTarget
,一个TouchTarget
代表一个可以处理Pointer
的子View
,当然一个View
可以处理多个Pointer
,比如两根手指都在某一个子View
区域。TouchTarget
内部使用一个int
来存储它能处理的PointerId
,一个int
32位,这也就是上层为啥最多只能允许同时最多32点触碰。
看一下Attention 3
处的代码,我们经常说view
的dispatchTouchEvent
如果返回false,那么它就不能系列动作后面的动作,这是为啥呢?因为Attention 3
处如果返回false
,那么它不会被记录到TouchTarget
中,ViewGroup认为你没有能力处理这个事件。
这里可以看到,ViewGroup
真正处理事件是在dispatchTransformedTouchEvent
里面,跟进去看看:
dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
//没有子类处理,那么交给viewgroup处理
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);
}
return handled;
}
可以看到这里不管怎么样,都会调用View
的dispatchTouchEvent
,这是真正处理这一次点击事件的地方。
dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
if (onFilterTouchEventForSecurity(event)) {
//先走View的onTouch事件,如果onTouch返回True
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
我们给View
设置的onTouch
事件处在一个较高的优先级,如果onTouch
执行返回true
,那么就不会去走view
的onTouchEvent
,而我们一些点击事件都是在onTouchEvent
中处理的,这也是为什么onTouch
中返回true,view
的点击相关事件不会被处理。
小小总结一下这个流程
ViewGroup
在接受到上级传下来的事件时,如果是一系列Touch
事件的开始(ACTION_DOWN
),ViewGroup
会先看看自己需不需要拦截这个事件(onInterceptTouchEvent
,ViewGroup
的默认实现直接返回false
表示不拦截),接着ViewGroup
遍历自己所有的View
。找到当前点击的那个View
,马上调用目标View
的dispatchTouchEvent
。如果目标View
的dispatchTouchEvent
返回false,那么认为目标View
只是在那个位置而已,它并不想接受这个事件,只想安安静静的做一个View
(我静静地看着你们装*)。此时,ViewGroup
还会去走一下自己dispatchTouchEvent,Done!
子View和父View的撕*大战
终于来到本文的重要环节,子View和父布局(ViewGroup
)是如何撕逼的。我们经常遇到这样的问题:在ListView
中放一个ViewPager
不能滑动的问题,其实这里就会涉及到子View和布局之间的协商,事件处理到底你上还是我上。
首先需要明确一点的是,一个事件肯定是由ViewGroup
传递给自己的子View
的,所以ViewGroup
具有绝对的权威来禁止事件往下传,这就是onInterceptTouchEvent
方法。可以看上面ViewGroup中的dispatchTouchEvent
的Attention 1
和 Attention 2
。
先看Attetion2
:
进行判断有有两个条件:1,如果是一次新的事件 or 在一次事件中但是已经有子View来处理这个事件,那么父类需要去看看是否拦截这次事件。否则,直接拦截(此时处于一系列动作的中间,而且没有子view来接盘,那么ViewGroup就直接拦下来)。
决定是否拦截有两个步骤,
disallowIntercept
是否驳回拦截,默认false
。注意这个值是子View
和撕*的关键,因为ViewGroup
开放了给这个标记赋值的接口requestDisallowInterceptTouchEvent()
,而且这个方法直接往上递归,这个ViewGroup
的各级父容器都会设置驳回拦截。onInterceptTouchEvent
虽然ViewGroup
中默认返回false,但是在很多有滑动功能的ViewGroup
里面(如scrollview ListView
等)会处理各种情况,决定是否拦截这个事件,所以就会出现之前说的ListView
中的Viewpager
不能滑动的问题,原因是事件被父View拦截了。
在Attetion1
的位置如果是一次新的ACTION_DOWN
,那么会把之前事件传递设置的各种状态清除。
对ViewGroup来说需要做什么
对于一个需要拦截事件的ViewGroup
,它通常都有一些特殊的操作,比如ScrollView
,比如ViewPager
,它重写onInterceptTouchEvent
是非常关键的,这也是能和子View
和谐相处的关键。举个例子,我自己定义了一个ViewGroup
:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
return true;
}
return super.onInterceptTouchEvent(ev);
}
这样会发生什么?
所有位于MyViewGroup中的子View收不到任何的事件,原因可以看一下Attention2
的代码,判断是否拦截是在系列动作按下时会进行判断,如果此时拦截,那么直接不会去查找相应处理的子View,所以touchtarget
为空,那么接下来的动作都直接被ViewGroup
笑纳。
所以哪怕再强势的ViewGroup
,一般都是在Down
的时候给子类机会去掉用requestDisallowInterceptTouchEvent
,如设置驳回拦截,那么在ViewGroup分发事件的时候,会跳过onInterceptTouchEvent
的执行。
子View需要做什么
对于子View来说,在合适的时机调用requestDisallowInterceptTouchEvent
即可。当然啥时候合适?对于一个View
来说,那就是在dispatchTouchEvent
或者onTouchEvent
来调用。
对于ViewGroup
来说,通常我们会在onInterceptTouchEvent
进行判断。比如我们经常会遇到在ListView
里面套了ViewPager
导致ViewPager
不能滑动的问题,通常的处理方式:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (absListView != null) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getX();
mDownY = event.getY();
//ACTION_DOWN的时候,赶紧把事件hold住
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if(Math.abs(event.getX() - mDownX)>Math.abs(event.getY()-mDownY)) {
getParent().requestDisallowInterceptTouchEvent(true);
}else {
//发现不是自己处理,还给父类
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
//其实这里是多余的
getParent().requestDisallowInterceptTouchEvent(false);
}
}
return super.onInterceptTouchEvent(event);
}
总结
本来打算写一个短篇的,结果一个不小心,弄成了长篇大论。
最后需要注意一点的是,所有我们上述讨论的内容都是在一层层递归中进行,而且requestDisallowInterceptTouchEvent
这个函数也是递归调用的。
我们可以认为ViewGroup
是一个具有绝对话语权但是从不专政的霸道总裁,它自己可以拦截处理某些事件,比如Viewpager
的横滑,但是它也可以给子View足够的空间去要求这个事件给自己处理。作为一名开发者,一方面在自己定义ViewGroup
时需要考虑能够给子View足够空间中断自己的拦截;一方面自己定义View时,我们需要在合适的时候跟父View索要事件。ViewPager(新版)
作为容器来说,它需要拦截横滑事件,同时,自己具备了和父View
争抢事件的能力,所以不管把ViewPager
放到什么布局中,它都能正确处理。看看它的onInterceptTouchEvent
怎么写的吧,完美的体现了这一思想。
View事件传递之父View和子View之间的那点事的更多相关文章
- Android View事件传递机制
ViewGroup dispatchTouchEvent onInterceptTouchEvent onTouch View dispatchTouchEvent onTouch 假设View的层级 ...
- 公共技术点( View 事件传递)
转载地址:http://p.codekk.com/blogs/detail/54cfab086c4761e5001b253e 本文为 Android 开源项目源码解析 公共技术点中的 View 事件传 ...
- Android的View 事件传递
欢迎转载,请附出处: http://blog.csdn.net/as02446418/article/details/47422891 1.基础知识 (1) 全部 Touch 事件都被封装成了 Mot ...
- React中父组件与子组件之间的数据传递和标准化的思考
React中父组件与子组件之间的数据传递的的实现大家都可以轻易做到,但对比很多人的实现方法,总是会有或多或少的差异.在一个团队中,这种实现的差异体现了每个人各自的理解的不同,但是反过来思考,一个团队用 ...
- Iframe父页面与子页面之间的调用
原文:Iframe父页面与子页面之间的调用 Iframe父页面与子页面之间的调用 专业词语解释如下: Iframe:iframe元素是文档中的文档. window对象: 浏览器会在其打 ...
- Iframe父页面与子页面之间的相互调用
iframe元素就是文档中的文档. window对象: 浏览器会在其打开一个HTML文档时创建一个对应的window对象.但是,如果一个文档定义了一个或者多个框架(即:包含一个或者多个frame或者i ...
- js父页面和子页面之间传值
今天和朋友一块讨论,怎样通过js在父页面和子页面之间传值的问题,总结例如以下: 需求描写叙述:父页面有多个子页面.实如今父页面点击子页面,传值到子页面. 看着非常easy,试了好久.主要纠结在怎样获取 ...
- Android Touch事件之一:Touch事件在父ViewGroup和子View之间的传递篇
2015-11-26 17:00:22 前言:Android的Touch事件传递和View的实现紧密相连,因此理解Touch事件的传递,有助于我们更好的理解View的工作原理. 1. 几个重要的方法: ...
- ViewGroup 和 View 事件传递及处理小谈
前言 在自定义组件的时候少不了会去处理一些事件相关的东西,关于事件这块网上有很多文章,有说的对的也有说的不对的,我在理解的时候也有过一段时间的迷惑,现在把自己理解的东西写下来,给有相同疑问的朋友提供些 ...
随机推荐
- position:fixed定位时 “高度坍塌” 问题的解决
问题:对于固定定位的元素,固定住高度,后面紧跟的模块会当做前面的固定元素不存在似的,这给布局带来了困扰 解决方法: 1.给第二个模块div设置margin-top的值,margin-top的值设为大于 ...
- UML基础知识
UML:Unified Modeling Language,即统一建模语言.是一种图形化的建模语言标准. 如上图,UML可以帮助我们做软件需求分析和软件设计两方面的工作,在不同的应用场景中,UML的一 ...
- Swift--基础(一)基本类型 符号 字符串(不熟的地方)
常量 变量 let age = 20 常量不可变 var num = 24 变量可变 let count:Int = 2 定义类型 Double(count) 类型转换 符号 1.?? let de ...
- 活动指示器UIActivityIndicatorView
活动指示器UIActivityIndicatorView可以告知用户有一个操作正在进行中 1.创建 UIActivityIndicatorView *activityIndicatorView ...
- UVA10090 数论基础 exgcd
题目链接:https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem ...
- include,include_once,require,require_once的区别
1.include,require在其被调用的位置处包含一个文件. 2.include_once,require_once函数的作用与include相同,不过它会首先验证是否已包含该文件.如果已经包含 ...
- [Mugeda HTML5技术教程之5] 创建新作品
前一节,我们介绍了Mugeda Studio.这一节我们讲一下怎么通过Studio创建新作品.首先登陆网站,如果还没有登陆账号,你可以登录 www.mugeda.com 免费注册一个.登录网站后,点击 ...
- php 图片上传预览(转)
网上找的图片上传预览: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http:/ ...
- Python番外 事务 那些事
Transaction 也就是所谓的事务了,通俗理解就是一件事情.从小,父母就教育我们,做事情要有始有终,不能半途而废. 事务也是这样,不能做一般就不做了,要么做完,要么就不做.也就是说,事务必须是一 ...
- 移动端远程关闭PC端实现(一)需求设计
公司有台半新不旧的电脑,因无甚大用,就拿来做了服务器,服务于民.服务器所提供的功能不是太多,无非是数据库以及svn服务. 公司每天下班会断电,我们吧会常常忘记关闭服务器,所以服务器非正常关机的次数约等 ...