作者:京东零售郭旭锋

1 为什么需求事情分发

和其他平台类似,Android 中 View 的布局是一个树形结构,各个 ViewGroup 和 View 是按树形结构嵌套布局的,然后会出现用户触摸的方位坐标可能会落在多个 View 的范围内,这样就不知道哪个 View 来呼应这个事情,为了处理这一问题,就出现了事情分发机制。

2 事情分发的要害办法

Android 中事情分发是从 Activity 开端的,能够看看各组件中事情分发的要害办法

组件 dispatchTouchEvent onInterceptTouchEvent onTouchEvent
Activity
ViewGroup
View

Activity:没有 onInterceptTouchEvent 办法,由于假如 Activity 阻拦事情,将导致整个页面都没有呼应,而 Activity 是体系应用和用户交互的媒介,不能呼应事情明显不是体系想要的成果。所以 Activity 不需求阻拦事情。

ViewGroup:三个办法都有,Android 中 ViewGroup 是一个布局容器,能够嵌套多个 ViewGroup 和 View,事情传递和阻拦都由 ViewGroup 完结。

View:事情传递的最末端,要么消费事情,要么不消费把事情传递给父容器,所以也不需求阻拦事情。

3 事情分发流程剖析

3.1 事情分发流程概览

Activity 并不是一个 View,那么 Activity 是如何将事情分发到页面的 ViewGroup 和 View 的呢。咱们先看看源码

# Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    // 调用 Window 方针的办法,开端事情分发
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    // 假如事情分发回来 false,也即事情没被消费,则调用自己的 onTouchEvent 办法
    return onTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }
    return false;
}

能够看到,Activity 中的事情分发办法 dispatchTouchEvent 调用了 getWindow().superDispatchTouchEvent(ev) 办法,而这儿的 WIndow 实践上是 PhoneWindow。

简单来说,Window 是一个抽象类,是一切视图的最顶层容器,视图的外观和行为都归他管,无论是布景显示、标题栏仍是事情处理都是他办理的领域,而 PhoneWindow 作为 Window的唯一亲儿子(唯一实现类),自然便是 View 界的皇帝了。

下来看看 PhoneWindow 的代码

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

PhoneWindow 中又调用了 mDecor.superDispatchTouchEvent(event) 办法。mDecor 是 DecorView 方针,再看看 DecorView 的代码

# DecorView
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
}
# FrameLayout
public class FrameLayout extends ViewGroup {
}
# ViewGroup
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ......
    }
}
# View
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ......
    }
}

能够看到,DecorView 实践上便是 ViewGroup,事情分发办法最终调用到了 ViewGroup 的 dispatchTouchEvent(MotionEvent ev) 办法。

DecorView 是 PhoneWindow 的一个方针,其职位便是跟在 PhoneWindow 身边专业为 PhoneWindow 服务的,除了自己要干活之外,也担任音讯的传递,PhoneWindow 的指示经过 DecorView 传递给下面的 View,而下面 View 的信息也经过 DecorView 回传给 PhoneWindow。

Android 中的事情分发是责任链模式的一种变形。事情由上往下传递,假如事情没有被消费则持续传递到下一层,假如事情被消费则中止传递,假如到最下层事情则没有被消费,则事情会层层传递给上一层处理。咱们都知道事情分发的源头在 Activity 中的 dispatchTouchEvent 办法中,事情从这儿开端,分发到布局中的各个 View 中,不断递归调用 ViewGroup/View 的 dispatchTouchEvent 办法。经过上面剖析能够看到,Activity 在接受到上层派发来的事情后,会把事情传递到自己的 dispatchTouchEvent 办法中,然后Activity 会把触摸、点击事情传递给自己的 mWindow 方针,最终传递给 DecorView 的 dispatchTouchEvent 办法,实践调用的是 ViewGroup 的 dispatchTouchEvent 办法。

3.2 事情分发源码剖析

