Android 之 ViewGroup 事件分发深入源码分析 [ 二 ]

Android 事件分发之源码分析系列
Android 之事件分发基础篇 [ 一 ]
Android 之 ViewGroup 事件分发深入源码分析 [ 二 ]
Android 之 View 事件分发深入源码分析 [ 三 ]
Android 之 View 事件分发深入源码分析 [ 总结 ]

友情提示:

本章内容主要是从源码上分析 ViewGroup.dispatchTouchEvent().
阅读本篇文章会引起强烈不适, 可能会带来头晕, 恶心, 干呕等一系列症状.

开篇先抛出几个问题:

  1. 为什么子 ViewACTION_DOWN 中调用了 requestDisallowInterceptTouchEvent(true); 设置不允许父容器拦截会失效 ?
  2. 如果子 View 消费掉 DOWN 事件后, 为什么后续的事件都会直接传给它? 是怎么实现的 ?
  3. 什么情况下会发送 ACTION_CANCEL 事件 ?
  4. ACTION_DOWN 事件被子 View 消费了,那 ViewGroup 能拦截剩下的事件吗?如果拦截了剩下事件,当前这个事件 ViewGroup 能消费吗?子 View 还会收到事件吗?

先整理这几个吧, 后续有需要会继续添加.
 
下面开始惊险刺激之旅

在上一篇有说, 事件分发是从 Activity 开始的. 也就是说当一个触摸事件发生后, 事件首先传到的是 Activity 的 dispatchTouchEvent() 方法, 现在进入到 Activity 中,

1. Activity.dispatchTouchEvent

//Activity.java 3396 行
public boolean dispatchTouchEvent(MotionEvent ev) {
    //1
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    //2
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    //3
    return onTouchEvent(ev);
}

//Activity.java 3216 行
public void onUserInteraction() {
}

//Activity.java 3141 行
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}

//window.java  1260行
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    final boolean isOutside =
            event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
            || event.getAction() == MotionEvent.ACTION_OUTSIDE;
    if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
        return true;
    }
    return false;
}
  • 分析 1
    ActivitydispatchTouchEvent 方法接收到按下的事件后, 会先调用 onUserInteraction 方法. 这个方法一般为 null, 如果开发者希望知道用户与设备的交互情况, 可以覆写这个方法. 但是当前 Activity 需要处于栈顶.

  • 分析 2
    getWindow().superDispatchTouchEvent(ev)
    首先 getWindow() 我们都知道了, 返回的是 window 的唯一实现类 PhoneWindow. 如果 PhoneWindow.superDispatchTouchEvent() 方法返回了 true, 这里就直接返回 true, 这里先不跟进去, 接着看第三点. 返回 false 的情况下. 调用 onTouchEvent()

  • 分析 3
    在第三点处又调用了 window.shouldCloseOnTouch() 方法, 这方法主要是判断是不是按下事件, 是不是在边界之外.
    shouldCloseOnTouch()返回值为 True 表示事件在边界外. 就消费事件.然后调用 Activityfinish()方法.
    返回值为 False 表示 不消费,

  • 小结

  • 当一个点击事件发生时, 会从 Activity 的事件分发开始. 即调用 Activity. dispatchTouchEvent() 开始事件分发.
  • 如果是按下事件 (DOWN), 就调用 onUserInteraction 方法, 这方法一般为空.
  • 接着就调用了 PhoneWindowdispatchTouchEvent() 事件分发方法将事件分发到 Activity 内部的 ViewGroup/View, 看它们处理不处理这个事件, 如果不处理, 则会返回 false, 然后 Activity 接收到返回值后会调用自身的 onTouchEvent()方法自己处理.
  • Activity.onTouchEvent()中会判断是不是应该关闭 Activity,

 

2. PhoneWindow.superDispatchTouchEvent

PhoneWindow.java 1829行

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

内部直接调用了 mDecorsuperDispatchTouchEvent(event) 方法.(mDecor 就是 DecorView). 进入到 DecorView 中看 superDispatchTouchEvent.


 

3. DecorView.superDispatchTouchEvent

DecorView.java 439 行

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

看到这里又调用了super.dispatchTouchEvent(event). DecorView 的继承自 Framelayout ,但是 FrameLayout 中, 并没有这个方法. FrameLayout 又继承自 ViewGroup, 所以这里调用的直接是 ViewGroupdispatchTouchEvent 方法.直接跟进去.


 

4. ViewGroup.dispatchTouchEvent

先大致了解下 dispatchTouchEvent 大概都做了什么

