文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

【Android】事件分发机制源码解析

2022-06-06 13:24

关注

文章目录1. 分发顺序2.源码分析2.1 Activity中的分发流程dispatchTouchEventonTouchEvent总结2.2 ViewGroup中的分发流程dispatchTouchEventonInterceptTouchEvent总结2.3 View中的分发流程dispatchTouchEventonTouchEventACTION_DOWNACTION_MOVEACTION_UPACTION_CANCEL总结 1. 分发顺序

Activity.dispatchTouchEvent()
ViewGroup.dispatchTouchEvent()
ViewGroup.onInterceptTouchEvent()
View.dispatchTouchEvent()
View.onTouchEvent()

2.源码分析 2.1 Activity中的分发流程 dispatchTouchEvent

首先事件进入

Activity.dispatchTouchEvent()

事件链:指由

ACTION_DOWN
开始,途经≥0个
ACTION_MOVE
,最终结束于
ACTION_UP
ACTION_CANCEL

有特殊情况,可能存在一次事件链中有两个

ACTION_DOWN
,这涉及到多指操作了,详情可以查看这篇文章:《每日一问 很多书籍上写:“事件分发只有一次 ACTION_DOWN,一次 ACTION_UP”严谨吗?》。大概是这样的:在ViewGroup中收到
ACTION_POINTER_DOWN
,即手指按下,但是该手指按下的View不是之前的View,因此会在分发过程中变成
ACTION_DOWN
,然后给到View。

下面开始正式的源码分析:

public boolean dispatchTouchEvent(MotionEvent ev) {
    // 一条事件链的开始
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();// 该方法为空实现
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

排除第一个

if
语句,直接看第二个。

getWindow()
返回的是一个
Window
对象,而
Window
对象是一个抽象对象,其唯一实现是
PhoneWindow
,因此这里返回的就是一个
PhoneWindow
。那么继续去看看
PhoneWindow.superDispatchTouchEvent()

@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

mDecor
是一个
DecorView
,那么继续跟进。

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

调用了父类的

dispatchTouchEvent()
。根据源码可以知道,
DecorView
是继承自
FrameLayout
的。那么这里也就相当于调用了
FrameLayout.dispatchTouchEvent()
。然而在
FrameLayout
中没有找到对应的方法,那么继续找
FrameLayout
的父类,也就是
ViewGroup
。此处留待后面分析
ViewGroup
的分发的时候再详看,目前先假设
ViewGroup
返回了false,即不消费该事件,那么该事件将由
Activity
进行消费。

onTouchEvent

现在回到最初的入口,看看后续代码,

onTouchEvent()


public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        return false;
    }

根据源码的注释中可以看到,如果没有重写该方法,默认是返回false的,即该事件没有被消费。但是为了刨根问底,还是看看

mWindow.shouldCloseOnTouch()
方法。

首先在

PhoneWindow
中没有找到该方法,猜测是在抽象类
Window
中。

public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    final boolean isOutside =
            event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
            || event.getAction() == MotionEvent.ACTION_OUTSIDE;
    if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
        return true;
    }
    return false;
}

通过同时满足以下几个条件来返回true:

mCloseOnTouchOutside
为true

decorView
不为null

当前事件为

ACTION_OUTSIDE
或当前事件为
ACTION_DOWN
但是超出边界

然而通过源码发现,

mCloseOnTouchOutside
默认下是为false的,意味着在默认情况下条件无法同时满足,因此一直返回false。

至此,在

Activity
中,一个事件的传递结束了。

总结

Activity
首先将事件通过
Window
传递给
DecorView
,然后
DecorView
通过默认的
ViewGroup.dispatchTouchEvent()
进行事件分发并返回结果。默认情况下,如果没有任何
ViewGroup
View
消费事件,那么该事件最后会去到
Activity.onTouchEvent()
,方法中没有对事件进行任何操作,相当于忽略了这个事件。

下面继续看看,什么时候

ViewGroup.dispatchTouchEvent()
返回true什么时候返回false。

2.2 ViewGroup中的分发流程 dispatchTouchEvent

