Android 源码详解:View的事件分发机制
Posted 鸽一门
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 源码详解:View的事件分发机制相关的知识,希望对你有一定的参考价值。
(在此声明:以下内容为阅读任玉刚老师的《android开发艺术探索》中结合自身理解融合而成)
问题:
1. 点击事件传递过程中,若所有View不消费此事件,结果?1
2. Window是如何传递事件给ViewGroup?2
3. 子元素是否可以干预父元素的分发过程?How?3
4. onInterceptTouchEvent 方法是否在每次判断点击事件都会被调用?3
5. ViewGroup是如何将点击事件传递给子View处理?
6. 若给某个View设置了OnTouchListener、onTouchEvent和OnClickListener,三者之间的优先级如何?
关于View的事件分发机制,之前写过一篇介绍主要是理论部分:http://blog.csdn.net/itermeng/article/details/52242224
回顾上面的问题,如果你有疑惑的地方,也许应该从源码的角度去解决,单纯的理论部分无法很好的解决疑惑,分析源码更加受益匪浅:
1. Activity 对点击事件的分发过程
1.1 逻辑简介
当一个点击操作(MotionEvent)发生时,根据事件传递机制,最先接收的是 Activity,由Activity 的 dispatchTouchEvent 来进行事件派发,实质是Activity内部的 Window 完成的。Window会将事件传递给 DecorView(指当前界面的底层容器,即setContentView设置的View的父容器),通过
Activity.getWindow.getDecorView()
可以获得。
1.2 Activity的dispatchTouchEvent
因此,首先从Activity的dispatchTouchEvent 方法入手:
//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev)
if(ev.getAction() == MotionEvent.ACTION_DOWN)
onUserInteraction();
if(getWindow().superDispatchTouchEvent(ev))
return true;
return onTouchEvent(ev);
以上代码可证实一开始说的:首先事件交给Activity 附属的 Window 进行分发:
1). 如果返回true,整个事件循环就结束了。
2). 返回false,意味着此点击事件一直传递下去都没有View消费掉(即所有View的 onTouchEvent 都返回了 false), 那么Activity 的 onTouchEvent 就会被调用。
————————————————–【第1个问题解决】———————————————
2. Window对点击事件的分发过程
第一个问题解决后,此点击事件已经传递到Window中,继续往下深究,解决第二个问题:Window是如何传递事件给ViewGroup?
public abstract boolean superDispatchTouchEvent(MotionEvent event);
通过源码可知Window是个抽象类,而它的 superDispatchTouchEvent 也是个抽象方法, 所以必须要找到Window的实现类:
2.1 Window的实现类 :PhoneWindow
其实是 PhoneWindow ,从Window的源码中可以看出,在Window的说明中:
Window类可以控制顶级View的外观和行为策略,它的唯一实现位于 android.policy.PhoneWindow 中,当你要实例化这个
Window类的时候,你并不知道它的细节,因为这个类会被重构,
只有一个工厂方法可以使用。尽管这看起来有些模糊,不过可以看一下 android.policy.PhoneWindow这个类。
所以,接下来查看 PhoneWindow 是如何处理点击事件的:
PhoneWindow.java
public boolean superDispatchEvent(MotionEvent event)
return mDecor.superDispatchTouchEvent(event);
由此可得知,PhoneWindow直接将事件传递给了DecorView,来查看DecorView:
2.2 DecorView
//This is the top-level view of the wondow,containing the window decor
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
private DecorView mDecor;
@Override
public final View getDecorView()
if(mDecor == null)
installDecor();
return mDecor;
看完以上代码,首先了解mDecor 是什么?
平常学习中,通过
((ViewGroup))getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);
这种方式可以获取Activity所设置的View,而这个mDecor 就是:
getWindow().getDecorView()
返回的View,所以通过 setContentView 方法设置的View是它的一个子View。
所以目前点击事件传递到了 DecorView,由于它继承于FrameLayout而且是个父View,所以最终事件会传递给View。代表此时点击事件已经传递给顶级View了!(这里对于顶级View的两种解释:在Activity中通过setContentView所设置的View 或者 叫做根View)
————————————————–【第2个问题解决】———————————————
3. 顶级View对点击事件的分发过程
3.1 逻辑简介
目前为止,第二个问题也解决掉,点击事件已经传递到 根View ,关于接下来的事件分发机制 和 方法的优先级调用,之前一篇博客分析过:
http://blog.csdn.net/itermeng/article/details/52242224
优先级排序:OnTouchListener >onTouchEvent >OnClickListener
这里简单回顾:点击事件达到顶级View(一般是个ViewGroup)后,会调用ViewGroup的dispatchTouchEvent 方法,此方法结果首先取决于onInterceptTouchEvent :
1). 若方法返回true,事件由ViewGroup处理,然后按照方法的优先级,若ViewGroup设置有mOnTouchListener ,则onTouch会被调用,否则调用onTouchEvent.(若都有设置,因为优先级的原因,onTouchEvent事件会被屏蔽掉!)最后如果在onTouchEvent中设置有mOnClickListener,onClick才会被调用。
2). 若方法返回false,代表顶级ViewGroup不拦截此事件,事件会依次传递给子View,这时子View的 dispatchTouchEvent会被调用,根据返回结果如此循环,直至点击事件被某个View消费掉!
3.2 ViewGroup 的 dispatchTouchEvent 方法
以上就是简述事件分发机制的逻辑理论,现在从源码的角度来证明以上观点!首先查看一开始ViewGroup 的 dispatchTouchEvent 方法:
//check for interception
final boolean interception;
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if(!disallowIntercept)
//ViewGroup是否拦截此点击事件由方法返回值决定
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); //restore action in case it was changed
else
//ViewGroup不拦截此点击事件
intercepted = false;
else
//ViewGroup拦截此点击事件
//There are no touch targets and this action is not an initial down
//so this view group continues to intercept touches.
intercepted = true;
以上代码可以看出,ViewGroup在如下两种情况会判断是否要拦截当前点击事件:
1). 点击事件类型为ACTION_DOWN
2). mFirstTouchTarget != null
来解释一下第二种情况:从后面的代码逻辑可得,当点击事件成功被ViewGroup的子元素处理后,会被赋值指向子元素(即mFirstTouchTarget != null
代表ViewGroup不拦截此点击事件,反之则反)。
一旦ViewGroup拦截此点击事件,mFirstTouchTarget != null
条件不成立。那么当ACTION_MOVE 和 ACTION_UP事件到来,最外层的if判断为false,即不会调用ViewGroup的 onInterceptTouchEvent方法,并且同一序列的其它时间都默认交给它处理!
也就是说 某个View一旦开始处理事件,若它不消耗ACTION_DOWN 事件(即onTouchEvent返回了 false),那么同一事件序列的其它事件都不会交给它来处理,并交由父元素处理!
3.3 requestDisallowInterceptTouchEvent
不过,有一种特殊情况:FLAG_DISALLOW_INTERCEPT 标记位,它是通过 requestDisallowInterceptTouchEvent 方法来设置,一般用于子View中。此标记一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的点击事件。为何是除了ACTION_DOWN以外?
因为ViewGroup在分发点击事件时,若是ACTION_DOWN 就会重置标记位,将导致之前设置的标记位无效。因此,当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件。
接下来从源码角度证明以上观点:
//Handle an initial down.
if(actionMasked == MotionEvent.ACTION_DOWN)
//Throw away all previous state when starting a new touch gesture.
//The framework may have dropped the up or cancel event for the previous gesture
cancelAndClearTouchTargets(ev);
resetTouchState();
以上代码证明了观点:ViewGroup在 ACTION_DOWN 事件时重置状态的操作,在resetTouchState 方法中重置标记位,因此子View调用requestDisallowInterceptTouchEvent 方法并不能影响对 ACTION_DOWN 事件的处理。
以上可证明书中结论中的两个观点:
(3)某个View一旦决定拦截,那么这一个事件序列都只能由它来处理
(如果时间序列可以传到它),并且它的 onInterceptTouchEvent
不会再被调用。
(11)事件传递过程是由外向内的,即事件总是先传递给父元素,
再由父元素分发给子View,通过requestDisallowInterceptTouchEvent
方法可以在子元素中干预父元素的分发过程,
但是ACTION_DOWN 事件除外。
最后总结:
onInterceptTouchEvent不是每次事件都会被调用,如果想提前处理所有的点击事件,要选择dispatchTouchEvent 方法,只有这个方法可以保证每次都会被调用,当然前提是事件能够传递到当前的ViewGroup。
————————————————–【第3,4个问题解决】———————————————
3.4 ViewGroup不拦截事件,分发机制
final View[] children = mChildren;
for(int i = childrenCount -1; i>=0; i--)
//......
resetCancelNextUpFlag(child);
if(dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) //★★★
mLastTouchDownTime = ev.getDownTime();
if(preorderedList != null)
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;
以上代码主要逻辑:首先遍历ViewGroup的所有子元素,然后判断是否能够接收到点击事件,这取决于两点:
1). 子元素是否在播动画
2). 点击事件的坐标是否落在子元素的区域内。
若满足以上两点,则点击事件会传递给这个子View来处理。
回到第五个问题:它是如何将点击事件传递给子View处理?
关键是最外层 if 判断中的 dispatchTransformedTouchEvent方法,查看其方法中重要部分:
//dispatchTransformedTouchEvent 方法
if(child == null)
handle = super.dispatchTouchEvent(Event);
else
handle = child.dispatchTouchEvent(Event);
其实dispatchTransformedTouchEvent 实质就是调用子元素的 dispatchTouchEvent方法,所以以上代码中如果child传递的不是null,便会直接调用dispatchTouchEvent,这样点击事件便交由子元素处理,完成一轮事件分发。
————————————————–【第5个问题解决】———————————————
3.5 mFirstTouchTarget
回到以上步骤,如果子元素的dispatchTouchEvent 返回true,这时先不考虑点击事件在这个子View内部的分发,回到ViewGroup 的 dispatchTouchEvent 方法中,此时 mFirstTouchTarget 会被赋值同时跳出for 循环 :
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
以上代码完成了 mFirstTouchTarget 的赋值并终止对子元素的遍历。在遍历的过程中,如果有子元素的dispatchTouchEvent 返回true,ViewGroup就会把此点击事件传递给子View,此时也将停止循环。对接以上逻辑。
但其实mFirstTouchTarget 真正的赋值过程是在 addTouchTarget 内部完成的,来查看此方法:
private TouchTarget addTouchTarget(View child , int pointerIdBits)
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
从此方法可看出,mFirstTouchTarget 其实是一种单链表结构。mFirstTouchTarget 是否被赋值,直接影响到ViewGroup 对事件的拦截策略。比如在3.2 环节介绍的,若mFirstTouchTarget 为 null,那么ViewGroup就默认拦截同一序列的点击事件!
如果遍历所有的子元素后,传递的点击事件并没有被合适地处理掉,原因有两种:
1). ViewGroup中没有子元素。
2). 子元素处理了点击事件,可是在与ViewGroup联系的 dispatchTouchEvent方法中返回了false(一般是因为子元素在onTouchEvent中返回了false)。
//Dispatch to touch target
if(mFirstTouchTarget == null)
//No touch targets so treat this as an ordinary view
handle = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
由上可得,在以上两种情况下,ViewGroup会自己处理点击事件,这也证明了书中第4条结论:
(4)某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件
(onTouchEvent返回了false),那么同一事件序列的其它事件不会交给它处理,重新将事件交由父元素处理,即父元素的onTouchEvent被调用。
以上,ViewGroup的事件传递机制以分析完,现下结束点击事件传递到View上,它的传递机制:
4. View 对点击事件的分发过程
注意这里的View不包含 ViewGroup。View对于点击事件的处理相较于ViewGroup容易些,因为它是一个单独的元素,没有子元素无法向下传递事件,所以它只能自己处理事件。
4.1 View 的dispatchTouchEvent
同第三点分析ViewGroup一样,首先分析View的dispatchTouchEvent 方法:
public boolean dispatchTouchEvent(MotionEvent event)
boolean result = false;
......
if(onFilterTouchEventForSecurity(event))
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对点击事件的处理过程:(最外层 if方法体中)
首先 内层中第一个if 中判断有没有设置 onTouchListener:
如果onTouchListener中的 onTouch方法返回true,那么result设置为true,则第二个if 中语句不会执行(onTouchEvent不会被执行)。即证实方法体中优先级排序,这样的好处是方便在外界处理点击事件。
4.2 onTouchEvent
接下来查看 onTouchEvent 的实现,先看View处于不可用状态下点击事件的处理过程:
//onTouchEvent 方法体中
if((viewFlags & ENABLED_MASK) == DISABLED)
if(event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0)
setPressed(false);
// A disabled view that is clickable still consume the touch
//events,it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE || (((viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
如上所示,不可用状态下的View照样会消耗点击事件,尽管它看起来不可用。
接下来查看onTouchEvent 对点击时间的具体处理:
//onTouchEvent 方法体中
if((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE))
switch(event.getAction())
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if((mPrivateFlags & PFLAG_PREPRESSED)!= 0 || prepressed )
...
if(!mHasPerformedLongPress)
removeLongPressCallback();
if(!focusTaken)
if(mPerformClick == null)
mPerformClick = new PerformClick();
if(!post(mPerformClick ))
performClick();
...
break;
...
由上可知,只要View的CLICKABLE 和LONG_CLICKABLE属性有一个为true,那么它会消费此事件,即 onTouchEvent方法返回true,不管它是否是DISABLE状态。证实了书中结论的8、9、10点:、
(8)View的 onTouchevent 默认都会消耗事件(返回true),
除非它是不可点击的(clickable 和 longClickable同时为false)。
View的longClickable属性默认都为false,clickable属性要分情况:
比如Button的为true,TextView的为false.
(9)View的enable属性不影响 onTouchEvent的默认返回值。
哪怕一个View是disable状态,只要它的clickable 或者 longClickable
有一个为true,那么它的onTouchEvent方法返回值为true。
(10)onClick会发生的前提是当前View是可点击的,
并且它收到了down、up事件
证实了以上几点结论后,回到上面代码中,当ACTION_UP事件发生时,会触发performClick方法,查看具体实现:
//performClick方法体中
public boolean performClick()
final boolean result;
final ListenerInfo li = mListenerInfo;
if(li != null && li.mOnClickListener != null)
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result =true;
else
result = false;
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
可得,如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法。
所以这也更加确定在讲解方法中优先级时:OnClickListener的优先级最低,在onTouch(如果View没有设置OnTouchListener)方法中才会考虑到OnClickListener。
以上为止,已经从源码的角度证明完分发机制中调用那3个方法的优先级了。
————————————————–【第6个问题解决】———————————————
4.3 setClickable 和 setLongClickable
接下来补充一个小细节,之前说过View 的long_clickable的属性值默认为false,而clickable属性值根据不同的View而定。其实通过setClickable 和setLongClickable可以分别改变两者属性。
注意:在给view设置 setOnClickListener 会自动将view 的 CLICKABLE 设为true,setOnLongClickListener 亦然,查看其源码:
public void setOnClickListener(OnClickListener l)
if(!isClickable())
setClickable(true);
getListenerInfo().mOnClickListener = l;
public void setOnLongClickListener(OnLongClickListener l)
if(!isLongClickable())
setLongClickable(true);
getListenerInfo().mOnLongClickListener = 1;
以上部分阅读完,再次回顾一开始提出的问题,相信应该已经解决了,而View的事件分发机制应该也掌握好了,推荐任玉刚老师的《Android开发艺术探索》。
希望对你有帮助 :)
以上是关于Android 源码详解:View的事件分发机制的主要内容,如果未能解决你的问题,请参考以下文章
Android6.0 ViewGroup/View 事件分发机制详解