Android GestureDetector 源码分析

前言

工作这段时间以来,陆陆续续看了一些类的源码,但是都没有详细的记录下来,只是在笔记上记录了一些功能性的代码,导致一段时间之后,完完全全忘记了。所以今天打算回顾梳理系统里的手势判断类 GestureDetector 。由于我们的项目是一个拍照类美化的 App,所以在之前的版本需求里,产品提了一个在预览页里加上 A/B 对比图的需求。这里的 A、B 指的是原图和添加了滤镜之后的图片,拍照进入预览页显示原图A,但是长按一小段时间之后显示加了滤镜的图片B。当时第一时间想到的就是使用 GestureDetector ,但是预览页的自定义 View 已经有了自己的事件拦截处理的逻辑,不想加上 GestureDetector 使代码更为复杂,所以便萌生了参考 GestureDetector 的想法。

概述

当我们的手指接触到屏幕的时候,会产生许多事件,例如 down,move,up,scroll,fling 等等,一般情况下, 如果我们需要捕获这些事件做一些简单的出来,只需要重写 View 的 onTouchEvent(event) 即可,但是如果是一些比较复杂的手势,比如单击,双击,长按,左右滑动,快速滑动,就需要我们去做一些额外的判断逻辑了,所以系统给我们提供了 GestureDetector ,以此简化我们的开发工作。接下来看一下系统提供的 GestureDetector .OnGestureListener 接口,以下接口方法中,返回 True 代码事件被消费了。源码分析基于android- 25!


  • onDown(MotionEvent e)

    当我们触碰到屏幕 Down 事件发生时,会立即回调此方法。

  • onShowPress(MotionEvent e)

    用户已经触发了 Down 事件,但是并没有触发 Move 或者 Up 事件。这方法通常是作为一种反馈,让用户知道他手指按下屏幕这一动作已经被识别了。

  • onSingleTapUp(MotionEvent e)

    当一个以 Up 事件结束的单击发生时,回调此方法。

  • onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)

    当一个滑动事件发生时回调此方法,其中 e1 代表滑动开始的第一个 Down 事件,e2 代表当前手指正在滑动的 Move 事件,distanceX 代表在 X 轴上前一次滑动和当次滑动的差值量,注意,distanceX 并不是 e1 和 e2 在 X 方向上的差值量。distanceY 同 distanceX 类似,只不过代表的是 Y 轴。

  • onLongPress(MotionEvent e)

    长按事件发生时回调此方法

  • onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)

    快速滑动发生时回调此方法,其中 e1 代表触发这个动作的 Down 事件,e2 代表这个动作结束手指抬起的 Up 事件, velocityX 代表在 X 方向上的滑动速度,velocityY 代表在 Y 方向上的滑动速度,数值单位均为 pixels /s ,即每秒划过多少个像素。

    在 GestureDetector 类中,我们还发现一个用于确认单击以及双击的接口 GestureDetector .OnDoubleTapListener


  • onSingleTapConfirmed(MotionEvent e)

    当确认一个单击事件发生时回调此方法,也就是说,手势判断器判断到这是一次单击事件,用户不会再次点击屏幕。

  • onDoubleTap(MotionEvent e)

    当双击事件发生时回调此方法

  • onDoubleTapEvent(MotionEvent e)

    双击事件发生过程中都会回调此方法,包括 Down 、Move 、Up 事件。

源码分析

1.构造方法

public GestureDetector(OnGestureListener listener) {
        this(null, listener, null);
    }
public GestureDetector(Context context, OnGestureListener listener) {
        this(context, listener, null);
    }
public GestureDetector(Context context, OnGestureListener listener, Handler handler) {
        if (handler != null) {
            //初始化Handler,用于处理延时消息
            mHandler = new GestureHandler(handler);
        } else {
            mHandler = new GestureHandler();
        }
        //设置回调监听器
        mListener = listener;
        if (listener instanceof OnDoubleTapListener) {
            setOnDoubleTapListener((OnDoubleTapListener) listener);
        }
        if (listener instanceof OnContextClickListener) {
            setContextClickListener((OnContextClickListener) listener);
        }
        init(context);
    }
private void init(Context context) {
        if (mListener == null) {
            throw new NullPointerException("OnGestureListener must not be null");
        }
        mIsLongpressEnabled = true;

        // Fallback to support pre-donuts releases
        int touchSlop, doubleTapSlop, doubleTapTouchSlop;
        if (context == null) {
            //noinspection deprecation
            touchSlop = ViewConfiguration.getTouchSlop();
            doubleTapTouchSlop = touchSlop; // Hack rather than adding a hiden method for this
            doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
            //noinspection deprecation
            mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
            mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity();
        } else {
            final ViewConfiguration configuration = ViewConfiguration.get(context);
            //滑动的时候,滑动数值大于这个值才认为滑动开始
            touchSlop = configuration.getScaledTouchSlop();
            doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop();
            //双击位置之间的最大距离,大于这个数值不认为是双击
            doubleTapSlop = configuration.getScaledDoubleTapSlop();
            //手指抛的最小速度
            mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
           //手指抛的最大速度
            mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
        }
        //计算平方,后面用到
        mTouchSlopSquare = touchSlop * touchSlop;
        mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
        mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
    }