public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    //要不要分发本次触摸事件
    if (onFilterTouchEventForSecurity(ev)) {
        ...
        //是否取消事件或者拦截事件
        if (!canceled && !intercepted) {
            ...
            //只处理 DOWN 事件
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                ...
                if (newTouchTarget == null && childrenCount != 0) {
                    ...
                    //循环子 View 分发 Down 事件
                    for (int i = childrenCount - 1; i >= 0; i--) {
                       ...
                    }
                    ...
                }
                //没有找到新的可以消费事件的子View,那就找最近消费事件的子View来接受事件
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                  ...
               }
            }
        }
        if (mFirstTouchTarget == null) {
            // 父 View 自己处理事件
        } else {
            //判断是否已经分发过 DOWN.
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget){
            }else {
              //判断是否需要分发 CANCEL 事件
            }
        }
    }
    return handler;
}

可以看到其实在 ViewGrou.dispatchTouchEvent 中基本步骤就是这些. 无非就是细节比较多罢了.

  1. 一个判断需要不需要分发本次触摸事件包含了全部逻辑.
  2. 在内部接着判断当前事件没有被取消并且也没有被拦截.
    • 如果都没有, 则进入 DOWN 的事件分发. (没有 else 逻辑)

3.最后判断是否有子 View 消费了事件, 没有的话父 View 自己处理事件, 有的话则发送 DOWN 的后续事件列. 包括 CANCEL 事件.

从上面的代码片段中可以看出, 只有在是 DOWN 事件的时候, 才会进入到 for 循环中去遍历当前 ViewGroup 下的所有子 View. 找到能处理 DOWN 事件的 View 并且添加到 mFirstTouchTarget 链表的头部.
然后会在最后判断有没有子 View 处理过事件(mFirstTouchTarget == null) .没有处理的话 ViewGroup 自己处理. 如果有处理, 就根据条件直接分发 DOWN 事件后的剩余事件包括 CANCEL 事件,


 

4.1 深度分析开始

由于这个方法太长, 我会采用分段分析.
ViewGroup.java 2542 行

// 1
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 2
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }
    // 3
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }
    // 4
    boolean handled = false;
  • 分析 1
    mFirstTouchTarget 用来记录当前触摸目标链表的起始对象.
    这里要说一下能够接收触摸事件流的子 View 是怎么被记录的. 其实就是使用一个 TouchTarget 来记录. 它是一个单链表结构, 并且有复用机制. TouchTarget 中与我们关联最大的两个成员就是 [ public View child ] : 用来保存能够处理触摸事件的 View. 另外一个是 [ public TouchTarget next ] : 这个是指向下一个 TouchTarget 对象.

  • 分析 2
    mInputEventConsistencyVerifier
    mInputEventConsistencyVerifierInputEventConsistencyVerifier 类型. 声明在 View 中, 官方翻译大概为: 输入一致性校验. 在 InputEventConsistencyVerifierisInstrumentationEnabled 方法为 True 的时候.会在 View 中初始化 mInputEventConsistencyVerifier 对象. 在 View.java 4739 行查看初始化

  • 分析 3
    通过触摸事件判断是否应该被有焦点的 View 处理事件, 如果同时存在拥有焦点的 View, 则设置为 False

  • 分析 4
    boolean handled = false;
    该变量表示是否处理了该事件, 这个变量也是最后的返回值.

 
