Android 事件冲突处理

概述

本文主要分享Android常见的事件冲突处理,处理方式有两种:

  • 外部拦截:父容器处理冲突
  • 内部拦截:子控件处理冲突

在介绍这两种处理方法之前,我们必须先了解两件事情:

  • 事件在控件中是如何传递的
  • 事件冲突产生的根本原因

事件在控件中是如何传递的

先来看一张事件分发的大致流程图:

通过流程图可知,事件的分发是从Activity的dispatchTouchEvent开始传递的,然后调用PhoneWindow的superDispatchTouchEvent,再调用DecorView的superDispatchTouchEvent,再调用到ViewGroup的dispatchTouchEvent方法,ViewGroup要先走分发流程,再走处理流程,而View只能走处理流程。下面便从ViewGroup的dispatchTouchEvent方法分析事件的传递流程。

DOWN事件

事件的分发是从Down事件开始的,Down事件只有一个,ViewGroup的dispatchTouchEvent方法对Down事件的处理方式有以下两种:

  • 拦截事件
  • 不拦截事件

接下来结合源码分析这两种处理方式有什么区别。

拦截事件

跟踪ViewGroup中dispatchTouchEvent方法针对ACTION_DOWN处理的关键代码:

    if (actionMasked == MotionEvent.ACTION_DOWN) {
        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;
    }
    ...
    if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    }

上述中有一句关键代码:
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
disallowIntercept表示是否不允许父控件拦截,由于在MotionEvent.ACTION_DOWN中调用了resetTouchState方法:

private void resetTouchState() {
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

所以在MotionEvent.ACTION_DOWN时disallowIntercept的值为fasle,此时会调用ViewGroup的onInterceptTouchEvent,因为拦截了Down事件,所以onInterceptTouchEvent返回true,此时事件停止向子控件分发,交给自身处理即mFirstTouchTarget==null,然后会调用到以下的关键代码:
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS)

跟踪dispatchTransformedTouchEvent方法的关键代码:

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

由源码可知,由于child==null会调用到 super.dispatchTouchEvent方法,即调用到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;
        }
    }

由源码可知,View的dispatchTouchEvent中会根据mOnTouchListener.onTouch或onTouchEvent是否返回true,来判断是否消费该事件。到这里拦截事件的基本流程就结束了。
这里补充一个小知识点,由于mOnTouchListener.onTouch是优先与onTouchEvent,所以当mOnTouchListener.onTouch返回true时,以下代码不会执行:

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

那如果此时控件同时设置了onClick事件便会失效,因为在onTouchEvent的ACTION_UP事件中调用了 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;
  }

不拦截事件

ViewGroup的onInterceptTouchEvent返回false(默认返回false)时,此时会将事件分发给子控件处理,如果子控件都不处理则自己处理该事件,关键代码如下:

    boolean alreadyDispatchedToNewTouchTarget = false;
    if (!canceled && !intercepted) {
               
        View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                ? findChildWithAccessibilityFocus() : null;
    
        if (actionMasked == MotionEvent.ACTION_DOWN
                || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            final int actionIndex = ev.getActionIndex(); // always 0 for down
            final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                    : TouchTarget.ALL_POINTER_IDS;
    
            removePointersFromTouchTargets(idBitsToAssign);
    
            final int childrenCount = mChildrenCount;
            if (newTouchTarget == null && childrenCount != 0) {
                final float x = ev.getX(actionIndex);
                final float y = ev.getY(actionIndex);
                // Find a child that can receive the event.
                // Scan children from front to back.
                final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                final boolean customOrder = preorderedList == null
                        && isChildrenDrawingOrderEnabled();
                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);
    
                    if (childWithAccessibilityFocus != null) {
                        if (childWithAccessibilityFocus != child) {
                            continue;
                        }
                        childWithAccessibilityFocus = null;
                        i = childrenCount - 1;
                    }
    
                    if (!child.canReceivePointerEvents()
                            || !isTransformedTouchPointInView(x, y, child, null)) {
                        ev.setTargetAccessibilityFocus(false);
                        continue;
                    }
    
                    newTouchTarget = getTouchTarget(child);
                    if (newTouchTarget != null) {
                        // Child is already receiving touch within its bounds.
                        // Give it the new pointer in addition to the ones it is handling.
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                        break;
                    }
    
                    resetCancelNextUpFlag(child);
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        // Child wants to receive touch within its bounds.
                        mLastTouchDownTime = ev.getDownTime();
                        if (preorderedList != null) {
                            // childIndex points into presorted list, find original index
                            for (int j = 0; j < childrenCount; j++) {
                                if (children[childIndex] == mChildren[j]) {
                                    mLastTouchDownIndex = j;
                                    break;
                                }
                            }
                        } else {
                            mLastTouchDownIndex = childIndex;
                        }
                        mLastTouchDownX = ev.getX();
                        mLastTouchDownY = ev.getY();
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        alreadyDispatchedToNewTouchTarget = true;
                        break;
                    }
    
                    // The accessibility focus didn't handle the event, so clear
                    // the flag and do a normal dispatch to all children.
                    ev.setTargetAccessibilityFocus(false);
                }
                if (preorderedList != null) preorderedList.clear();
            }
    
            if (newTouchTarget == null && mFirstTouchTarget != null) {
                // Did not find a child to receive the event.
                // Assign the pointer to the least recently added target.
                newTouchTarget = mFirstTouchTarget;
                while (newTouchTarget.next != null) {
                    newTouchTarget = newTouchTarget.next;
                }
                newTouchTarget.pointerIdBits |= idBitsToAssign;
            }
        }
    }

