RecyclerView 吸顶效果

RecyclerView 不同组别标题的吸顶效果图如下:

该效果是通过自定义 ItemDecoration 实现的,看代码之前先来看一下 ItemDecoration 的工作原理。

一、ItemDecoration 简介

1.ItemDecoration 源码

RecyclerView 的抽象静态内部类 ItemDecoration 负责管理 RecyclerView 中各个 ItemView 的装饰(如分隔线、高亮显示、可视化分组边界):

    /**
    * ItemDecoration 允许向 Adapter 中的数据集中的特定项视图添加特殊的绘图和布局
    * 偏移量,这对于在 Item 之间绘制分隔线、高亮显示、可视化分组边界等都很有用。
    * 所有的 ItemDecoration 都按照添加的顺序绘制,其中 onDraw() 是在 ItemView 
    * 之前绘制,onDrawOver() 是在 ItemView 之后绘制。
    */
    public abstract static class ItemDecoration {
        /**
         * onDraw 会先于 ItemView 绘制,如果二者绘制的内容由重叠区域,
         * onDraw 绘制的内容会被 ItemView 的内容覆盖。
         */
        public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
            onDraw(c, parent);
        }

        @Deprecated
        public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) {
        }

        /**
         * 在 ItemView 之后进行绘制,绘制内容在 ItemView 上面。
         */
        public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
                @NonNull State state) {
            onDrawOver(c, parent);
        }

        @Deprecated
        public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) {
        }

        @Deprecated
        public void getItemOffsets(@NonNull Rect outRect, int itemPosition,
                @NonNull RecyclerView parent) {
            outRect.set(0, 0, 0, 0);
        }

        /**
         * 检索给定 Item 的偏移量,单位是px。outRect 的每个字段都指定了 Item  
         * View 应该被嵌入的像素值,类似于 padding 或 margin。默认实现是将  
         * outRect 的边界设为0。
         * 如果这个 ItemDecoration 不影响 ItemView 的位置,需要将 outRect 的四个
         * 字段设置为0。
         *
         * 如果需要获取 Adapter 中的额外数据,可以调用通过
         * RecyclerView#getChildAdapterPosition(View) 来获取 Adapter 的位置。
         */
        public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
                @NonNull RecyclerView parent, @NonNull State state) {
            getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
                    parent);
        }
    }

ItemDecoration 有两个需要注意的地方:

  1. getItemOffsets() 中的 outRect 用来指定绘制 ItemDecoration 的预留空间。
  2. ItemDecoration 与 RecyclerView 中的 ItemView 的绘制顺序:ItemDecoration#onDraw() -> ItemView#onDraw() -> ItemDecoration#onDrawOver(),先执行绘制的方法,其绘制内容在较下层。

getItemOffsets() 与 outRect

其实 RecyclerView 中的每个 ItemView 外都有一个 outRect 用以指定在 left、top、right 和 bottom 四个方向上的预留空间(下图灰色区域):

源码中,在 RecyclerView 的 getItemDecorInsetsForChild() 中会对每个 ItemView 的 outRect 占用空间进行计算:

    
    // 由于每个 ItemView 可以有多个 ItemDecoration 修饰,所以把它们存在列表中
    final ArrayList<ItemDecoration> mItemDecorations = new ArrayList<>();
    
    Rect getItemDecorInsetsForChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (!lp.mInsetsDirty) {
            return lp.mDecorInsets;
        }

        if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
            // changed/invalid items should not be updated until they are rebound.
            return lp.mDecorInsets;
        }
        // 计算结果也用一个 Rect 表示,初始化为(0, 0, 0, 0)
        final Rect insets = lp.mDecorInsets;
        insets.set(0, 0, 0, 0);
        final int decorCount = mItemDecorations.size();
        // 对当前 child 上所有的 ItemDecoration 进行四个方向上的累加计算
        for (int i = 0; i < decorCount; i++) {
            mTempRect.set(0, 0, 0, 0);
            // 调用 getItemOffsets() 获取 outRect 信息
            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
            insets.left += mTempRect.left;
            insets.top += mTempRect.top;
            insets.right += mTempRect.right;
            insets.bottom += mTempRect.bottom;
        }
        lp.mInsetsDirty = false;
        return insets;
    }

