RecyclerView<第四十二篇>:ItemTouchHelper详解

ItemTouchHelper是RecyclerView高级篇的重要知识之一,它可以实现Item的拖拽以及侧滑删除。

先来看一下效果,如下:

365.gif

在了解效果的前提下,我将一步步剖析其实现原理。

(1)基本使用

我们首先需要了解,ItemTouchHelper如何使用,看以下代码

    //声明一个Callback
    ItemTouchHelper.Callback callback = new ItemTouchHelper.Callback() {
        @Override
        public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
            //返回方向值
            return 0;
        }

        @Override
        public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
            //拖拽处理
            return false;
        }

        @Override
        public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
            //滑动处理
        }
    };
    //创建helper对象
    ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
    //关联recyclerView
    itemTouchHelper.attachToRecyclerView(recyclerview);

或者

    //声明一个Callback
    ItemTouchHelper.Callback callback = new ItemTouchHelper.SimpleCallback(dragDirs, swipeDirs) {

        @Override
        public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
            //拖拽处理
            return false;
        }

        @Override
        public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
            //滑动处理
        }
    };
    //创建helper对象
    ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
    //关联recyclerView
    itemTouchHelper.attachToRecyclerView(recyclerview);

attachToRecyclerView方法理解起来比较简单,就是将RecyclerView与ItemTouchHelper进行关联,唯一不清楚的是,Callback中的三个方法没有任何的业务逻辑,我们必须在这三个方法中处理这三个方法的业务逻辑,如果不处理,那么ItemTouchHelper则无任何效果。

在实现Callback内部三个方法业务逻辑之前,我们需要了解这三个方法分别代表的意义:

  • getMovementFlags:移动标志;
  • onMove:拖拽的处理
  • onSwiped:滑动的处理
(2)getMovementFlags: 获取移动状态

ItemTouchHelper帮助类自带创建移动标志的方法,如下代码:

    /**
     * Convenience method to create movement flags.
     * <p>
     * For instance, if you want to let your items be drag & dropped vertically and swiped
     * left to be dismissed, you can call this method with:
     * <code>makeMovementFlags(UP | DOWN, LEFT);</code>
     *
     * @param dragFlags  The directions in which the item can be dragged.
     * @param swipeFlags The directions in which the item can be swiped.
     * @return Returns an integer composed of the given drag and swipe flags.
     */
    public static int makeMovementFlags(int dragFlags, int swipeFlags) {
        return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags)
                | makeFlag(ACTION_STATE_SWIPE, swipeFlags)
                | makeFlag(ACTION_STATE_DRAG, dragFlags);
    }

dragFlags表示拖拽方向标志,swipeFlags表示滑动方向标志,这个标志可以理解为一种状态,那么这两个值传递怎样的参数呢?

    //控制拖拽的方向(一般是上下左右)
    int dragFlags= ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
    //控制快速滑动的方向(一般是左右)
    int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;

我们看一下以上代码,假设:

dragFlags被赋予上下左右四个方向状态,所以拖拽方向状态为:上、下、左、右
swipeFlags 被赋予左右两个方向状态,所以滑动方向状态为:左、右

将拖拽方向状态dragFlags以及滑动方向状态swipeFlags传递到makeMovementFlags方法中,接着看一下makeFlag这个方法:

    /**
     * Shifts the given direction flags to the offset of the given action state.
     *
     * @param actionState The action state you want to get flags in. Should be one of
     *                    {@link #ACTION_STATE_IDLE}, {@link #ACTION_STATE_SWIPE} or
     *                    {@link #ACTION_STATE_DRAG}.
     * @param directions  The direction flags. Can be composed from {@link #UP}, {@link #DOWN},
     *                    {@link #RIGHT}, {@link #LEFT} {@link #START} and {@link #END}.
     * @return And integer that represents the given directions in the provided actionState.
     */
    @SuppressWarnings("WeakerAccess")
    public static int makeFlag(int actionState, int directions) {
        return directions << (actionState * DIRECTION_FLAG_COUNT);
    }

