作为一名 Android 开发者,每天接触最多的就是 View 了。Android View 虽然不是四大组件,但其并不比四大组件的地位低。而 View 的核心知识点事件分发机制则是不少刚入门同学的拦路虎,也是面试过程中基本上都会问的。理解 View 的事件能够让你写出更好自定义 View 以及解决滑动冲突。
1、 View 事件认识
1.1 MotionEvent 事件
当你用手指轻触屏幕,这个过程在 Android 中主要可以分为以下三个过程:
- ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件
- ACTION_MOVE:手指在屏幕上移动时候产生该事件
- ACTION_UP:手指从屏幕上松开的瞬间产生该事件
从 ACTION_DOWN 开始到 ACTION_UP 结束我们称为一个事件序列
正常情况下,无论你手指在屏幕上有多么骚的操作,最终呈现在 MotionEvent 上来讲无外乎下面两种动作。
- 点击(点击后抬起,也就是单击操作):ACTION_DOWN -> ACTION_UP
- 滑动(点击后再滑动一段距离,再抬起):ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP
1.2 理论知识
- public boolean dispatchTouchEvent(MotionEvent ev)
return true: 表示消耗了当前事件,有可能是当前 View 的 onTouchEvent
或者是子 View 的 dispatchTouchEvent
消费了,事件终止,不再传递。
return false: 调用父 ViewGroup 或 Activity 的 onTouchEvent
。 (不再往下传)。
return super.dispatherTouchEvent: 则继续往下(子 View )传递,或者是调用当前 View 的 onTouchEvent 方法;
总结:用来分发事件,即事件序列的大门,如果事件传递到当前 View 的 onTouchEvent
或者是子 View 的 dispatchTouchEvent
,即该方法被调用了。 另外如果不消耗 ACTION_DOWN 事件,那么 down, move, up 事件都与该 View 无关,交由父类处理(父类的 onTouchEvent
方法)
- public boolean onInterceptTouchEvent(MotionEvent ev)
return true: ViewGroup 将该事件拦截,交给自己的onTouchEvent
处理。
return false: 继续传递给子元素的dispatchTouchEvent
处理。
return super.dispatherTouchEvent: 事件默认不会被拦截。
总结:在 dispatchTouchEvent
内部调用,顾名思义就是判断是否拦截某个事件。(注:ViewGroup 才有的方法,View 因为没有子View了,所以不需要也没有该方法) 。而且这一个事件序列(当前和其它事件)都只能由该 ViewGroup 处理,并且不会再调用该 onInterceptTouchEvent
方法去询问是否拦截。
- public boolean onTouchEvent(MotionEvent ev)
return true: 事件消费,当前事件终止。
return false: 交给父 View 的 onTouchEvent
。
return super.dispatherTouchEvent: 默认处理事件的逻辑和返回 false 时相同。
总结:在dispatchTouchEvent
内部调用
上面三个方法之间的调用关系可以用下面的代码表示:
public boolean dispatchTouchEvent(MotionEvent ev) { boolean consume = false;//事件是否被消费 if (onInterceptTouchEvent(ev)){//调用 onInterceptTouchEvent 判断是否拦截事件 consume = onTouchEvent(ev);//如果拦截则调用自身的onTouchEvent方法 }else{ consume = child.dispatchTouchEvent(ev);//不拦截调用子View的dispatchTouchEvent方法 } return consume;//返回值表示事件是否被消费,true事件终止,false调用父View的onTouchEvent方法 }
1.3 事件传递顺序
对于一个点击事件,Activity 会先收到事件的通知,接着再将其传给 DecorView(根 view),通过 DecorView 在将事件逐级进行传递。具体传递逻辑见下图:
可以看出事件的传递过程都是从父 View 到子 View。但是这里有三点需要特别强调一下
- 子 View 可以通过 requestDisallowInterceptTouchEvent 方法干预父 View 的事件分发过程( ACTION_DOWN 事件除外),而这就是我们处理滑动冲突常用的关键方法。
- 对于 View(注意!ViewGroup 也是 View)而言,如果设置了onTouchListener,那么 OnTouchListener 方法中的 onTouch 方法会被回调。onTouch 方法返回 true,则 onTouchEvent 方法不会被调用(onClick 事件是在 onTouchEvent 中调用)所以三者优先级是 onTouch->onTouchEvent->onClick
- View 的 onTouchEvent 方法默认都会消费掉事件(返回 true),除非它是不可点击的(clickable 和 longClickable 同时为 false),View 的longClickable 默认为 false,clickable 需要区分情况,如 Button 的 clickable 默认为 true,而TextView的 clickable 默认为 false。
2、View 事件分发源码
先从 Activity 中的 dispatchTouchEvent 方法出发:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { return super.dispatchTouchEvent(ev); }
Activity 将事件传给父 Activity 来处理,下面看父 Activity 是怎么处理的。
/** * Called to process touch screen events. You can override this to * intercept all touch screen events before they are dispatched to the * window. Be sure to call this implementation for touch screen events * that should be handled normally. * * @param ev The touch screen event. * * @return boolean Return true if this event was consumed. */ public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
其中有个 onUserInteraction 方法,该方法是只要用户在 Activity 的任何一处点击或者滑动都会响应,一般不使用。接下去看getWindow().superDispatchTouchEvent(ev) 所代表的具体含义。getWindow() 返回对应的 Activity 的 window。一个Activity 对应一个 Window 也就是 PhoneWindow, 一个 PhoneWindow 持有一个 DecorView 的实例, DecorView 本身是一个 FrameLayout。这句话一定要牢记。
/** * Retrieve the current {@link android.view.Window} for the activity. * This can be used to directly access parts of the Window API that * are not available through Activity/Screen. * * @return Window The current window, or null if the activity is not * visual. */ public Window getWindow() { return mWindow; }
Window 的源码有说明 The only existing implementation of this abstract class is
android.view.PhoneWindow,Window 的唯一实现类是 PhoneWindow。那么去看 PhoneWindow 对应的代码。
public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
PhoneWindow 又调用了 DecorView 的 superDispatchTouchEvent 方法。而这个 DecorView 就是 Window 的根 View,我们通过 setContentView 设置的 View 是它的子 View(Activity 的 setContentView,最终是调用 PhoneWindow 的 setContentView )
到这里事件已经被传递到根 View 中,而根 View 其实也是 ViewGroup。那么事件在 ViewGroup 中又是如何传递的呢?
2.1 ViewGroup 事件分发
public boolean dispatchTouchEvent(MotionEvent ev) { ...... final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); //清除FLAG_DISALLOW_INTERCEPT,并且设置mFirstTouchTarget为null resetTouchState(){ if(mFirstTouchTarget!=null){mFirstTouchTarget==null;} mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; ...... }; } final boolean intercepted;//ViewGroup是否拦截事件 // mFirstTouchTarget是ViewGroup中处理事件(return true)的子View //如果没有子View处理则mFirstTouchTarget=null,ViewGroup自己处理 if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev);//onInterceptTouchEvent ev.setAction(action); } else { intercepted = false; //如果子类设置requestDisallowInterceptTouchEvent(true) //ViewGroup将无法拦截MotionEvent.ACTION_DOWN以外的事件 } } else { intercepted = true; //actionMasked != MotionEvent.ACTION_DOWN并且没有子View处理事件,则将事件拦截 //并且不会再调用onInterceptTouchEvent询问是否拦截 } ...... ...... }
先看标红的代码,这句话的意思是:当 ACTION_DOWN 事件到来时,或者有子元素处理事件( mFirstTouchTarget != null ),如果子 view 没有调用 requestDisallowInterceptTouchEvent 来阻止 ViewGroup 的拦截,那么 ViewGroup 的 onInterceptTouchEvent 就会被调用,来判断是否是要拦截。所以,当子 View 不让父 View 拦截事件的时候,即使父 View onInterceptTouchEvent 中返回true 也没用了。
另外,FLAG_DISALLOW_INTERCEPT 这个
标记位是通过子 View requestDisallowInterceptTouchEvent
方法设置的。 具体可参看如下代码。
@Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // We‘re already in this state, assume our ancestors are too return; } if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // Pass it up to our parent if (mParent != null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); } }
同时,如果这个 ViewGroup 有父 View 的时候,还得让父父 View 不能拦截。继续看 ViewGroup 的 dispatchTouchEvent 方法。
public boolean dispatchTouchEvent(MotionEvent ev) { 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); ...... if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); //如果子View没有播放动画,而且点击事件的坐标在子View的区域内,继续下面的判断 continue; } //判断是否有子View处理了事件 newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { //如果已经有子View处理了事件,即mFirstTouchTarget!=null,终止循环。 newTouchTarget.pointerIdBits |= idBitsToAssign; break; } if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { //点击dispatchTransformedTouchEvent代码发现其执行方法实际为 //return child.dispatchTouchEvent(event); (因为child!=null) //所以如果有子View处理了事件,我们就进行下一步:赋值 ...... newTouchTarget = addTouchTarget(child, idBitsToAssign); //addTouchTarget方法里完成了对mFirstTouchTarget的赋值 alreadyDispatchedToNewTouchTarget = true; break; } } } private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; } private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { ...... if (child == null) { //如果没有子View处理事件,就自己处理 handled = super.dispatchTouchEvent(event); } else { //有子View,调用子View的dispatchTouchEvent方法 handled = child.dispatchTouchEvent(event); ...... return handled; }
上面为 ViewGroup 对事件的分发,主要有 2 点
- 如果有子 View,则调用子 View 的 dispatchTouchEvent 方法判断是否处理了事件,如果处理了便赋值 mFirstTouchTarget,赋值成功则跳出循环。
- ViewGroup 的事件分发最终还是调用 View 的
dispatchTouchEvent
方法,具体如上代码所述。
2.2 View 的事件分发
public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); }
上述方法只有以下3个条件都为真,dispatchTouchEvent() 才返回 true;否则执行 onTouchEvent()。
- mOnTouchListener != null
- (mViewFlags & ENABLED_MASK) == ENABLED
- mOnTouchListener.onTouch(this, event)
这也就说明如果调用了 setOnTouchListener 设置了 listener, 就会先调用 onTouch 方法。没有的话才会去调用
onTouchEvent 方法。接下去,我们看 onTouchEvent 源码。
public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); } // 如果进行了事件代理,就会被拦截,不会在往下面走了 if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } // 若该控件可点击,则进入switch判断中 if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { switch (event.getAction()) { // a. 若当前的事件 = 抬起View(主要分析) case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; ...// 经过种种判断,此处省略 // 执行performClick() ->>分析1 performClick(); break; // b. 若当前的事件 = 按下View case MotionEvent.ACTION_DOWN: if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPrivateFlags |= PREPRESSED; mHasPerformedLongPress = false; postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); break; // c. 若当前的事件 = 结束事件(非人为原因) case MotionEvent.ACTION_CANCEL: mPrivateFlags &= ~PRESSED; refreshDrawableState(); removeTapCallback(); break; // d. 若当前的事件 = 滑动View case MotionEvent.ACTION_MOVE: final int x = (int) event.getX(); final int y = (int) event.getY(); int slop = mTouchSlop; if ((x < 0 - slop) || (x >= getWidth() + slop) || (y < 0 - slop) || (y >= getHeight() + slop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); // Need to switch from pressed to not pressed mPrivateFlags &= ~PRESSED; refreshDrawableState(); } } break; } // 若该控件可点击,就一定返回true return true; } // 若该控件不可点击,就一定返回false return false; } /** * 分析1:performClick() */ public boolean performClick() { if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; // 只要我们通过setOnClickListener()为控件View注册1个点击事件 // 那么就会给mOnClickListener变量赋值(即不为空) // 则会往下回调onClick() & performClick()返回true } return false; }
从上面的代码我们可以知道,当手指抬起的时候,也就是处于 MotionEvent.ACTION_UP 时,才会去调用 performClick()。而 performClick 中会调用 onClick 方法。
也就说明了:三者优先级是 onTouch->onTouchEvent->onClick
至此 View 的事件分发机制讲解完毕。
参考文献:
原文地址:https://www.cnblogs.com/huansky/p/9656394.html