在上面的分析中提到,事件第一次到

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;
    // onFilterTouchEventForSecurity 当视图或window被隐藏、遮挡时返回false
    if (onFilterTouchEventForSecurity(ev)) {
        ...暂时省略部分代码
    }
    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

首先整体的看这个方法,在关键代码(我省略的那部分)的前后,都进行了一些验证性的动作,先不关注。首先涉及到事件分发的第一个关键方法,

onFilterTouchEventForSecurity()
,这个方法主要是判断当前View和当前window是否被遮挡或隐藏,如果是的话则返回false。也就意味着,如果view被隐藏、遮挡并且触发事件所在的window也遮挡、隐藏,那么事件就不会继续进行传递了,直接返回了false。

而根据前面的分析可以知道,返回false之后,事件会传递到

Activity.onTouchEvent()
中,而在其中并没有对事件进行消费处理,因此事件链就这样结束了。

现在,按照正常流程,分析下前面省略的关键代码。

final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
// 翻译:处理初次按下
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Throw away all previous state when starting a new touch gesture.
    // The framework may have dropped the up or cancel event for the previous gesture
    // due to an app switch, ANR, or some other state change.
    // 翻译:开始新的触摸手势时,放弃所有先前的状态。
    // 由于应用程序切换,ANR或某些其他状态更改,框架可能已放弃
    // 上一个手势的抬起或取消事件。
    // 主要是清空mFirstTouchTarget,清空前会向之前的接收事件的view发送一个cancel事件
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

如果是

ACTION_DOWN
的话,就先清空之前的接收事件的目标以及触摸状态等。调用的两个方法,主要看
cancelAndClearTouchTargets()

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);
            // 向之前的接收事件的view发送一个cancel事件
            dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
        }
        clearTouchTargets();//将mFirstTouchTarget设置为null
        if (syntheticEvent) {
            event.recycle();
        }
    }
}

先进行了一个判断,由于

ACTION_DOWN
是一个事件链的开始,因此这里的
mFirstTouchTarget
是上一条事件链的目标View。注意看,如果传入的事件为空的话,则会构建一个
ACTION_CANCEL
事件。然后遍历目标View去分发该事件,另外需要注意的是分发事件的方法
dispatchTransformedTouchEvent()
,第二个参数这里传的是true。继续跟进,查看是如何分发事件的。

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);
        // child为null时,调用view.dispatchTouchEvent
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    ...省略了部分代码
}

这里我省略掉后面分发正常事件的代码。这里可以看到,

cancel
也就是方法的第二个参数,在上面传过来的时候是传的true,因此这里会进入这个if语句。

在这里,再次将事件设置成

ACTION_CANCEL
,然后判断是否有子View,如果有则向子View传递事件,否则将当前ViewGroup当成一个View来处理事件。
super.dispatchTouchEvent(event)
即调用
View.dispatchTouchEvent(event)
,因为ViewGroup是继承自View的,也就相当于让当前ViewGroup来自己处理这个事件。这个后续再进行分析。

现在回到

ViewGroup.dispatchTouchEvent()
,知道当遇到
ACTION_DOWN
时,ViewGroup会向上一条事件链的目标发送
ACTION_CANCEL
事件,然后将
mFirstTouchTarget
置为null。

继续向下看。

// Check for interception.
// 检查是否需要拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {// 事件链开始或之前有消费事件的View
    // 这里是判断是否允许拦截事件,可以用requestDisallowInterceptTouchEvent()
    // 来进行设置,默认情况下disallowIntercept=false
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        // 这里先判断是否需要将事件拦截下来自己处理,默认返回false
        // 这里也就是事件分发顺序中dispatchTouchEvent→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;
}

这一段主要是判断是否要拦截当前事件,也是在这里,将事件传递到了

ViewGroup.onInterceptTouchEvent()
中。注意一下,调用拦截方法是有条件的。

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;
}

默认情况下,是返回false的,也即是不拦截事件。上面返回true的特殊情况不考虑,毕竟很少出现用鼠标的情况。

OK,现在继续回到