这个方法最后的返回值为三种状态:无操作状态、拖拽状态、滑动状态(注意和拖拽方向状态、滑动方向状态区别开),最终makeMovementFlags的返回值为:

    return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags)
            | makeFlag(ACTION_STATE_SWIPE, swipeFlags)
            | makeFlag(ACTION_STATE_DRAG, dragFlags);

将无操作状态、滑动状态、拖拽状态合并为移动状态

以上所述的状态优点多,您可能会被绕晕,所以专门为大家绘制了一个思维导图:

图片.png

如图所示,起初通过上下左右这四个方向状态,一步一步的转变为移动状态。

如果您使用SimpleCallback内部类,在内部有一个方法如下:

    @Override
    public int getMovementFlags(@NonNull RecyclerView recyclerView,
            @NonNull ViewHolder viewHolder) {
        return makeMovementFlags(getDragDirs(recyclerView, viewHolder),
                getSwipeDirs(recyclerView, viewHolder));
    }

这个方法表明,使用SimpleCallback内部类不需要重写getMovementFlags方法,只需要传递dragDirs, swipeDirs这两个参数即可,其实和Callback内部类差不多,如果使用Callback内部类,那么getMovementFlags完善后的代码如下:

    //声明一个Callback
    ItemTouchHelper.Callback callback = new ItemTouchHelper.Callback() {

        @Override
        public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
            //控制拖拽的方向(一般是上下左右)
            int dragFlags= ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
            //控制快速滑动的方向(一般是左右)
            int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
            return makeMovementFlags(dragFlags, swipeFlags);//计算movement flag值
        }

        @Override
        public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
            //拖拽处理
            return false;
        }

        @Override
        public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
            //滑动处理
        }
    };

就这样,getMovementFlags方法已经实现完成,代码的意思是:在拖拽时,允许Item上、下、左、右四个方向拖拽,在滑动时,允许Item左、右两个方向滑动。

当然,如果您只想让Item向左滑动,那么将以下代码

        int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;

改成

       int swipeFlags = ItemTouchHelper.LEFT;

或者

            //控制快速滑动的方向(一般是左右)
            int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
            //排除右方向
            swipeFlags &=~ItemTouchHelper.RIGHT;

即可。

有关添加状态移除状态相关算法我补充一下吧,虽然不是文章的重点(其实,我怕您看不懂)

  • 使用按位或添加状态,如:
 flag |= ItemTouchHelper.RIGHT
  • 使用按位与和按位非移除状态,如:
flag &= ~ItemTouchHelper.RIGHT
(3)onMove: 拖拽处理

方法如下:

        @Override
        public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
            //拖拽处理

            return true;
        }

这个方法是有返回值的,true:允许拖拽 false:禁止拖拽

假设,有一个需求,将Item拖拽到某个位置,并和当前位置的Item交换位置,这个该怎么做呢?

这个需求的处理,可以在onMove方法中处理,因为它的三个参数recyclerView、viewHolder、target为这个需求提供了一切前提,viewHolder是被拖拽的Item,target是目标Item,只需要将viewHolder和target交换位置就可以实现这个需求,代码实现也比较简单,如下:

        @Override
        public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
            // 拖拽时,每移动一个位置就会调用一次。

            if (recyclerView == null){
                return false;
            }

            RecyclerView.Adapter adapter = recyclerView.getAdapter();
            if (adapter == null){
                return false;
            }

            if (list != null && list.size() > 0) {
                //获取被拖拽的Item的Position
                int from = viewHolder.getAdapterPosition();
                //获取目标Item的Position
                int endPosition = target.getAdapterPosition();
                //交换List集合中两个元素的位置
                Collections.swap(list, from, endPosition);
                //交换界面上两个Item的位置
                adapter.notifyItemMoved(from, endPosition);
            }
            return true;
        }

效果如下:

359.gif
(4)onSwiped: 滑动处理

onSwiped是为了实现Item的左右滑动,由于我在getMovementFlags方法中已经允许让Item左右侧滑,所以此时Item是只吃左右滑动的,效果如下:

360.gif

如图所示,在界面删Item已经被删除,但是,您知道的,List数据对应的数据没有被删除,而且屏幕上显示的RecyclerView没有被刷新,这些处理需要在onSwiped方法中处理,代码如下:

        @Override
        public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
            //滑动处理
            int position = viewHolder.getAdapterPosition();
            if(list != null && list.size() > 0){
                //删除List中对应的数据
                list.remove(position);

                if (recycleViewAdapter == null){
                    return;
                }
                //刷新页面
                recycleViewAdapter.notifyItemRemoved(position);

            }
        }
    };

效果如下:

361.gif
(5)getMoveThreshold: 设置拖拽距离百分比

看一下以下方法:

        @Override
        public float getMoveThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
            return .5f;
        }

如果RecyclerView是垂直方向,那么当拖拽到当前Item高度的50%时开始执行onMove方法。

下面我将贴出两张图,分别表示0.5和5的情况:

返回值为0.5的情况:

359.gif

返回值为4的情况:

362.gif
(6)getMoveThreshold: 设置拖拽距离百分比
        @Override
        public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
            //返回值滑动消失的距离, 这里是相对于RecycleView的宽度,0.5f表示为RecycleView的宽度的一半,取值为0~1f之间
            return .5f;
        }

它的返回值默认为50%,一般没有特殊需求就按照默认。

(7)getSwipeEscapeVelocity: 精确的计算出滑动消失的距离
        @Override
        public float getSwipeEscapeVelocity(float defaultValue) {
            //返回值滑动消失的距离,滑动小于这个值不消失,大于消失,默认为屏幕的三分之一
            //defaultValue默认值为120dp,转成px为 defaultValue * density
            return defaultValue;
        }

defaultValue默认值为120dp,转成px为defaultValue * density(density为像素缩放因子)

(8)getSwipeVelocityThreshold: 控制惯性速率
        @Override
        public float getSwipeVelocityThreshold(float defaultValue) {
            //设置滑动速率,这里的速率是指以一定的速率滑动Item,松开时Item依然从某一个方向移动的速率(可以理解为惯性的速率)
            //defaultValue默认值为800dp,转成px为 defaultValue * density
            return defaultValue;
        }

defaultValue默认值为800dp,转成px为 defaultValue * density(density为像素缩放因子)

(9)isLongPressDragEnabled: 是否允许长按拖拽
        @Override
        public boolean isLongPressDragEnabled() {
            return true;
        }

它的返回值默认为true。

(10)isItemViewSwipeEnabled: 是否允许左右滑动删除
        @Override
        public boolean isItemViewSwipeEnabled() {
            return true
        }

它的返回值默认为true。

(11)onSelectedChanged: 拖拽或滑动时被调用
        @Override
        public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
            super.onSelectedChanged(viewHolder, actionState);
        }

从静止状态变为拖拽或者滑动的时候会回调该方法,参数actionState表示当前的状态。

这个方法可以用于处理在拖拽或者滑动时的UI变化。

(12)getAnimationDuration: 设置动画时间
        @Override
        public long getAnimationDuration(@NonNull RecyclerView recyclerView, int animationType, float animateDx, float animateDy) {
            return super.getAnimationDuration(recyclerView, animationType, animateDx, animateDy);
        }

当手指松开时,Item会移动到某个固定的位置,这个动作就是Item动画,而getAnimationDuration方法可以设置动画的时间,默认为200ms或者250ms。

(13)clearView: 初始化话,清空View的状态
        @Override
        public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
            super.clearView(recyclerView, viewHolder);
        }

当拖拽或滑动动画结束时,也就是手指松开并等待动画结束时会执行clearView方法,这个方法可以用来初始化数据。

(14)onChildDraw: 在Item底部绘制图形
        @Override
        public void onChildDraw(@NonNull Canvas canvas, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
            super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
        }