下面继续

    // 5
    if (onFilterTouchEventForSecurity(ev)) {
       // 6
       final int action = ev.getAction();
       // 7
       final int actionMasked = action & MotionEvent.ACTION_MASK;
    

注: 这个 if 判断贯穿了整个 dispatchTouchEvent 方法

  • 分析 5
    主要检查要不要分发本次触摸事件, 检测通过才会执行该 if 中的逻辑, 否则就放弃对这次事件的处理. 在这个方法内部会先检查 View 有没有设置被遮挡时不处理触摸事件的 flag, 再检查收到事件的窗口是否被其他窗口遮挡. 都检查通过返回 True, 检查不通过则直接返回 handled. (上面在分析 4 中 handled, 默认为 False )

  • 分析 6
    获得事件, 包括触摸事件的类型和触摸事件的索引.
    Android 会在一条 16 位指令的高 8 位中存储触摸事件的索引, 低 8 位是触摸事件的类型.

  • 分析 7
    保存 上面分析 6 中 action 变量中的低 8 位, 其余为 0, 作为 actionMasked, 也就是获取事件类型.

 
接着开始一系列的初始化及重置工作.

      // 8
      if (actionMasked == MotionEvent.ACTION_DOWN) {
           // 9
           cancelAndClearTouchTargets(ev);
           // 10
           resetTouchState();
      }
  • 分析 8
    只有在 DOWN 事件的时候才会进入if 内逻辑

  • 分析 9
    因为每次事件流的开始, 都是从 DOWN 事件开始的, 所以需要清除上一次接收触摸事件 View 的状态. 主要就是清除上一次触摸事件事件流中能够接收事件的所有子 ViewPFLAG_CANCEL_NEXT_UP_EVENT 标志, 并且模拟了一个 ACTION_CANCEL 事件分发给他们, 可以重置这些子 View 的触摸状态, 例如取消它们的长按或者点击事件. 下面看下 cancelAndClearTouchTargets() 代码

    private void cancelAndClearTouchTargets(MotionEvent event) {
        // 如果触摸事件目标队列不为空才执行后面的逻辑
        if (mFirstTouchTarget != null) {
            boolean syntheticEvent = false;
            if (event == null) {
                final long now = SystemClock.uptimeMillis();
                // 自己创建一个ACTION_CANCEL事件
                event = MotionEvent.obtain(now, now,MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                // 设置事件源类型为触摸屏幕
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                // 标记一下,这是一个合成事件
                syntheticEvent = true;
            }
            // TouchTarget是一个链表结构,保存了事件传递的子一系列目标View
            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                // 检查View是否设置了暂时不再接收事件的标志位,如果有清除该标志位
                // 这样该View就能够接收下一次事件了。
                resetCancelNextUpFlag(target.child);
                // 将这个取消事件传给子View
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            // 清空触摸事件目标队列
            clearTouchTargets();
            if (syntheticEvent) {
                // 如果是合成事件,需要回收它
                event.recycle();
            }
        }
    }   
    
  • 分析 10
    resetTouchState() 清除 ViewGroup 触摸的相关状态. 在方法内, 会再调用一次 clearTouchTargets() 方法清除触摸事件队列. 然后再次清除 View 中不接收 TouchEvent 的标志位. 最后最重要的来了, 设置为允许拦截事件. 下面看resetTouchState() 方法

    private void resetTouchState() {
        // 再清除一次事件传递链中的View
        clearTouchTargets();
        // 再次清除View中不接收TouchEvent的标志
        resetCancelNextUpFlag(this);
        // 设置为允许拦截事件
        // FLAG_DISALLOW_INTERCEPT 通过子 VIew 调用 requestDisallowInterceptTouchEvent 设置
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }
    

    这里也验证了上面第一个问题:
    问: 为什么子 ViewACTION_DOWN 中调用了 requestDisallowInterceptTouchEvent(true); 设置不允许父容器拦截会失效 ?
    答: 因为 ViewGroup 中的 dispatchTouchEvent 方法在分发事件时, 如果是 DOWN 事件的时候, 就会重置 FLAG_DISALLOW_INTERCEPT 这个标记位, 将导致子 View 中设置的这个标记位无效. 因此子 View 在调用 requestDisallowInterceptTouchEvent(true); 方法并不能影响 ViewGroupDOWN 事件的处理.
    补充: requestDisallowInterceptTouchEvent(true); 一旦设置后, ViewGroup 将无法拦截除了 DOWN 事件以外的其他事件.

 
接着向下看. 下面这段代码主要是判断 VIewGroup 是否拦截当前事件.

        // 11
        final boolean intercepted;
        // 12
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            // 13
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
        // 14
        if (intercepted || mFirstTouchTarget != null) {
            ev.setTargetAccessibilityFocus(false);
        }
        // 15
        final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
        // 16
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        // 17
        TouchTarget newTouchTarget = null;
        // 18
        boolean alreadyDispatchedToNewTouchTarget = false;
  • 分析 11
    intercepted 这个变量用于检查是否拦截当前事件.

  • 分析 12
    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)
    这个判断限制了必须是 DOWN事件, 或者 mFirstTouchTarget != null 才会进入内部逻辑去判断要不要拦截事件.否则直接认定为拦截.intercepted = true
    如果当前事件是 DOWN 事件, 那么第一个条件成立 (其实这个时候 mFirstTouchTarget 是等于 null 的, 因为在分析 10 中清除了触摸链中的目标)
    如果当前事件不是 DOWN, 那么肯定是 DOWN 的后续事件, 那么第一个条件不成立, 看第二个条件 mFirstTouchTarget != null, 之前说过 mFirstTouchTarget 是用来记录当前触摸目标链表的起始对象. 只有子 View 处理了事件时, mFirstTouchTarget 才会被赋值 (后面会有分析到)

  • 分析 13
    其中 FLAG_DISALLOW_INTERCEPT 是一个常量的标记位, 意思是对于父 ViewGroup 向内部的子 View 传递事件不允许拦截的标记位.
    默认的 mGroupFlags 对应的位置只 0, 在 View 初始化代码里 mGroupFlags 并没有被初始化相对应位置的值.
    FLAG_DISALLOW_INTERCEPT 的值是一个十六进制 0x80000 转换成二进制就是 0000 0000 0000 1000 0000 0000 0000 0000, 共 32 位, 而 mGroupFlags & FLAG_DISALLOW_INTERCEPT 位运算之后, 0 的位置全部变为了 0 , 对于 1 的那个位置, mGroupFlags 的对应位置是 1, 才是 1. 否则是 0.
    也就是说, 在正常没有外力影响的情况下, boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 结果是 False. 因为mGroupFlags & FLAG_DISALLOW_INTERCEPT 的结果一定是 0.
    那么什么是外力影响呢, 就是上面说的子 View 通过调用 getParent.requestDisallowInterceptTouchEvent(true) 来改变 mGroupFlags 对应位置的值. 该方法在 ViewGroup.3136 行.

    若这个方法的参数是 True 的话( 传入 True 表示子 View 不允许 ViewGroup 拦截 ), 在这个方法内会执行 mGroupFlags |= FLAG_DISALLOW_INTERCEPT; 这个时候 mGroupFlags 对应位置的值就变为了 1. 那么在这里再进行位运算 disallowIntercept 就会为 True, 然后再进行取反进行判断为 False, if 不成立, 直接进 else, intercepted 被赋值为 False.

    若这个方法参数是 False (子 View 允许拦截), 则会在 requestDisallowInterceptTouchEvent 方法内执行 mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; 这个时候 mGroupFlags 对应的位置就变成了0. 那么这里的 disallowIntercept 结果就是 False, 意味着允许拦截 . 再取反为 True, 执行 if 内逻辑.

    if 中会先调用 onInterceptTouchEvent 拦截方法并把返回值给 intercepted, 这是什么意思呢, 就是说虽然说你子 View 允许我拦截了, 但是我需要确定一下自己是否需要拦截( 调用 onInterceptTouchEvent ).

    关于 分析 13 处 简单理解就是, 你让不让我拦截, 不让我拦截 intercepted = false, 让我拦截的话, 我还要看看我自己是否真的需要拦截, 拦截 intercepted = true, 不拦截那么 intercepted 还是为 false

    关于上面的问题 4 : ACTION_DOWN 事件被子 View 消费了,那 ViewGroup 能拦截剩下的事件吗?如果拦截了剩下事件,当前这个事件 ViewGroup 能消费吗?子 View 还会收到事件吗?
    问题 4 最起码有一点可以确认了, 那就是 ACTION_DOWN 被子 View 消 费了, 那么ViewGroup 还是能拦截剩下后续的事件的. (前提是子 View 没有 调用 requestDisallowInterceptTouchEvent 方法传入 True )
    为什么呢? 看分析 12, DOWN 被子 View 消费了, 那么 mFirstTouchTarget 肯定是会被赋值的. 这样还是会进入到 if 中去. 执行分析 13 处的逻辑. 剩下的自己分析分析.

  • 分析 14
    如果ViewGroup 拦截了事件, 或者事件已有目标组件进行处理, 那么就去除辅助功能标记, 进行普通的事件分发.

  • 分析 15
    canceled 标识本次事件是否需要取消.

  • 分析 16
    split 检查父 ViewGroup 是否支持多点触控, 即将多个 TouchEvent 分发给子 View. 在 Android 3.0 以后默认为 True.

  • 分析 17
    newTouchTarget = null 声明后续会使用到的变量
    当事件已经做出分发时, 记录分发对应的 View 控件

  • 分析 18
    alreadyDispatchedToNewTouchTarget = false
    记录事件是否已经做出分发, 后面用于过滤已经分发的事件, 避免事件重复分发.

 
继续向下看.

        // 19
        if (!canceled && !intercepted) {
            // 20
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null;
            // 21
            if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                // 22
                final int actionIndex = ev.getActionIndex(); 
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;
                // 23
                removePointersFromTouchTargets(idBitsToAssign);
                // 24
                final int childrenCount = mChildrenCount;
                // 25
                if (newTouchTarget == null && childrenCount != 0) {
                    // 26
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // 27
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
  • 分析 19
    当前 if 判断没有 else 分支. 对于被拦截和取消的事件,不会执行 if 中的所有方法.

  • 分析 20
    ev.isTargetAccessibilityFocus() : 检查 TouchEvent 是否可以触发 View 获取焦点.
    可以则查找当前 ViewGroup 中有没有获得焦点的子 View, 有就获取, 没有就为 null.

  • 分析 21
    这个 if 也没有 else 分支. 对事件的类型进行判断, 主要处理 DOWN 事件.
    [ 当前的触摸事件类型是不是DOWN ], [ 支持多点触控且是 ACTION_POINTER_DOWN ], [ 需要鼠标等外设支持 ]
    也就是说一个事件流只有一开始的 DOWN 事件才会去遍历分发事件, 后面的事件将不会再通过遍历分发, 而是直接分发到触摸目标队列的 VIew 中去

  • 分析 22
    获得事件的 actionIndex 与 位分配 ID .
    位分配 ID, 通过触控点的 PointerId 计算. 其逻辑为:
    1 << ev.getPointerId(actionIndex), 即对 0000 0001 左移, 移动的位数为 PointerId 的值. 一般情况下 PointerId 从 0 开始, 每次 + 1. 即把 PointerId 记录通过位进行保存, 0对应 0000 0001, 2对应 0000 00100, 5对应, 0010 0000.

  • 分析 23
    清除之前触摸事件中的目标.
    方法为:

    //检查是否有记录的 PointId, 并清除
    private void removePointersFromTouchTargets(int pointerIdBits) {
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        // mFirstTouchTarget 不为 null
        while (target != null) {
            //获取对应的 TouchTarget 链表的下一个对象
            final TouchTarget next = target.next;
            //判断是否存在记录了 mFirstTouchTarget 中触控点的 TouchTarget
            if ((target.pointerIdBits & pointerIdBits) != 0) {
                target.pointerIdBits &= ~pointerIdBits;
                if (target.pointerIdBits == 0) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                    } else {
                        predecessor.next = next;
                    }
                    //如果存在就移除
                    target.recycle();
                    //并指向链表中的下一个 TouchTarget 对象.
                    target = next;
                    continue;
                }
            }
    
            predecessor = target;
            target = next;
        }
    }
    
  • 分析 24
    获取子 View 的数量.

  • 分析 25
    newTouchTarget == null && childrenCount != 0: 如果有子 View 并且在上面分析 17 的地方声明的 newTouchTarget = null 才会进入 if 中. 当前 if 也没有 else 逻辑. 第一次 DOWN 事件发生的时候, newTouchTarget 肯定为 null, 如果条件不成立则代码会直接跳转到下面的分析 36 处执行.

  • 分析 26
    final float x = ev.getX(actionIndex);: 获得触摸事件的坐标.

  • 分析 27
    ArrayList<View> preorderedList = buildTouchDispatchChildList(); : 调用 buildTouchDispatchChildList() 方法创建待遍历的 View 列表.
    boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); : 是否采用自定义 View 顺序. 这个顺序将决定哪个 View 会先接收到事件.
    初始化了 preorderedListmChildren 两个子 View 集合, 为什么需要两个呢?.

    通过 buildTouchDispatchChildList() 方法构建待遍历的 View 集合会有如下特点

    1. 如果 ViewGroup 的子 View 数量不大于 1, 为 null.
    2. 如果 ViewGroup 的所有子 View 的 z 轴都为 0 , 为 null.
    3. View 的排序和 mChildren 一样是按照 View 添加顺序从前往后排的, 但是还是会受到子 View z 轴的影响. z 轴大的会往后排.

    所以这两个集合之间的最大区别就是, preorderedList 中 z 轴大的子 View 会往后排. 而 mChildren 不会.

 
