ViewDragHelper 的基本使用(一)

ViewDragHelper 的使用和分析

使用方法

一个简单的例子

假设要实现一个可以对内部的 view 进行自由拖拽的 ViewGroup,效果如图:

图.1 可随手拖拽的view

可以重写 onTouchEvent(MotionEvent event) 方法,对 MotionEvent 进行判断和处理,从而实现拖拽的效果。但是使用 ViewDragHelper 可以很方便的实现。只要写很少的代码,如下:

public class DragLayout extends FrameLayout {

    private static final String TAG = "DragLayout";

    private ViewDragHelper mDragHelper;

    public DragLayout(@NonNull Context context) {
        super(context);
        init();
    }

    public DragLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public DragLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                Log.d(TAG, "tryCaptureView, left="+child.getLeft()+"; top="+child.getTop());
                return true;
            }

            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx) {
                Log.d(TAG, "left=" + left + "; dx=" + dx);
                return left;
            }

            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {
                Log.d(TAG, "top=" + top + "; dy=" + dy);
                return top;
            }
        };
        mDragHelper = ViewDragHelper.create(this, callback);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mDragHelper.processTouchEvent(event);
        return true;
    }
}

然后再在 xml 中写布局文件,如下:

<?xml version="1.0" encoding="utf-8"?>
<com.viewdraghelperlearn.DragLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_gravity="center"
        android:layout_margin="10dp"
        android:background="@color/colorAccent"
        android:gravity="center" />

    <TextView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_gravity="center"
        android:layout_margin="10dp"
        android:background="@color/colorPrimaryDark"
        android:gravity="center" />

    <TextView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_gravity="center"
        android:layout_margin="10dp"
        android:background="@color/colorPrimary"
        android:gravity="center" />

</com.viewdraghelperlearn.DragLayout>

即可实现图中的效果。该ViewGroup中的任何子view都有随手指头拖拽效果。

在上面的 DragLayout 中,基本上只做了三件事:

  1. 创建 ViewDragHelper 的实例;
  2. onInterceptTouchEvent(MotionEvent ev) 传递给 ViewDragHelper 的 shouldInterceptTouchEvent(ev)
  3. onTouchEvent(MotionEvent event) 传递给 ViewDragHelper 的 processTouchEvent(event);

先说一下第一条,如何创建一 个ViewDragHelper 的实例。

创建 ViewdragHelper

ViewDragHelper 提供了两个 create() 方法来创建实例,分别传入两个和三个参数:

/**
 *工厂方法创建新的 ViewDragHelper 的实例.
 *
 * @param forParent 与 ViewDragHelper 相关联的父 ViewGroup
 * @param 滑动和拖拽的事件的回调
 * @return 新的 ViewDragHelper 的实例
 */
public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
    return new ViewDragHelper(forParent.getContext(), forParent, cb);
}

/**
 * 工厂方法创建新的 ViewDragHelper 的实例.
 *
 * @param 与 ViewDragHelper 相关联的父 ViewGroup
 * @param 灵敏度,越大越灵敏,1.0f是正常值
 * @param 滑动和拖拽的事件的回调
 * @return 新的 ViewDragHelper 的实例
 */
public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
    final ViewDragHelper helper = create(forParent, cb);
    helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
    return helper;
}

第二个方法比第一个方法多了一个灵敏度的参数。
先使用第一个方法创建实例,需要传入两个参数。

第一个参数是 ViewGroup,拖拽事件就是发生在这个 ViewGroup 里面的子 View 上。