ViewGroup.dispatchTouchEvent()
中查看后面的代码。

// Check for cancelation.
// 检查是否取消
final boolean canceled = resetCancelNextUpFlag(this)
        || actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
// 翻译:如有必要,更新触摸目标列表以使指针向下。
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;// 新的接收事件的目标
boolean alreadyDispatchedToNewTouchTarget = false;// 标记事件是否已经分发
// 下面这个if只给down事件进入的
if (!canceled && !intercepted) {
    // 这里是找到当前已经获得焦点的View,下面会用到
    View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
            ? findChildWithAccessibilityFocus() : null;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        ...忽略部分关键代码
    }
}

首先进行了是否取消的检查,然后开始了第一个比较关键的代码块了,由于代码过长,所以这里先省略掉了。省略的那部分关键代码,进行了两个if的判断,首先假设第一个if通过,即该事件即不取消也不拦截。那么进入第二个代码判断,满足一下条件之一即可进入:

ACTION_DOWN
事件 允许多指操作且事件为
ACTION_POINTER_DOWN
(手指按下)
ACTION_HOVER_MOVE
事件,不属于常见情况。

下面开始分析第一个关键代码块了。这里假设进入的条件是满足了

ACTION_DOWN

...省略部分代码
for (int i = childrenCount - 1; i >= 0; i--) {
    // 倒序的方式遍历,优先获取到新加入的View
    // 这里是可以改变获取view的顺序的,详细的话可以查看getAndVerifyPreorderedIndex
    // 和getAndVerifyPreorderedView
    final int childIndex = getAndVerifyPreorderedIndex(
            childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(
            preorderedList, children, childIndex);
    // 如果前面找到了当前获取焦点的View,那么将事件优先传递给它
    if (childWithAccessibilityFocus != null) {
        if (childWithAccessibilityFocus != child) {
            continue;
        }
        childWithAccessibilityFocus = null;
        i = childrenCount - 1;// 将循环重置,防止由于优先级的问题错过了前面的view		
    }
    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        // 该子View不能接受event事件:visibility!=visiable或animation=null
        // 或者事件没有落在子view的范围内
        ev.setTargetAccessibilityFocus(false);
        continue;
    }
    // 查找子view是否在之前的触摸目标内,由于这里假设给down
    // 事件进入,因此这里获取肯定没有的,为null
    // 如果是多指的话,那么这里可能不为null
    newTouchTarget = getTouchTarget(child);
    if (newTouchTarget != null) {
        // Child is already receiving touch within its bounds.
        // 翻译:子View已经在其范围内接触了。
        // Give it the new pointer in addition to the ones it is handling.
        // 翻译:除了要处理的手指外,还要为其提供新的手指。
        // 能进入到这里,表示是有新的手指按下了
        // 那么下面的分发代码就不走了
        newTouchTarget.pointerIdBits |= idBitsToAssign;
        break;
    }
    resetCancelNextUpFlag(child);
    // 调用dispatchTransformedTouchEvent把事件分发给子View
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        // true表示子View消费了事件
        ...省略部分代码
        // 这里为mFirstTouchTarget赋值了
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
    }
}
...省略部分代码

使用一个倒序的for循环遍历子View,调用

canViewReceivePointerEvents()
判断子View是否能有接收事件的能力,调用
isTransformedTouchPointInView()
判断事件是否落在子View中。

private static boolean canViewReceivePointerEvents(@NonNull View child) {
    return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
            || child.getAnimation() != null;
}

通过这个方法可以知道,如果子View不是

VISIBLE
且没有设置动画的情况下,是不能接收事件的。

找到目标子View之后,调用

dispatchTransformedTouchEvent()
进行事件分发,这里需要注意,第二个参数为false,并且child是不为null的。

现在回到之前分析过的

