阅读 2505

Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

其实我一直准备写一篇关于 Android 事件分发机制的文章,从我的第一篇博客开始,就零零散散在好多地方使用到了 Android 事件分发的知识。也有好多朋友问过我各种问题,比如:onTouch 和 onTouchEvent 有什么区别,又该如何使用?为什么给 ListView 引入了一个滑动菜单的功能,ListView 就不能滚动了?为什么图片轮播器里的图片使用 Button 而不用 ImageView?等等…… 对于这些问题,我并没有给出非常详细的回答,因为我知道如果想要彻底搞明白这些问题,掌握 Android 事件分发机制是必不可少的,而 Android 事件分发机制绝对不是三言两语就能说得清的。

在我经过较长时间的筹备之后,终于决定开始写这样一篇文章了。目前虽然网上相关的文章也不少,但我觉得没有哪篇写得特别详细的 (也许我还没有找到),多数文章只是讲了讲理论,然后配合 demo 运行了一下结果。而我准备带着大家从源码的角度进行分析,相信大家可以更加深刻地理解 Android 事件分发机制。

阅读源码讲究由浅入深,循序渐进,因此我们也从简单的开始,本篇先带大家探究 View 的事件分发,下篇再去探究难度更高的 ViewGroup 的事件分发。

那我们现在就开始吧!比如说你当前有一个非常简单的项目,只有一个 Activity,并且 Activity 中只有一个按钮。你可能已经知道,如果想要给这个按钮注册一个点击事件,只需要调用:

button.setOnClickListener(new OnClickListener() {

public void onClick(View v) {

		Log.d("TAG", "onClick execute");
复制代码

这样在 onClick 方法里面写实现,就可以在按钮被点击的时候执行。你可能也已经知道,如果想给这个按钮再添加一个 touch 事件,只需要调用:

button.setOnTouchListener(new OnTouchListener() {

public boolean onTouch(View v, MotionEvent event) {

		Log.d("TAG", "onTouch execute, action " + event.getAction());
复制代码

onTouch 方法里能做的事情比 onClick 要多一些,比如判断手指按下、抬起、移动等事件。那么如果我两个事件都注册了,哪一个会先执行呢?我们来试一下就知道了,运行程序点击按钮,打印结果如下:

可以看到,onTouch 是优先于 onClick 执行的,并且 onTouch 执行了两次,一次是 ACTION_DOWN,一次是 ACTION_UP(你还可能会有多次 ACTION_MOVE 的执行,如果你手抖了一下)。因此事件传递的顺序是先经过 onTouch,再传递到 onClick。

细心的朋友应该可以注意到,onTouch 方法是有返回值的,这里我们返回的是 false,如果我们尝试把 onTouch 方法里的返回值改成 true,再运行一次,结果如下:

我们发现,onClick 方法不再执行了!为什么会这样呢?你可以先理解成 onTouch 方法返回 true 就认为这个事件被 onTouch 消费掉了,因而不会再继续向下传递。

如果到现在为止,以上的所有知识点你都是清楚的,那么说明你对 Android 事件传递的基本用法应该是掌握了。不过别满足于现状,让我们从源码的角度分析一下,出现上述现象的原理是什么。

首先你需要知道一点,只要你触摸到了任何一个控件,就一定会调用该控件的 dispatchTouchEvent 方法。那当我们去点击按钮的时候,就会去调用 Button 类里的 dispatchTouchEvent 方法,可是你会发现 Button 类里并没有这个方法,那么就到它的父类 TextView 里去找一找,你会发现 TextView 里也没有这个方法,那没办法了,只好继续在 TextView 的父类 View 里找一找,这个时候你终于在 View 里找到了这个方法,示意图如下:

然后我们来看一下 View 中 dispatchTouchEvent 方法的源码:

public boolean dispatchTouchEvent(MotionEvent event) {

if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&

            mOnTouchListener.onTouch(this, event)) {

return onTouchEvent(event);
复制代码

这个方法非常的简洁,只有短短几行代码!我们可以看到,在这个方法内,首先是进行了一个判断,如果 mOnTouchListener != null,(mViewFlags & ENABLED_MASK) == ENABLED 和 mOnTouchListener.onTouch(this, event) 这三个条件都为真,就返回 true,否则就去执行 onTouchEvent(event) 方法并返回。

先看一下第一个条件,mOnTouchListener 这个变量是在哪里赋值的呢?我们寻找之后在 View 里发现了如下方法:

public void setOnTouchListener(OnTouchListener l) {
复制代码

Bingo!找到了,mOnTouchListener 正是在 setOnTouchListener 方法里赋值的,也就是说只要我们给控件注册了 touch 事件,mOnTouchListener 就一定被赋值了。

第二个条件 (mViewFlags & ENABLED_MASK) == ENABLED 是判断当前点击的控件是否是 enable 的,按钮默认都是 enable 的,因此这个条件恒定为 true。

第三个条件就比较关键了,mOnTouchListener.onTouch(this, event),其实也就是去回调控件注册 touch 事件时的 onTouch 方法。也就是说如果我们在 onTouch 方法里返回 true,就会让这三个条件全部成立,从而整个方法直接返回 true。如果我们在 onTouch 方法里返回 false,就会再去执行 onTouchEvent(event) 方法。

现在我们可以结合前面的例子来分析一下了,首先在 dispatchTouchEvent 中最先执行的就是 onTouch 方法,因此 onTouch 肯定是要优先于 onClick 执行的,也是印证了刚刚的打印结果。而如果在 onTouch 方法里返回了 true,就会让 dispatchTouchEvent 方法直接返回 true,不会再继续往下执行。而打印结果也证实了如果 onTouch 返回 true,onClick 就不会再执行了。

根据以上源码的分析,从原理上解释了我们前面例子的运行结果。而上面的分析还透漏出了一个重要的信息,那就是 onClick 的调用肯定是在 onTouchEvent(event) 方法中的!那我们马上来看下 onTouchEvent 的源码,如下所示:

public boolean onTouchEvent(MotionEvent event) {

final int viewFlags = mViewFlags;

if ((viewFlags & ENABLED_MASK) == DISABLED) {

return (((viewFlags & CLICKABLE) == CLICKABLE ||

                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));

if (mTouchDelegate != null) {

if (mTouchDelegate.onTouchEvent(event)) {

if (((viewFlags & CLICKABLE) == CLICKABLE ||

            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {

switch (event.getAction()) {

case MotionEvent.ACTION_UP:

boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;

if ((mPrivateFlags & PRESSED) != 0 || prepressed) {

boolean focusTaken = false;

if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {

                        focusTaken = requestFocus();

if (!mHasPerformedLongPress) {

                        removeLongPressCallback();

if (mPerformClick == null) {

                                mPerformClick = new PerformClick();

if (!post(mPerformClick)) {

if (mUnsetPressedState == null) {

                        mUnsetPressedState = new UnsetPressedState();

                        mPrivateFlags |= PRESSED;

                        postDelayed(mUnsetPressedState,

                                ViewConfiguration.getPressedStateDuration());

                    } else if (!post(mUnsetPressedState)) {

                        mUnsetPressedState.run();

case MotionEvent.ACTION_DOWN:

if (mPendingCheckForTap == null) {

                    mPendingCheckForTap = new CheckForTap();

                mPrivateFlags |= PREPRESSED;

                mHasPerformedLongPress = false;

                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

case MotionEvent.ACTION_CANCEL:

                mPrivateFlags &= ~PRESSED;

case MotionEvent.ACTION_MOVE:

final int x = (int) event.getX();

final int y = (int) event.getY();

if ((x < 0 - slop) || (x >= getWidth() + slop) ||

                        (y < 0 - slop) || (y >= getHeight() + slop)) {

if ((mPrivateFlags & PRESSED) != 0) {

                        removeLongPressCallback();

                        mPrivateFlags &= ~PRESSED;
复制代码

相较于刚才的 dispatchTouchEvent 方法,onTouchEvent 方法复杂了很多,不过没关系,我们只挑重点看就可以了。

首先在第 14 行我们可以看出,如果该控件是可以点击的就会进入到第 16 行的 switch 判断中去,而如果当前的事件是抬起手指,则会进入到 MotionEvent.ACTION_UP 这个 case 当中。在经过种种判断之后,会执行到第 38 行的 performClick() 方法,那我们进入到这个方法里瞧一瞧:

public boolean performClick() {

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

if (mOnClickListener != null) {

        playSoundEffect(SoundEffectConstants.CLICK);

        mOnClickListener.onClick(this);
复制代码

可以看到,只要 mOnClickListener 不是 null,就会去调用它的 onClick 方法,那 mOnClickListener 又是在哪里赋值的呢?经过寻找后找到如下方法:

public void setOnClickListener(OnClickListener l) {
复制代码

一切都是那么清楚了!当我们通过调用 setOnClickListener 方法来给控件注册一个点击事件时,就会给 mOnClickListener 赋值。然后每当控件被点击时,都会在 performClick() 方法里回调被点击控件的 onClick 方法。

这样 View 的整个事件分发的流程就让我们搞清楚了!不过别高兴的太早,现在还没结束,还有一个很重要的知识点需要说明,就是 touch 事件的层级传递。我们都知道如果给一个控件注册了 touch 事件,每次点击它的时候都会触发一系列的 ACTION_DOWN,ACTION_MOVE,ACTION_UP 等事件。这里需要注意,如果你在执行 ACTION_DOWN 的时候返回了 false,后面一系列其它的 action 就不会再得到执行了。简单的说,就是当 dispatchTouchEvent 在进行事件分发的时候,只有前一个 action 返回 true,才会触发后一个 action。

说到这里,很多的朋友肯定要有巨大的疑问了。这不是在自相矛盾吗?前面的例子中,明明在 onTouch 事件里面返回了 false,ACTION_DOWN 和 ACTION_UP 不是都得到执行了吗?其实你只是被假象所迷惑了,让我们仔细分析一下,在前面的例子当中,我们到底返回的是什么。

参考着我们前面分析的源码,首先在 onTouch 事件里返回了 false,就一定会进入到 onTouchEvent 方法中,然后我们来看一下 onTouchEvent 方法的细节。由于我们点击了按钮,就会进入到第 14 行这个 if 判断的内部,然后你会发现,不管当前的 action 是什么,最终都一定会走到第 89 行,返回一个 true。

是不是有一种被欺骗的感觉?明明在 onTouch 事件里返回了 false,系统还是在 onTouchEvent 方法中帮你返回了 true。就因为这个原因,才使得前面的例子中 ACTION_UP 可以得到执行。

那我们可以换一个控件,将按钮替换成 ImageView,然后给它也注册一个 touch 事件,并返回 false。如下所示:

imageView.setOnTouchListener(new OnTouchListener() {

public boolean onTouch(View v, MotionEvent event) {

		Log.d("TAG", "onTouch execute, action " + event.getAction());
复制代码

运行一下程序,点击 ImageView,你会发现结果如下:

在 ACTION_DOWN 执行完后,后面的一系列 action 都不会得到执行了。这又是为什么呢?因为 ImageView 和按钮不同,它是默认不可点击的,因此在 onTouchEvent 的第 14 行判断时无法进入到 if 的内部,直接跳到第 91 行返回了 false,也就导致后面其它的 action 都无法执行了。

好了,关于 View 的事件分发,我想讲的东西全都在这里了。现在我们再来回顾一下开篇时提到的那三个问题,相信每个人都会有更深一层的理解。

1. onTouch 和 onTouchEvent 有什么区别,又该如何使用?

从源码中可以看出,这两个方法都是在 View 的 dispatchTouchEvent 中调用的,onTouch 优先于 onTouchEvent 执行。如果在 onTouch 方法中通过返回 true 将事件消费掉,onTouchEvent 将不会再执行。

另外需要注意的是,onTouch 能够得到执行需要两个前提条件,第一 mOnTouchListener 的值不能为空,第二当前点击的控件必须是 enable 的。因此如果你有一个控件是非 enable 的,那么给它注册 onTouch 事件将永远得不到执行。对于这一类控件,如果我们想要监听它的 touch 事件,就必须通过在该控件中重写 onTouchEvent 方法来实现。

2. 为什么给 ListView 引入了一个滑动菜单的功能,ListView 就不能滚动了?

如果你阅读了 Android 滑动框架完全解析,教你如何一分钟实现滑动菜单特效这篇文章,你应该会知道滑动菜单的功能是通过给 ListView 注册了一个 touch 事件来实现的。如果你在 onTouch 方法里处理完了滑动逻辑后返回 true,那么 ListView 本身的滚动事件就被屏蔽了,自然也就无法滑动 (原理同前面例子中按钮不能点击),因此解决办法就是在 onTouch 方法里返回 false。

3. 为什么图片轮播器里的图片使用 Button 而不用 ImageView?

提这个问题的朋友是看过了 Android 实现图片滚动控件,含页签功能,让你的应用像淘宝一样炫起来 这篇文章。当时我在图片轮播器里使用 Button,主要就是因为 Button 是可点击的,而 ImageView 是不可点击的。如果想要使用 ImageView,可以有两种改法。第一,在 ImageView 的 onTouch 方法里返回 true,这样可以保证 ACTION_DOWN 之后的其它 action 都能得到执行,才能实现图片滚动的效果。第二,在布局文件里面给 ImageView 增加一个 android:clickable="true" 的属性,这样 ImageView 变成可点击的之后,即使在 onTouch 里返回了 false,ACTION_DOWN 之后的其它 action 也是可以得到执行的。

今天的讲解就到这里了,相信大家现在对 Android 事件分发机制又有了进一步的认识,在后面的文章中我会再带大家一起探究 Android 中 ViewGroup 的事件分发机制,感兴趣的朋友请继续阅读 Android 事件分发机制完全解析,带你从源码的角度彻底理解 (下) 。 )

文章分类
Android
文章标签