Android事件分发机制

作者: 森码 | 来源:发表于2019-04-03 21:08 被阅读1次

首先,我们要明白事件的定义『当用户触摸屏幕时,将产生的触摸行为』
其实,我们需要处理的就是把一个MotionEvent对象处理掉,而能处理它的其实只有三个方法,dispatchTouchEvent(MotionEvent event)、onInterceptTouchEvent(MotionEvent event)、onTouchEvent(MotionEvent event),但是如果搭配上ViewGroup、View和Activity处理的流程可能就要变的复杂一点了,下面我们来具体分析。

来源与传递

首先我们一定要先了解,Activity和View是什么样的关系,为什么Activity中的事件都能传递给View去处理。
本质上Activity其实只是一个『容器』,但是这个容器并不是直接承载了View,而是通过Window,这里我们不深究,你只需要把Activity也看成是一个ViewGroup就好了。


Activity与View的层级关系

我们可以认为,MotionEvent就是从Activity来的(其实并不是,具体参看)而它调用的第一个方法就是dispatchTouchEvent,我们看源码它将这个事件交给了谁。

//1. Activity.dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

随后,我们找到Window的实现类PhoneWindow,查看superDispatchTouchEvent方法。

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

很明显,MotionEvent进入到了DecorView

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

而DecorView继承于FrameLayout,但是FrameLayout中并没有这个方法,我们继续往上层找即ViewGroup中,这里事件的分发才真正开始。

传递开始

在这里我们要先明白一个概念,在一次点击事件中其实是有多MotionEvent的,MotionEvent也根据Action的不同来表达不同的动作,比如DOWN、MOVE、UP、CANCEL。
一次快速的点击包含一个DOWN和UP,而点击屏幕、拖动、抬起手指却是包含了一个DOWN、一个UP和若干个MOVE,再明白了这一系列动作之后,我们开始去传递事件了。

在ViewGroup的dispatchTouchEvent方法中这个方法的作用是去分发事件,分发——就是要找到真正需要处理事件的子View,根据我们上面说的,难道一系列的事件来了之后,我们要一个一个的去寻找处理它的子View吗?当然没有必要,我们只需要通过Down事件找到那个View,之后的后续事件都交给它处理就好了,如果没有子View去『吃掉』这个事件,再考虑自己处理或者交给上层。

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        //清空上一次的DOWN事件遗留
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }
    // 检查是否需要拦截
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        // 从字面意思上我们可以猜出,不允许拦截,如果为false,就去调用onInterceptTouchEvent()方法去拦截事件
        // 这个字段非常有用,子View可以通过requestDisallowInterceptTouchEvent()方法来控制父ViewGroup是否去拦截点击事件
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
        } 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;
    }
    if (!canceled && !intercepted) {
        final View[] children = mChildren;
        for (int i = childrenCount - 1; i >= 0; i--) {
            final int childIndex = getAndVerifyPreorderedIndex(
                    childrenCount, i, customOrder);
            // 获取到每个child view
            final View child = getAndVerifyPreorderedView(
                    preorderedList, children, childIndex);
            // 这两个方法也很关键,canViewReceivePointerEvents()表示:view是否可见或者是否正在执行动画
            // isTransformedTouchPointInView() 表示:此次滑动或者点击的范围是否在View的范围内
            // 也就是说,如果这个子View正在执行动画或者不可见,或者不在滑动范围内,是不能处理点击事件的。
            if (!canViewReceivePointerEvents(child)
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                continue;
            }
            // 这里其实就是调用了child.dispatchTouchEvent(event);
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                // 为mFirstTouchTarget赋值,就是找到了处理事件的那个View
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                break;
            }
        }
    }

    // 把事件传递给目标View
    if (mFirstTouchTarget == null) {
        // 没有找到目标View,交给子View处理或者自己调用TouchEvent处理
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        // 处理MOVE、UP等后续事件(因为上面都是针对DOWN事件的,也因为找到了mFirstTouchTarget,后续事件可以不参与上面的判断而直接来分发)
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
                final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
            }
        }
    }
    return handled;
}

这里是ViewGroup中事件的分发方法,省去了很多不必要的行,大家可以对照注释去理解。

ViewGroup如果找到了处理的View,就会调用子View的dispatchTouchEvent()方法,这个方法要相对简单很多,但是对于各种listener的调用会对大家的今后的使用有一定帮助。