计算的结果在 RecyclerView 对每个 Item 进行测量时会用到:

        
        public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            
            // 获取 child 的 outRect 的测量结果,并分别在宽度和高度上累加
            final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
            widthUsed += insets.left + insets.right;
            heightUsed += insets.top + insets.bottom;

            // RecyclerView 的左右 padding,以及 child 的左右 margin 再加上包括
            // 所有 outRect 在水平方向上占用的空间之和,作为已经被使用的空间,在
            // 计算 child 的 MeasureSpec 时会从总宽度中减掉。
            final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
                    getPaddingLeft() + getPaddingRight()
                            + lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
                    canScrollHorizontally());
            // 高度计算与宽度类似
            final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
                    getPaddingTop() + getPaddingBottom()
                            + lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
                    canScrollVertically());
            if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
                child.measure(widthSpec, heightSpec);
            }
        }

也就是说,getItemOffsets() 中由 outRect 指定的每一个 Item 的预留空间,都会在 RecyclerView 进行测量时,被计算在内。

onDraw() 与 onDrawOver()

当 RecyclerView 进行绘制时,它的父 ViewGroup 的 dispatchDraw() 会调用到 drawChild() 开始绘制子控件:

#ViewGroup
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

这样就来到了 RecyclerView 当中:

#RecyclerView

    @Override
    public void draw(Canvas c) {
        super.draw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDrawOver(c, this, mState);
        }
        ……
    }

先 super 去到父类的 draw():

#View
    
    // 不是本文重点,省略绝大部分代码
    public void draw(Canvas canvas) {
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         *      7. If necessary, draw the default focus highlight
         */
         
         // Step 3, draw the content
        onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);
    }

执行到 Step3 就会调用到 RecyclerView 的 onDraw():

#RecyclerView

    @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }

在这里,先调用了 ItemDecorations 的 onDraw()。然后回到 View 中的 Step4,又会执行到 ViewGroup 的 dispatchDraw(),这一次再分发绘制事件,其实就是 RecyclerView 分发去绘制各个 ItemView 的,这是个递归过程,不再赘述。只需知道,经过这一步,ItemView 都绘制完成了。

代码再向上返回,RecyclerView draw() 的 super.draw() 执行完了,接着就会再遍历 mItemDecorations 执行 ItemDecoration 的 onDrawOver() 了。

由此可见,绘制的先后顺序就是 ItemDecoration#onDraw() -> ItemView#onDraw() -> ItemDecoration#onDrawOver(),先绘制的显示在下层,后绘制的在上层,如果绘制出现了重叠的部分,当然就是在上层的优先显示啦:

到这里 ItemDecoration 中两个重要的知识就说完了,这两点也是我们实现 RecyclerView 标题吸顶的关键。

2.DividerItemDecoration