现在来到了事件分发中最关键最核心的地方. 朋友们, 你们还好吗? 都还健在吗

                    // 分析 28
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        // 分析 29
                        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
                        // 分析 30
                        if (childWithAccessibilityFocus != null) {
                            if (childWithAccessibilityFocus != child) {
                                continue;
                            }
                            childWithAccessibilityFocus = null;
                            i = childrenCount - 1;
                        }
                        // 分析 31
                        if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                        // 分析 32
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }
                        // 分析 33
                        resetCancelNextUpFlag(child);
                        // 分析 34
                        if (dispatchTransformedTouchEvent(ev, false, child,idBitsToAssign)) {
                            mLastTouchDownTime = ev.getDownTime();
                            if (preorderedList != null) {
                                for (int j = 0; j < childrenCount; j++) {
                                    if (children[childIndex] == mChildren[j]) {
                                        mLastTouchDownIndex = j;
                                        break;
                                    }
                                }
                            } else {
                                mLastTouchDownIndex = childIndex;
                            }
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            // 分析 35
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }
                // 分析 36
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
        }
    }

为了防止有点蒙圈的朋友, 这里再发一下, 整个方法的大致流程

public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    //要不要分发本次触摸事件
    if (onFilterTouchEventForSecurity(ev)) {
        ...
        //是否取消事件或者拦截事件
        if (!canceled && !intercepted) {
            ...
            //只处理 DOWN 事件
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                ...
                if (newTouchTarget == null && childrenCount != 0) {
                    ...
//===========================================现在要分析的地方是这里开始 ===========================================
                    //循环子 View 分发 Down 事件
                    for (int i = childrenCount - 1; i >= 0; i--) {
                       ...
                    }
                    ...
                }
                //没有找到新的可以消费事件的子View,那就找最近消费事件的子View来接受事件
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                  ...
               }
            }
        }
