注册

View的事件分发机制

1 基本概念

1.1 事件分发的对象是谁?

  • 当用户触摸屏幕时将产生点击事件(Touch事件),其相关细节(发生触摸的位置、时间等)会被封装成MotionEvent对象。MotionEvent对象就是事件分发的对象。

  • 事件类型

    事件类型具体动作
    MotionEvent.ACTION_DOWN按下,手指触碰屏幕(事件的开始)
    MotionEvent.ACTION_UP抬起,手指离开屏幕(通常情况下,事件的结束)
    MotionEvent.ACTION_MOVE滑动,手指在屏幕上滑动
    MotionEvent.ACTION_CANCEL手势被取消了,不再接受后续事件(非人为原因)
    MotionEvent.ACTION_OUTSIDE标志着用户触碰到了正常的UI边界
    MotionEvent.ACTION_POINTER_DOWN代表用户又使用一个手指触摸到屏幕上,也就是说,在已经有一个触摸点的情况下,又新出现了一个触摸点。
    MotionEvent.ACTION_POINTER_UP非最后一个手指抬起
  • 一连串事件通常都是以DOWN事件开始、UP事件结束,中间有 0 ~ 无数个MOVE事件。

when(event?.action?.and(MotionEvent.ACTION_MASK)){} //多指触控需要和MotionEvent.ACTION_MASK取并,才能检测到

1.2 事件分发的本质

  • 将产生的MotionEvent传递给某个具体的View 处理(消费)的整个过程
  • 一旦事件被某个View消费就会返回true,所有View都没有消费的话就会返回false。

1.3 事件分发的顺序

  • Activity → ViewGroup → View
  • 事件最先传递到Activity中,再传递给DecorView(ViewGroup对象),也就是整颗View树的根节点,紧接着沿着View树向下传递(递归过程),直到传递到叶子结点(View对象)。分发过程中,事件一旦在任意地方被消费掉,分发就直接结束

事件是如何到达Activity的?(建议看完这篇文章后,最后来看)

首先触摸信息被系统底层驱动获取,然后交给InputManagerService处理,也就是IMS。IMS会根据这个触摸信息通过WMS找到要分发的window,然后IMS将触摸信息发送给window对应的ViewRootImpl(所以WMS只是提供window相关信息——ViewRootImpl)。随后ViewRootImpl将触摸信息分发给顶层View。在Activity中顶层View就是DecorView,DecorView重写了onDispatchTouchEvent,会将触摸信息分发个Window.Callback接口,而Activity实现了这个接口,并在创建布局的时候将自己设置给了DecorView,所以其实是重新分发回Activity了。

详见文章:juejin.cn/user/393150…

2 事件的分发机制

  • 因此理解View事件的分发机制,就是要理解Activity、 ViewGroup 和View分别是如何分发事件的。

  • Activity、 ViewGroup 和View处理分发离不开以下三个方法:

    方法作用调用时机
    dispatchTouchEvent()分发事件传递到当前对象时(最先调用的方法)
    onTouchEvent()处理事件**(ViewGroup没有重写,调用的是View的)**在dispatchTouchEvent()内部调用
    onInterceptTouchEvent()拦截事件**(三者中只有ViewGroup才有的方法)**在dispatchTouchEvent()内部调用

2.1 Activity的事件分发机制

MotionEvent最先传递到Activity,然后调用dispatchTouchEvent()方法

2.1.1 Activity的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
//当是按下事件时,调用onUserInteraction()
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//调用PhoneWindow的superDispatchTouchEvent(ev)
if (getWindow().superDispatchTouchEvent(ev)) {
//如果PhoneWindow中消费了事件,意味着分发结束了,直接返回true
return true;
}
//如果PhoneWindow中没有消费事件,调用Activity的onTouchEvent,看看Activity会不会消费此事件
return onTouchEvent(ev);
}

非重点(可跳过):onUserInteraction()是个空实现,可被重写,它会在按下屏幕(Activity范围内)的时候回调(还会dispatchKeyEvent、dispatchTrackballEvent等其他事件的一开始调用,但是像按键和轨迹球现在的Android几乎已经见不到了)。此外还会在很多onUserLeaveHint()回调的地方一起回调,onUserLeaveHint()就是因为用户自身选择进入后台时回调(系统选择不会)。总结onUserInteraction()会在和Activity交互时回调(事件,home返回,点击通知栏跳转其他地方等)

来看看PhoneWindow中的superDispatchTouchEvent(ev)方法:

public boolean superDispatchTouchEvent(MotionEvent event) {
//调用DecorView的superDispatchTouchEvent(event)
return mDecor.superDispatchTouchEvent(event);
}

来看看DecorView中的superDispatchTouchEvent(ev)方法:

public boolean superDispatchTouchEvent(MotionEvent event) {
//调用父类的dispatchTouchEvent
return super.dispatchTouchEvent(event);
}

由于FrameLayout没有重写dispatchTouchEvent,所以进入ViewGroup的dispatchTouchEvent

2.2.2 ViewGroup的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
...
//定义返回对象,默认返回false
boolean handled = false;
...
//调用onInterceptTouchEvent看是否需要拦截
intercepted = onInterceptTouchEvent(ev);
//既不取消也不拦截则遍历子View来处理
if (!canceled && !intercepted) {
...
final int childrenCount = mChildrenCount;
...
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
//看点击事件的位置是否在某个子View内部
if (!child.canReceivePointerEvents()|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
...
//若存在这样的子View的话调用dispatchTransformedTouchEvent方法,该方法根据child是否为空做出不同反应
//child不为空,调用child.dispatchTouchEvent(event)
//child为空,则调用super.dispatchTouchEvent(event)
//即都是调用View的dispatchTouchEvent
handled = dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
}
...
//若没有子View消费事件,则ViewGroup看看自己是否要消费此事件
//child为空,内部调用super.dispatchTouchEvent(event),即调用ViewGroup的父类View.dispatchTouchEvent(event)
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
...
}
...
return handled;
}

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
}
2.2.3 ViewGroup的onInterceptTouchEvent方法
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;
}

没啥好分析的,拦截就返回true,不拦截就返回false,可重写此方法来拦截事件。

2.2.4 View的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
...
//定义返回结果
boolean result = false;
...
ListenerInfo li = mListenerInfo;
if (li != null
&& li.mOnTouchListener != null //1.设置了setOnTouchListener则为true
&& (mViewFlags & ENABLED_MASK) == ENABLED //2.判断当前点击的控件是否enable,大部分控件默认都是enable
&& li.mOnTouchListener.onTouch(this, event)) { //3.mOnTouchListener.onTouch方法的返回值
//以上三个条件都满足则返回true,意味着点击事件已被消费
result = true;
}
//若仍未被消费,调用onTouchEvent方法
if (!result && onTouchEvent(event)) {
result = true;
}
...
return result;
}

可以看出OnTouchListener中的onTouch方法优先级高于onTouchEvent(event)方法

2.2.5 View的onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
...
switch (action) {
//抬起事件,performClickInternal内部调用performClick方法
case MotionEvent.ACTION_UP:
...
performClickInternal();
...
//点击和移动事件内部会判断是否长按,用于抬起事件判断是否触发长按的回调
case MotionEvent.ACTION_DOWN:
...
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
...
case MotionEvent.ACTION_CANCEL:
...
case MotionEvent.ACTION_MOVE:
...
}
...
}

switch外层还嵌套着判断,提供默认返回:若该控件可点击,返回true,不可点击,返回false。

public boolean performClick() {
...
final boolean result;
final ListenerInfo li = mListenerInfo;
//如果回调了onClick方法,证明事件被消费,返回true。没有则返回false
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
...
return result;
}
2.2.6 Activity的onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}

return false;
}

来看看Window中的shouldCloseOnTouch方法

public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
return true;
}
return false;
}
setFinishOnTouchOutside(true)//因此,像Dialog这种可以修改mCloseOnTouchOutside的值,实现点击外部时关闭

分发流程总结:从Activity.dispatchTouchEvent开始分发事件给DecorView这个ViewGroup,在从ViewGroup.dispatchTouchEvent向下分发,ViewGroup中先调用onInterceptTouchEvent判断是否需要拦截,如果不需要拦截就递归分发直到叶子结点的子View,View调用dispatchTouchEvent中有onTouchListener的话先调用onTouch方法,在根据返回情况调用自身onTouchEvent方法,onTouchEvent中抬起事件中检查是否有onClickListener,有的话调用onClick方法消费事件,没有的话,回到ViewGroup,所以子View都不消费事件的话调用自身父类的onTouchEvent,就是View中的,同样检查一遍。如果DecorView所有子View都不消费,且自身也不消费,就回到Acticity。调用Activity的onTouchEvent,如果有设置点击Activity外消费的话,且事件确实是Activity外部的话就有Activity消费,否则返回false。

0 个评论

要回复文章请先登录注册