从以上可以看出,构造 GestureDetector 的过程中,主要做的就是初始化一些变量,并且获取一些数值作为阈值。此外,还需要注意到 GestureHandler ,手势的

private class GestureHandler extends Handler {
      //...忽略构造函数
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case SHOW_PRESS:
                //回调 onShowPress 方法
                mListener.onShowPress(mCurrentDownEvent);
                break;
                
            case LONG_PRESS:
                //处理长按消息
                dispatchLongPress();
                break;
                
            case TAP:
                // If the user's finger is still down, do not count it as a tap
                if (mDoubleTapListener != null) {
                    if (!mStillDown) {
                        //此时,手指已抬起,回调确认单击的方法
                        mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
                    } else {
                      //未抬起,在手指抬起的时候回调确认单击的方法
                        mDeferConfirmSingleTap = true;
                    }
                }
                break;

            default:
                throw new RuntimeException("Unknown message " + msg); //never
            }
        }
    }

2. 处理手势信息

接下来也就到了 GestureDetector 的核心部分,也就是如何处理手势信息,并判断是具体的哪种手势

public boolean onTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 0);
        }

        final int action = ev.getAction();

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        //初始化速度追踪器,并计算速度
        mVelocityTracker.addMovement(ev);
        //下面需要得出 X 、Y 方向上的中心焦点,如果是多点触碰,需要计算平均值得出中心焦点
        // 这里判断这个 action 是不是有非主要的手指抬起来了,如果是的话,记录它的index,下面计算的时候
        //忽略这个触点
        final boolean pointerUp =
                (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;
        final int skipIndex = pointerUp ? ev.getActionIndex() : -1;

        // Determine focal point
        float sumX = 0, sumY = 0;
        final int count = ev.getPointerCount();
        for (int i = 0; i < count; i++) {
            if (skipIndex == i) continue;
            sumX += ev.getX(i);
            sumY += ev.getY(i);
        }
      //pointerUp 为 true,需要排除掉
        final int div = pointerUp ? count - 1 : count;
        final float focusX = sumX / div;
        final float focusY = sumY / div;

        boolean handled = false;
      //根据不同的 action,做不同的处理
        switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_POINTER_DOWN:
            //...
            break;

        case MotionEvent.ACTION_POINTER_UP:
            //...
            break;

        case MotionEvent.ACTION_DOWN:
            //...
            break;

        case MotionEvent.ACTION_MOVE:
            //...
            break;

        case MotionEvent.ACTION_UP:
            //...
            break;

        case MotionEvent.ACTION_CANCEL:
          //...
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0);
        }
        return handled;
    }

从以上可以看出,onTouchEvent() 方法主要统一对不同的输入事件作统一的处理。

2.1 Down 事件处理

    case MotionEvent.ACTION_DOWN:
            if (mDoubleTapListener != null) {
                //如果设置了双击监听器,进入此结构体
                boolean hadTapMessage = mHandler.hasMessages(TAP);
                //第一次点击的时候发送了TAG类型的消息,取消掉,防止出错
                if (hadTapMessage) mHandler.removeMessages(TAP);
                if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage &&
                        isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
                    // This is a second tap
                    //判断到这是第二次点击,设置变量值,回调相关方法
                    mIsDoubleTapping = true;
                    // Give a callback with the first tap of the double-tap
                    handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
                    // Give a callback with down event of the double-tap
                    handled |= mDoubleTapListener.onDoubleTapEvent(ev);
                } else {
                    // This is a first tap
                    // 这是一个第一次的单击,先发送延时消息,如果在这段时间之内没有再次点击,GestureHandler 里就会处理 这个 TAG 类型的消息
                    mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
                }
            }
          //设置相关变量
            mDownFocusX = mLastFocusX = focusX;
            mDownFocusY = mLastFocusY = focusY;
            if (mCurrentDownEvent != null) {
                mCurrentDownEvent.recycle();
            }
            mCurrentDownEvent = MotionEvent.obtain(ev);
            mAlwaysInTapRegion = true;
            mAlwaysInBiggerTapRegion = true;
            mStillDown = true;
            mInLongPress = false;
            mDeferConfirmSingleTap = false;

            if (mIsLongpressEnabled) {
              //允许长按,发送延时的长按消息,在  TAP_TIMEOUT + LONGPRESS_TIMEOUT 这段时间之内,
              //如果没有其他手势操作(移动手指、抬起手指),会认为是长按手势,并且回调LongPress 方法。
                mHandler.removeMessages(LONG_PRESS);
                mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
                        + TAP_TIMEOUT + LONGPRESS_TIMEOUT);
            }
            //一小段时间之后,回调 onShowPress(MotionEvent e) 方法,反馈给用户
            mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
            handled |= mListener.onDown(ev);
            break;
    //判断第二次点击是否有效
     private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
            MotionEvent secondDown) {
        //第一次点击后,超过了限定范围,认为无效
        if (!mAlwaysInBiggerTapRegion) {
            return false;
        }

        final long deltaTime = secondDown.getEventTime() - firstUp.getEventTime();
        //第一次点击的手指抬起和第二次点击的手指落下,中间时间间隔不符合要求,无效
        if (deltaTime > DOUBLE_TAP_TIMEOUT || deltaTime < DOUBLE_TAP_MIN_TIME) {
            return false;
        }

        int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
        int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
        //范围没超过mDoubleTapSlopSquare ,认为有效
        return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
    }