dispatchTransformedTouchEvent()
方法,这次将会执行后面的正常分发逻辑。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    ...省略ACTION_CANCEL代码
    final MotionEvent transformedEvent;
    if (newPointerIdBits == oldPointerIdBits) {// 不是新手指按下的事件
        if (child == null || child.hasIdentityMatrix()) {
            // hasIdentityMatrix用于判断view是否进行过矩阵变换
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                // 对事件的坐标进行偏移,因为这是相对坐标
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);
                // 事件分发给子View的dispatchTouchEvent
                // 体现了事件传递中的ViewGroup.dispatchTouchEvent→View.dispatchTouchEvent
                handled = child.dispatchTouchEvent(event);
                // 分发完成后将之前的偏移量移除
                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }
    ...省略了部分代码

首先判断是否是多指事件,这里先假设为不是,也即

newPointerIdBits == oldPointerIdBits
条件成立。然后判断
child
是否为null或调用
child.hasIdentityMatrix()
判断是否进行过矩阵变换。这里需要关注的是
child.hasIdentityMatrix()
当使用过补间动画平移进行变化时,这个方法会返回false。那么这里的话依然假设没有进行过矩阵变换。

前面说到,这次传来的

child
是不为null的,那么就可以正常的进行事件分发了。直接调用了
child.dispatchTouchEvent()
,完成向子View的事件传递,并返回结果:子View是否消费事件。

下面假设是多指事件或者

child
进行过矩阵变化,看看后面的代码针对这种情况是如何进行事件分发的。

if (child == null) {
    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());
    }
    handled = child.dispatchTouchEvent(transformedEvent);
}

可以很明显的看到,这里事件调用了

transform()
方法,并且传入的矩阵是
child.getInverseMatrix()
。根据方法名称可以猜到,这个方法返回的矩阵是执行变化前的,也就意味着获取到的是
child
本身的矩阵。这也就导致了事件的触发区域依然在之前的位置了。

现在回到前面,假设这里子View消费了事件,也即

dispatchTransformedTouchEvent()
返回了true,然后会调用
addTouchTarget()
mFirstTouchTarget
赋值,然后构建一个
TouchTarget
对象,其
next
mFirstTouchTarget
child
为目标View,然后将该
TouchTarget
返回并赋值给
newTouchTarget

到这里,关于

ACTION_DOWN
如何找到目标View就完成了。通过这里的分析,可以知道,一条事件链只有在
ACTION_DOWN
ACTION_PONITER_DOWN
的时候才会去找目标View,后续的事件将直接传递到目标View。

下面看看如果没有找到目标View或不是

ACTION_DOWN
事件会如何进行处理。

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    // 翻译:没有触摸目标,因此请将其视为普通视图。
    // 没有触摸目标,事件将会由默认的View.dispatch去处理
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else {
    ...省略找到目标view后分发事件的代码
}

如果没有找到目标View,则开始分发事件,注意

dispatchTransformedTouchEvent()
的第三个参数,这里传的是null。回想前面看过的源码,可以知道,在方法经常数出现这么一个if语句:

if (child == null) {
    handled = super.dispatchTouchEvent(event);
} else {
    ...省略部分代码
    handled = child.dispatchTouchEvent(event);
}
return handled;

因此可以知道,如果

child
即方法的第三个参数为null的情况下,是会将该ViewGroup当成普通的View去处理事件,即调用
View.dispatchTouchEvent()

下面看看如何分发其他非

ACTION_DOWN
事件。

TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
    final TouchTarget next = target.next;
    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
        // down事件,并且被消费,那么就会进入到这里
        // 而且target.next是Null,所以在下次循环的时候就会退出
        handled = true;
    } else {
        // 如果拦截事件的话,这里为true
        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                || intercepted;
        if (dispatchTransformedTouchEvent(ev, cancelChild,
                target.child, target.pointerIdBits)) {
            // 将事件分发到子view中
            handled = true;
        }
        if (cancelChild) {
            if (predecessor == null) {
                mFirstTouchTarget = next;
            } else {
                predecessor.next = next;
            }
            target.recycle();// 这里将target.child清空了,即没有目标view了
            target = next;
            continue;
        }
    }
    predecessor = target;
    target = next;
}

这里的话,使用循环去向

target.child
分发事件,不难理解。但是关键需要关注这一段代码:

