1、基础知识
1.1 定义
当用户触摸手机屏幕会产生点事件(Touch事件),Touch事件的相关细节(触摸位置、时间等)将封装成MotionEvent对象。
1.2 事件分发的对象
这里事件指的是点击事件(Touch事件),即对点击事件进行分发。
1.3 事件的类型
1.MotionEvent.ACTION_DOWN
当手指按下触摸手机屏幕的时候出发该事件,所有事件的开始。
2.MotionEvent.ACTION_UP
抬起手指的时候出发该事件
3.MotionEvent.ACTION_MOVE
手指滑动的时候出发该事件
4.MotionEvent.ACTION_CANCEL
非认为原因的结速事件
- 特别说明
从手指按下屏幕到手指离开屏幕这个过程会产生一系列的事件,一般情况下都是以DOWN事件开始,UP事件结束,中间产生很多个MOVE事件。
经过以上的了解我们知道当我们触摸手机屏幕到离开手机屏幕的时候,会产生一系列的事件,这些事件将封装成MotionEvent对象。而我们对事件的分发,其实就是对MotionEvent对象进行分发。
1.4 事件分发的本质
将点击事件(MotionEvent)分发到某个具体的View以及处理的整改过程
1.5 事件在那些对象直接传递
事件主要在Activity、ViewGroup、View之间传递。并且按照下图这个先后顺序进行传递。
image.png
也就是说当一个点击事件产生之后,将首先传递到Activity,然后到ViewGroup,最后传递到View。
1.6 事件分发涉及到的核心方法
- dispatchTouchEvent
分发、传递事件。当某个事件传递到当前View的时候,该方法被调用。 - onTouchEvent
处理点击事件,在dispatchTouchEvent内部调用,也就是说如果某个时间传递到当前View首先会调用dispatchTouchEvent方法,如果需要当前View处理当前事件,则在dispatchTouchEvent内部调用onTouchEvent方法对事件进行处理消耗。 - onInterceptTouchEvent
判断是否拦截了某个事件。在ViewGroup的dispatchTouchEvent方法内部调用,因为只有ViewGroup才会有子View,才会存在对事件的拦截。
2、事件分发机制 源码解析
经过上面的分析我们知道时间分发的流程顺序为:Activity-ViewGroup-View。
所以要理解事件分发的机制我们要从三个方法去研究:
1.Activity的事件分发
2.ViewGroup的事件分发
3.View的事件分发
2.1 Activity的事件分发
当一个点击事件产生时,最先调用Activity的dispatchTouchEvent方法进行事件的分发。
2.1.1 源码解析
/**
* 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) {
//1、如果一个事件为DOWN事件的时候,调用onUserInteraction方法
//该方法为一个空方法,我们可以不用深究,作用是实现屏保功能。
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//2、如果条件为true则返回true。
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//3.如果以上没有结束,调用onTouchEvent方法。
return onTouchEvent(ev);
}
从源码上分析,
1、首先判断事件为DOWN事件的话,则调用onUserInteraction方法,该方法为一个空方法,我们不去深究。
2、接着第二步调用getWindow().superDispatchTouchEvent(ev)方法,如果该方法返回true,则整个方法返回true。接下来我们来看下getWindow().superDispatchTouchEvent(ev)的实现。
我们知道在getWindow()返回的是一个Window对象,而Window对象是一个抽象类,主要实现类为PhonWindow。所以我们来看下PhonWindow中的superDispatchTouchEvent方法。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
@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);
}
.......
在该方法中直接调用 mDecor.superDispatchTouchEvent(event)。mDecor也就是DecorView对象,他是PhonWindow的内部类,是所有View的顶级View。接下来我们来看 DecorView中superDispatchTouchEvent方法的源码。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
在superDispatchTouchEvent方法中直接调用了父类的dispatchTouchEvent方法。而DecorView父类是谁呢,我们知道DecorView继承了FrameLayout,而FrameLayout继承了ViewGroup。其实这里调用父类的dispatchTouchEvent方法,也就是调用了ViewGroup的dispatchTouchEvent方法。到这里Activity的中的事件就传递到了ViewGroup对象。如果ViewGroup的dispatchTouchEvent方法返回true则整个方法就返回true,事件分发结束。
3、那么如果在第二步中没有返回true。那么继续往下走。调用Activity中的onTouchEvent方法。其实就是由Activity处理事件。
return onTouchEvent(ev);
接下来我们来看下onTouchEvent的源码
public boolean onTouchEvent(MotionEvent event) {
//当一个事件在window的边界外结束Activity返回true
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
//一般情况下都返回false
return false;
}
也就是说当一个事件由Activity往下传递,没有被任何一个View接受的时候,那么该事件就由Activity调用自己onTouchEvent方法自己处理事件。
2.1.2 小结
当一个点击事件产生的时候,从Activity开始进行分发。我们用一张流程图来梳理下Activity的事件分发具体流程。
image.png
2.2 ViewGroup的事件分发
2.2.1 源码解析
从Activity的事件分发分析,我们知道ViewGroup的事件分发是从
dispatchTouchEvent方法开始的。
接下来我们来看下dispatchTouchEvent方法的源码:
由于方法比较长,我们把核心的部分截取出来。
1.判断ViewGroup的点击事件是否要拦截
// Check for interception.
final boolean intercepted;
//1、如果事件是按下事件、或者mFirstTouchTarget 不为空,mFirstTouchTarget 的意思在
//后面的代码我们知道。意思是如果子View消耗了事件,那么mFirstTouchTarget 就会指向
//该子View
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 2、判断是不是禁用拦截事件。FLAG_DISALLOW_INTERCEPT标记决定了是否禁用拦截
//事件。如果禁用拦截事件之后,也就是说,除了DOWN之外的事件,ViewgGroup都不会拦
//截,不会调用onInterceptTouchEvent方法去判断。
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 3、调用onInterceptTouchEvent方法,判断是否拦截事件
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;
}
上面的源码主要是判断是否拦截当前的点击事件。在其中有个两个地方要注意:
(1)表示事件是否是DOWN事件,即事件的开始,或者mFirstTouchTarget 不为空,表示在之前有子View消耗过时间。如果这两个条件都不成立,就意味着除了DOWN之外的事件,如果之前的子View没有消耗事件,即一直拦截事件,也就是我们按下屏幕的时候,当子View没有消耗事件,那么在之后产生的一系列事件都会被ViewGroup拦截。
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null)
(2)表示是否禁用拦截,FLAG_DISALLOW_INTERCEPT这个标记位决定,通常是在子View中调用requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法来设置。也就是说,如果我们在子View设置了禁用拦截,那么除了DOWN事件之外的其他事件都不会拦截。也就是说ViewGroup的onInterceptTouchEvent方法不一定会被调用。所以当我们做解决事件冲突的时候,我们最好是重写dispatchTouchEvent方法。
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
如果事件不拦截,我们继续往下看源码。 Find a child that can receive the event,注释的意思,找一个可以接受事件的子View。
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//1、依次遍历子View
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;
}
//2.判断当前的子view能否接受事件,判断点击的xy坐标是否在子view上
if (!canViewReceivePointerEvents(child)
|| !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;
}
通过上面的源码我们逐个分析:
2.首先依次遍历Viewgroup的子View.
//1、依次遍历子View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
- 判断点击的事件xy坐标是否在当前子view上,且子View没有在坐标系中移动(执行动画)。如果条件成立继续往下走,把事件分发给子View。
//2.判断点击的事件xy坐标是否在当前子view上,且子View没有在坐标系中移动(执行动画)。
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
4.将点击事件分发给子View。我们看到核心代码
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
这里就是将事件分发给子View。
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;
}
5.子View对事件进行分发。我们来看下:dispatchTransformedTouchEvent的源码,在这里首先判断子View是否为空,如果为空,则调用父类的dispatchTouchEvent方法。如果不为空,则调用
child.dispatchTouchEvent(event);把事件分发到子View。而这里的子View也可能是ViewGroup。如果是ViewGroup那又重新按照上面的流程进行分发。那如果不是ViewGroup,那么就完成了一次ViewGroup把事件分发到View。
/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
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;
}
6.子View无法消耗事件.
我们继续看之前的判断条件。如果dispatchTransformedTouchEvent方法返回false,则表示子View无法消耗事件。如果为true则表示子View消耗了点击事件,事件分发结束。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
..
..
..
//如果子View 消耗了事件,则调用addTouchTarget方法添加target
newTouchTarget = addTouchTarget(child, idBitsToAssign);
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
我们来看下为false的情况下,继续往下走。
Did not find a child to receive the event.注释中表示找不到接受事件的子View。
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
我们来看下这个关键条件 if (mFirstTouchTarget == null)。我们知道如果子View消耗了事件的话,那么mFirstTouchTarget 会赋值。如果mFirstTouchTarget 为空的话,代表子View没有消耗事件。接着继续调换dispatchTransformedTouchEvent方法。这个时候我们看到传入的chirld为空。
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
在dispatchTransformedTouchEvent方法中,如果child 为空,那么调用父类的dispatchTouchEvent方法。ViewGroup的父类,也就是View.dispatchTouchEvent。
2.2.2 ViewGroup事件分发小结
1、判断事件是否需要拦截
2、如果拦截,则调用父类的dispatchTouchEvent方法。
3、如果不拦截,将事件分发给子View
4、遍历所有的子View、判断点击事件xy坐标是否在子View上,找到可以接受事件的子View。
5、把事件分发给子View
6、如果子View消耗事件,则事件成功分发到了子View
7、如果子View不消耗事件,或者没有子View。则调用父类的dispatchTouchEvent方法。把事件交给父类。
2.3 View的事件分发
2.3.1 源码解析
从上面分析可知View的事件分发,也是由dispatchTouchEvent方法开始的,我们来看核心代码
public boolean dispatchTouchEvent(MotionEvent event) {
// .........
//......
//.....
//1.判断事件有没有被窗口遮挡住,如果被挡住,则不消耗事件,否则继续往下走
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
//2、判断View有没有设置mOnTouchListener
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//3.调用onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
}
在上面的分析2和3比较重要,首先判断View本身有没有设置mOnTouchListener。如果设置了则调用mOnTouchListener.onTouch消耗事件,否则才调用onTouchEvent。从而得出结论。
mOnTouchListener.onTouc的优先级高于onTouchEvent。
接着我们继续看onTouchEvent的源码
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
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;
}
// A disabled view that is clickable still consumes the touch,events, it just doesn't respond to them.
从这个注释我们知道,View如果设置了不可用状态,依然会消耗点击事件,只是看起来不生效。
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
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;
}
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();
}
}
}
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:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_MOVE:
break;
}
return true;
}
return false;
在上面的代码中,我们看到如果clickable为true,即View的CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE,其中一个为true则调用。 performClickInternal();方法中调用了performClick方法。
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
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设置了mOnClickListener 。则调用onClick方法。这样事件就被View消耗掉了。从而我们知道,onClick方法是在onTouchEvent中执行的,而如果View设置了mOnTouchListener,则会被mOnTouchListener消耗掉。所以可知mOnTouchListener优先级高于onTouchEvent,并且高于onClick。
2.3.2 View事件分发小结
1、判断窗口有没有被遮挡,如果遮挡了,则View不消化事件。如果没有被遮挡,则往下走。
2、判断有没有设置mOnTouchListener。如果设置了,则由mOnTouchListener消耗事件。否则继续往下走
3、调用onTouchEvent方法,在onTouchEvent方法中,调用performClick方法
4、performClick方法,如果设置了mOnClickListener,则调用mOnClickListener的onClick方法。事件分发结束
5、mOnTouchListener优先级大于onTouchEvent大于onClick
网友评论