Android触摸事件分发机制

Android 中的事件分为按键事件和触摸事件,这里我们对触摸事件进行阐述。

Touch 事件是由一个 ACTION_DOWN,n个 ACTION_MOVE,一个 ACTION_UP 组成 onClick,onLongClick,onScroll 等事件。
Android 中的控件都是继承 View 这个基类的,而控件分为两种:一种是继承 View 不能包含其他控件的控件;一种是继承 ViewGroup 可以包含其他控件的控件,暂且称为容器控件,比如 ListView,GridView,LinearLayout 等。

这里先对几个函数讲解下:

  • public boolean dispatchTouchEvent (MotionEvent ev) 这个方法分发 TouchEvent

  • public boolean onInterceptTouchEvent(MotionEvent ev) 这个方法拦截 TouchEvent

  • public boolean onTouchEvent(MotionEvent ev) 这个方法处理 TouchEvent

    其中 view 类中有 dispatchTouchEvent 和 onTouchEvent 两个方法,ViewGroup 继承 View,而且还新添了一个onInterceptTouchEvent 方法。Activity 中也无 onInterceptTouchEvent 方法,但有另外两种方法。我们可以发现上面 3 个方法都是返回 boolean,那各代表什么意思呢?

public boolean dispatchTouchEvent (MotionEvent ev)

描述

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 screenevents that should be handled normally.

Parameters
ev The touch screen event.

Returns
boolean Return true if this event was consumed.

它会被调用处理触摸屏事件,可以重写覆盖此方法来拦截所有触摸屏事件在这些事件分发到窗口之前。通常应该处理触摸屏事件,一定要调用这个实现。当返回值为 true 时,表示这个事件已经被消费了。

public boolean onInterceptTouchEvent (MotionEvent ev)

描述

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.
Using this function takes some care, as it has a fairly complicated interaction with 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:
1. You will receive the down event here.
2. The down event will be handled either by a child of this viewgroup, or given to your own onTouchEvent() method to handle; this means you should implement onTouchEvent() to return true, so you will continue to see therest 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 inonTouchEvent() like normal.
3. For as long as you return false from this function, each following event (up to and including the final up) will be delivered first hereand then to the target’s onTouchEvent().
4. 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 ACTION_CANCEL, and all further events will be delivered to your onTouchEvent() method and no longer appear here.

Parameters
ev The motion event being dispatched down the hierarchy.
Returns
Return true to steal motionevents from the children and have them dispatched to this ViewGroup through onTouchEvent(). The current target will receive an ACTION_CANCEL event, and nofurther messages will be delivered here.

基本意思就是:

  1. ACTION_DOWN 首先会传递到 onInterceptTouchEvent()方法

  2. 如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return false, 那么后续的move,up 等事件将继续会先传递给该 ViewGroup,之后才和 down 事件一样传递给最终的目标 view 的 onTouchEvent()处理。

  3. 如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return true, 那么后续的move,up等事件将不再传递给onInterceptTouchEvent(), 而是和down事件一样传递给该ViewGroup的onTouchEvent()处理,注意,目标 view 将接收不到任何事件。

  4. 如果最终需要处理事件的 view 的 onTouchEvent()返回了 false,那么该事件将被传递至其上一层次的 view 的onTouchEvent()处理。

  5. 如果最终需要处理事件的 view 的 onTouchEvent()返回了 true,那么后续事件将可以继续传递给该 view 的onTouchEvent()处理。


Android touch 事件传递机制:

我们可以看看 android 源代码

Activity.java 中

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

暂且不管onUserInteraction方法因为它只是一个空方法如果你没实现的话。getWindow().superDispatchTouchEvent(ev)。其中getWindow()返回的是PhoneWindow。
PhoneWindow.java:

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