经过剖析,能够知道 Android 中事情分发的要害办法便是 ViewGroup 和 View 中的相关办法,如下

# View
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
    public boolean dispatchTouchEvent(MotionEvent event) {
        // ... 省掉部分代码
        boolean result = false;
        // ... 省掉部分代码
        if (onFilterTouchEventForSecurity(event)) {
            // ... 省掉部分代码
            // 1. 首要调用 onTouchEvent 办法,回来 true 阐明事情被消费,不然没被消费
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        // ... 省掉部分代码
        return result;
    }
    public boolean onTouchEvent(MotionEvent event) {
        // ... 省掉部分代码
        // 2. 默许可点击则回来 true,也便是消费事情。Button 或设置过 OnClickListener,则 View 可点击
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    // ... 省掉部分代码
                    break;
                case MotionEvent.ACTION_DOWN:
                    // ... 省掉部分代码
                    break;
                case MotionEvent.ACTION_CANCEL:
                    // ... 省掉部分代码
                    break;
                case MotionEvent.ACTION_MOVE:
                    // ... 省掉部分代码
                    break;
            }
            return true;
        }
        return false;
    }
}

View 中的办法逻辑比较简单,如补白 1 所示,dispatchTouchEvent 首要便是做一些安全查看,查看经过后会调用 onTouchEvent 办法。而 onTouchEvent 办法中逻辑如补白 2 所示,假如 View 是可点击的,则默许会认为消费事情,不然不消费,一般 Button 控件,或设置过 OnClickListener 的控件,View 会被默许设置为可点击。

下面看看 ViewGroup 代码

# ViewGroup
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // ... 省掉部分代码
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            // 1. 假如是 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();
            }
            final boolean intercepted;
            // 2. 假如是 DOWN 事情,会判别当时 ViewGroup 是否要阻拦事情。这儿受两个要素影响:
            //    一是 FLAG_DISALLOW_INTERCEPT,假如设置不阻拦,则不会调用 onInterceptTouchEvent,直接设置为不阻拦
            //    二是没设置 FLAG_DISALLOW_INTERCEPT 标志,默许答应阻拦,会调用 onInterceptTouchEvent 办法
            // 3. 假如不是 DOWN 事情,可能是 MOVE 或 UP 事情,mFirstTouchTarget 是记载需求持续进行事情分发的下一级子 View,包括ViewGroup 或 View,这儿也分为两种状况
            //    假如 mFirstTouchTarget 不为空,阐明需求持续向下一级子 View/ViewGroup 分发事情,这时阐明前次 DOWN 事情找到了下级有消费事情的子 View,且无阻拦事情
            //    假如 mFirstTouchTarget 为空,阐明没找到要消费事情的子 View,或事情被阻拦了
            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;
            }
            // ... 省掉部分代码
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            // 4. 下面逻辑首要便是遍历寻觅能消费事情的 View,假如事情被阻拦,则不需求再寻觅
            if (!canceled && !intercepted) {
                // ... 省掉部分代码
                // 5. 只要 DOWN 事情才需求寻觅,其他事情时现已确认是否找到,都不需求再找消费事情的 View 了
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    // ... 省掉部分代码
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        // ... 省掉部分代码
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            // ... 省掉部分代码
                            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
                            // 6. 这个办法是要害
                            //    假如 child 不为空,则会再调用 child.dispatchTouchEvent 办法,达到层层递归的作用
                            //    假如 child 为空,则会调用 super.dispatchTouchEvent 办法,super 是 View,实践上调用了 onTouchEvent 办法,自己判别是否消费事情
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // ... 省掉部分代码
                                // 7. 回来 true,阐明找到了消费事情的 View,下面办法会给 mFirstTouchTarget 赋值,下面 mFirstTouchTarget 将不为空
                                //    注:mFirstTouchTarget 并不是最终消费事情的 View,而是下一级包括消费事情 View 的链表方针,或是直接消费事情的 View 的链表方针
                                //    每一个 ViewGourp 都会记载一个 mFirstTouchTarget,mFirstTouchTarget.child 记载了下一层消费事情的 ViewGroup 或 View
                                //    一同,alreadyDispatchedToNewTouchTarget 变量会设置为 true
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                            // ... 省掉部分代码
                        }
                        // ... 省掉部分代码
                    }
                    // ... 省掉部分代码
                }
            }
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // 8. 当没有找到消费事情的 View,或事情被阻拦,mFirstTouchTarget 都不会被赋值,这儿 child 为空,会调用自己的 onTouchEvent 办法
                handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    // 9. 阐明找到了消费事情的 View,并且现已分发,直接设置为已处理
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
                        // 10. 此办法和补白 6 和 8 都相同,这儿多了 cancel 的处理逻辑。假如事情被阻拦,需求给本来消费事情的 View 发一个 CANCEL 事情
                        if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
            // ... 省掉部分代码
        }
        // ... 省掉部分代码
        return handled;
    }
    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;
    }
    // 没有覆写这个办法,实践调用的是 View 的 onTouchEvent 办法
    public boolean onTouchEvent(MotionEvent event) {
    }
}

