View的时间体系——View的事件分发机制
1. 什么是事件分发?
- 通过手指对屏幕的触摸来完成整个交互过程
- 用户通过屏幕与手机交互的时候,每点击一次,长按,移动等都是一个事件
- 事件分发机制:某一个事件从屏幕传递到各个View,由View来使用这一事件(消费事件)或者忽略这一事件(不消费事件),这整个过程的控制。
2. 事件分发的对象
- 系统把事件封装为MotionEvent对象,事件分发的过程就是MotionEvent的分发的过程。
a. 事件的类型
- 按下(ACTION_DOWN)
- 移动(ACTION_MOVE)发生在按下之后,不包含按下动作
- 抬起(ACTION_UP)
- 取消(ACTION_CANCEL)
b. 事件序列
- 从手指按下屏幕开始,到手指离开屏幕所产生的一系列事件
c. 传递层级
Activity->Window->DecorView->ViewGroup->View
3. 事件分发中的三个主要方法
-
dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发。如果时间能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
-
onInterceptTouchEvent(MotionEvent event)
在上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
-
onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
4. 事件分发解析
1. Activity对点击事件的分发过程
- 当一个点击操作发生时,时间最先传递给当前的Activity,所以先从Activity的dispatchTouchEvent()方法开始分析。
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进行分发
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mo61v08y-1580537188567)(C:\Users\大狼狗skr~\AppData\Roaming\Typora\typora-user-images\1580528429119.png)]
- 通过源码我们知道Window是一个抽象类,PhoneWindow是Window的实现类。
PhoneWindow.superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
-
由于Window的唯一实现类是PhoneWindow,所以接下来看PhoneWindow处理点击事件
-
PhoneWindow将事件直接传递给了DecorView
DecorView.superDispatchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
- 通过
((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)
这种方式就可以获取Activity所设置的View,这个mDecor就是getWindow().getDecorView()
返回的View,而我们通过setContentView()
设置的View是它的一个子View。 - 目前事件传递到了DecorView这里,而DecorView继承自FrameLayout,且是父View,所以事件最终会传递给View
- 从这里开始,事件已经传递到了顶级View,即在Activity中通过setContentView所设置的View,顶级View一般是ViewGroup
2. ViewGroup对点击事件的分发过程
// 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); // 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;
}
- 从上面的代码可以看出,ViewGroup在事件类型为DOWN或者mFirstTarget!=null的条件下会拦截当前事件。
- mFirstTarget:当事件由ViewGroup的子元素成功处理时,mFirstTarget会被赋值并指向子元素。当ViewGroup不拦截事件并将事件交由子元素处理时,mFirstTarget!=null。一旦事件由当前ViewGroup拦截,mFirstTarget!=null就不成立。那么当ACTION_MOVE和ACTION_UP事件到来时,由于判断条件是(actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)这个条件为false,则直接将intercepted = true
- FLAG_DISALLOW_INTERCEPT:这个标记位是通过子View的requestDisallowInterceptTouchEvent方法来设置的。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的点击事件。
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
- onInterceptTouchEvent默认返回false,也就是说ViewGroup一般不进行拦截
总结:当ViewGroup决定拦截事件后,那么后续的点击事件默认会交给它处理并且不再调用它的onInterceptTouchEvent方法。FLAG_DISALLOW_INTERCEPT这个标记的作用是让ViewGroup不再拦截事件,前提是ViewGroup不拦截ACTION_DOWN事件(因为当ViewGroup判断事件为ACTION_DOWN的时候,会重置标记)。
接着向下看,当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View进行处理
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 there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
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)) {
// 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);
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);
}
if (preorderedList != null) preorderedList.clear();
}
- 遍历ViewGroup的所有子元素,然后判断子元素是否能接收到点击事件。是否能接受到点击事件主要由两点来衡量:子元素是否在播放动画和点击事件的坐标位置是否落在子元素区域内。如果某个子元素满足这两个条件,那么事件就会传递给它来处理。
- dispatchTransformedTouchEvent实际上调用的就是子元素的dispatchTouchEvent方法,在他的内部有如下内容
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
- 如果子元素的dispatchTouchEvent方法返回true,那么mFirstTarget就会被赋值同时跳出for循环
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
mFirstTarget真正的赋值过程是在addTouchTarget内部完成的,mFirstTarget是否被赋值,将直接影响到ViewGroup对事件的拦截
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
-
这几行代码完成了mFirstTarget的赋值并终止对子元素的遍历。如果子元素dispatchTouchEvent返回false,ViewGroup就会把事件分发给下一个子元素。
-
如果遍历所有的子元素后,事件都没有被合适的处理,这包含两种情况:第一种是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的第三个参数为null,则会调用super.dispatchTouchEvent(event);交给View处理。
3. View对点击事件的处理过程
public boolean dispatchTouchEvent(MotionEvent event) {
......
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不包含ViewGroup,因为它没有子元素,因此无法向下传递事件。
- 首先会判断有没有设置OnTouchListener,如果OnTouchListener的onTouch方法返回true,那么onTouchEvent就不会被调用,可见OnTouchListener的优先级高于onTouchEvent
onTouchEvent
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
-
代码中的注释翻译过来就是:
可单击的禁用视图仍然消耗触摸事件,只是没有响应。也就是说当view处于不可用状态下照样会消耗点击事件。
-
如果view设置有代理,那么会执行TouchDelegate的onTouchEvent方法
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
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)) {
performClickInternal();
}
}
}
}
mIgnoreNextUpEvent = false;
break;
......
}
return true;
}
-
只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent返回true,不管是不是DISABLE状态。
-
当ACTION_UP发生时,会触发performClick方法,如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法。
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
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);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
- View的LONG_CLICKABLE属性默认为false,而CLICKABLE属性是否为false和具体的view有关,确切的来说,可点击的View其CLICKABLE为true,不可点击的View其CLICKABLE为false。通过setClickable和setLongClickable可以分别改变其属性,另外,setOnClickListener会自动将View的CLICKABLE属性设为true,setOnLongClickListener也一样。