我们再看看系统是如何实现 ItemDecoration 的。系统提供了一个 DividerItemDecoration 用来绘制 Item 之间的分隔线。首先在 getItemOffsets() 中会在绘制方向上预留出一个分割线占用的空间大小:

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
            RecyclerView.State state) {
        if (mDivider == null) {
            outRect.set(0, 0, 0, 0);
            return;
        }
        
        if (mOrientation == VERTICAL) {
            // 竖直方向布局时,在底部留出一个 mDivider 的高度
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            // 水平方向布局时,在右侧留出一个 mDivider 的宽度
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }

再根据 RecyclerView.LayoutManager 中指定的方向(竖直 or 水平),绘制分隔线:

    
    // 分隔线,用 Drawable 画
    private Drawable mDivider;
    
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (parent.getLayoutManager() == null || mDivider == null) {
            return;
        }
        // mOrientation 方向由 RecyclerView.LayoutManager 指定
        if (mOrientation == VERTICAL) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }
    
    private void drawVertical(Canvas canvas, RecyclerView parent) {
        canvas.save();
        final int left;
        final int right;
        
        // 先看 RecyclerView 中的布尔成员 mClipToPadding,为 true 表示则表示
        // View 不能在 padding 区域内进行绘制。可以通过布局中的 
        // android:clipToPadding 属性进行设置,默认为 true。
        if (parent.getClipToPadding()) {
            // 对 canvas 进行剪裁,边界将 padding 刨除在外
            left = parent.getPaddingLeft();
            right = parent.getWidth() - parent.getPaddingRight();
            canvas.clipRect(left, parent.getPaddingTop(), right,
                    parent.getHeight() - parent.getPaddingBottom());
        } else {
            // 如果可以在 padding 区域内绘制,那么绘制区域不变。
            left = 0;
            right = parent.getWidth();
        }

        // 计算出每一个 child 的分隔线的位置,并画出分隔线
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            // 获取装饰边界 mBounds
            parent.getDecoratedBoundsWithMargins(child, mBounds);
            final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
            final int top = bottom - mDivider.getIntrinsicHeight();
            // 设置分隔线的位置,其实是画一个矩形
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(canvas);
        }
        canvas.restore();
    }

思路就是先用 parent.getClipToPadding() 确定绘制区域。该方法的意思是确定,是否要将绘制的 canvas 根据父容器的 padding 进行剪裁。如果该方法返回 true,意味着父布局不允许你在 padding 内进行绘制,那么就要将 canvas 减去相应的 padding。

确定好绘制范围后,再计算出每个 ItemView 要绘制内容的位置。以 DividerItemDecoration 为例,获取到装饰边界的 mBounds 后,先确定底部坐标 bottom,再用 bottom 减去分隔线高度得到 top,left 与 right 前面已经确定好了,这样用来绘制分隔线的矩形的四边就确定了,最后调用 Drawable 的绘制方法即可。

二、实现 RecyclerView 标题吸顶效果

先说一下准备工作,RecyclerView 要显示的数据做成 JavaBean:

public class Data {

    private String name;
    private String groupName; // 分组名称
    
    // getters and setters...   
}

RecyclerView 的 Adapter 中要提供一个根据位置判断当前 Item 是否为分组的第一个元素:

public class DataAdapter extends RecyclerView.Adapter<DataAdapter.DataViewHolder> {

    private Context context;
    private List<Data> list;

    public DataAdapter(Context context, List<Data> list) {
        this.context = context;
        this.list = list;
    }
    
    public boolean isGroupHeader(int position) {
        if (position == 0) {
            return true;
        }

        // 如果当前位置的 GroupName 和上一个位置的 GroupName 不同即为 GroupHeader
        String currentGroupName = getGroupName(position);
        String previousGroupName = getGroupName(position - 1);
        return !currentGroupName.equals(previousGroupName);
    }

    public String getGroupName(int position) {
        return list.get(position).getGroupName();
    }
    
    // ViewHolder 就是一个 TextView 显示 Data 的 name 字段的,省略...
}

下面就开始实现自定义的 ItemDecoration。先重写 getItemOffsets(),给每个 Item 的预留空间设置好:

public class DataItemDecoration extends RecyclerView.ItemDecoration {

    private static final int DIVIDER_HEIGHT_IN_PX = 1;
    
    private int groupHeaderHeight;
    private Paint backgroundPaint;
    private Paint textPaint;

    public DataItemDecoration(Context context) {
        groupHeaderHeight = dp2px(context, 60);

        // 头部背景画笔
        backgroundPaint = new Paint();
        backgroundPaint.setColor(Color.RED);

        // 头部文字画笔
        textPaint = new Paint();
        textPaint.setTextSize(50);
        textPaint.setColor(Color.WHITE);
    }
    
    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);

        if (parent.getAdapter() instanceof DataAdapter) {
            DataAdapter adapter = (DataAdapter) parent.getAdapter();
            int position = parent.getChildLayoutPosition(view);
            if (adapter.isGroupHeader(position)) {
                // 如果是头部,就在上方预留出 Header 的高度
                outRect.set(0, groupHeaderHeight, 0, 0);
            } else {
                // 如果不是头部,则在上方留出1px画分隔线
                outRect.set(0, DIVIDER_HEIGHT_IN_PX, 0, 0);
            }
        }
    }
}