第二个参数是一个 callback;这个回调用来指示拖拽时的各种状态和事件的变化,回调中的方法有很多,一共13个。先看几个相对常用和重要的,其余的放到后面再看。

  1. public abstract boolean tryCaptureView(View child, int pointerId);
    这是唯一的一个抽象方法,需要自己实现的。返回值表示是否捕捉这个 view 的拖拽事件。这个方法会调用多次,哪怕这个 view 已经被捕捉过了,在下一次开始拖拽的时候,还是会回调这个方法。如果只想对 ViewGroup 内的特定的 view 进行拖拽的处理,只需要返回类似于 child == mDragView 这样的形式就行了。

  2. public int clampViewPositionHorizontal(View child, int left, int dx); 这个方法约束了 View 在水平方向上的运动。该方法默认是返回0的,所以一般都是需要重写的。这个方法有三个参数:第一个 View 自然就是拖动的 View;第二个参数 left,指的是拖动的 View 理论上将要滑动到的水平方向上的值;第三个参数 dx 可以理解为滑动的速度,单位是 px 每秒。返回值是水平方向上的实际的x坐标的值。上面的DragLayout 中直接返回了 left,就是说需要滑动到哪里,child 这个 View 就 滑动到哪里。

  3. clampViewPositionVertical(View child, int top, int dy) 这个方法和 public int clampViewPositionHorizontal(View child, int left, int dx); 是一样的,只不过约束的是View 在竖直方向上的运动。

可以看到图1中的方块是可以拖拽并滑动到屏幕边缘并且超出屏幕边缘的。假设需要让图中的方形块不滑动超出屏幕的边缘,就需要在 clampViewPositionHorizontal 中动手脚。

以下不超出屏幕边缘的实现代码参考了Android ViewDragHelper完全解析 自定义ViewGroup神器Each Navigation Drawer Hides a ViewDragHelper 这两篇文章。

不超出屏幕边缘,意味着方块的 x 坐标>=paddingleft,方块的 x 坐标<=ViewGroup.getWidth()-paddingright-child.getWidth;
于是 clampViewPositionHorizontal 写成:

@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
    Log.d(TAG, "left=" + left + "; dx=" + dx);
    // 最小 x 坐标值不能小于 leftBound
    final int leftBound = getPaddingLeft();
    // 最大 x 坐标值不能大于 rightBound
    final int rightBound = getWidth() - child.getWidth() - getPaddingRight();
    final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
    return newLeft;
}

同样,clampViewPositionVertical(View child, int top, int dy) 应该写成:

@Override
public int clampViewPositionVertical(View child, int top, int dy) {
    Log.d(TAG, "top=" + top + "; dy=" + dy);
    // 最小 y 坐标值不能小于 topBound
    final int topBound = getPaddingTop();
    // 最大 y 坐标值不能大于 bottomBound
    final int bottomBound = getHeight() - child.getHeight() - getPaddingBottom();
    final int newTop = Math.min(Math.max(top, topBound), bottomBound);
    return newTop;
}

效果如图2所示,可以看到无法拖动到超出屏幕边缘,因为就算 left 或者 top 的值已经是负数的时候,就返回的是 leftBound 和 topBound;当 left 或者 top 的值已经是大于屏幕宽度或者高度的时候,就返回的是 rightBound 和 bottomBound。

图.2 方块不能滑动超过屏幕边缘

ViewDragHelper.Callback 中的方法的使用

ViewDragHelper.Callback 里面一共有 13 个方法。在上面只说了 3 个,下面说一下其他的方法。

1. onViewReleased(View releasedChild, float xvel, float yvel)

这个方法在 View 释放的时候调用,就是说这个 View 已经不再被拖拽的时候调用。View 已经不再被拖拽的时候,该 View 可能并没有停止滑动,xvel 和 yvel 表示的是此时该 View 在水平和竖直方向上的速度,单位是px/s。

在使用微信语音通话的时候,可以看到一个方形的悬浮框,这个悬浮框在可以拖动,并且当你放手的时候,这个悬浮框就会自动跑到屏幕边缘。当放手时候悬浮框的位置靠近左边的时候就自动跑到左边缘,当放手时候悬浮框的位置靠近右边的时候就自动跑到右边缘。

图.4 微信语音悬浮框自动贴边