此函数调用super.dispatchTouchEvent(event),Activity的rootview是PhoneWindow.DecorView,它继承FrameLayout。通过super.dispatchTouchEvent把touch事件派发给各个Activity的是子view。同时我可以看到,如果子view拦截了事件,则不会执行onTouchEvent函数。

ViewGroup.java中dispatchTouchEvent方法:

由于代码过长这里就不贴出来了,但也知道它返回的是
return target.dispatchTouchEvent(ev);
这里target指的是所分发的目标,可以是它本身,也可以是它的子View。
ViewGroup.java中的onInterceptTouchEvent方法:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    return false;
}

默认情况下返回false,即不拦截touch事件。

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) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            return true;
        }

        if (onTouchEvent(event)) {
            return true;
        }
    }

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }
    return false;
}

这里我们很清楚可以知道如果if条件不成立则dispatchTouchEvent的返回值是onTouchEvent的返回值。

View.java中的onTouchEvent方法

/**
 * Implement this method to handle touch screen motion events.
 *
 * @param event The motion event.
 * @return True if the event was handled, false otherwise.
 */
public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;

    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 (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

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

                    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:
                mHasPerformedLongPress = false;

                if (performButtonActionOnTouchDown(event)) {
                    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();
                    }
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    setPressed(true);
                    checkForLongClick(0);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
                break;

            case MotionEvent.ACTION_MOVE:
                final int x = (int) event.getX();
                final int y = (int) event.getY();

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

所以很容易得到触摸事件默认处理流程(以ACTION_DOWN事件为例):
这里写图片描述

当触摸事件ACTION_DOWN发生之后,先调用Activity中的dispatchTouchEvent函数进行处理,紧接着ACTION_DOWN事件传递给ViewGroup中的dispatchTouchEvent函数,接着viewGroup中的dispatchTouchEvent中的ACTION_DOWN事件传递到调用ViewGroup中的onInterceptTouchEvent函数,此函数负责拦截ACTION_DOWN事件。由于viewGroup下还包含子View,所以默认返回值为false,即不拦截此ACTION_DOWN事件。如果返回false,则ACTION_DOWN事件继续传递给其子view。由于子view不是viewGroup的控件,所以ACTION_DOWN事件接着传递到onTouchEvent进行处理事件。此时消息的传递基本上结束。从上可以分析,motionEvent事件的传递是采用隧道方式传递。隧道方式,即从根元素依次往下传递直到最内层子元素或在中间某一元素中由于某一条件停止传递。

接下来继续分析,事件的处理。刚才ACTION_DOWN事件传递到view的onTouchEvent函数中处理了,默认是返回true,接着view的dispatchTouchEvent返回true,再接着viewGroup的dispatchTouchEvent返回true,最后Activity的dispatchTouchEvent返回true。我们发现,motionEvent事件的处理采用冒泡方式。冒泡方式,从最内层子元素依次往外传递直到根元素或在中间某一元素中由于某一条件停止传递。

下图为程序调试结果:

ACTION_DOWN事件输出:
这里写图片描述

ACTION_MOVE事件输出:
这里写图片描述

现在我们来做一些改变,就接着以ACTION_DOWN为例

情况一:
我们在View中onTouchEvent中ACTION_DOWN返回false,输出结果如下:
这里写图片描述
可以发现ACTION_DOWN事件传递到上层的ViewGroup的onTouchEvent,同时返回true,说明事件被ViewGroup消费了。同时之后的touch事件(ACTION_MOVE等)不再传递给view,只传递到ViewGroup,由ViewGroup的onTouchEvent函数处理touch事件。同时onInterceptTouchEvent也不再调用。

情况二:
我们在View中onTouchEvent中ACTION_MOVE返回false,输出结果如下:
这里写图片描述

由于view未消费此ACTION_MOVE事件,按照原理来说应该是将事件处理冒泡到ViewGroup去处理,但结果却是Activity处理的。我们知道,触摸事件首先发生的就是ACTION_DOWN事件,我们在onInterceptTouchEvent所解释就可以发现ACTION_DOWN与ACTION_MOVE等事件有区别,ACTION_DOWN事件作为起始事件,它的重要性是要超过ACTION_MOVE和ACTION_UP的,如果发生了ACTION_MOVE或者ACTION_UP,那么一定曾经发生了ACTION_DOWN。也就是说ACTION_DOWN事件被view消费了,而ACTION_MOVE事件没被消费,传递到ViewGroup,由于之前ViewGroup没处理ACTION_DOWN事件,所以它也不处理ACTION_MOVE。但Activity却不一样,它可以接受所有事件。

情况三:
这次在ViewGroup中的onInterceptTouchEvent中ACTION_DOWN返回true
结果如下:
这里写图片描述
它直接把事件发送给ViewGroup的onTouchEvent处理,此后不再拦截事件直接到viewGroup中的onTouchEvent处理。

情况四:
在ViewGroup中的onInterceptTouchEvent中ACTION_MOVE返回true
结果如下:
这里写图片描述
ACTION_MOVE被ViewGroup拦截了,上次处理ACTION_DOWN的view则会收到ACTION_CANCEL事件,之后ViewGroup不再拦截后续事件,事件直接在ViewGroup中的onTouchEvent处理。

还有很多情况,这里不一一列出了。

我发现重写了onTouchEvent函数就无法获取onClick和onLongClick事件。接下来讨论当重写了onTouchEvent,android是如何区分onClick,onLongClick事件的。搞清楚此问题对于如何响应UI各种事件是很重要的,例如类似android桌面的应用程序图标,可以点击,然后长按拖动。
Android中onclick,onLongClick是都是由ACTION_DOWN,ACTION_UP组成。如果在同一个View中onTouchEvent、onclick、onLongClick都进行了重写。onTouchEvent最先捕获ACTION_DOWN、ACTION_UP等单元事件。接下来才可能发生onClick、onLongClick事件。一个onclick事件是由ACTION_DOWN和ACTION_UP组成的。一个onLongClick事件至少有一个ACTION_DOWN。那android具体是怎么实现的呢,可以看源代码:

View.java中:
上面我已经展示了onTouchEvent方法,但由于过长我折叠了一部分代码,现在展开
这里写图片描述
这个if条件内执行就是click事件处理及longClick事件处理。先看ACTION_DOWN事件
这里写图片描述
我们看到有个postDelayed方法,此方法意思为延时把线程插入到消息队列。即ACTION_DOWN后触发一个postDelayed方法。mPendingCheckForTap属于CheckForTap的实例。
这里写图片描述
在里面开启一个线程当为LONG_CLICKABLE,调用postCheckForLongClick方法。
这里写图片描述
再看mPendingCheckForLongPress这个线程。
这里写图片描述
当上面一系列条件全都符合的情况就调用performLongClick方法。
这里写图片描述
此方法就调用我们熟悉的onLongClick函数。
这里写图片描述
至此onLongClick事件已经分析完。再接着看ACTION_UP事件

直接关注performClick函数:
这里写图片描述
这里我们同样看到了我们熟悉的onClick方法。
所以android这种机制是保证了此onClick和onLongClick能与onTouchEvent并存。接下来考虑onClick与onLongClick是否并存,其实这个问题前面已经阐述了。只要此事件没被消费,它还会接着传递下去。从上面知道onLongClick是在单独的线程执行,发生在ACTION_UP之前。Onclick发生在ACTION_UP之后,也就是说,如果在onLongClick返回false,onClick就会发生,而onlongClick返回true,则代表此事件已经被消费。Onclick不再发生。
如果多次设置onclick事件,则最顶层的onclick覆盖掉底层onclick事件;多次设置onLongClick事件,则只执行底层view的onLongClick方法。当ACTION_DOWN调用之后返回false。
可以看到ACTION_DOWN被消费了,所以不会让上层处理了。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值