onChildDraw方法有如下几个参数:

  • canvas: 画布,可以用于绘制图形
  • recyclerView:RecyclerView对象
  • viewHolder:被操作的Item
  • dX:Item被操作时的X轴方向的偏移量
  • dY:Item被操作时的Y轴方向的偏移量
  • actionState:移动状态,在前文已经给出移动的三种状态:无操作状态拖拽状态滑动状态
  • isCurrentlyActive:滑动分为两类。其一:手指滑动;其二:Item位移动画滑动;isCurrentlyActive可以用于判断是哪种滑动,如果isCurrentlyActive为true则表示手指滑动

我们看一下以下代码

        private Paint paint;
        private RectF rect;

        @Override
        public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
            super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);

            //Item底部底部绘制图形
            c.translate(-dX, 0);
            paint = new Paint();
            paint.setColor(Color.RED);
            paint.setStyle(Paint.Style.FILL);

            //在布局中,我设置的是4dp,这里需要转成px
            float padding = 4 * getResources().getDisplayMetrics().density + 0.5f;

            //获取X轴Item位置
            float x = viewHolder.itemView.getX() + padding;
            float y = viewHolder.itemView.getY() + padding;
            float width = viewHolder.itemView.getWidth() - 2 * padding;
            float height = viewHolder.itemView.getHeight() - 2 * padding;
            rect = new RectF(x, y, x + width, y + height);
            c.drawRect(rect, paint);

        }

代码的意思也很简单,就是使用Canvas绘制一个矩形,演示效果如下:

363.gif
(15)onChildDrawOver: 在Item上面绘制图形
        @Override
        public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
            super.onChildDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
        }

onChildDrawOver方法有如下几个参数:

  • canvas: 画布,可以用于绘制图形
  • recyclerView:RecyclerView对象
  • viewHolder:被操作的Item
  • dX:Item被操作时的X轴方向的偏移量
  • dY:Item被操作时的Y轴方向的偏移量
  • actionState:移动状态,在前文已经给出移动的三种状态:无操作状态拖拽状态滑动状态
  • isCurrentlyActive:滑动分为两类。其一:手指滑动;其二:Item位移动画滑动;isCurrentlyActive可以用于判断是哪种滑动,如果isCurrentlyActive为true则表示手指滑动

我们看一下以下代码

        @Override
        public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
            //Item上面绘制图形
            //c.translate(-dX, 0);
            paint = new Paint();
            paint.setColor(Color.GRAY);
            paint.setStyle(Paint.Style.FILL);

            //在布局中,我设置的是4dp,这里需要转成px
            float padding = 4 * getResources().getDisplayMetrics().density + 0.5f;

            //获取X轴Item位置
            float x = viewHolder.itemView.getX() + padding;
            float y = viewHolder.itemView.getY() + padding;
            float width = viewHolder.itemView.getWidth() - 2 * padding;
            float height = viewHolder.itemView.getHeight() - 2 * padding;
            rect = new RectF(x + width - 200, y + height / 2 - 50, x + width - 100, y + height / 2 + 50);
            c.drawRect(rect, paint);
        }

上面代码的意思是:在Item上面画一个正方形。

效果图如下:


364.gif

图中可以看到,Item上已经绘制了一个正方形。