查看效果发现在每个分组的第一个 Item 上方会出现一个白色区域,即是 outRect:

在 onDraw() 中,不论是在 outRect 中绘制,还是在其余 Item 上方绘制分隔线时,都需要计算绘制区域的边界。以图中 outRect 为例,它的 bottom 就是 Item00 的 top,在已知 outRect 高度为 groupHeaderHeight 的情况下,outRect 的 top 就是 ItemView00.getTop() - groupHeaderHeight。代码如下:

    /**
     * 所有 GroupHeader 的内容都使用 onDraw() 绘制。
     */
    @Override
    public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(canvas, parent, state);

        if (parent.getAdapter() instanceof DataAdapter) {
            DataAdapter adapter = (DataAdapter) parent.getAdapter();
            // 获取当前屏幕可见的 Item 数量
            int childCount = parent.getChildCount();
            // 确定 canvas 的左右边界,刨除 padding
            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();
            for (int i = 0; i < childCount; i++) {
                View child = parent.getChildAt(i);
                int position = parent.getChildLayoutPosition(child);
                if (adapter.isGroupHeader(position)) {
                    // 绘制 GroupHeader 的背景
                    canvas.drawRect(left, child.getTop() - groupHeaderHeight, right, child.getTop(), backgroundPaint);
                    String groupName = adapter.getGroupName(position);
                    // 绘制 GroupHeader 的文字
                    canvas.drawText(groupName, left + 20, getTextBaselineY(textPaint, child.getTop(), groupHeaderHeight), textPaint);
                } else {
                    // 不是 GroupHeader 就画一个分隔线
                    canvas.drawRect(left, child.getTop() - DIVIDER_HEIGHT_IN_PX, right, child.getTop(), backgroundPaint);
                }
            }
        }
    }

顶置的 GroupHeader 需要用 onDrawOver() 绘制(因为要显示在最上方,覆盖 ItemView 和 onDraw() 绘制的 GroupHeader),并且要实现一个被新来的 GroupHeader 向上推的效果:

这种效果其实只需要让顶置的 GroupHeader 的 bottom 不断变小就能做到,关键问题是什么时候开始变?变化的数值如何计算?

我们的思路是(有个前提,GroupHeader 的高度与 ItemView 的高度是一样的,在例子中都被设置为60dp):

  1. 先找到 RecyclerView 中第一个可见 Item 在 Adapter 中的位置。
  2. 在上面提到的前提条件下,第二个可见的 Item 如果是一个 GroupHeader,就要开始改变顶置的 GroupHeader 的 bottom 值了,否则就仍是在固定位置画顶置的 GroupHeader。
  3. 顶置的 GroupHeader 的 bottom 变化应该跟随第一个可见 Item 的可见部分高度变化(向上滑动的过程中,第一个可见 Item 的可见部分高度逐渐变小,让 GroupHeader 的 bottom 也跟随它变化即可)。

代码如下:

    /**
     * 吸顶的 GroupHeader 用 onDrawOver() 绘制,就会遮挡 ItemView 和
     * 用 onDraw() 绘制的 GroupHeader
     */
    @Override
    public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(canvas, parent, state);

        if (parent.getAdapter() instanceof DataAdapter) {
            DataAdapter adapter = (DataAdapter) parent.getAdapter();

            // 获取 RecyclerView 中第一个可见 Item 的位置以及对应的 View
            int position = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
            View itemView = parent.findViewHolderForAdapterPosition(position).itemView;
            String groupName = adapter.getGroupName(position);

            // 计算边界,其中 top 应该是 RecyclerView 的 paddingTop
            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();
            int top = parent.getPaddingTop();

            // 此时的临界条件是:第二个可见的 Item 为 GroupHeader
            boolean isGroupHeader = adapter.isGroupHeader(position + 1);

            // 计算两种情况下,顶置的 GroupHeader 的 bottom 以及文字 BaseLine 的 Y 轴坐标
            int bottom;
            int baseLineY;
            if (isGroupHeader) {
                // 到达临界条件,顶置 GroupHeader 的 top 不变,但是 bottom 要动态计算
                int firstItemVisibleHeight = Math.min(groupHeaderHeight, itemView.getBottom());
                bottom = firstItemVisibleHeight + top;
                baseLineY = getTextBaselineY(textPaint, bottom, groupHeaderHeight);

            } else {
                // 没到临界条件就按照固定位置画
                bottom = top + groupHeaderHeight;
                baseLineY = getTextBaselineY(textPaint, top + groupHeaderHeight, groupHeaderHeight);

            }

            canvas.drawRect(left, top, right, bottom, backgroundPaint);
            canvas.drawText(groupName, left + 20, baseLineY, textPaint);
        }
    }

