view的事件分发主要是指点击事件,也就是系统对MotionEvent的传递
准备or拓展
首先,拓展了解下Android界面的架构图
UI界面架构Activity –>PhoneWindow –>DectorView –>TitleView/ContentView
- PhoneWindow:每个activity都包含一个window对象,具体由PhoneWindow实现
- DectorView:作为界面的根View,继承FrameLayout,是整个窗口界面的顶层视图。
- TitleView:继承FrameLayout,actionbar就是设置在这里
- ContentView:ID为content的FrameLayout,activity_main.xml就是设置在这里面
主界面详细结构
- DecorView下层的ViewGroup装载了一个LinerLayout,而DecorView本身会在onResume()中装载,系统将其添加到PhoneWindow中。
key point:requestWindowFeature(Window.FEATURE_NO_TITLE)一定要在setContentView()方法之前调用才会生效
(至于原因,去看源码…)
View的事件分发(or传递)
1. 点击事件的产生
这里所谓的点击事件所对应的对象就是MotionEvent, 典型有三种类型:
- ACTION_DOWN :手指接触屏幕
- ACTION_MOVE :手指在屏幕上移动
- ACTION_UP:手指从屏幕上松开的一瞬间
正常情况下,一次手指触摸屏幕的行为会产生一系列点击事件。事件序列即:
DOWN -> UP 或者 DOWN -> MOVE-> … ->MOVE -> UP ;
一个事件序列由DOWN开始,中间含有n个MOVE事件,最终以UP结束。
2. 事件的传递规则
- 2.1 事件的传递顺序:Activity -> Window -> Views
事件总是先传递给Activity,Activity再传递给Window,最后Window再传给顶层View,如果顶层不处理,继续传给下一层View,重复下去。如果所有的View都不处理事件,那么事件最终会交给Activity.OnTouchEvent()处理
先上张图,下面都会围绕这张图展开
传递规则
- 2.2 事件传递的3个核心方法:
- public boolean dispatchTouchEvent(MotionEvent event)
分发点击事件,将事件向下传递给目标View,如果当前View是目标View,则交给本View处理。返回值受当前View的OnTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。 - public boolean onInterceptTouchEvent(MotionEvent event)
在dispatchTouchEvent内部调用,用来判断是否拦截事件,如果当前View拦截了某个事件,则调用onTouchEvent处理事件,并且在同一个事件序列当中,此方法不会再被调用,返回结果表示是否拦截当前事件。 - public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View不再接收到事件。
- public boolean dispatchTouchEvent(MotionEvent event)
注:在处理事件时,如果一个View设置了OnTouchListener,那么listener中的OnTouch方法的返回值将影响事件的下一步传递,如果为true,则OnTouchEvent不会被调用,如果为false,则调用当前View的OnTouchEvent。在OnTouchEvent中,如果设置了OnClickListener,则OnClick方法会被调用。
- 2.3 默认机制:
(1)默认情况下,一个事件序列只能被一个View处理。
某个View一旦决定拦截某个事件,那么这个事件序列都会交给此View。反之,如果某View不消耗ACTION_DOWN事件(即它的onTouchEvent返回false),那么此事件序列不会再给它处理,会返回给此View的上一级去处理(即调用上一级的onTouchEvent)。
(2)默认情况下,View的OnTouchEvent都会消耗事件,除非是不可点击状态(即clickable为false),而且view的enable属性不影响OnTouchEvent的默认返回值,只要其clickable为true,OnTouchEvent就会返回true。
3. 源码分析
从故事开始的地方说起,
1.当一个MotionEvent产生,事件最先传给activity,由activity进行分发,
activity会将事件交给window的dispatchTouchEvent去处理,如果下级View们都不处理,则调用自身的onTouchEvent处理
注:可以在activity中重写此方法,以便在事件发送到窗口之前拦截所有触摸屏事件
2.事件传给Window
1234567891011121314151617181920
//Window public abstract boolean superDispatchTouchEvent(MotionEvent event); //PhoneWindow(实现类) public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } ``` 抽象类window会将事件交由它的实现类 PhoneWindow去处理 PhoneWindow直接将事件传递给了DecorView3.事件再到DecorView ``` java public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
在上面曾提到,DecorView是继承自FrameLayout,所以直接调用父类的dispatchTouchEvent去处理事件,就这样将事件从activity传到了根View
4.事件进入正轨,从根ViewGroup开始,我们拆分来看
(一)
123456789101112131415161718192021222324
public boolean dispatchTouchEvent(MotionEvent ev) { ... ... // Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); } //mFirstTouchTarget:First touch target in the linked list of touch targets. // Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); } else { intercepted = false; } } else { intercepted = true; }}
先看其中的两个操作check for interception 、 handle an initial down;
(1)check for interception:
从字面意思来看:判断是否拦截事件。在当前事件为ACTION_DOWN时或者mFirstTouchTarget != null时,ViewGroup会判断是否拦截事件。这里的mFirstTouchTarget != null是指有子view要处理此事件。所以一旦ViewGroup决定拦截事件,那么mFirstTouchTarget就为null,而且当事件序列中后续的move和up事件到来时,判断条件不满足,导致ViewGroup中的OnInterceptTouchEvent不会被重复调用。
满足条件后,ViewGroup会判断标记位FLAG_DISALLOW_INTERCEPT的值(一般在子View中设置),如果禁止拦截,Viewgroup将无法拦截除了ACTION_DOWN之外的事件(因为在处理ACTION_DOWN事件的时候,Viewgroup会在下面的”initial down”中resetTouchState重置标志位);反之则调用onInterceptTouchEvent去判断是否拦截事件。
(2)handle an initial down:
在ACTION_DOWN事件到来的时候,清除上一个触摸手势所保存的状态,包括FLAG_DISALLOW_INTERCEPT标志位的重置。所以在面对ACTION_DOWN事件时,ViewGroup总会调用OnInterceptTouchEvent判断是否拦截事件。
(二)
1234567891011121314151617181920212223242526272829303132333435363738394041424344
public boolean dispatchTouchEvent(MotionEvent ev) { .... if (!canceled && !intercepted) { // If the event is targeting accessiiblity focus we give it to the // view that has accessibility focus and if it does not handle it .... final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { .... if (!canViewReceivePointerEvents(child)|| !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } .... if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){ .... newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } } } // Dispatch to touch targets. if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev,canceled,null,TouchTarget.ALL_POINTER_IDS; } ...}private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { ..... if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } return handled; .....}
如果ViewGroup不拦截事件,则交由分发给子项。具体实现:
先遍历ViewGroup所有子元素,判断子元素是否能收到点击事件,如果能,则事件会传递给它来处理。可以看到dispatchTransformedTouchEvent其实就是调用子view的dispatchTouchEvent。如果子View选择处理事件,则dispatchTouchEvent就会返回true,同时mFirstTouchTarget就会被赋值并结束循环。如果当前子元素不处理,则进入下一个子元素的循环。
如果ViewGroup没有子元素,或者子元素的dispatchTouchEvent返回了false,导致mFirstTouchTarget未被赋值,这时ViewGroup就会自己处理事件。
5.事件交给了View
(1)View中的dispatchTouchEvent
从代码中可以看出,View会先判断有没有设置OnTouchListener,如果有且onTouch方法返回true,则不再调用下面的onTouchEvent,这样就方便在外部处理MotionEvent。
(2)View中的onTouchEvent
这个地方就应证了上面所说的,view的enable属性不影响view消耗事件,只和CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE属性有关。
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
public boolean onTouchEvent(MotionEvent event) { if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) { case MotionEvent.ACTION_UP: .... if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check removeLongPressCallback(); if (!focusTaken) { // Use a Runnable and post this rather than calling // performClick directly. This lets other visual state // of the view update before click actions start. if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } } } break; case MotionEvent.ACTION_DOWN: // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { .... } else { // Not inside a scrolling container, so show the feedback right away setPressed(true, x, y); checkForLongClick(0, x, y); } break; case MotionEvent.ACTION_CANCEL: .... break; case MotionEvent.ACTION_MOVE: .... break; } return true; }}private final class CheckForLongPress implements Runnable { public void run() { if (isPressed() && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick(mX, mY)) { mHasPerformedLongPress = true; } } }}
可以看到,当ACTION_UP事件发生时,如果不是longPress会调用performClick,在这个方法里,如果view设置了OnClickListener,则会调用它的OnClick方法,而且performClick是通过Runnable发布调用的,方便在处理事件之前更新View状态。
当ACTION_DOWN事件发生时,View会检查是否是LongClick事件,如果长按事件设置了回调,则执行回调方法。而且如果回调返回true的话,mHasPerformedLongPress就会设置为true,所以在长按时,当上面UP事件发生,performClick就无法调用,即短按事件就会被屏蔽。
总结
View事件分发的流程大概就是这样,后面还会继续研究滑动冲突的问题。最后,说说源码分析的那点事儿,过程很蓝瘦,但是得学会取舍,一时看不懂先略过,注重大体逻辑,跟着翻译走,没准在后面就能明白前面变量或者方法的意义,水到渠成。