能够看到,ViewGroup 中的事情分发逻辑仍是比较复杂,但捉住要害点后则很简单能看清它的本来相貌

(1)分发的事情包括 DOWN、MOVE、UP、CANCEL 几种,用户一个完好的动作便是由这几个事情组合而成的

(2)只要 DOWN 事情中会寻觅消费事情的方针 View,其他事情不会再寻觅

(3)DOWN 事情寻觅到方针 View 后,后续其他事情都会直接分发至方针 View

(4)事情能够被阻拦,阻拦后原方针 View 会收到 CANCEL 事情,后续将不会再收就任何事情(这也是这套机制不支持丰厚的嵌套滑动的原因)

3.3 事情分发情形剖析

3.3.1 分发进程没有任何 View 阻拦和消费

Android事件分发-基础原理和场景分析

(1)事情回来时,为了简化理解,dispatchTouchEvent 直接指向了父 View 的 onTouchEvent ,实践上它仅仅是回来给父 View 的 dispatchTouchEvent 一个 false 值(影响了 mFirstTouchTarget 的值),父 View 依据回来值来调用本身的onTouchEvent 办法

(2)ViewGroup 是依据 onInterceptTouchEvent 的回来值(影响了 mFirstTouchTarget 的值)确认是调用子 View 的 dispatchTouchEvent 仍是本身的 onTouchEvent 办法

(3)假如一切 View 都没有消费 DOWN 事情,后续 MOVE 和 UP 不会再往下传递,会直接传递给 Activity 的 onTouchEvent 办法

3.3.2 最底层View消费事情,且上层View没有阻拦事情

Android事件分发-基础原理和场景分析

(1)若没有 ViewGroup 对事情进行阻拦,而最底层 View 消费了此事情,也便是接纳到 DOWN 事情时 View 的 onTouchEvent 回来 true,事情将不会再向上传递给各个 ViewGroup 的 onTouchEvent 办法,而是直接回来,后续的 MOVE 和 UP 事情也将会直接交给 View 进行处理

3.3.3 最底层View没有消费事情,ViewGroup2消费了事情,且上层View没有阻拦事情

Android事件分发-基础原理和场景分析

(1)假如 View 没有消费事情,在层层调用父布局的 onTouchEvent 办法时,有 View 消费此事情,如 ViewGroup2 消费此事情,后续 MOVE 和 UP 事情将会传递给 ViewGroup2 的 onTouchEvent 办法,并且不会再调用 ViewGroup2 的 onInterceptTouchEvent 办法

(2)源码 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {} 这个代码中首要调用 onInterceptTouchEvent() 办法和处理是否阻拦

第一次是 DOWN 事情会进行判别,所以会调用 onInterceptTouchEvent 阻拦办法