这个效果使用 ViewDragHelper 也可以很好的实现。也只需要几行代码,主要也就是在 onViewReleased 里面进行操作。
先看一下效果,如图4所示:

图.4 松手时方块自动滑到屏幕边缘

代码如下:

private int mCurrentTop;
private int mCurrentLeft;

ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
    @Override
    public boolean tryCaptureView(View child, int pointerId) {
          mDragOriLeft = child.getLeft();
          mDragOriTop = child.getTop();
        Log.d(TAG, "tryCaptureView, left=" + child.getLeft() + "; top=" + child.getTop());
        return true;
    }
    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        Log.d(TAG, "left=" + left + "; dx=" + dx);
        // 最小 x 坐标值不能小于 leftBound
        final int leftBound = getPaddingLeft();
        // 最大 x 坐标值不能大于 rightBound
        final int rightBound = getWidth() - child.getWidth() - getPaddingRight();
        final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
        mCurrentLeft = newLeft;
        return newLeft;
    }
    @Override
    public int clampViewPositionVertical(View child, int top, int dy) {
        Log.d(TAG, "top=" + top + "; dy=" + dy);
        // 最小 y 坐标值不能小于 topBound
        final int topBound = getPaddingTop();
        // 最大 y 坐标值不能大于 bottomBound
        final int bottomBound = getHeight() - child.getHeight() - getPaddingBottom();
        final int newTop = Math.min(Math.max(top, topBound), bottomBound);
        mCurrentTop = newTop;
        return newTop;
    }
    @Override
    public void onViewReleased(View releasedChild, float xvel, float yvel) {
        super.onViewReleased(releasedChild, xvel, yvel);
        Log.d(TAG, "onViewReleased, xvel=" + xvel + "; yvel=" + yvel);
        int childWidth = releasedChild.getWidth();
        int parentWidth = getWidth();
        int leftBound = getPaddingLeft();// 左边缘
        int rightBound = getWidth() - releasedChild.getWidth() - getPaddingRight();// 右边缘
        // 方块的中点超过 ViewGroup 的中点时,滑动到左边缘,否则滑动到右边缘
        if ((childWidth / 2 + mCurrentLeft) < parentWidth / 2) {
            mDragHelper.settleCapturedViewAt(leftBound, mCurrentTop);
        } else {
            mDragHelper.settleCapturedViewAt(rightBound, mCurrentTop);
        }
        invalidate();
    }
};
mDragHelper = ViewDragHelper.create(this, callback);

增加了两个参数,分别是 mCurrentTop 和 mCurrentleft ,指代了当前拖拽的 View 的当前的水平和竖直方向的位置,分别在 clampViewPositionHorizontalclampViewPositionVertical 里面对其赋值;然后在 onViewReleased 中,判断松手时候的方块的位置,方块的中点超过 ViewGroup 的中点时,滑动到左边缘,否则滑动到右边缘。通过 ViewDragHelper 的 settleCapturedViewAt 方法来将方块 View 设定到某个位置。

这里需要注意的是,仅仅调用 settleCapturedViewAt 是不能达到目的的,还需要重写一下 ViewGroup 的 computeScroll 方法。

@Override
public void computeScroll() {
    super.computeScroll();
    if (mDragHelper != null && mDragHelper.continueSettling(true)) {
        invalidate();
    }
}
2. onEdgeTouched(int edgeFlags, int pointerId)onEdgeDragStarted(int edgeFlags, int pointerId)onEdgeLock(int edgeFlags)

这三个方法都与边缘相关,常见的侧滑菜单和滑动返回都可以利用这几个方法实现。android有一个下拉菜单,就是从屏幕状态栏上方往下拉,可以拉出一个菜单。这里利用 ViewDragHelper 的边缘检测的几个方法来实现一个从屏幕下方网上拉而拉出菜单的例子。效果如下:

底部上拉菜单

