Android的MotionEvent事件分发机制


个人博客:http://zhangsunyucong.top

android事件的源头在哪里?

当用户触摸屏幕或者按键等时,形成事件,事件经过linux底层Event节点捕获之后,一直传到android应用层。中间传递的过程不是本文的重点,我也不是很清楚(哈哈哈)。本文的重点是事件在应用层的分发机制。

事件在View树中的分发过程

View树:
图片

在Android中,事件的分发过程就是MotionEvent在view树分发的过程。默认是中从上而下,然后从下而上的传递的,直到有view、viewgroup或者Activity处理事件为止。

为什么要先从上而下?是为了在默认情况下,屏幕上层叠的所有控件都有机会处理事件。这个阶段我们称为事件下发阶段。

为什么要从下而上?是为了在从上而下分发时,事件没有控件处理时,再从下而上冒泡事件,是否有控件愿意处理事件。如果中间没有控件处理,事件就只能由Acitivity处理了。这个阶段我们称为事件的冒泡阶段。

准备

事件序列:从用户手指触摸屏幕开始,经过滑动到手指离开屏幕。这个操作产生了一个dowm事件,一系列move事件,最后一个up事件结束。我们把这一个操作产生的事件称为一个事件序列。

Acitivity中和事件传递有关的函数
事件分发:dispatchTouchEvent
事件处理:onTouchEvent

ViewGrop中和事件传递有关的函数
事件分发:dispatchTouchEvent
事件拦截:onInterceptTouchEvent
事件处理:onTouchEvent

View中和事件传递有关的函数
事件分发:dispatchTouchEvent
事件处理:onTouchEvent

从上面可以看出,ViewGrop中多了事件拦截onInterceptTouchEvent函数,是为了询问自己是否拦截事件(在事件分发中询问),如果没有拦截就传递事件给直接子view,如果拦截就将事件交给自己的事件处理函数处理。View中没有事件拦截函数,因为view是在view树中的叶节点,已经没有子view。

下面是先进行源码分析,然后再验证得出一些结论。代码迟点上传github。
用图表示布局的层级关系:
图片

这里分析事件的分发过程,是从down事件的分发开始,以及分析它在两个阶段的传递过程:下发阶段和冒泡阶段。

事件下发阶段

(1)在Acitvity中的源码分析:

Activity#dispatchTouchEvent

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

在第4行,Acivity将事件传递给了Window,Window是一个抽象类。在手机系统中它的实现是PhoneWindow.下面进入PhoneWindow中。

PhoneWindow#superDispatchTouchEvent

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

从上面可以看出,事件已经从Acitivity到PhoneWindow,再传到了DecorView。DecorView是一个继承FrameLayout的ViewGroup,从而事件进入了View树的传递。

重写在Acitvity中的事件传递方法

重写Activity#dispatchTouchEvent:
1、返回false,事件不分发,所有事件在Acitivity的分发函数中就中断(真的不见了),连Acitivity的事件处理函数都到达不了。
2、返回true,所有事件在Acitivity的分发函数中就中断,和false一样
3、返回父函数方法,事件就传给直接子view分发

(2)在ViewGruop中的源码分析:
ViewGruop#dispatchTouchEvent

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.
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
    || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            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;
}

在5-11行,是每个新的事件系列开始前,会重置事件相关的状态。这里我们关注两个地方。第一个是第17行的disallowIntercept标志,第二个是第19行调用了事件拦截函数,询问是否拦截事件。

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

onInterceptTouchEvent的代码很简单。

重写在ViewGroup中的事件传递方法
重写ViewGroup#dispatchTouchEvent:
1、返回false,不分发,down事件给父ViewGroup处理,以后的事件全部直接通过父ViewGroup分发函数给父ViewGroup的事件处理函数处理。
2、返回true,则所有的事件都从头来到这里就中断,不见了。
3、返回父函数方法,看下面拦截函数

重写ViewGroup#onInterceptTouchEvent(询问是否拦截):
1、返回true,就调用处理函数,在处理函数中是否消耗down事件
2、返回false,是否是最后一个view?否,down事件就分发给子View;是,就调用一次它的处理函数,进入冒泡阶段(就是一寸事件处理函数调用)
3、返回父函数的方法,和返回false一样

重写ViewGroup的onTouchEvent,当down事件来到中onTouchEvent时,
1、返回true,就消耗down事件,后面全部事件从头分发到处理函数(不用再询问是否拦截)。后面的事件根据是否消耗而是否消失(不消耗就消失),消失的所有事件由Acitivity处理(注意消失的事件也是从头传递到这里再传给Acitivity的)。
2、返回false,将down事件冒泡回去,看谁会处理。
3、返回父函数方法,是默认不消耗。

(3)在View中的源码分析:
View#dispatchTouchEvent

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
            && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
    }

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

这里关注的地方是,第9行和第13行。第9行是当前view如果设置了onTouch事件,并且它返回了true,那它就直接将result设置为true,事件就消耗了,不会再继续传递下去,只到达onTouch。第13行,是事件处理函数。可以看出onTouch是优先于onTouchEvent的。

View#onTouchEvent

....
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
...                

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)) {
                        performClick();
                    }
                }
            }
            ...
        }
        ...
    }
    return true;
}

view根据是否可以点击等等一系列判断什么的。这里关注up事件中的第42-53行,有performClick。

View#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);

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}

如果view设置了mOnClickListener,即点击事件,会调用view的点击事件。如果在父view中拦截了up事件,使up事件到达不了这里,会使view的点击事件失效。

可以知道,onTouch是优先于onTouchEvent,onTouchEvent优先于onclick。

事件冒泡阶段

当down事件到达了最后一个子view,如果仍然没有view愿意处理它,就调用一次最后一个子view的事件处理函数,是否处理dowm事件,如果不处理,就一直冒泡回去,直到有view的onTouchEvent处理为止。如果都不处理,就只有Acitivity自己处理了。整个事件冒泡阶段就是一串onTouchEvent的回溯过程,自下而上。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值