Down 事件总结:

  • 单击判断:如果接受到一次单击事件,会先发送 TAG 类型的延迟消息,在这段时间内,如果消息没被取消,则会进入 GestureHandler 的 TAG 确认操作。
  • 双击判断:已经有了第一次的单击事件,这时候需要判断第二次点击和第一次单击的距离、时间,条件符合则认为是双击,回调相关方法。
  • 长按判断:点击的时候就发送延迟的长按消息,要是这段时间内没被取消,就回调长按的方法。

2.2 Move 事件处理

case MotionEvent.ACTION_MOVE:
          //已经触发长按操作,一系列事件可以认为是长按,接下来不做其他处理
            if (mInLongPress || mInContextClick) {
                break;
            }
            final float scrollX = mLastFocusX - focusX;
            final float scrollY = mLastFocusY - focusY;
            if (mIsDoubleTapping) {
                // Give the move events of the double-tap
                //上面说过,双击过程中 onDoubleTapEvent(ev) 方法会被回调,这个过程还没结束
                //回调这个方法
                handled |= mDoubleTapListener.onDoubleTapEvent(ev);
            } else if (mAlwaysInTapRegion) {
                //第一次 Down 事件里,mAlwaysInTapRegion 为 true
                final int deltaX = (int) (focusX - mDownFocusX);
                final int deltaY = (int) (focusY - mDownFocusY);
                int distance = (deltaX * deltaX) + (deltaY * deltaY);
                if (distance > mTouchSlopSquare) {
                  // 距离超过一个数值,可以认为是滑动,进入onScroll 模式,回调相关函数
                    handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                    mLastFocusX = focusX;
                    mLastFocusY = focusY;
                    mAlwaysInTapRegion = false;
                  //移除之前发送的延迟消息
                    mHandler.removeMessages(TAP);
                    mHandler.removeMessages(SHOW_PRESS);
                    mHandler.removeMessages(LONG_PRESS);
                }
                if (distance > mDoubleTapTouchSlopSquare) {
                    mAlwaysInBiggerTapRegion = false;
                }
            } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
                //最后的条件判断,继续 onScroll 回调
                handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                mLastFocusX = focusX;
                mLastFocusY = focusY;
            }
            break;

Move 事件总结:

  • 如果已经触发了长按,并且回调了长按的相关函数,就会认为接下来的 Move 事件都会直接返回。
  • 如果是双击,因为双击过程还未结束,所以 Move 事件会一直回调 onDoubleTapEvent(ev) 方法。
  • 如果滑动的距离超过一定的数值平方,会进入 onScroll 模式,接下来的滑动,只要超过 1px,就继续回调 onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) 方法。

2.3 Up 事件处理