(16)完整代码
    //声明一个Callback
    ItemTouchHelper.Callback callback = new ItemTouchHelper.Callback() {

        @Override
        public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
            //控制拖拽的方向(一般是上下左右)
            int dragFlags= ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
            //控制快速滑动的方向(一般是左右)
            int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;

            return makeMovementFlags(dragFlags, swipeFlags);//计算movement flag值
        }

        @Override
        public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
            // 拖拽时,每移动一个位置就会调用一次。

            if (recyclerView == null){
                return false;
            }

            RecyclerView.Adapter adapter = recyclerView.getAdapter();
            if (adapter == null){
                return false;
            }

            if (list != null && list.size() > 0) {
                //获取被拖拽的Item的Position
                int from = viewHolder.getAdapterPosition();
                //获取目标Item的Position
                int endPosition = target.getAdapterPosition();
                //交换List集合中两个元素的位置
                Collections.swap(list, from, endPosition);
                //交换界面上两个Item的位置
                adapter.notifyItemMoved(from, endPosition);
            }
            return true;
        }

        @Override
        public float getMoveThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
            //设置拖拽距离百分比
            //如果RecyclerView是垂直方向,那么当拖拽到当前Item高度的50%时开始执行onMove方法
            return .5f;
        }

        @Override
        public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {

            //滑动处理
            int position = viewHolder.getAdapterPosition();
            if(list != null && list.size() > 0){
                //删除List中对应的数据
                list.remove(position);

                if (recycleViewAdapter == null){
                    return;
                }
                //刷新页面
                recycleViewAdapter.notifyItemRemoved(position);

            }
        }

        @Override
        public boolean isLongPressDragEnabled() {
            return true;
        }

        @Override
        public boolean isItemViewSwipeEnabled() {
            return true;
        }

        @Override
        public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
            super.onSelectedChanged(viewHolder, actionState);
        }


        private Paint paint;
        private RectF rect;

        @Override
        public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
            super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);

            //Item底部底部绘制图形
            c.translate(-dX, 0);
            paint = new Paint();
            paint.setColor(Color.RED);
            paint.setStyle(Paint.Style.FILL);

            //在布局中,我设置的是4dp,这里需要转成px
            float padding = 4 * getResources().getDisplayMetrics().density + 0.5f;

            //获取X轴Item位置
            float x = viewHolder.itemView.getX() + padding;
            float y = viewHolder.itemView.getY() + padding;
            float width = viewHolder.itemView.getWidth() - 2 * padding;
            float height = viewHolder.itemView.getHeight() - 2 * padding;
            rect = new RectF(x, y, x + width, y + height);
            c.drawRect(rect, paint);

        }


        @Override
        public long getAnimationDuration(@NonNull RecyclerView recyclerView, int animationType, float animateDx, float animateDy) {
            return 1000;
        }


        @Override
        public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
            super.clearView(recyclerView, viewHolder);
        }

        @Override
        public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
            //Item上面绘制图形
            //c.translate(-dX, 0);
            paint = new Paint();
            paint.setColor(Color.GRAY);
            paint.setStyle(Paint.Style.FILL);

            //在布局中,我设置的是4dp,这里需要转成px
            float padding = 4 * getResources().getDisplayMetrics().density + 0.5f;

            //获取X轴Item位置
            float x = viewHolder.itemView.getX() + padding;
            float y = viewHolder.itemView.getY() + padding;
            float width = viewHolder.itemView.getWidth() - 2 * padding;
            float height = viewHolder.itemView.getHeight() - 2 * padding;
            rect = new RectF(x + width - 200, y + height / 2 - 50, x + width - 100, y + height / 2 + 50);
            c.drawRect(rect, paint);
        }


        @Override
        public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
            //返回值滑动消失的距离, 这里是相对于RecycleView的宽度,0.5f表示为RecycleView的宽度的一半,取值为0~1f之间
            return .5f;
        }

        @Override
        public float getSwipeEscapeVelocity(float defaultValue) {
            //返回值滑动消失的距离,滑动小于这个值不消失,大于消失,默认为屏幕的三分之一
            //defaultValue默认值为120dp,转成px为 defaultValue * density
            return defaultValue;
        }

        @Override
        public float getSwipeVelocityThreshold(float defaultValue) {
            //设置滑动速率,这里的速率是指以一定的速率滑动Item,松开时Item依然从某一个方向移动的速率(可以理解为惯性的速率)
            //defaultValue默认值为800dp,转成px为 defaultValue * density
            return defaultValue;
        }
    };
    //创建helper对象
    ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
    //关联recyclerView
    itemTouchHelper.attachToRecyclerView(recyclerview);

以上代码只要演示一遍,基本就会明白ItemTouchHelper的作用,ItemTouchHelper常用方法是getMovementFlagsonMoveonSwiped,强烈建议重点学习。

[本章完...]

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

推荐阅读更多精彩内容