if (cancelChild) {
            if (predecessor == null) {
                mFirstTouchTarget = next;
            } else {
                predecessor.next = next;
            }
            target.recycle();// 这里将target.child清空了,即没有目标view了
            target = next;
            continue;
        }

这段代码实现的是:当该ViewGroup拦截事件的话,那么会将之前的目标View给清空,也就导致后续分发事件的时候,

target.child
为null。并且
mFirstTouchTarget
最后也会变成null,因为
next
在最后的目标View肯定是null的。

总结

首先可以确定的是,具体进行事件分发的方法是

dispatchTransformedTouchEvent()
,而该方法的第三个参数就是目标View。如果为null的话,就会把事件交给当前ViewGroup去处理,调用当前ViewGroup的
super.dispatchTouchEvent()

通过前面的源码分析可以得出以下结论:

如果当前ViewGroup拦截了事件链中的任何一次事件,那么该事件链后续的事件都会被拦截下来,即使
onInterceptTouchEvent()
返回false;并且后续的事件也不会再走
onInterceptTouchEvent()
。 能接收事件的View要么是可见的(
Visibility=Visible
)要么设置了动画(
getAnimation()!=null
)。 对于执行过矩阵变换的View,会获取最初的矩阵去当做目标位置进行事件分发。这是为什么补间动画执行后原来的位置才能响应事件的原因。

至此,ViewGroup中的分发流程就走完了。其实可以看到,整个事件分发流程的主要就是在ViewGroup,因为View是不具备分发功能的,毕竟它下面没东西了。

2.3 View中的分发流程

在View中,关键方法是

onTouchEvent()
,如果没有重写该方法的话,默认下所有控件对事件的处理都是在这里执行的。如果说
ViewGroup.dispatchTouchEvent()
是事件分发流程中的主要步骤,那么
View.onTouchEvent()
就是事件处理的主要步骤,也是事件分发流程中最后的步骤。

尽管View不具备再将事件传递下去的资格,然而它依然有

dispatchTouchEvent()
方法。在方法中主要是确定事件该交由谁处理,是自己的
onTouchEvent()
处理还是给
onTouchListener
处理?

dispatchTouchEvent

和ViewGroup一样,上来首先判断当前事件是否要求传递给已经获取焦点的View。

if (event.isTargetAccessibilityFocus()) {
    // We don't have focus or no virtual descendant has it, do not handle the event.
    // 没有焦点,无法处理该事件
    if (!isAccessibilityFocusedViewOrHost()) {
        return false;
    }
    // We have focus and got the event, then use normal event dispatch.
    event.setTargetAccessibilityFocus(false);
}

如果当前事件要求传递给获取到焦点的View,但是当前View没有获取到焦点,那么直接返回false,无法处理事件。否则就进入正常的流程。

final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Defensive cleanup for new gesture
    stopNestedScroll();
}

属于优化手感的代码,如果是

ACTION_DOWN
事件的话,则停止滚动。

下面是方法的关键代码,首先进行了一个判断当前View是否可以接收事件的判断,如果可以则进行分发。

if (onFilterTouchEventForSecurity(event)) {// 判断当前view和当前window是否能接收事件
    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)) {
        // 这里通知到onTouchListener,如果返回true则返回true
        result = true;
    }
    // 如果前面的listener返回了true的话,那么onTouchEvent也就不执行了
    if (!result && onTouchEvent(event)) {
        result = true;
    }
}

这里的话主要是将事件分发给了3个地方处理,首先给到

handleScrollBarDragging()
,根据名字可以知道这个是处理滚动条事件的,该方法只处理来自鼠标的事件,因此一般情况下都返回false。

然后将事件交给

onTouchListener
处理,并记录处理结果。

最后再判断,前面两者是否已经消费了事件,如果是的话则不再将事件分发给

onTouchEvent()
,否则再将事件交给
onTouchEvent()
去处理。

通过这里我们可以知道,如果我们在

onTouchListener
中返回了true,即消费了事件的话,那么该事件将不再进入
View.onTouchEvent()
。也就意味着无法处理点击、长按等事件处理了。