遍历子控件集合时,会根据子控件的dispatchTransformedTouchEvent方法判断是否有子控件处理了事件,若有子控件处理,会执行如关键代码:

    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

这里采用了链表来存储目标控件,此时mFirstTouchTarget不为空,那如果子控件都没有处理,是如何将事件再交给父控件处理呢?继续跟踪源码:

if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        TouchTarget predecessor = null;
        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;
                }
                if (cancelChild) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                    } else {
                        predecessor.next = next;
                    }
                    target.recycle();
                    target = next;
                    continue;
                }
            }
            predecessor = target;
            target = next;
        }
    }

当没有子控件都没有处理事件时,mFirstTouchTarget=null,此时会调用ViewGroup的dispatchTransformedTouchEvent方法自己处理该事件,如果有子控件处理,会执行以下判断:

     if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
        handled = true;
    }

因为有子控件处理,此时alreadyDispatchedToNewTouchTarget = true、mFirstTouchTarget=newTouchTarget、 target.next=null即满足上述条件,while循环只会执行一次,到这里不拦截事件的基本流程就结束了。

MOVE事件

Move事件的传递也是通过以下两种方式进行分析:

  • 不拦截Move事件传递
  • 拦截Move事件传递

Move事件正常传递

父控件不拦截事件时,Move事件的传递也是通过dispatchTouchEvent方法传递给目标控件的,关键代码如下:

    boolean alreadyDispatchedToNewTouchTarget = false;
    View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                        .....
                }//此时是Move事件,不会执行这段代码
    }
    .....
    TouchTarget predecessor = null;
    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;
            }
            if (cancelChild) {
                if (predecessor == null) {
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }
        predecessor = target;
        target = next;
    }

这里需要注意由于此时alreadyDispatchedToNewTouchTarget=false,所以会走else分支,会执行以下关键代码:

 if (dispatchTransformedTouchEvent(ev, cancelChild,
        target.child, target.pointerIdBits)) {
    handled = true;
}

将Move事件交给对应的目标控件(Down事件保存的Target),到这里正常的Move事件就执行完了。

拦截Move事件传递

分析拦截事件时,先来看一段代码:

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

通过上面Down事件分析可知,由于Down事件做了重置操作,所以disallowIntercept的值为false,即if分支的代码一定会执行,此时拦截子控件的Move事件,会执行以下关键代码:

    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                    || intercepted;
    if (dispatchTransformedTouchEvent(ev, cancelChild,
            target.child, target.pointerIdBits)) {
        handled = true;
    }

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        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;
        }
}

因为拦截了move事件,此时intercepted=true, cancelChild=true,此时会设置子控件的Action为MotionEvent.ACTION_CANCEL,取消子控件的事件,并且注意以下代码:

 final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                    || intercepted;
    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;
    }

由于此时cancelChild=true, mFirstTouchTarget被设置成null,本次Move事件就结束了,注意ViewGroup是在下一Move事件才能够接收到事件,因为下一次Move事件会重新走dispatchTouchEvent方法,关注以下代码:

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