public boolean dispatchTouchEvent(MotionEvent event) {
    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        stopNestedScroll();
    }

    if (onFilterTouchEventForSecurity(event)) {
        // 各种listener的合集(OnClickListener、OnLongClickListener、OnTouchListener等等)
        ListenerInfo li = mListenerInfo;
        // 如果我们设置了OnTouchListener监听,同时OnTouchListener的onTouch方法返回的是true,并且是可用状态,
        // 那么,这个View就不会调用自己的onTouchEvent方法了
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        // 如果上面不调用result = true,这里去调用onTouchEvent方法。注意:View里面没有onInterceptTouchEvent方法
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    return result;
}

于是,我们的事件传递到了TouchEvent中。
但是有一个问题,我们收到这个MotionEvent之后该做什么?回想我们在开发中用到的东西,最多的难道不是setOnClickListener或者setOnLongClickListener,再或者setOnTouchListener吗?而这里就是处理这些方法的中心。

public boolean onTouchEvent(MotionEvent event) {
    // 这里主要看viewFlags这个参数,如果设置了Clicklistener或者LongClickener都会把这个值置位相应的标志位,也就是说是可点击的。
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

    // 这里注意,虽然有的组件设置为了不可用,但是也是会『吃掉』点击事件的,只不过没有回应。
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        return clickable;
    }

    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                // 把各种回调都移除,因为一次点击已经结束
                if (!clickable) {
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    break;
                }
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    // View是否获取了焦点
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }
                    // 这里其实判断OnClick事件的一个重要分支,mHasPerformedLongPress顾名思义就是是否触发的长按事件,但是如果
                    // OnLongClick方法返回false,依然是会触发OnClick事件
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // Only perform take click actions if we were in the pressed state
                        if (!focusTaken) {
                            // 这里其实就是触发OnClick方法的触发点,但是触发的时候不是直接调用,而是用post的方式,
                            // 这样可以在点击事件的反馈效果之前,让其他的视觉效果也能相继触发
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClickInternal();
                            }
                        }
                    }
                }
                break;

            case MotionEvent.ACTION_DOWN:
                // 开启触发LongClick的触发点,如果是在可滚动的容器内,需要延迟100ms判断,如果不在开始触发,触发也是通过post执行,delay的时间是500ms
                if (isInScrollingContainer) {
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    checkForLongClick(0, x, y);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                // 取消各种回调
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if (clickable) {
                    // 绘制各个热点区域,比如水波纹效果
                    drawableHotspotChanged(x, y);
                }
                // 是否已经移出了View中
                if (!pointInView(x, y, mTouchSlop)) {
                    // 移除tap和LongPress的触发回调
                    removeTapCallback();
                    removeLongPressCallback();
                }
                break;
        }
        return true;
    }
    return false;
}

由源码我们可以看出,View的TouchEvent方法就是一个大型的回调调用方法,里面判断各种条件,去把合适的回调方法调用,来满足我们开发的需求。比如我们可以明显的看出Onclick方法是在UP事件中触发的,LongClick可以制约Click的触发。

结论
  1. 一个事件序列从手指接触屏幕到手指离开屏幕,在这个过程中产生一系列的事件,以DOWN事件为开始,包含若干个MOVE,以UP事件为结尾。
  2. 正常情况下,一个事件序列只能被一个View拦截并且消耗。
  3. 某个View一旦决定拦截,那么这个事件序列都将由它的onTouchEvent处理,并且它的onInterceptTouchEvent调用。
  4. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件,那么同一事件序列中其他事件都不会再交给它处理。并且重新交由它的父元素处理。
  5. 事件传递的过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过reqeustDisallowInterceptTouchEvent方法可以在子View中干预父元素的事件分发过程,但ACTION_DOWN事件除外。
  6. ViewGroup默认不拦截任何事件,onInterceptTouchEvent默认返回false。View没有onInterceptTouchEvent方法,一旦有事件传递给它,那么它的onTouchEvent方法就会被调用。
  7. View的onTouchEvent默认会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable默认都为false,clickable要分情况,不如Button的clickable默认为true,TextView的clickable默认为false。
  8. View的enable属性不影响onTouchEvent的返回值。哪怕一个View为disable状态,只要它的clickable或者LongClickable有一个为true,那么它的onTouchEvent就返回true。
  9. onClick事件会响应的前提是当前View是可点击的,并且收到了ACTION_DOWN的ACTION_UP的事件,并且受长按事件的影响,当长按事件返回true时,onClick不会响应。
  10. onLongClick在ACTION_DOWN里判断是否进行响应,要想执行长按事件该View必须是longClickable的并且设置了OnLongClickListener。
总结

事件的分发其实并不复杂,但是这个设计思路以及细节的设计特别值得我们推敲,我一直在想,如果让我们自己设计一个事件分发模型,我们会怎么做呢?


事件分发模型

相关文章

网友评论

    本文标题:Android事件分发机制

    本文链接:https://www.haomeiwen.com/subject/wslubqtx.html