Android 从源码角度分析事件分发机制(三)

说明:终于写到了事件分发机制的最后一篇,如果还没看过Android学习笔记之事件分发机制(一)Android学习笔记之事件分发机制(二)的话可以先看看,再结合源码会有助于理解。

前言

第一篇主要讲了dispatchTouchEventonTouchonTouchEventonClick之间的关系。
第二篇主要讲了事件的分发路径: Activity -> ViewGroup -> View。
这两篇都还有一些东西讲得不是很清楚,所以这篇会顺带把之前一些难以理解的地方给讲明白。

源码版本

Android 22
其他版本的源码可能会有一些不同,但大概的思路都是一样的。
说明:为了节省篇幅和复杂性,源码我只提取了其中有用到的。具体的源码请大家自己查看。

主线一

首先,我们先来看这一条主线:dispatchTouchEventonTouchonTouchEventonClick之间的关系。
找到View.java中的dispatchTouchEvent

/**
 * Pass the touch screen motion event down to the target view, or this
 * view if it is the target.
 *
 * @param event The motion event to be dispatched.
 * @return True if the event was handled by the view, false otherwise.
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    return result;
} 

先来看下注释:
将点击屏幕事件分发到指定的View。
当事件被当前的View处理(消费)时返回true,否则返回false。

然后跳到第13行。if中有四个判断条件。第一个和第二个我们可以直接认为是true了,因为onTouch能被执行也就意味着前两个条件为true,不用去追踪源码了。看一下第三个条件吧,这里的意思是判断当前的View是否是Enable的,Button默认是Enable的,所以第三个条件也为true。也就是说,onTouch决定了result的值。假设onTouch返回了true,result的值变为true。来到19行,第一个条件为false,所以直接跳出判断,后面的onTouchEvent是不会被执行的。
所以第一个结论来了,当前View为Enable的前提下,只有当onTouch返回false时,onTouchEvent才会被执行。

假设我们将onTouch返回false,再进入onTouchEvent中探个究竟。有点长,所以只挑重点来讲。

public boolean onTouchEvent(MotionEvent event) {
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                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) {
                        // 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)) {
                                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();
                }
                break;
            case MotionEvent.ACTION_DOWN:
                ...
            case MotionEvent.ACTION_CANCEL:
                ...
            case MotionEvent.ACTION_MOVE:
                ...
        }
        return true;
    }
    return false;
}

先看2-10行。如果当前View是被disable但仍然可以点击的,返回true,即当前View消费掉此次事件,但没有对它们做出反应。
从12行开始了一个很长的if块,一直到74行(中间省略了很多代码)。先不管if里面是什么,只看12行和73行。如果当前View是可以点击的,最后会返回true消费掉该事件。如果不可点击,直接返回false。
好了,再来看12行到73行之间的代码,在46行找到了

performClick();

看一下源码

public boolean performClick() {
    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);
    return result;
}

看到了很熟悉的onClick有没有!执行完onClick后会返回true,即消费掉该事件。

先来总结一下主线一吧。对于一个View来说,事件首先会到达dispatchTouchEvent,然后在该方法里面会先执行onTouch,接着如果onTouch返回false的话就去执行onTouchEvent,然后onClick方法在onTouchEvent中被调用。onTouchonTouchEvent结合起来得到的最后结果会作为dispatchTouchEvent的返回值。

看到这里,希望你能看得明白。如果可以的话,那么接下来的也会很好理解了,不过我更希望你顺着这个思路自己分析Activity和ViewGroup的源码。

主线二

主线二是: Activity -> ViewGroup -> View
先看Activity.java

/**
 * 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) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

结合之前第二篇的实验结果,onTouchEvent最开始是没有被执行的,也就是说,事件分发发生在这里面。

if (getWindow().superDispatchTouchEvent(ev)) {
    return true;
}

当事件被消费的时候,getWindow().superDispatchTouchEvent(ev)返回true,从而让Activity的dispatchTouchEvent返回true。

再来看onTouchEvent的源码

/**
 * Called when a touch screen event was not handled by any of the views
 * under it.  This is most useful to process touch events that happen
 * outside of your window bounds, where there is no view to receive it.
 *
 * @param event The touch screen event being processed.
 *
 * @return Return true if you have consumed the event, false if you haven't.
 * The default implementation always returns false.
 */
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}

看一下注释就可以了,当事件没有被任何View处理的时候,事件会返回给Activity处理。这也就可以解释为什么onTouchEvent有时候会被执行有时候没被执行了。