//==================================================== 结束线 ====================================================
        if (mFirstTouchTarget == null) {
            // 父 View 自己处理事件
        } else {
            //判断是否已经分发过 DOWN.
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget){
            }else {
              //判断是否需要分发 CANCEL 事件
            }
        }
    }
    return handler;
}
  • 分析 28
    以"从尾到头"的方式遍历 View 列表, (就是从后往前遍历.)
    这就是为什么覆盖在上层的 View 总是能够优先获取到事件的原因.

  • 分析 29
    根据 childIndex 获取子 View 对象.
    如果 preorderedList 不为空, 则从 preorderedList 获取子 View.
    如果为空, 则从 mChildren 中获取子 View

  • 分析 30
    childWithAccessibilityFocus 在上面 分析 20 处声明并赋值.
    如果这个具有焦点的子 View 不为 null
    如果不为 null 那么接着判断这个具有焦点的子 View 与从
    分析 29 处取出的子 View 对象是否是同一个. 如果不是同一个 View 那么直接跳到 分析 28 处, 进行下一次遍历. 如果是同一个, 则把 childWithAccessibilityFocus 赋值为 null 并且把循环的 i 指向 childrenCount - 1 的位置. 意思大概是下次遍历的时候跳过这个子 View 关于这点还未细看, 网上看到有的帖子说是 如果当前子View具有可访问的焦点时,会让该子View优先获得这次事件. 有知道的朋友可以帮忙斧正.

  • 分析 31
    canViewReceivePointerEvents : 判断子 View 是否正常显示(VISIBLE)或者子 View 是否在播放动画.
    isTransformedTouchPointInView : 检测触摸事件是否在该子 View 的范围内.

    • 如果在子 View 原有的位置上没有看到子 View,同时子 View 也不是因为动画而离开原来的位置, 那么肯定是隐藏了, 因此不符合事件消费的条件, 所以执行 continue 跳过.
    • 如果用户触摸的位置不在子 View 的范围内,肯定也不符合事件消费的条件,同样执行 continue 跳过

    那么为什么要这样判断呢?其目的在于确保了子 View 即使是因为动画(例如位移动画)的原因离开了原来的位置, 子 View 也可以正常分发触摸了原范围内的事件, 这也正是子 View 执行位移动画后点击位置为什么没有跟随子 View 来到新位置的原因

  • 分析 32
    getTouchTarget 方法中, 其逻辑就是如果 mFirstTouchTarget 表示的链表中的某一个节点就是当前的 child, 则返回它赋值给 newTouchTarget, 若找不到则返回 null.
    下面的判断主要用于多点触控的情况, 例如手指触摸某个子 View 触发了 ACTION_DOWN, 这时另一根手指也放在这个视图上触发了 ACTION_POINTER_DOWN, 此时就需要通过在链表中查找当前子 View 的结果来判断两根手指触摸的是否为同一个 View, newTouchTarget != null 表示触摸了同一个子 View 那么就将触摸点Id复制给新的 newTouchTarget 对象,并执行 break 跳出遍历, (因为这个 View 之前已经收到了 DOWN 事件.)

  • 分析 33
    resetCancelNextUpFlag(child); 重置子 ViewPFLAG_CANCEL_NEXT_UP_EVENT 标志位.

  • 分析 34
    在这里才会真正的将事件分发给子 View 去处理.
    dispatchTransformedTouchEvent() 方法中调用了 child.dispatchTouchEvent. 方法返回 True 表示子 View 消费了, 继续执行 if 中的逻辑, 并结束遍历, 也就是说剩下的 View 都不会接收到这个事件了. 返回 False, 则继续遍历寻找下一个满足条件的子 View. dispatchTouchEvent 方法后面会有说到. 注意这里传入的 cancelfalse, child 是当前的子 View.先记住.

  • 分析 35

    • newTouchTarget = addTouchTarget(child, idBitsToAssign); 将消费事件的 View 添加到 mFirstTouchTarget 触摸链中, 并赋值给 newTouchTarget
      addTouchTarget() 方法内部逻辑. 根据传入的 View 生成一个新的 TouchTarget, 并将新生成 TouchTargetnext 指向 mFirstTouchTarget, 再将新生成 TouchTarget 赋值给 mFirstTouchTarget, 最后返回这个 TouchTarget
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { 
      final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
      target.next = mFirstTouchTarget;
      mFirstTouchTarget = target;
      return target;
    }
    
    • alreadyDispatchedToNewTouchTarget = true;
      设置标志位,证明当前接收到的动作事件已经分发过了,这个标志后续的判断中会用到, 这个标记位 只会在这里设置为 True`.
  • 分析 36
    if (newTouchTarget == null && mFirstTouchTarget != null)
    这里有可能是从分析 25 处跳转来的, 也有可能是执行完 DOWN 事件的分发来的. newTouchTarget 在不是 DOWN 事件或者没有找到处理事件的 View 时为 null, 但是这个判断是在 DOWN 事件逻辑内, 那这里意思就是没有找到处理事件的 View.
    mFirstTouchTargetDOWN 事件时, 如果找到了处理事件的 View 就不为 null
    分析 36 处这里的意思就是如果上面没有找到可以处理事件的子 View, 那么就找最近处理过事件的子 View 来接收事件.并且给 newTouchTarget 赋值.

 
到这里, DOWN 事件已经算是分发完成了. 现在接着看后续的其他事件.
剩下的就是

public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    //要不要分发本次触摸事件
    if (onFilterTouchEventForSecurity(ev)) {
        ...
        //是否取消事件或者拦截事件
        if (!canceled && !intercepted) {
            ...
            //只处理 DOWN 事件
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                ...
                if (newTouchTarget == null && childrenCount != 0) {
                    ...
                    //循环子 View 分发 Down 事件
                    for (int i = childrenCount - 1; i >= 0; i--) {
                       ...
                    }
                    ...
                }
                //没有找到新的可以消费事件的子View,那就找最近消费事件的子View来接受事件
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                  ...
               }
            }
        }
//===========================================现在要分析的地方是这里开始 ===========================================
        if (mFirstTouchTarget == null) {
            // 父 View 自己处理事件
        } else {
            //判断是否已经分发过 DOWN.
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget){
            }else {
              //判断是否需要分发 CANCEL 事件
            }
        }
//==================================================== 结束线 ====================================================
    }
    return handler;
}

剩下的就剩这一块了. 代码不是很长, 就一次性贴出来了.

        // 分析 37
        if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
        } else {
            // 分析 38
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                 // 分析 39
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    // 分析 40
                    final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    //分析 41
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
  • 分析 37
    if (mFirstTouchTarget == null) 都走到这一步了, mFirstTouchTarget 还为空是什么意思? 说明没有到现在还没找到处理事件的 View 呀, 那怎么办呢. 看到 if 内又调用了 dispatchTransformedTouchEvent(), 在 分析 34 的地方处理 DOWN 事件的时候调用了一次. 这里和上面传入的参数不同了, 第三个参数 child 传入的是 null, 表示没有 View 处理事件. 需要 ViewGroup 自己处理事件.

  • 分析 38
    遍历 mFirstTouchTarget 进行分发事件.

  • 分析 39
    alreadyDispatchedToNewTouchTarget : 是在分析 35 处被赋值的. 有子 View 处理了 DOWN 事件, 该变量才会为 True.
    这个判断意思是: 只有 DOWN 时, 并且有子 View 处理了事件才会走 if 中的逻辑, 目的是为了避免重复分发事件, 因为在上面分析 34 的时候已经分发并处理过了. 所以这里直接把最终返回值 handled = true.

  • 分析 40
    走到这里, 就是真正开始分发除 DOWN 事件外的事件了. 包括 CANCEL 事件. 因为走到这一步, 首先确定了 mFirstTouchTarget 不为空, 代表有子 View 处理了事件, 接着又进到 else , 表示当前事件不是 DOWN 事件.
    cancelChild 表示处理 DOWN 类型事件的目标控件的后续事件分发被拦截的情况. 父 View 拦截,或者 子 View 原本不可接收 TouchEvent 的状态,cancelChildTrue. 如果子 View 处理了剩下的事件, handled = true.
    接着又调用了 dispatchTransformedTouchEvent 方法, 第二个参数这里传入了 cancelChild, 第三个参数传入了之前处理 DOWN 事件的子 View.

    这里回答了问题 3, 什么情况下会发送 ACTION_CANCEL 事件 ?
    这里的 cancelChildtrue , 给它发送 CANCEL 事件 (CANCEL 是在这里进行发送, 并且子 View 接收过一次前驱事件.)
    cancelChildfalse , 就分发除 DOWN 外的剩余事件

  • 分析 41
    如果 cancelChildTrue, 表示 ViewGroup 拦截了事件, 然后需要清空事件队列. 这样就会使后续的触摸事件直接被 ViewGroup 默认拦截.
    这里就很好理解了, 清空了事件队列后, 在下次事件来的时候, 执行到上面的分析 12, 那么就会直接进入到 else, 给 intercepted 赋值为 true. 然后在分析 19 处也不会进入if 逻辑, 直接就到分析 37 处 ViewGroup 直接就自己处理了.

那么问题 4 的后半段也能解释了,
ACTION_DOWN 事件被子 View 消费了,那 ViewGroup 能拦截剩下的事件吗?首先这点是已经确定可以拦截的.参考上面的分析 12, 分析 13 处.
如果拦截了剩下事件,当前这个事件 ViewGroup 能消费吗?子 View 还会收到事件吗? 答案是: ViewGroup 可以消费, 子 View 会收到一次 CANCEL 取消事件,然后不会再收到别的事件了. 为什么这样说咧.

  • 首先第一次子 View 处理了 DOWN 事件, 这个时候, ViewGroup 没有拦截 DOWN 事件, 那么在分析 41 处, 就不会清空事件队列, 这时候 mFirstTouchTarget 有值,目标 View 就是处理 DOWN 事件的那个.
  • 接着 MOVE 事件来了, 在走到分析 12 处的时候 mFirstTouchTarget != null 条件成立, 在分析 13 处, 如果子 View 允许 ViewGroup 拦截, 那么就会调用分析 13 中的 onInterceptTouchEvent(), 这时 ViewGroup 拦截 MOVE 事件. intercepted = true. 分析 19 处不成立, 直接进入分析 37 处. 这时候 mFirstTouchTarget 还不等于 null 接着进入到 else, 在分析 39 处 alreadyDispatchedToNewTouchTarget 肯定为 false, 因为当前事件不是 DOWN 事件. 那么就走到了分析 40 处. cancelChild = true , 接着调用 dispatchTransformedTouchEvent() 方法, 传入的 cancelChild = true 在这个方法内就会发送 CANCEL 事件给子 View. 接着就会执行分析 41 处逻辑, 清空事件队列.
  • 再下个 MOVE 来的时候, 走到分析 12 处, 不成立, 分析 19 处不成立, 直接到分析 37处条件成立, ViewGroup 自己处理. 第三个参数传入 null 表示自己处理.

关于问题 2 [ 如果子 View 消费掉 DOWN 事件后, 为什么后续的事件都会直接传给它? 是怎么实现的 ? ] 在这里也能得到解答了.
首先 DOWN 事件分发完后, mFirstTouchTarget 有值了, 那么在后续事件到来后, 分析 19 成立 , 但是分析 21 不成立, 分析 37 处也不成立, 分析 39 处也不成立, 直接就执行了分析 40 处的逻辑, 直接就把 mFirstTouchTarget 中的目标 View 传入了 dispatchTransformedTouchEvent() 方法. 所以只要消费掉 DOWN 事件后, 后续事件都会直接分发给目标 View, 而不会再去遍历查找目标 View.

到这里, ViewGroup 的 dispatchTouchEvent 讲解完了, 接下来看 dispatchTransformedTouchEvent 这个方法到底做了什么


 

5. dispatchTransformedTouchEvent

ViewGroup.java 2988行.

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
    final boolean handled;
    //分析 42
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
     //分析 43
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
    if (newPointerIdBits == 0) {
        return false;
    }
    //分析 44
    final MotionEvent transformedEvent;
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);
                handled = child.dispatchTouchEvent(event);
                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }
    // 分析 45
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    transformedEvent.recycle();
    return handled;
}
  • 分析 42
    这段逻辑主要是检测是否需要发送 CANCEL 事件.
    如果传入的参数 canceltrue, 或者 actionACTION_CANCEL. 则设置消息类型为 ACTION_CANCEL, 并将 ACTION_CANCEL 分发给对应的对象.

    • 如果没有子 View , 也就是传入的 childnull 的情况下, 会将消息分发给当前的 ViewGroup, 只不调用的是 ViewdispatchTouchEvent
    • 如果有子 View,将这个取消事件传递给子 View (在上面的分析 40 处, ViewGroup 拦截的情况下会进入到此处),并且调用 childdispatchTouchEvent.
  • 分析 43
    先获取触摸事件的触摸点 ID, 接着与 期望的触摸点 ID 进行位运算, 并把结果赋值给 newPointerIdBits, newPointerIdBits == 0,则不消费此次事件直接返回 false.

  • 分析 44
    newPointerIdBits = oldPointerIdBits 表示是相同的触摸点, 再判断传入的 child 是否为空, 或者传入的 child 的变换矩阵还是不是单位矩阵. 如果满足再次判断传入的 child 是否为 null, 为 null 说明需要 ViewGrou 去处理事件. 不为 null 就将事件分发给 child 处理.
    如果是分发给 child 处理, 会计算事件的偏移量. 因为 childViewGroup 中可能会发生位置变化. 需要除去这些移动距离, 以保证事件到达 childonTouchEvent() 中时, 能够正确表示它在 child 中的相对坐标. 就相当于事件也要跟着 child 的偏移而偏移.

  • 分析 45
    如果不是相同的触摸点, 则意味着需要先转化 MotionEvent 事件, 然后再对转换后的对象进行事件分发.

dispatchTransformedTouchEvent 方法会对触摸事件进行重新打包后再分发, 如果它的第三个参数 childnull, 则会将事件分发给 ViewGroup 自己, 只不过此时需要将 ViewGroup 看做是一个 View.


 

到这里 ViewGroup.dispatchTouchEvent 就分析的差不多了, 如果你能坚持的看到这里, 相信你对 Android 中的事件分发机制学习的差不多了, 剩下的还剩两个 View.dispatchTouchEvent 以及 View.onTouchEvent 这两个方法的分析了. 会在后面讲解.

 


ViewGroup.dispatchTouchEvent 流程图如下

ViewGroup 事件分发流程
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342