View.dispatchTouchEvent()
中的关键逻辑就这么多,后面的代码不涉及到事件分发,因此就不继续分析了。下面去看看
View.onTouchEvent()
中,对事件的默认处理是怎么样的。

onTouchEvent
// 当前view是否可点击
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {// 当前view不可用
    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;
    }
}
...省略默认处理的代码
    return false;

首先判断当前View是否允许点击,该变量在后面需要用到。

然后判断View是否是处于不可用状态,如果不可用的话则直接将是否可点击作为是否消费事件返回。也就是那段注释所说的:可单击的禁用视图仍然消耗触摸事件,只是不响应它们。

最后将事件交给代理去处理,如果代理处理了事件,那么事件分发到此结束。否则就继续往下,使用默认的实现对事件进行处理。

这里可以看到,如果需要干预事件的处理的话,可以对View设置一个代理,然后在代理中进行自己的处理逻辑。是否覆盖默认逻辑则取决于在代理中是否返回了true。

下面看看,View中默认处理事件的逻辑是怎么样的。

首先要注意的是,要进入默认处理的逻辑需要满足以下条件之一:

当前View可点击 View.flag为TOOLTIP(长按或悬浮时会出现工具条提示)

一般来说满足第一个即可。然后会根据不同的动作进行处理。下面将按照

ACTION_DOWN
ACTION_MOVE
ACTION_UP
ACTION_CANCEL
来进行分析。

ACTION_DOWN
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
    mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
// 注意这里将这个变量设为false了,在ACTION_UP会用到这个变量
mHasPerformedLongPress = false;
if (!clickable) {
    // 不允许点击,那么这里就去触发长按事件
    // 前面知道,这里进入的条件只有两个,既然不满足可点击
    // 那么这里肯定就是长按的时候会出现工具条提示
    checkForLongClick(0, x, y);
    break;
}
if (performButtonActionOnTouchDown(event)) {
    // 一般情况下都返回false
    break;
}
// !!!注意,下面的代码只有在可点击的时候才会去执行!!!
// Walk up the hierarchy to determine if we're inside a scrolling container.
// 翻译:遍历层次结构以确定我们是否在滚动容器内。
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
// 翻译:对于滚动容器内部的视图,如果滚动,则将按下的反馈延迟一小段时间。
if (isInScrollingContainer) {
    mPrivateFlags |= PFLAG_PREPRESSED;
    if (mPendingCheckForTap == null) {
        mPendingCheckForTap = new CheckForTap();
    }
    mPendingCheckForTap.x = event.getX();
    mPendingCheckForTap.y = event.getY();
    // 延迟100ms后再触发反馈,即else的代码
    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
    // Not inside a scrolling container, so show the feedback right away
    // 立刻显示反馈
    setPressed(true, x, y);
    checkForLongClick(0, x, y);
}

ACTION_DOWN
中,主要是涉及到了长按的逻辑。
checkForLongClick()
CheckForTap
都是和长按有关的。它们都是通过向当前View的Handler发送一个Runnable去执行长按的。

private void checkForLongClick(int delayOffset, float x, float y) {
    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
        mHasPerformedLongPress = false;
        if (mPendingCheckForLongPress == null) {
            mPendingCheckForLongPress = new CheckForLongPress();
        }
        mPendingCheckForLongPress.setAnchor(x, y);
        mPendingCheckForLongPress.rememberWindowAttachCount();
        mPendingCheckForLongPress.rememberPressedState();
        // post一个Runnable,延迟500ms,然后Runnable会触发长按performLongClick()
        postDelayed(mPendingCheckForLongPress,
                ViewConfiguration.getLongPressTimeout() - delayOffset);
    }
}
    private final class CheckForLongPress implements Runnable {
        private int mOriginalWindowAttachCount;
        private float mX;
        private float mY;
        private boolean mOriginalPressedState;
        @Override
        public void run() {
            if ((mOriginalPressedState == isPressed()) && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                if (performLongClick(mX, mY)) {// 触发长按事件
                    // 注意,这里将变量设为了true,即表示触发了长按事件
                    mHasPerformedLongPress = true;
                }
            }
        }
    }