由于此时是Move事件并mFirstTouchTarget=null,所以此时走else分支intercepted = true,Move事件会交给自身处理,关联代码:

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}

小结一下,当父控件拦截Move事件时,第一次会将子控件的事件类型设置为MotionEvent.ACTION_CANCEL并将mFirstTouchTarget赋值为null,此时第一次Move事件结束(由于子控件的dispatchTransformedTouchEvent返回true),第二次以后的Move事件才会传递到父控件。

UP与Cancel事件

一次完整的事件,首先有Down事件开始,中间有多个Move事件,最后由Up/Cancel事件结束,Up事件是正常结束,而Cancel事件是被父控件拦截后产生的

事件分发完整流程图

为了进一步理解上述的源码分析流程,下面提供一张完整的事件分发流程图:

896629-20171007002836974-997068426.png

事件冲突处理

通过前面的铺垫,可以知道事件冲突只能在Move事件中处理,可以通过外部拦截和内部拦截处理事件冲突,这里以SwipeRefreshLayout嵌套ViewPager为例:

外部拦截

根据父控件的滑动逻辑在onInterceptTouchEvent方法中返回true/false,核心代码:

public class CustomSRL2 extends SwipeRefreshLayout {

    //外部拦截成员变量
    private float startX;
    private float startY;
    //ViewPager是否滚动
    boolean mIsVpMove = false;
    //触发移动事件的最小距离,如果小于这个距离就不触发移动控件,如Viewpager就是用这个距离来判断用户是否翻页
    private int mTouchSlop;

    public CustomSRL2(Context context) {
        this(context, null);
    }

    public CustomSRL2(Context context, AttributeSet attrs) {
        super(context, attrs);
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    //外部拦截
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = ev.getX();
                startY = ev.getY();
                mIsVpMove = false;
                break;
            case MotionEvent.ACTION_MOVE:
                //若此时ViewPager还在滑动,则返回false,不拦截
                if (mIsVpMove) {
                    return false;
                }
                float x = ev.getX();
                float y = ev.getY();
                float deltaX = Math.abs(x - startX);
                float deltaY = Math.abs(y - startY);
                if (deltaX > mTouchSlop && deltaX > deltaY) {
                    mIsVpMove = true;
                    return false;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mIsVpMove = false;
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
}

内部拦截

根据子控件的滑动逻辑调用父控的requestDisallowInterceptTouchEvent(true/false)方法通知父控件是否不拦截事件,核心代码:

public class CustomSRL2 extends SwipeRefreshLayout {

    public CustomSRL2(Context context) {
        super(context);
    }

    public CustomSRL2(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    //以下代码为内部拦截代码
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //在ACTION_DOWN事件返回false,不拦截事件,将事件交给子控件处理
        if(ev.getAction() == MotionEvent.ACTION_DOWN){
            super.onInterceptTouchEvent(ev);
            return false;
        }
        return true;//拦截事件
    }
}

public class CustomVPInner extends ViewPager {

    private float startX;
    private float startY;

    public CustomVPInner(Context context) {
        super(context);
    }

    public CustomVPInner(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    //内部拦截:使用ViewCompat.setNestedScrollingEnabled(this,true),参考以下代码
    /**
     * public void requestDisallowInterceptTouchEvent(boolean b) {
     *         // if this is a List < L or another view that doesn't support nested
     *         // scrolling, ignore this request so that the vertical scroll event
     *         // isn't stolen
     *         if ((android.os.Build.VERSION.SDK_INT < 21 && mTarget instanceof AbsListView)
     *                 || (mTarget != null && !ViewCompat.isNestedScrollingEnabled(mTarget))) {
     *             // Nope.
     *         } else {
     *             super.requestDisallowInterceptTouchEvent(b);
     *         }
     *     }
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = ev.getX();
                startY = ev.getY();
                ViewCompat.setNestedScrollingEnabled(this,true);
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                float x = ev.getX();
                float y = ev.getY();
                float deltaX = Math.abs(x - startX);
                float deltaY = Math.abs(y - startY);
                if (deltaX < deltaY) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
        }

        //打印ViewPager是否消费了该事件,如果没有,事件还是会交给SwipeRefreshLayout处理
        boolean consume = super.dispatchTouchEvent(ev);
        Log.e("fmt","consume=" + consume);

        return super.dispatchTouchEvent(ev);
    }
}

完整代码实现

百度链接
密码:1cq9

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