接着看ViewGroup.java,只看一个方法就好了。

/**
* Implement this method to intercept all touch screen motion events.  This
* allows you to watch events as they are dispatched to your children, and
* take ownership of the current gesture at any point.
*
* <p>Using this function takes some care, as it has a fairly complicated
* interaction with {@link View#onTouchEvent(MotionEvent)
* View.onTouchEvent(MotionEvent)}, and using it requires implementing
* that method as well as this one in the correct way.  Events will be
* received in the following order:
*
* <ol>
* <li> You will receive the down event here.
* <li> The down event will be handled either by a child of this view
* group, or given to your own onTouchEvent() method to handle; this means
* you should implement onTouchEvent() to return true, so you will
* continue to see the rest of the gesture (instead of looking for
* a parent view to handle it).  Also, by returning true from
* onTouchEvent(), you will not receive any following
* events in onInterceptTouchEvent() and all touch processing must
* happen in onTouchEvent() like normal.
* <li> For as long as you return false from this function, each following
* event (up to and including the final up) will be delivered first here
* and then to the target's onTouchEvent().
* <li> If you return true from here, you will not receive any
* following events: the target view will receive the same event but
* with the action {@link MotionEvent#ACTION_CANCEL}, and all further
* events will be delivered to your onTouchEvent() method and no longer
* appear here.
* </ol>
*
* @param ev The motion event being dispatched down the hierarchy.
* @return Return true to steal motion events from the children and have
* them dispatched to this ViewGroup through onTouchEvent().
* The current target will receive an ACTION_CANCEL event, and no further
* messages will be delivered here.
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return false;
}

在第二篇中已经讲过了这个方法,现在是想来看下它的注释。
大致意思是说:实现这个方法来截获所有的触摸屏幕事件,可以在事件发给你(ViewGroup,下同)的孩子之前监听到事件,并接管这些事件,从而使你的孩子无法收到这些触摸事件。
使用这个方法需要小心点,它和View.onTouchEvent有着复杂的交互,使用这个方法需要同时也重写onTouchEvent。事件会以下面的顺序接收到:

  • 你会在这里收到ACTION_DOWN事件
  • ACTION_DOWN事件会被你的孩子处理或者你自己的onTouchEvent处理。这意味着你必须在onTouchEvent中返回true,进而你才能继续看到其余的事件(而不是寻找你的父节点去处理)。还有,在onTouchEvent中返回true的话,在onInterceptTouchEvent中你不会再接收到剩下的任何事件,所有的事件会像正常情况一样在onTouchEvent中被处理。
  • 如果在该方法中返回false的话,接下来的事件会先被分发到这里,然后到达目标View的onTouchEvent
  • 如果在该方法中返回true的话,目标View会接收到ACTION_CANCEL。进一步的事件将不会出现在这里而是直接到达你的onTouchEvent方法。
    返回true会从你的子节点中偷走事件,然后将事件分发给自己的onTouchEvent处理,目标View会收到ACTION_CANCEL事件,进一步的消息将不会出现在onInterceptTouchEvent中。

讲了一大段,其实讲得有点啰嗦。大致意思就是如果在这个方法中返回true的话,事件会被自己的onTouchEvent方法处理,不会传递到孩子节点中。同时,在onTouchEvent中要返回true,否则系统就会去寻找父节点处理该事件。

主线一和主线二就讲到这里了,希望大家能自己分析一下源码,再自己写一写效果会加倍的.

第二篇的最后还遗留了一个问题,看完这篇,分析起来就很清晰了.
onInterceptTouchEvent返回true的时候, CustomLayout自己的onTouchEvent会被调用,最后返回super.onTouchEvent(event),而这里的结果最后又会作为dispatchTouchEvent的返回值,从而判断是否消费了该事件.为了方便大家查看,我再贴一下图

调试信息

从结果来看,super.onTouchEvent(event)的值为false.不信?自己试试看呗.返回false后,该事件没有被任何View消费(注意:该事件是不会分发给CustomButton的),最后回传给了MainActivity自己处理,由于CustomLayout没有消费该事件,所以ACTION_DOWN在MainActivity中又被处理了一次.
后来,我们让CustomLayout中的onTouchEvent返回true,即CustomLayout消费了该事件,所以才有了后面的事件.

调试信息

The End

安卓的事件分发机制写到这里总算完了,希望这几篇博文能让你对事件分发机制有进一步的了解.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值