View事件分发
- 前言
- 事件分发机制
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/62598cb466e19e6bcc744f565dbebca0.jpeg)
前言
在Android中,事件分发机制是一块很重要的知识点,掌握这个机制能帮你在平时的开发中解决掉很多的View事件冲突问题,这个问题也是面试中问的比较多的一个问题了,今天就来一探究竟。
事件分发机制
事件分发原因
Android中页面上的View是以树型结构显示的,View会重叠在一起,当我们点击的地方有多个View可以响应的时候,这个点击事件应该给谁呢?为了解决这个问题,Google设计了View的事件分发机制
事件分发对象
Touch事件,即将每一个Touch事件(MotionEvent)传递给View,至于最终这个事件有没有处理看接收事件者的逻辑而定
当用户触摸屏幕的时候,就会产生Touch事件(Touch事件被封装成MotionEvent对象),其主要分为如下几种
- MotionEvent.ACTION_DOWN:使用手指点击屏幕这一瞬间,产生该事件,是所有事件的开始
- MotionEvent.ACTION_MOVE:使用手指在屏幕滑动的时候产生该事件
- MotionEvent.ACTION_CANCLE:非人为原因结束当前事件
- MotionEvent.ACTION_UP:手指离开屏幕一瞬间产生该事件
一次完整的Touch事件,是从用户手指触摸屏幕(伴随着一次ACTION_DOWN事件)到用户手指离开屏幕(伴随着一次ACTION_UP事件)这一过程,整个过程如下
ACTION_DOWN(一次) --> ACTION_MOVE(N次) --> ACTION_UP(一次)
事件分发方法
- dispatchTouchEvent(MotionEvent ev) :从方法名也能看出它的作用是对事件进行分发;当一个事件由底层驱动检测到了之后,会进行上报,最终会交由Activity的该方法处理,来决定是自己消费还是继续传递下去
- onInterceptTouchEvent(MotionEvent ev) :当一个事件分发到ViewGroup后,它可以决定是否对该事件进行拦截,该方法只有ViewGroup拥有
- onTouchEvent(MotionEvent event) :这是事件分发流程的最后一个方法了,即是否消费该次事件
事件分发参与者
- Activity:包含ViewGroup和View
- ViewGroup:包含ViewGroup和View
- View:并不包含其它View,只有自己
事件分发流向一般是Activity --> ViewGroup --> … --> View
注意:
- 子View可以通过requestDisallowInterceptTouchEvent方法干预父View的事件分发过程(ACTION_DOWN事件除外),而这就是我们处理滑动冲突常用的关键方法
- 如果View设置了onTouchListener,在重写的onTouch方法中返回true,那么它的onTouchEvent方法不会被调用,因为在View的dispatchTouchEvent中onTouch优先于onTouchEvent执行;onClick方法也不会被调用,因为onClick是在onTouchEvent中回调的
事件分发流程
- 当手指触摸屏幕后,底层Input驱动从/dev/input/路径下读写以event[NUMBER]为名的硬件输入设备节点获取事件(可以通过adb shell getevent 查看你的设备下的节点,Android也是从这些节点获取这些原始数据再封装后提供给开发者使用;如果做游戏开发可能就直接获取这些原始数据自己处理了),经过一系列调用后传递到了DecorView的dispatchTouchEvent方法
- 在DecorView中,会通过Window的内部接口Callback,将事件继续传递,因为Activity实现了该接口,故事件分发到Activity;Activity获取到事件后,在dispatchTouchEvent方法中先将事件分发到该Activity所在的window,实际类型是PhoneWindow,这个window又将事件交给它的顶级view即DecorView处理
- DecorView是FrameLayout的子类,即ViewGroup的子类,自己没有处理,只是继续将事件交由ViewGroup处理;就这样一个事件就从Activity转到了ViewGroup
- ViewGroup在dispatchTouchEvent方法进行分发,如果自己的onInterceptTouchEvent方法拦截此次事件,就把事件交给自身的onTouchEvent方法处理;反之遍历自己的子View,继续将事件分发下去,只要有一个子View消费了这个事件,那就停止遍历
- 事件会传递到子View的dispatchTouchEvent方法,如果给子View注册了OnTouchListener,且返回true,那事件分发就到此结束;反之就会继续将事件传递到子View的onTouchEvent方法
- 子View会在ACTION_UP事件中回调View的onClick监听,如果子View没有消费此次事件,就会按照分发流程反过来传递回去到Activity;如果到了Activity还没人消费(包括Activity自己),那就会销毁这个事件
事件分发源码
以下源码基于API24
对应上面的流程,当有Touch事件后,步骤如下
DecorView.dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
此处的cb指的是window内部的Callback接口,Activity实现了这个接口,接下来进入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用来处理触摸屏事件,我们可以重写这个方法,并返回true/false,这样在事件分发到window前就能进行拦截,Activity内的ViewGroup或者View将收不到事件
一个触摸屏事件都是以ACTION_DOWN开始,那就肯定会进入 onUserInteraction()方法
public void onUserInteraction() {
}
这是一个空方法,它的调用时机如下:
当一个按键事件,触摸屏事件或者trackball事件分发到Activity的时候,它就会被调用;如果你希望在Activity正在运行的时候了解用户和设备用某种方式交互,可以重写这个方法;不过需要注意的是这个方法只响应touch-down这种触摸手势,不会响应接下来的touch-move和touch-up
与这个方法相对应的一个方法就是onUserLeaveHint,它同样也是一个空方法,它的调用时机如下:
当在用户操作的情况下Activity进入后台,这个方法会作为Activity生命周期的一部分被调用;比如,用户按下home键,当前Activity就会进入后台,它就会被调用,并且是在onPause之前调用;但是比如有电话打进来了导致Activity被动进入后台,这个方法就不会被调用
接下来进入第二个if语句
getWindow().superDispatchTouchEvent
通过getWindow()获取到的是一个Window对象,但是它是在Activity的attach方法中进行实例化,实际类型是PhoneWindow,也是在这里实现了Callback接口
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window) {
......
mWindow = new PhoneWindow(this, window);
mWindow.setCallback(this);
......
}
这里就转到PhoneWindow,如下
PhoneWindow.superDispatchTouchEvent
//这是窗口的顶层视图
private DecorView mDecor
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
DecorView .superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView 是FrameLayout的子类,FrameLayout又是ViewGroup的子类,这里就会走到ViewGroup
ViewGroup.dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//用于调试目的的一致性验证程序
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
//这个变量用于标记事件是否被消费
boolean handled = false;
//根据应用安全策略过滤触摸事件
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 处理initial down发生后的初始化操作
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 新的ACTION_DOWN事件来了,需要取消并清除之前的touch Targets
//清空掉mFirstTouchTarget
cancelAndClearTouchTargets(ev);
//重置触摸状态
resetTouchState();
}
//标记是否拦截事件
final boolean intercepted;
// 当ACTION_DOWN来了或者已经发生过ACTION_DOWN,并且将mFirstTouchTarget赋值 就检测ViewGroup是否需要拦截事件.
//只有发生过ACTION_DOWN事件,mFirstTouchTarget != null
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//子View可以通过调用父View的requestDisallowInterceptTouchEvent方法设置mGroupFlags值
//以此告诉父View是否拦截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//如果子view 没有告诉父View别拦截事件,那父View就判断自己是否需要拦截事件
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // 重新恢复action 以防被改变了
} else {
//这里表明子View告诉父View不要拦截事件
intercepted = false;
}
} else {
//当mFirstTouchTarget=null(没有子View被分配处理),且不是initial down事件时(事件已经初始化过了),ViewGroup继续拦截触摸
//继续设置为true
intercepted = true;
}
// 如果当前事件是ACTION_CANCEL,或者view.mPrivateFlags被设置了PFLAG_CANCEL_NEXT_UP_EVENT
//那么当前事件就取消了
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
//split表示当前的ViewGroup是不是支持分割MotionEvent到不同的View当中
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
//新的TouchTarget
TouchTarget newTouchTarget = null;
//是否把事件分发给了新的TouchTarget
boolean alreadyDispatchedToNewTouchTarget = false;
//不取消事件,同时不拦截事件才进入该区域
if (!canceled && !intercepted) {
//把事件分发给所有的子视图,寻找可以获取焦点的视图
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
//如果是这三种事件就得遍历子View
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// 对于这个PointerId 清空更早的 touch targets
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
//如果当前ViewGroup有子View且newTouchTarget=null
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 在视图里从前到后扫描一遍获取可以接收事件的子View
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//遍历所有子View,找到一个来接收事件
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//如果当前子View没有获取焦点,则跳过这个子View
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//如果当前子View不可见且没有播放动画 或者 不在触摸点范围内,跳过这个子View
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//如果在触摸目标列表找到了与该子View对应的TouchTarget,说明这个view正在接收事件,不需要再遍历,直接退出
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//子view处于触摸位置,就将事件分发给子View,如果该子View返回true,说明消费了这个事件,就跳出遍历
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 获取TouchDown的时间点
mLastTouchDownTime = ev.getDownTime();
// 获取TouchDown的Index
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;
}
//获取TouchDown的x,y坐标
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
//添加到触摸目标列表 同时给mFirstTouchTarget赋值
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();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// 到这里说明没有子View接收事件,那就把最近一次的触摸目标赋值给newTouchTarget
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// mFirstTouchTarget赋值是在通过addTouchTarget方法获取的;
// 只有处理ACTION_DOWN事件,才会进入addTouchTarget方法。
// 这也正是当View没有消费ACTION_DOWN事件,则不会接收其他MOVE,UP等事件的原因
if (mFirstTouchTarget == null) {
// 那就只能ViewGroup自己处理事件了
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 到这里就说明有子View接收了ACTION_DOWN事件,那后续的move up等事件就继续分发给这个触摸目标
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//如果view.mPrivateFlags被设置了PFLAG_CANCEL_NEXT_UP_EVENT 或者事件被ViewGroup拦截了
//那子View需要取消事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//继续分发事件给子View
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
//当发生抬起或取消事件,更新触摸目标列表
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
//如果是多点触摸下的手指抬起事件,就要根据idBit从TouchTarget中移除掉对应的Pointer(触摸点)
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
这个方法内容有点多,需要拆分开分析
第一步:事件初始化
第一个进来的是ACTION_DOWN事件,那需要做一些初始化:
- 第一件事就是清空所有的 TouchTarget,并将mFirstTouchTarget值为null;mFirstTouchTarget的类型也是TouchTarget,是ViewGroup的一个内部类,描述一个触摸的视图和它捕获的指针的id;mFirstTouchTarget 可以理解为如果事件由子View去处理时mFirstTouchTarget 会被赋值并指向子View
- 第二件事是重置状态值,通过FLAG_DISALLOW_INTERCEPT重置mGroupFlags值
ViewGroup.cancelAndClearTouchTargets
/**
* 取消和清空所有的 touch targets.
*/
private void cancelAndClearTouchTargets(MotionEvent event) {
if (mFirstTouchTarget != null) {
boolean syntheticEvent = false;
if (event == null) {
final long now = SystemClock.uptimeMillis();
event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
syntheticEvent = true;
}
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
clearTouchTargets();
if (syntheticEvent) {
event.recycle();
}
}
}
/**
* 清空所有的 touch targets.
*/
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
/**
* 重置所有触摸状态以准备新周期.
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
第二步:拦截判断
接下来就需要判断是否需要拦截事件:
首先看条件是
//标记是否拦截事件
final boolean intercepted;
// 当ACTION_DOWN来了或者已经发生过ACTION_DOWN,并且将mFirstTouchTarget赋值 就检测ViewGroup是否需要拦截事件.
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//子View可以通过调用父View的requestDisallowInterceptTouchEvent方法设置mGroupFlags值
//以此告诉父View是否拦截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//如果子view 没有告诉父View别拦截事件,那父View就判断自己是否需要拦截事件
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // 重新恢复action 以防被改变了
} else {
//这里表明子View告诉父View不要拦截事件
intercepted = false;
}
} else {
//当mFirstTouchTarget=null(没有子View被分配处理),且不是initial down事件时(事件已经初始化过了),ViewGroup继续拦截触摸
//继续设置为true
intercepted = true;
}
- 当事件是ACTION_DOWN或者 mFirstTouchTarget != null才会去判断要不要拦截,由第一步可知,当事件是ACTION_DOWN的时候,mFirstTouchTarget 肯定为null,所以这里只有两种情况会进入:ACTION_DOWN事件来了需要判断拦截;ACTION_DOWN事件中如果有子View接收了事件(这样mFirstTouchTarget 就赋值了),那接下来的事件也需要判断是否拦截事件
- 上面条件的反向逻辑就是事件是ACTION_DOWN事件以后的事件(比如move或者up)且mFirstTouchTarget 为null,说明在ACTION_DOWN事件中就判断了需要拦截事件或者没有子View处理事件,那接下来的事件就没必要分发了,继续拦截
第一个if语句里面是拦截判断逻辑是
- 先通过与运算获得mGroupFlags 的值,子view可以通过调用父view的requestDisallowInterceptTouchEvent方法设置mGroupFlags 的值,告诉父view不要拦截事件
- 如果disallowIntercept 为true,说明子view要求父view不要拦截,就将intercepted 设置false
- 如果disallowIntercept 为false,表明子view没有提出不要拦截请求,那就调用onInterceptTouchEvent看看自己是不是需要拦截事件
ViewGroup.requestDisallowInterceptTouchEvent
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// 如果已经设置过了,就返回
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// 依次告诉父view
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
ViewGroup.onInterceptTouchEvent
/**
* ViewGroup可在这个方法里拦截所有触摸事件,默认是不拦截事件,开发者可以重写这个方法决定是否要拦截
* 如下四个条件都成立,返回true,拦截事件
* 第一个:触摸事件是否来自鼠标指针设备
* 第二个:触摸事件是否是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;
}
第三步:ACTION_DOWN事件分发
接下来就需要遍历子View,然后将ACTION_DOWN事件分发给能接收事件的子View
- 如果当前子View没有获取焦点,则跳过这个子View
- 如果当前子View不可见且没有播放动画 或者 不在触摸点范围内,跳过这个子View
- 如果在触摸目标列表找到了与该子View对应的TouchTarget,说明这个view正在接收事件,不需要再遍历,直接退出
- 如果子view处于触摸位置,就调用dispatchTransformedTouchEvent方法将事件分发给子View,如果该方法返回true,说明子View消费了这个事件,那就不需要再寻找子view接收事件了,跳出遍历
ViewGroup.dispatchTransformedTouchEvent
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;
}
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
//由于某些原因,发生不一致的操作,那么将抛弃该事件
if (newPointerIdBits == 0) {
return false;
}
//分发的主要区域
final MotionEvent transformedEvent;
//判断预期的pointer id与事件的pointer id是否相等
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
//不存在子视图时,ViewGroup调用View.dispatchTouchEvent分发事件,再调用ViewGroup.onTouchEvent来处理事件
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
//将触摸事件分发给子ViewGroup或View;
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY); //调整该事件的位置
}
return handled;
}
transformedEvent = MotionEvent.obtain(event); //拷贝该事件,来创建一个新的MotionEvent
} else {
//分离事件,获取包含newPointerIdBits的MotionEvent
transformedEvent = event.split(newPointerIdBits);
}
if (child == null) {
//不存在子视图时,ViewGroup调用View.dispatchTouchEvent分发事件,再调用ViewGroup.onTouchEvent来处理事件
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
//将该视图的矩阵进行转换
transformedEvent.transform(child.getInverseMatrix());
}
//将触摸事件分发给子ViewGroup或View;
handled = child.dispatchTouchEvent(transformedEvent);
}
//回收transformedEvent
transformedEvent.recycle();
return handled;
}
该方法是ViewGroup真正处理事件的地方,分发子View来消费事件,过滤掉不相干的pointer ids。当子视图为null时,MotionEvent将会发送给该ViewGroup;不为null,最终调用View.dispatchTouchEvent方法来分发事件。
这个方法调用完毕,回到ViewGroup.dispatchTouchEvent会调用addTouchTarget方法
ViewGroup.addTouchTarget
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
可以看到在这里给 mFirstTouchTarget赋值了
当子控件消费了事件,mFirstTouchTarget不为空;当子控件没有消费事件或者被拦截,mFirstTouchTarget为空
第四步:ACTION_MOVE ACTION_UP事件分发
在第三步过后,ViewGroup可能会找到有子View消费事件
- 如果事件被拦截,mFirstTouchTarget==null,那接下来的事件最终调用View.dispatchTouchEvent方法来分发事件
- 如果ViewGroup没有子View,mFirstTouchTarget==null,那接下来同上
- 如果有子View,但是子View没消费事件,mFirstTouchTarget==null,那接下来同上
- 如果有子View,且子View消费了ACTION_DOWN事件,但是在dispatchTouchEvent返回了false(即dispatchTransformedTouchEvent返回false,那addTouchTarget就不会被调用),mFirstTouchTarget==null,那接下来的处理也同上
- 接下来就是mFirstTouchTarget不为null了,那就需要将后续事件分发给消费ACTION_DOWN事件的View了
通过对ViewGroup.dispatchTouchEvent方法的分析,我们知道不管有没有子View消费事件,最终事件都会进入View.dispatchTouchEvent方法,那我们继续一探究竟
View.dispatchTouchEvent
/**
* 将触摸事件向下传递到目标视图,或者这个View是目标视图。
*
* @return 返回true 表示消费了事件,反之返回false
*/
public boolean dispatchTouchEvent(MotionEvent event) {
......
boolean result = false;
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
//在Down事件之前,如果存在滚动操作则停止。不存在则不进行操作
stopNestedScroll();
}
//过滤触摸事件以应用安全策略
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
ListenerInfo li = mListenerInfo;
// 如果给View设置了OnTouchListener
//且该view没有禁用的
//且OnTouchListener.onTouch返回true
//那说明该View消费了该事件,返回true
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果OnTouchListener.onTouch没有消费事件且View的onTouchEvent方法返回true,那返回true
if (!result && onTouchEvent(event)) {
result = true;
}
}
// 如果这是手势的结束,则在嵌套滚动后清理;
//如果我们尝试了ACTION_DOWN但是我们不想要其余的手势,也要取消它。
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
这里有两点比较重要
- 如果开发者设置OnTouchListener监听,且在onTouch方法返回true,说明view消费了事件
- 如果没有设置监听,那就调用View的onTouchEvent方法去处理事件
可以看出OnTouchListener.onTouch是优先于onTouchEvent执行的,只要前者返回true,那后者就不会执行了,事件到此为止结束
接下来看看onTouchEvent的逻辑
View.onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
//如果这个view是禁用的,可以通过setEnabled()设置是否禁用
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// 即使设置了禁用,但是只要这个view满足CLICKABLE ,LONG_CLICKABLE ,CONTEXT_CLICKABLE其中一种
//任然算消费该事件,只是没有响应而已
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
//当View状态为ENABLED
//且这个view满足CLICKABLE LONG_CLICKABLE CONTEXT_CLICKABLE其中一种,就消费这个事件
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) {
// 获取焦点处于可触摸模式
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) {
//这是Tap操作,移除长按回调方法
removeLongPressCallback();
// 如果处于按下状态尽执行点击操作
if (!focusTaken) {
// 使用Runnable并发布而不是直接调用performClick
//这样可以在单击操作开始之前更新视图的其他可视状态
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
//调用View.OnClickListener
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;
}
// 确定是否处于可滚动的视图内
boolean isInScrollingContainer = isInScrollingContainer();
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
//当处于可滚动视图内,则延迟TAP_TIMEOUT,再反馈按压状态,用来判断用户是否想要滚动。默认延时为100ms
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
//当不再滚动视图内,则立刻反馈按压状态
setPressed(true, x, y);
//检测是否是长按,如果长按,回调OnLongClickListener.onLongClick
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;
}
return false;
}
这里有几点需要注意
- 只要是这个view满足CLICKABLE ,LONG_CLICKABLE ,CONTEXT_CLICKABLE其中一种,不管通过setEnabled()设置禁用还是可用,都会返回true,认为消费事件
- View的longClickable默认为false,clickable需要区分情况,如Button的clickable默认为true,而TextView的clickable默认为false;但是View的setOnClickListener会默认将View的clickable设置成true,View的setOnLongClickListener同样会将View的longClickable设置成true
- 在ACTION_DOWN操作中,如果是长按,回调OnLongClickListener.onLongClick
- 在ACTION_UP操作中,回调OnClickListener.onClick
Activity.OnTouchEvent
所有流程走完,假如没有一个View消费事件,那最终会回到Activity.OnTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//循环判断是否有ViewGroup或者View消费事件,如果没有,事件回到activity
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
事件分发流程图
这里借用网络中的图片
注意点
- 触摸事件由Activity.dispatchTouchEvent先处理;再一层层往下分发,当中间的ViewGroup都不消费或者拦截时,进入最底层的View,开始由最底层的OnTouchEvent来处理,如果一直不消费,则最后返回到Activity.OnTouchEvent
- 只有ViewGroup有onInterceptTouchEvent拦截方法;在分发过程中,中间任何一层ViewGroup都可以直接拦截,则不再往下分发,而是交由发生拦截操作的ViewGroup的OnTouchEvent来处理
- 子View可调用父ViewGroup的requestDisallowInterceptTouchEvent方法,来设置disallowIntercept=true,从而阻止父ViewGroup的onInterceptTouchEvent拦截操作
- OnTouchEvent由下往上冒泡时,当中间任何一层的OnTouchEvent消费该事件,则不再往上传递,表示事件已消费
- 如果dispatchTouchEvent在进行事件分发的时候,View没有消费ACTION_DOWN事件,即返回true,则之后的ACTION_MOVE等事件都将无法接收
- 不管View是DISABLED(禁用)的还是ENABLED(可用)的,只要是CLICKABLE (可点击),LONG_CLICKABLE(可长按) ,都会消费事件
- View的setOnClickListener会默认将View的clickable设置成true,View的setOnLongClickListener同样会将View的longClickable设置成true;所有View的setClickable和setLongClickable最好在两个监听方法后调用
- onTouch优先于onTouchEvent执行,onClick和onLongClick在onTouchEvent中被调用,且onLongClick优先于onClick被执行;如果onTouch返回true,就不会执行onTouchEvent;onTouch只有View设置了OnTouchListener,且是enable的才执行该方法
至此,事件分发机制及源码分析就结束了