第2次非 DOWN 事情,不会再调用 onInterceptTouchEvent 办法。原因如下:

◦假如 DOWN 事情的时候进行过阻拦,也便是 onInterceptTouchEvent() 办法回来 true,则 mFirstTouchTarget 必定为 null,不会调用 onInterceptTouchEvent 办法。由于后面不会对这个值赋值,会往下走逻辑,直接调用到此 View 或 ViewGroup 的 onTouchEvent() 办法

◦假如 DOWN 事情没有阻拦,但子 View 的 onTouchEvent 都回来 false,只要当时 ViewGroup 的 onTouchEvent 回来 true,mFirstTouchTarget 也相同为 null,也不会调用 onInterceptTouchEvent 办法。由于 mFirstTouchTarget 本质是找能接纳事情的子 View,一切子 View 都不接纳事情,mFirstTouchTarget 就必然为 null

3.3.4 ViewGroup2阻拦了并消费了DOWN事情,其他View没有阻拦事情

Android事件分发-基础原理和场景分析

(1)ViewGroup2 阻拦 DOWN 事情后,View 不会接纳就任何事情。ViewGroup2 消费事情后,后续 MOVE 和 UP 事情会交给 ViewGroup2 的 onTouchEvent 办法进行处理,且不会再调用 ViewGroup2 的onInterceptTouchEvent 办法

3.3.5 View消费了DOWN事情,ViewGroup2阻拦且消费了MOVE事情,其他View没有阻拦事情

Android事件分发-基础原理和场景分析

(1)View 中 DOWN 事情正常传递

(2)当 ViewGroup2 阻拦 MOVE 事情后,当时 mFirstTouchTarget 不为空,首先 View 会收到转换后的 CANCEL 事情,mFirstTouchTarget 会置为空,下次 MOVE 事情由于 mFirstTouchTarget 为空,会调用到自己的 onTouchEvent 办法

3.3.6 View消费 DOWN 事情,ViewGroup2阻拦且消费了MOVE事情,一定条件后,ViewGroup1再次阻拦和消费MOVE事情,其他View没有阻拦事情

Android事件分发-基础原理和场景分析

3.4 事情分发总结

(1)整个分发进程中没有任何阻拦和消费,DOWN 事情会层层往下分发,并层层往上回来 false,MOVE 和 UP 事情则会交给 Activity 的 onTouchEvent 办法进行处理,不再往下分发

(2)分发进程中没有任何阻拦但有消费,DOWN 事情会层层往下分发,并层层往上回来false,直到有消费回来 true,MOVE 和 UP 事情则会层层往下分发,最终直接交给消费事情的 View 进行处理,然后层层回来 true

(3)分发进程中有阻拦且阻拦后消费,DOWN 事情会层层往下分发,直到有阻拦后直接交给消费的 View 进行处理,MOVE 和 UP 事情则会层层往下分发,最终直接交给消费事情的 View 进行处理,然后层层回来true

(4)分发进程中不阻拦 DOWN 事情,但阻拦 MOVE 事情且阻拦后消费,第一次阻拦,之前收到 DOWN 事情的子 View 会收到 CANCEL 事情,并层层回来;后续 MOVE 和 UP 会层层往下分发,最终直接交给消费事情的 View 进行处理

(5)分发进程中不阻拦 DOWN 事情,但阻拦 MOVE 事情且阻拦后不消费,第一次阻拦,之前收到 DOWN 事情的子 View 会收到 CANCEL 事情,并层层回来;后续 MOVE 和 UP 会层层往下分发,最终交给阻拦的 View 进行处理,此刻由于阻拦的 View 没有消费,会层层往上回来 false,最终会交给 Activity 的 onTouchEvent 办法进行处理

以上,是个人的一些剖析和经验,欢迎有兴趣的小伙伴一同学习和探讨!

本文正在参与「金石计划」