到这基本上算实现了这个功能,但是有一个 bug,我们在布局中给 RecyclerView 加一个 paddingTop 属性,再来看效果:

可以看到主要有两个问题:

  1. 白色背景的 paddingTop 部分不应该被绘制。
  2. 顶置的 GroupHeader 被向上推的效果没有了。

问题1,多余的绘制部分是由 onDraw() 绘制的,看代码发现,在绘制时并没有对绘制区域的合法性进行检查,应该加上:

    @Override
    public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(canvas, parent, state);

        if (parent.getAdapter() instanceof DataAdapter) {
            DataAdapter adapter = (DataAdapter) parent.getAdapter();
            int childCount = parent.getChildCount();

            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();
            for (int i = 0; i < childCount; i++) {
                View child = parent.getChildAt(i);
                int position = parent.getChildLayoutPosition(child);

                // 新增代码,先判断是否会在 RecyclerView 的 paddingTop 区域内
                // 绘制,如果是就跳过该 child 的绘制过程。
                int minTop = Math.min(child.getTop() - groupHeaderHeight, child.getTop() - DIVIDER_HEIGHT_IN_PX);
                if (minTop < parent.getPaddingTop()) {
                    continue;
                }

                if (adapter.isGroupHeader(position)) {
                    canvas.drawRect(left, child.getTop() - groupHeaderHeight, right, child.getTop(), backgroundPaint);
                    String groupName = adapter.getGroupName(position);
                    canvas.drawText(groupName, left + 20, getTextBaselineY(textPaint, child.getTop(), groupHeaderHeight), textPaint);
                } else {
                    canvas.drawRect(left, child.getTop() - DIVIDER_HEIGHT_IN_PX, right, child.getTop(), backgroundPaint);
                }
            }
        }
    }

问题2的原因是在给 RecyclerView 增加了 paddingTop 后,第一个可见 Item 的可见高度在计算时也应该相应的减掉 paddingTop 的数值:

    @Override
    public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(canvas, parent, state);

        if (parent.getAdapter() instanceof DataAdapter) {
            DataAdapter adapter = (DataAdapter) parent.getAdapter();

            int position = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
            View itemView = parent.findViewHolderForAdapterPosition(position).itemView;
            String groupName = adapter.getGroupName(position);

            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();
            int top = parent.getPaddingTop();

            boolean isGroupHeader = adapter.isGroupHeader(position + 1);

            int bottom;
            int baseLineY;
            if (isGroupHeader) {
                int firstItemVisibleHeight = Math.min(groupHeaderHeight, itemView.getBottom() - parent.getPaddingTop()); // 更新计算方式
                bottom = firstItemVisibleHeight + top;
                baseLineY = getTextBaselineY(textPaint, bottom, groupHeaderHeight);

            } else {
                bottom = top + groupHeaderHeight;
                baseLineY = getTextBaselineY(textPaint, top + groupHeaderHeight, groupHeaderHeight);

            }

            canvas.drawRect(left, top, right, bottom, backgroundPaint);
            canvas.drawText(groupName, left + 20, baseLineY, textPaint);
        }
    }

更改后的效果图:

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

推荐阅读更多精彩内容