代码也很短,只有100行:

public class BottomMenuLayout extends LinearLayout {

    private static final String TAG = "BottomMenuLayout";

    private ViewDragHelper mDragHelper;
    private View mContent;
    private View mBottomMenu;

    public BottomMenuLayout(Context context) {
        super(context, null);
        init();
    }

    public BottomMenuLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs, 0);
        init();
    }

    public BottomMenuLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        setOrientation(VERTICAL);
        ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                return child == mBottomMenu;
            }

            @Override
            public void onEdgeTouched(int edgeFlags, int pointerId) {
                super.onEdgeTouched(edgeFlags, pointerId);
                Log.d(TAG, "onEdgeTouched");
            }

            @Override
            public boolean onEdgeLock(int edgeFlags) {
                Log.d(TAG, "onEdgeLock");
                return super.onEdgeLock(edgeFlags);
            }

            @Override
            public void onEdgeDragStarted(int edgeFlags, int pointerId) {
                Log.d(TAG, "onEdgeDragStarted");
                mDragHelper.captureChildView(mBottomMenu, pointerId);
            }


            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {
                return Math.max(getHeight() - child.getHeight(), top);
            }


            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
                if (yvel <= 0) {
                    mDragHelper.settleCapturedViewAt(0,
                            getHeight() - releasedChild.getHeight());
                } else {
                    mDragHelper.settleCapturedViewAt(0, getHeight());
                }
                invalidate();
            }

        };
        mDragHelper = ViewDragHelper.create(this, callback);
        // 触发边缘为下边缘
        mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_BOTTOM);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        // 假设第一个子 view 是内容区域,第二个是菜单
        mContent = getChildAt(0);
        mBottomMenu = getChildAt(1);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mDragHelper != null && mDragHelper.continueSettling(true)) {
            invalidate();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mDragHelper.processTouchEvent(event);
        return true;
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mBottomMenu != null && mContent != null) {
            mBottomMenu.layout(0, getHeight(), mBottomMenu.getMeasuredWidth(),
                    getHeight() + mBottomMenu.getMeasuredHeight());
            mContent.layout(0, 0, mContent.getMeasuredWidth(), mContent.getMeasuredHeight());
        }
    }
}

布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<com.testcollection.viewdrag.BottomMenuLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.enhao.testcollection.views.viewdrag.BottomMenuActivity">

    <TextView
        android:id="@+id/content_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white"
        android:gravity="center"
        android:text="内容区域"/>

    <TextView
        android:id="@+id/menu_view"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:background="@color/colorAccent"
        android:gravity="center"
        android:alpha="0.4"
        android:textColor="@android:color/black"
        android:text="底部菜单区域"/>

</com.testcollection.viewdrag.BottomMenuLayout>

tryCaptureView中,捕捉到的是底部菜单的 View,内容区域的 View 不需要捕捉:

@Override
public boolean tryCaptureView(View child, int pointerId) {
    return child == mBottomMenu;
}

onEdgeDragStarted中,手动捕获底部菜单的 View,调用 ViewDragHelper 的 captureChildView 方法。onEdgeDragStarted 表示用户开始从边缘拖拽。而 onEdgeTouched 表示开始触摸到 ViewGroup 的边缘,此时并不一定开始有拖拽的动作。

@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
    Log.d(TAG, "onEdgeDragStarted");
    mDragHelper.captureChildView(mBottomMenu, pointerId);
}

此处在 tryCaptureViewonEdgeDragStarted 中都捕获了底部菜单的 mBottomMenu,是不是重复了?答案不是的,这两个地方都要捕获。可以试验一下,假设 tryCaptureView 中直接返回 false,当然这个 mBottomMenu 还是能从底部边缘滑出来,但是当滑出来之后,就不能再滑动回去了,因为滑出来之后再往下滑动,就不是执行 onEdgeDragStarted 而是执行 tryCaptureView 了,所以 tryCaptureView 要也要捕获到 BottomMenu,即返回 child == mBottomMenu 才行。