首先发送一个延迟Runnable,在Runnable中去触发长按,即

performLongClick()
。这个延迟时间就是View判断当前事件是否是长按的一个阈值,500毫秒。

这种通过Handler的延时机制来执行延时操作的方法在Android源码中很多地方都有用到。

ACTION_MOVE
if (clickable) {
    // 将当前触摸位置传递给background、foreground
    drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {// 划着划着滑出视图范围了
    // Outside button
    // Remove any future long press/tap checks
    // 移除之前添加的的长按、按压等runnable
    // 这里不移除click的原因是,click只有在ACTION_UP的时候才会添加
    removeTapCallback();
    removeLongPressCallback();
    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false);// 取消按压状态
    }
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}

基本都在注释里面了,大意是当滑出当前View的范围后,之前在

ACTION_DOWN
中添加了长按、按下等Runnable都会被从队列中移除,并且即使后来又滑入了当前View的范围,因为没有走
ACTION_DOWN
,所以也不会触发长按了。

ACTION_UP

由于代码过长,因此分段进行分析。

if ((viewFlags & TOOLTIP) == TOOLTIP) {
    handleTooltipUp();// 关闭工具提示
}
if (!clickable) {
    // 移除可能在down的时候发送的Runnable
    removeTapCallback();
    removeLongPressCallback();
    mInContextButtonPress = false;
    mHasPerformedLongPress = false;
    mIgnoreNextUpEvent = false;
    break;
}

再次提醒,前面说到的代码都是在允许点击或有ToolTip的情况下才会执行的。

首先会判断是否有ToolTip,如果有的话则去post一个ToolTip的Runnable。关于ToolTip,这是个在SDK≥26后出现的一个功能。调用

setToolTipText()
可以设置提示文本,当长按的时候会弹出一个小窗口显示文本信息。那么这个功能和长按监听器如何处理冲突呢?这里不具体分析,结论是:当
onLongClickListener()
返回false的时候,将会去显示ToolTip,返回true则不显示。

注意上面

if(!clickable)
中使用了
break
,所以下面的代码都是在可点击的情况下才执行的!

boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
    boolean focusTaken = false;
    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
        focusTaken = requestFocus();// 获取焦点
    }
   	...省略部分代码	
    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
		// 这是个点击事件,移除长按runnable
        removeLongPressCallback();
        if (!focusTaken) {
            if (mPerformClick == null) {
                mPerformClick = new PerformClick();
            }
            if (!post(mPerformClick)) {// post失败的情况下,直接触发click
                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();
    }
    // 移除按压,检查长按的runnable
    removeTapCallback();
}
mIgnoreNextUpEvent = false;

整个代码主要执行了两个动作,移除非点击相关的Runnable,以及Post一个点击Runnable。

需要满足以下几个条件才会当成点击事件处理:

mHasPerformedLongPress
为false,该变量在触发长按,即
performLongClick()
后为true。
mIgnoreNextUpEvent
为false,该变量仅在非Touch事件才有可能为true。 当前View能够获取到焦点。

这里有个问题,那就是为什么不直接调用

performClick()
去触发点击,而是要使用Handler去post呢?根据源码注释,官方是这样解释的:

This lets other visual state of the view update before click action start.

谷歌翻译:这样,在单击操作开始之前,View的其他视觉状态就会更新。

ACTION_CANCEL
// 执行一些清除操作
if (clickable) {
    setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;

移除所有可能添加的Runnable,重置状态。

总结
onTouchListener
中返回了true的话,事件将不再传递到
onTouchEvent()
,即事件被消费了。 可单击的禁用View依然会消费事件,但是不会触发
onClickListener()
。 如果设置了代理,且在代理的
onTouchEvent()
返回true的话,事件将不再进行默认的处理。 如果按下之后,进行滑动,并且滑出了当前View的范围,那么将不再触发长按事件,即使后面又滑入当前View的范围。
作者:大东Pd


阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     801人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     348人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     311人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     432人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     220人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