case MotionEvent.ACTION_UP:
            mStillDown = false;
            MotionEvent currentUpEvent = MotionEvent.obtain(ev);
            if (mIsDoubleTapping) {
                // Finally, give the up event of the double-tap
                //是双击的过程,回调onDoubleTapEvent(ev) 方法
                handled |= mDoubleTapListener.onDoubleTapEvent(ev);
            } else if (mInLongPress) {
              //长按的过程,移除可能存在的 TAP 消息 
                mHandler.removeMessages(TAP);
                mInLongPress = false;
            } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
              //如果点击之后没有移动,或者移动的距离没有超过一定的数值,可以认为只是单击而已
                handled = mListener.onSingleTapUp(ev);
                if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
              //GestureHandler 里 mDeferConfirmSingleTap 被设置为 true,这里在手指抬起时进行回调
                    mDoubleTapListener.onSingleTapConfirmed(ev);
                }
            } else if (!mIgnoreNextUpEvent) {
                //进入这个结构体的时候,说明前面没有触发双击或者长按操作,并且滑动的距离也超过了最小值,              
                //mAlwaysInTapRegion 被设置为 false
                // A fling must travel the minimum tap distance
                final VelocityTracker velocityTracker = mVelocityTracker;
                final int pointerId = ev.getPointerId(0);
                velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
                final float velocityY = velocityTracker.getYVelocity(pointerId);
                final float velocityX = velocityTracker.getXVelocity(pointerId);

                if ((Math.abs(velocityY) > mMinimumFlingVelocity)
                        || (Math.abs(velocityX) > mMinimumFlingVelocity)){
                  //速度超过了最小值,可以认为是一个抛的动作
                    handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
                }
            }
            //回收,方便下面使用
            if (mPreviousUpEvent != null) {
                mPreviousUpEvent.recycle();
            }
            // Hold the event we obtained above - listeners may have changed the original.
            mPreviousUpEvent = currentUpEvent;
            if (mVelocityTracker != null) {
                // This may have been cleared when we called out to the
                // application above.
                mVelocityTracker.recycle();
                mVelocityTracker = null;
            }
            mIsDoubleTapping = false;
            mDeferConfirmSingleTap = false;
            mIgnoreNextUpEvent = false;
            mHandler.removeMessages(SHOW_PRESS);
            mHandler.removeMessages(LONG_PRESS);
            break;

Up 事件总结:

  • 判断是双击的过程,则继续回调 onDoubleTapEvent(ev) 方法,结束这个过程.
  • 如果是长按的,也结束这个过程。
  • 说明前面没有触发双击或者长按操作,并且滑动的距离也超过了最小值,最后手指抬起时,速度超过最小值,可以认为是抛的动作,回调 onFling() 方法。
  • 如果点击之后没有移动,或者移动的距离没有超过一定的数值,认为只是简单的点击,结束这个过程。之前 点击的时候 发生了延迟的 TAG消息,但是需要手指抬起才能回调 onSingleTapConfirmed() 方法,如果处理延迟消息的时候,手指还未抬起,则先设置变量标记(处理逻辑如下图),等手指抬起再回调。
case TAP:
                // If the user's finger is still down, do not count it as a tap
                if (mDoubleTapListener != null) {
                    if (!mStillDown) {
                        //此时,手指已抬起,回调确认单击的方法
                        mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
                    } else {
                      //未抬起,在手指抬起的时候回调确认单击的方法
                        mDeferConfirmSingleTap = true;
                    }
                }

2.4 多点触控处理

case MotionEvent.ACTION_POINTER_DOWN:
            mDownFocusX = mLastFocusX = focusX;
            mDownFocusY = mLastFocusY = focusY;
            // Cancel long press and taps
            //有其他手指落下,移除所有消息,重置标记变量
            cancelTaps();
            break;

        case MotionEvent.ACTION_POINTER_UP:
            mDownFocusX = mLastFocusX = focusX;
            mDownFocusY = mLastFocusY = focusY;

            // Check the dot product of current velocities.
            // If the pointer that left was opposing another velocity vector, clear.
            mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
            final int upIndex = ev.getActionIndex();
            final int id1 = ev.getPointerId(upIndex);
            final float x1 = mVelocityTracker.getXVelocity(id1);
            final float y1 = mVelocityTracker.getYVelocity(id1);
            for (int i = 0; i < count; i++) {
                if (i == upIndex) continue;

                final int id2 = ev.getPointerId(i);
                final float x = x1 * mVelocityTracker.getXVelocity(id2);
                final float y = y1 * mVelocityTracker.getYVelocity(id2);
                //如果剩下的手指速度方向是和抬起那根手指的速度相反方向的,就说明不是fling,清空速度监听
                final float dot = x + y;
                if (dot < 0) {
                    mVelocityTracker.clear();
                    break;
                }
            }
            break;
private void cancelTaps() {
        mHandler.removeMessages(SHOW_PRESS);
        mHandler.removeMessages(LONG_PRESS);
        mHandler.removeMessages(TAP);
        mIsDoubleTapping = false;
        mAlwaysInTapRegion = false;
        mAlwaysInBiggerTapRegion = false;
        mDeferConfirmSingleTap = false;
        mInLongPress = false;
        mInContextClick = false;
        mIgnoreNextUpEvent = false;
    }

在 onTouchEvent(MotionEvent ev) 方法的前面,我们对于多点触控的处理都是取平均值的,对多个手指的UP和DOWN事件处理,其实就是做一些取消操作而让多点触摸不影响单点触摸的应用,例如在多个手指落下的时候取消点击信息等。

后记

第一次分析源码,如果有错误或者需要补充的,欢迎留言讨论。

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

推荐阅读更多精彩内容