clampViewPositionVertical 中,返回竖直方向上要到达的位置。
onViewReleased 中,判断y方向的速速,如果<=0,即往上滑,就把菜单完全展现出来,如果往下滑动,就把菜单隐藏。利用 mDragHelper.settleCapturedViewAt 来设置菜单的位置。

@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
    if (yvel <= 0) {
        mDragHelper.settleCapturedViewAt(0,
                getHeight() - releasedChild.getHeight());
    } else {
        mDragHelper.settleCapturedViewAt(0, getHeight());
    }
    invalidate();

通过 mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_BOTTOM); 来设置要监测的边缘拖拽。

还有一个方法 onEdgeLock(int edgeFlags) 没有使用到,这个方法返回 true 会锁住当前的边界。

3. getViewHorizontalDragRange(View child)getViewVerticalDragRange(View child)

这两个方法分别返回子 View 在水平和竖直方向可以被拖拽的范围,返回值的单位是 px。
假设在前面的方块(即TextView) 设置 android:clickable="true",则再运行程序,会发现方块拖不动了,为什么呢?因为触摸事件被 TextView 消耗掉了。

这篇文章(Android自定义ViewGroup神器-ViewDragHelper)解释的很清楚:

子View是可被点击的,那么会触发ViewGroup的onInterceptTouchEvent方法。默认情况下,事件会被子View消耗掉,这显然是有问题的,因为这样ViewGroup的onTouch方法就不会被调用,而onTouch方法中正是我们的关键方法:dragHelper.processTouchEvent。

在 ViewDragHelper 的 shouldInterceptTouchEvent 的源码中

public boolean shouldInterceptTouchEvent(MotionEvent ev) {
    final int action = MotionEventCompat.getActionMasked(ev);
    switch (action) {
        case MotionEvent.ACTION_MOVE: {          
            final int pointerCount = ev.getPointerCount();
            for (int i = 0; i < pointerCount; i++) {           
                final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
                            toCapture);
                final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
                // 如果getViewHorizontalDragRange和getViewVerticalDragRange的返回值都为0,则break
                if (horizontalDragRange == 0 && verticalDragRange == 0) {
                    break;
                }
                
                // tryCaptureViewForDrag方法中会设置mDragState=STATE_DRAGGING
                if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
                    break;
                }
            }
            break;
        }
    }
    return mDragState == STATE_DRAGGING;
}

shouldInterceptTouchEvent 返回true的条件是 mDragState == STATE_DRAGGING,然而 mDragState 是在 tryCaptureViewForDrag 方法中被设置为STATE_DRAGGING的。

所以,如果horizontalDragRange == 0 && verticalDragRange == 0 这个条件一直为true的话,tryCaptureViewForDrag 方法就得不到调用了。

horizontalDragRangeverticalDragRange 分别是 Callback 的 getViewHorizontalDragRangegetViewVerticalDragRange 方法返回的值,这两个方法默认情况下都返回 0。

重写这两个方法:

@Override
public int getViewHorizontalDragRange(View child) {
    Log.d(TAG, "getViewHorizontalDragRange");
    return getMeasuredWidth() - child.getMeasuredWidth();
}
@Override
public int getViewVerticalDragRange(View child) {
    Log.d(TAG, "getViewVerticalDragRange");
    return getMeasuredHeight() - child.getMeasuredHeight();
}

方块(即TextView) 就能拖拽并且能响应点击事件了。

参考链接:

  1. Android自定义ViewGroup神器-ViewDragHelper

  2. Android ViewDragHelper完全解析 自定义ViewGroup神器

  3. Each Navigation Drawer Hides a ViewDragHelper

  4. 神奇的 ViewDragHelper,让你轻松定制拥有拖拽能力的 ViewGroup

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

推荐阅读更多精彩内容