Android系列View的事件分发机制
Posted jzyhywxz
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android系列View的事件分发机制相关的知识,希望对你有一定的参考价值。
本文将介绍View的一个核心知识点:事件分发机制,了解并掌握事件分发机制是作为一个android程序员必不可少的技能。
事件和事件序列
当我们使用手机时,可以说无时无刻不在和事件打交道:当我们在手机屏幕上点击、滑动或者长按时,都会产生一系列的事件,而手机正是通过这些事件和我们进行交互的。
一个完整的事件序列从手指接触屏幕开始,到手指离开屏幕结束,这个事件序列以一个DOWN事件开头,中间是一串MOVE事件,并以一个UP事件结尾。例如:当我们点击屏幕时,事件序列为DOWN->UP;当我们在屏幕上滑动时,事件序列为DOWN->MOVE->…->MOVE->UP。
一个事件由一个MotionEvent对象表示,常见的事件类型有ACTION_DOWN、ACTION_MOVE和ACTION_UP,分别表示手指刚接触屏幕、手指在屏幕上移动和手指离开屏幕。
事件分发概述
当一个事件产生时,系统需要把这个事件传递给一个View处理,这个传递过程就是事件分发过程。事件的分发过程由3个很重要的方法共同完成:dispatchTouchEvent
、onInterceptTouchEvent
和onTouchEvent
,其中:
dispatchTouchEvent
方法用来分发事件,如果事件能够到达当前View,则此方法一定会被调用,返回true表示消耗了当前事件;onInterceptTouchEvent
方法用来判断是否拦截事件,如果当前View拦截了某个事件,则在同一事件序列中不会再调用此方法,返回true表示拦截了当前事件;onTouchEvent
方法用来处理事件,返回true表示消耗了当前事件,如果不消耗,则当前View不会再收到同一事件序列中的后续事件。
它们的关系可以用下面的伪代码表示:
boolean dispatchTouchEvent(MotionEvent ev)
boolean consume = false;
if (onInterceptTouchEvent(ev))
consume = onTouchEvent(ev);
else
consume = child.dispatchTouchEvent(ev);
return consume;
通过上面的伪代码,我们可以大致了解事件传递的规则:当一个事件到达父View时,此时会调用父View的dispatchTouchEvent
方法,在此方法中,首先判断父View是否要拦截此事件,如果拦截就调用父View的onTouchEvent
方法,否则把此事件交给其子View,并调用子View的dispatchTouchEvent
方法。
事实上,当一个事件产生时,它是按照这个顺序传递的:Activity->Window->View,即事件先传递给当前的Activity,Activity再传递给Window,Window再传递给顶级View,顶级View收到事件后,再按照事件分发机制把事件传递给子View。
下面我们结合源码一起来跟踪事件的分发过程。
Activity对事件的分发过程
当一个事件发生时,会最先传递给当前的Activity,并由Activity的dispatchTouchEvent
方法进行事件分发。
在dispatchTouchEvent
方法中,事件分发的工作是由Window对象完成的。Window对象会将事件传递给decor view(一般就是调用setContentView
方法所设置的View的父容器),decor view再进一步执行事件分发。
现在来具体看下dispatchTouchEvent
方法的源码:
/* Activity.dispatchTouchEvent */
boolean dispatchTouchEvent(MotionEvent ev)
if (ev.getAction() == MotionEvent.ACTION_DOWN)
onUserInteraction();
if (getWindow().superDispatchTouchEvent(ev))
return true;
return onTouchEvent(ev);
通过源码可以知道:
1. 首先,如果事件是一个DOWN事件的话,则调用当前Activity的onUserInteraction
方法。查看源码会发现,默认的onUserInteraction
方法其实是一个空方法,这里可以忽略;
2. 接着,通过getWindow
方法取得Window对象,并调用其superDispatchTouchEvent
方法进行事件分发。这里有一个问题,就是Window类中的superDispatchTouchEvent
方法是一个抽象方法,换句话说,我们需要找到相应的Window子类,实际上,getWindow
方法返回的是一个PhoneWindow对象;
3. 最后,如果getWindow().superDispatchTouchEvent(ev)
返回false的话,即此事件没有被消耗,则执行当前Activity的onTouchEvent
方法。也就是说,如果没有找到一个View能够消耗此事件,则最终会重新交给当前的Activity处理,并且Activity默认的onTouchEvent
方法总是返回false。
经过分析后发现,事件分发实际上是在第2步中进行的。那我们接着看下PhoneWindow的superDispatchTouchEvent
方法:
/* PhoneWindow.superDispatchTouchEvent */
boolean superDispatchTouchEvent(MotionEvent event)
return mDecor.superDispatchTouchEvent(event);
可以看到,这里的superDispatchTouchEvent
方法很简单,就是把事件交给decor view继续进行事件分发,那么这个decor view到底是什么呢?
其实,decor view是一个DecorView对象,它一般就是当前界面(Activity)的底层容器,换句话说,我们在Activity中调用setContentView
方法所设置的View是DecorView的一个子View。由于DecorView继承自FrameLayout且是一个ViewGroup,因此事件最终会传递到View。从这里开始,事件已经传递到了顶级View(即在Activity中通过setContentView
所设置的View),这一点也可以从源码看出:
/* DecorView.superDispatchTouchEvent */
boolean superDispatchTouchEvent(MotionEvent event)
return super.dispatchTouchEvent(event);
这里DecorView的superDispatchTouchEvent
方法调用了父类的dispatchTouchEvent
方法,由于DecorView是一个ViewGroup,因此事件传递给ViewGroup继续分发。
顶级View对事件的分发过程
上面分析了事件是如何从Activity经过Window又传递到顶级View的,现在我们继续跟踪事件是如何在顶级View中进行分发的。
由于源码过长,这里我们先说一下事件在顶级View中的分发过程。简单来说,如果顶级View拦截事件,即onInterceptTouchEvent
方法返回true,则事件交由顶级View处理,否则事件会传给顶级View所在的点击事件链上的子View,这时子View的dispatchTouchEvent
方法会被调用。这样就完成了一个层次的事件分发,接下来的传递过程也一样,如此循环就完成了整个View层次的事件分发。
GroupView是如何判断是否要拦截事件的?
下面我们按照上述的事件分发过程分析下源码,首先是GroupView是如何判断是否要拦截事件的:
/* ViewGroup.dispatchTouchEvent */
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); // restore action in case it was changed
else
intercepted = false;
else
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
这里我们先要意识到一点,只要intercepted
被赋值为true,就意味着ViewGroup会拦截事件。那么什么情况下intercepted
会被赋值为true呢?一种情况是如果事件不是DOWN事件并且没有子View能够消耗此事件,则ViewGroup一定会拦截事件;另一种情况是交由ViewGroup的onInterceptTouchEvent
方法来判断,如果onInterceptTouchEvent
返回true,则GroupView会拦截事件。
在这里顺便提一下mFirstTouchTarget
,详细的解释到后面讲ViewGroup向子View分发事件时再说,这里只要知道mFirstTouchTarget != null
表示有子View能够消耗事件,换句话说,如果ViewGroup拦截了事件,则mFirstTouchTarget
最终会为null。
另外,注意到这段代码中还有一个变量disallowIntercept
,它表示FLAG_DISALLOW_INTERCEPT
标志位是否为1,如果此标志位为1,则ViewGroup是不能拦截事件的。在子View中,可以通过requestDisallowInterceptTouchEvent
方法设置此标志位为1让父View(ViewGroup)无法拦截事件。
GroupView是如何向子View分发事件的?
从上面的分析可知,当intercepted
为false时,GroupView会向子View分发事件。对应的源码如下:
/* ViewGroup.dispatchTouchEvent */
// Find a child that can receive the event.
// Scan children from front to back.
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--)
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
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;
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
// Child wants to receive touch within its bounds.
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
上述代码是简化后的,方便我们看清事件分发的过程。可以看到,整个流程就是在一个for循环中寻找一个能够处理事件的子View,如果找到这样一个子View,并且这个子View不在mFirstTouchTarget
链表中,就把它加到mFirstTouchTarget
链表头。
为了让大家进一步理解,这里讲3个点:
1) 如何判断一个子View是否能够消耗事件?
2) newTouchTarget
和mFirstTouchTarget
到底是什么?
3) 如果找到一个能够消耗事件的子View,接下来做了些什么?没找到又做了些什么?
========== 问题1 ==========
通过dispatchTransformedTouchEvent
方法可以判断一个子View是否能够消耗事件,返回true表示可以消耗,反之不能。下面是dispatchTransformedTouchEvent
方法简化后的源码:
/* ViewGroup.dispatchTransformedTouchEvent */
boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits)
if (child == null)
handled = super.dispatchTouchEvent(event);
else
handled = child.dispatchTouchEvent(event);
return handled;
这里由于把子View作为child
参数传递给dispatchTransformedTouchEvent
方法,因此会调用子View的dispatchTouchEvent
方法,可以看到,这里开始进行下一层次的事件分发。
========== 问题2 ==========
newTouchTarget
和mFirstTouchTarget
都是TouchTarget类型的对象,TouchTarget就好像一个链表,可以把它想象成LinkedList,其中的ViewWrapper封装了子View和其它一些信息。TouchTarget.next
指向下一个TouchTarget对象,TouchTarget.child
指向子View。
当ViewGroup进行事件分发时,如果找到一个子View能够消耗事件,则把newTouchTarget
“指向”它;进一步的,如果此子View不在mFirstTouchTarget
链表中,则把它加到mFirstTouchTarget
链表头。换句话说,newTouchTarget
表示每次事件分发找到的那个能够处理事件的子View,而mFirstTouchTarget
表示历史上所有能够处理事件的子View链表。
========== 问题3 ==========
显而易见,当找到一个子View来处理事件时,会记录当前的某些状态(在上述代码中删减掉了)并立即跳出循环,并且newTouchTarget
会指向此子View,mFirstTouchTarget
不为null。
然后看下没找到子View的情况:
/* ViewGroup.dispatchTransformedTouchEvent */
// 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);
可以看到,如果没找到能够消耗事件的子View,则把ViewGroup当作普通的View来处理事件,这也可以从dispatchTransformedTouchEvent
方法里看出:由于这里的child参数为null,因此会调用父类(View)的dispatchTouchEvent
方法。
========== 总结 ==========
从问题1和问题3可以看出,事件最后都会交由View的dispatchTouchEvent
方法处理,因此我们也跟踪到了事件分发的最后一层:View对事件的处理。
View对事件的处理过程
接着上面的内容,现在终于到了真正处理事件的地方,我们先来看看View的dispatchTouchEvent
方法:
/* View.dispatchTouchEvent */
boolean dispatchTouchEvent(MotionEvent 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;
View对事件的处理就比较简单了,因为这里没有向下传递的过程,所以只需要自己处理事件。从上面的源码可以看出,首先会判断有没有设置OnTouchListener,如果设置了并且其onTouch
方法返回true,则不会调用自身的onTouchEvent
方法,这也说明了监听器的优先级比回调方法的优先级高。
接着再看看默认的onTouchEvent
方法:
/* View.onTouchEvent */
boolean onTouchEvent(MotionEvent event)
if ((viewFlags & ENABLED_MASK) == DISABLED)
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
if (mTouchDelegate != null)
if (mTouchDelegate.onTouchEvent(event))
return true;
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)
switch (action)
case MotionEvent.ACTION_UP:
if (mPerformClick == null)
mPerformClick = new PerformClick();
if (!post(mPerformClick))
performClick();
break;
return true;
首先,注意到就算是disable状态的View一样会消耗事件;其次,如果View设置了TouchDelegate,还会执行TouchDelegate的onTouchEvent
方法;最后,在UP事件发生时,会调用performClick
方法。
在performClick
方法中,可以看到:
/* View.performClick */
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
方法。
事件分发总结
经过上述的讲解,我们已经成功的跟踪了一个事件是如何从Activity传递到每一层View的,现在再简单总结一下:
Activity.dispatchTouchEvent
↓
PhoneWindow.superDispatchTouchEvent
↓
DecorView.superDispatchTouchEvent
↓
ViewGroup.dispatchTouchEvent
(intercepted==false) | (intercepted==true)
----------------------------------------
↓ ↓
ViewGroup.dispatchTransformedTouchEvent ViewGroup.onInterceptTouchEvent
↓
View.dispatchTouchEvent
| (优先)
-------------------------
↓ ↓
View.onTouchEvent OnTouchListener.onTouch
↓
View.performClick
↓
OnClickListener.onClick
- 事件首先传递给当前的Activity,Activity会委托Window进行事件分发;
- 接着Window又把事件传递给DecorView,DecorView会把事件传递给通过
setContentView
方法设置的View,此时事件进入顶级View; - 通常顶级View是一个ViewGroup,此时会调用其
dispatchTouchEvent
方法进行事件分发。如果ViewGroup拦截事件,则事件不会传递给子View,ViewGroup自己消耗事件;如果ViewGroup不拦截事件,则在ViewGroup的所有子View中找到一个能够消耗事件的子View,并把事件传递给它; - 当事件到达View(不是ViewGroup)时,此时事件不会再向下传递,View只要负责处理事件即可;
- 如果一个ViewGroup的所有子View都不能消耗事件,则此事件会重新交给ViewGroup处理,如果此ViewGroup也不能消耗事件,则继续把事件传递给它的父View处理,依次类推,如果所有View层次都不能消耗事件,则事件最终会回到当前的Activity,并交由Activity处理。
以下是一些关于事件分发的结论,方便大家理解整个事件传递机制:
1. 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在此过程中会产生一系列事件,这个事件序列以DOWN事件开头,中间是一串MOVE事件,并且以UP事件结尾;
2. 当某个View一旦决定拦截事件时,则这个事件序列都只能交给它处理,并且它的onInterceptTouchEvent
方法只会调用一次。换句话说,当一个View开始拦截事件后,那么系统会把同一个事件序列的后续事件都交给它处理,因此不必再调用onInterceptTouchEvent
方法了;
3. 某个View一旦开始处理事件,如果它不消耗DOWN事件(其onTouchEvent
方法返回了false),则同一事件序列中的后续事件都不会再交给它处理,并且事件将重新交给它的父View去处理(即调用父View的onTouchEvent
方法)。这就好比上级交给下级做一件事,如果这件事没做好,那么短期内上级不会再把事件交给这个下级做了;
4. 如果View不消耗除DOWN事件以外的其他事件,那么这个事件序列会消失,此时不会调用父View的onTouchEvent
方法;虽然当前View可以继续接受同一事件序列的后续事件,但最终这些事件会交给Activity处理;
5. ViewGroup默认不拦截任何事件,Android源码中的ViewGroup的onInterceptTouchEvent
方法默认返回false;
6. View(不是GroupView)没有onInterceptTouchEvent
方法,一旦有事件传递给它,就会调用它的onTouchEvent
方法;
7. View的onTouchEvent
方法默认都会消耗事件,哪怕它是disable的,只要它有一个clickable为true,onTouchEvent
方法就返回true;
8. 事件传递是由外向内的,即事件总是先传递给父View,再传递给子View,通过requestDisallowInterceptTouchEvent
方法可以在子View中干预父View的事件拦截,但DOWN事件除外。
关注微信公众号,有更多精彩文章哦(o゚▽゚)o
以上是关于Android系列View的事件分发机制的主要内容,如果未能解决你的问题,请参考以下文章
Android6.0 ViewGroup/View 事件分发机制详解