Activity构成
点击事件由MotionEvent来表示,当一个点击事件产生后,事件最先传递给Activity。所以我们大致了解一下Activity的构成
- PhoneWindow:Window抽象类的实现类,我们使用getWindow()方法得到的就是一个PhoneWindow
- DecorView:Activity中的根View,继承了FrameLayout
- TitleView:DecorView中的子View
- ContentView:DecorView的子View,我们平常应用所写的布局展示在这里
点击事件的传递规则
当我们点击屏幕时,就产生了点击事件,这个事件被封装成了一个类:MotionEvent。然后系统会将产生的MotionEvent传递给View的层级,MotionEvent传递的过程就是点击事件分发。点击事件分发过程由下面三个很重要的方法来完成。
- public boolean dispatchTouchEvent(MotionEvent ev): 用来进行事件的分发,如果事件能够传递给当前View,此方法一定会被调用。返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
- public boolean onInterceptTouchEvent(MotionEvent ev):在dispatchTouchEvent方法的内部调用,用来进行事件的拦截。如果ViewGroup拦截了某个事件,那么在同一个事件序列中此方法不会再次调用,返回结果表示是否拦截当前事件。
- public boolean onTouchEvent(MotionEvent ev):在dispatchTouchEvent方法中调用,用来处理点击事件。返回结果表示是否消耗当前事件,如果不消耗则在同一个事件序列中,当前View无法再次接受到事件
上述三个方法的关系用伪代码表示如下:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume=onTouchEvent(ev)
}else{
consume=child.dispatchTouchEvent(ev);
}
return consume;
}
通过上面伪代码我们可以了解到,如果一个根ViewGroup拿到点击事件后首先会调用它的 dispatchTouchEvent方法,然后判断onInterceptTouchEvent方法是否返回true。返回ture表示拦截当前点击事件,然后就会调用它自己的onTouchEvent方法来处理点击事件;返回false表示不拦截当前点击事件,事件就会传递到子元素的,调用子元素的dispatchTouchEvent方法进行分发重复上面的步骤,直到事件被最终处理。
如果View设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被调用,这时如果onTouch方法返回false,onTouchEvent方法才会被调用,可以看到onTouch方法的优先级比onTouchEvent方法高。
如果我们的View设置了onClickListener,那么onClick方法会被调用。onClick方法在onTouchEvent方法中调用优先级最低,而且只能监听到点击事件。
当点击事件产生后,事件首先会传递给当前的Activity,这里会调用Activity的dispathTouchEvent方法,当然具体的事件处理工作都是交由Activity中PhoneWindow来完成,然后PhoneWindow再把事件处理工作交给DecorView,之后事件处理工作交给根ViewGroup。
考虑一种情况如果一个View的onTouchEvent方法返回true,那么它的父容器的onTouchEvent方法就会被调用,如果所有的元素都不处理这个事件,事件最终就会传递给Activity处理,即Activity的onTouchevent方法就会被调用。
- 同一个事件序列是指手指从接触屏幕的那一刻起,到手指离开屏幕的那一刻,由一个down事件开始,中间有一个或多个move事件,最终以up事件结束
- 正常情况下一个事件只能被一个View拦截消耗,因为一旦一个元素拦截了点击事件,那么同一个事件序列内的所有事件都会直接交给它处理。因此同一个事件序列中的事件不能分别交给两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理
- 某个View一旦决定拦截,一整个事件序列都只能由它来处理(如果事件序列能传递给他的话),并且他的onInterceptTouchEvent不会再被调用。
- 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理。
- 如果View不消耗掉除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续事件,最终这些消失的点击事件会传递给Activity处理
- ViewGroup默认不拦截任何事件,源码中ViewGroup的onInterceptTouchEvent方法默认返回false
- View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,它的onTouchEvent方法就会被调用。
- View的onTouchEvent默认会消耗掉点击事件(返回true),除非他是不可点击的(clickable和longClickable同时为false),View的LongClickable默认都为false,clickable分情况比如Button的clickable为true,TextView默认为false
- View的enable属性不影响onTouchEvent的默认返回值,哪怕一个View是disable状态的,只要它的clickable或者LoneClickable一个为true那么它的onTouchEvent就返回true
- onClick会发生的前提是当前View是可点击的并且它收到了down和up事件
- 事件传递是由外向内的,事件总是先传递给父元素在由父元素分发给子View,子View通过requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。
ViewGroup事件分发的源码解析
点击事件由MotionEvent来表示,点击事件最先会传递给Activity进行处理我们先从Activity的dispatchTouchEvent开始分析。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
我们可以看到事件首先交给Activity附属的Window进行分发,如果返回true整个事件循环就结束了,返回false意味着点击事件没人处理,所有的View的onTouchEvent都返回了false,那么Activity的onTouchEvent就会被调用。
接下来我们看Window是怎么处理点击事件的。
public abstract boolean superDispatchTouchEvent(MotionEvent event);
我们看到Window是一个抽象类,而Window的superDispatchTouchEvent也是一个抽象方法,我们找到它的实现类。
The only existing implementation of this abstract class is android.view.PhoneWindow, which you should instantiate when needing a Window
从注释我们可以看到Window的实现类是android.view.PhoneWindow。然后我们看PhoneWindow中的superDispatchTouchEvent方法
public boolean superDispatchTouchEvent(MotionEvent event){
return mDecor.superDispatchTouchEvent(evrnt);
}
PhoneWindow直接将点击事件传递给了DecorView,DecorView是什么呢
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks
通过上面代码可以看到DecorView就是就是一个FrameLayout。
它也是Activity中的根View。它包含了一个TitleView和一个ContentView,而ContentView就是我们在Activity中通过setContentView设置进去的布局。
在使用中我们通过 ((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)可以获取到我们在Activity设置的View,而getWindow().getDecorView()很显然返回的就是DecorView。
由于DecorView继承至FrameLayout而且是父View,我们知道FrameLayout属于ViewGroup所以我们接下来看一下ViewGroup的事件分发过程,从ViewGroup的dispatchTouchEvent方法开始。方法的代码比较长分段来说明
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//如果事件为Down对事件进行初始化
if (actionMasked == MotionEvent.ACTION_DOWN) {//1
cancelAndClearTouchTargets(ev);
//resetTouchState方法中会把mFirstTouchTarget重置为null
resetTouchState();
}
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//2
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//3
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
- 注释1处:如果事件为ACTION_DOWN首先会对点击事件进行初始化,并且在resetTouchState方法中会把mFirstTouchTarget重置为null也会重置FLAG_DISALLOW_INTERCEPT标志位。(这里进行初始化是因为一个完整的事件序列是从DOWN开始到UP事件结束,所以如果是Down事件说明是一个新的事件序列,所以进行初始化重置为默认状态)
- 注释2处:ViewGroup会在事件类型为ACTION_DOWN或者mFirstTouchTarget != null这两种情况下判断是否拦截当前事件,mFirstTouchTarget != null是什么意思呢?从后面的代码逻辑可以看出来,当事件由ViewGroup的子元素成功处理时mFirstTouchTarget会被赋值并指向子元素。也就是说ViewGroup不拦截事件并将事假交给子元素处理时mFirstTouchTarget != null。一旦事件被当前的ViewGroup拦截时mFirstTouchTarget != null就不成立,那么当ACTION_MOVE,和ACTION_UP事件到来时,由于(actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null)为false将导致onInterceptTouchEvent不被调用,并且序列的其他事件都将默认交给此ViewGroup处理。 - 注释3处:这里出现了一个FLAG_DISALLOW_INTERCEPT标志位,这个标记通过requestDisallowInterceptTouchEvent方法来设置一般用于子View中,FLAG_DISALLOW_INTERCEPT设置后ViewGroup无法拦截除了ACTION_DOWN以外的其他点击事件,因为FLAG_DISALLOW_INTERCEPT标志位会在ViewGroup的ACTION_DOWN事件里进行重置所以requestDisallowInterceptTouchEvent方法并不能影响ViewGroup对ACTION_DOWN事件的处理
- 从上面的分析我们知道onInterceptTouchEvent方法不是每次事件都会被调用,如果我们想提前处理所有的点击事件要选择dispatchTouchEvent方法。且onInterceptTouchEvent方法在源码中默认返回false不进行事件拦截如果要拦截事件需要重写这个方法返回true
当ViewGroup不拦截事件的时候,事件会向下分发给她的子View进行处理,源码如下:
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {//1
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {//2
ev.setTargetAccessibilityFocus(false);
continue;
}
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)) {//3
// 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);//4
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
- 注释1处:首先遍历ViewGroup的子元素,判断子元素是否能接受到点击事件,如果子元素能够接收到点击事件则交给子元素来处理。需要处理这个遍历是倒叙遍历的,即从最上层的子View开始往内遍历
- 注释2处:判断触摸点是否在子View的范围内,或者子View是否在播放动画,如果满足这两个条件则事件传递给它来处理
- 注释3处:dispatchTransformedTouchEvent方法实际上就是调用了子元素的dispatchTouchEvent方法源码如下:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
上面的代码可以看到如果传递的child不是null就会调用child.dispatchTouchEvent(event)进行事件分发。如果子元素的dispatchTouchEvent方法返回false,ViewGroup就会把事件分发给下一个子元素。(如果还有下一个子元素的话)
- 注释4处:这里完成了mFirstTouchTarget的赋值并终止对子元素的变量。mFirstTouchTarget真正的赋值操作是在addTouchTarget内部完成的,mFirstTouchTarget其实是一种单链表结构。mFirstTouchTarget是否被赋值直接影响到ViewGroup对事件的拦截策略,前面已经说过,如果mFirstTouchTarget为null,那么ViewGroup就默认拦截接下来同一序列中所有的点击事件。
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
如果变量所有子元素事件都没有被合适的处理比如ViewGroup没有子元素或者子元素处理了点击事件但是在dispatchTouchEvent中返回了false,一般是在onTouchEvent中返回了false,这两中情况下ViewGroup就会自己处理点击事件代码如下:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
上面这段代码dispatchTransformedTouchEvent的第三个参数child为null,从前面分析可以知道,它会调用super.dispatchTouchEvent(event)方法,即点击事件开始交给View来处理。
View事件分发的源码解析
View对点击事件的处理比较简单,这里主要View不包含ViewGroup先看它的dispatchTouchEvent方法,代码如下:
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
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对事件处理就比较简单了,它没有子元素只需要自己处理点击事件,从上面的源码可以看出View处理事件首先会判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会调用,这样做的好处是方便在外界处理点击事件。
接着再分析OnTouchListener的实现,先看View处于不可用状态下点击事件的处理过程
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
很显然不可用状态下的View照样会返回true 消耗掉点击事件。如果View有mTouchDelegate代理还会执行mTouchDelegate.onTouchEvent(event)接着就是onTouchEvent对点击事件的具体处理
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
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();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} 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:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
从上面的代码可以看到只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么onTouchEvent()就会返回True消耗这个事件,CLICKABLE和LONG_CLICKABLE代表View可以被点击和长按,可以通过View的setClickabke和setLongClickable方法来设置,也可以通过View的setOnClickListenr和setOnLongClickListener来设置,它们会自动将View设置为CLICKABLE和LONG_CLICKABLE。从源码中可也以看出这一点
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
接着再ACTION_UP事件中会调用performClick方法,如果View设置了OnClickListener那么在performClik方法内部会调用它的onClick方法,代码如下:
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;
}