RecyclerView(3)-LayoutMagager源码解析,LinearLayoutManager

上一节RecyclerView(2)- 自定义Decoration打造时光轴效果也已经写完了,希望有看到我文章的同学能有一些收获。layoutManager可以说是一个重中之重,代码量非常多,且涉及到复用机制的调用等等。等源码分析过后,同学们应该可以通过自定义LayoutManager打造奇形怪状的奇葩的UI需求了。

· RecyclerView(1)- Decoration源码解析
· RecyclerView(2)- 自定义Decoration打造时光轴效果
· RecyclerView(3)- LayoutMagager源码解析,LinearLayoutManager
· RecyclerView(4)- 核心、Recycler复用机制_1
· RecyclerView(4)- 核心、Recycler复用机制_2
· RecyclerView(5)- 自定义LayoutManager(布局、复用)
· RecyclerView(6)- 自定义ItemAnimator
· RecyclerView(7)- ItemTouchHelper
· RecyclerView(8)- MultiTypeAdapter文章MultiTypeAdapter Github地址
文章视频地址:链接:http://pan.baidu.com/s/1hssvXC4 密码:18v1

分析LinearLayoutManager,先罗列几个想弄明白的问题

· 1、如何摆位置;
· 2、按需加载布局,位置摆放的规则
· 3、滑动时itemview的位置改变遵循的规则

LayoutManager如何摆位置;

先从 测量 onMeasure开始

RecyclerView.class
    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout.mAutoMeasure) { // 自动测量
             mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);// 调用一次recyclerview的测量
             //....
            dispatchLayoutStep2();
        }
        else{//自定义测量规则
        
        }
    }

    private void dispatchLayoutStep2() {
        //....
         mState.mItemCount = mAdapter.getItemCount();
        //.... 自定义摆放位置
        mLayout.onLayoutChildren(mRecycler, mState);
        //....
    }

可以看到在recyclerview的 onMeasure 调用了 mLayout的onLayoutChildren方法 并将Recycler与 包含了 适配器一些信息的包装成一个 State参数 传入mLayout的onLayoutChildren

接下来看一下LinearLayoutManager

LinearLayoutManager.class
    //初始化
    public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
        setOrientation(orientation);
        setReverseLayout(reverseLayout);
        setAutoMeasureEnabled(true);
    }
    public void setAutoMeasureEnabled(boolean enabled) {
        mAutoMeasure = enabled;
    }
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // layout algorithm:
        // 1) by checking children and other variables, find an anchor coordinate and an anchor
        //  item position.
        // 2) fill towards start, stacking from bottom
        // 3) fill towards end, stacking from top
        // 4) scroll to fulfill requirements like stack from bottom.
        // create layout state
        if (DEBUG) {
            Log.d(TAG, "is pre layout:" + state.isPreLayout());
        }
        if (mPendingSavedState != null || mPendingScrollPosition != NO_POSITION) {
            if (state.getItemCount() == 0) {
            // 如果没有项目 移除布局
                removeAndRecycleAllViews(recycler);
                return;
            }
        }
        
        // ...
        this.updateLayoutStateToFillStart(this.mAnchorInfo);
        this.mLayoutState.mExtra = extraForStart;
        fill(recycler, mLayoutState, state, false);
        //...
        this.updateLayoutStateToFillEnd(this.mAnchorInfo);
        this.mLayoutState.mExtra = extraForEnd;
        this.mLayoutState.mCurrentPosition += this.mLayoutState.mItemDirection;
        this.fill(recycler, this.mLayoutState, state, false);
    }
    // 填充view
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
            //.. 摆位置
             layoutChunk(recycler, state, layoutState, layoutChunkResult);
            //...  布局回收
              this.recycleByLayoutState(recycler, layoutState);
    }
    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        //...
        addView(view);  // -> addView(View child, int index) ->  addViewInt(View child, int index, boolean disappearing)
        //计算位置
         this.measureChildWithMargins(view, 0, 0);
            result.mConsumed = this.mOrientationHelper.getDecoratedMeasurement(view);
            int left;
            int top;
            int right;
            int bottom;
            if(this.mOrientation == 1) {
                if(this.isLayoutRTL()) {
                    right = this.getWidth() - this.getPaddingRight();
                    left = right - this.mOrientationHelper.getDecoratedMeasurementInOther(view);
                } else {
                    left = this.getPaddingLeft();
                    right = left + this.mOrientationHelper.getDecoratedMeasurementInOther(view);
                }

                if(layoutState.mLayoutDirection == -1) {
                    bottom = layoutState.mOffset;
                    top = layoutState.mOffset - result.mConsumed;
                } else {
                    top = layoutState.mOffset;
                    bottom = layoutState.mOffset + result.mConsumed;
                }
            } else {
                top = this.getPaddingTop();
                bottom = top + this.mOrientationHelper.getDecoratedMeasurementInOther(view);
                if(layoutState.mLayoutDirection == -1) {
                    right = layoutState.mOffset;
                    left = layoutState.mOffset - result.mConsumed;
                } else {
                    left = layoutState.mOffset;
                    right = layoutState.mOffset + result.mConsumed;
                }
            }
        
        //布局
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        //...
    }
    private void addViewInt(View child, int index, boolean disappearing) {
          final ViewHolder holder = getChildViewHolderInt(child);
          mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);//依附在parent  这边是 parent 是 recyclerView
          
          // 对holder 做一些变量改变
    }

RecyclerView.class   
    static class LayoutState {
        View next(RecyclerView.Recycler recycler) {
            //..
            //从 recyler 中取出view
            final View view = recycler.getViewForPosition(mCurrentPosition);
            //...
            return view;
        }
    }

一开始的注释说了整个大概的流程:
1、先检查children和其它变量,找到一个锚点坐标与锚点(如果实在线性布局中,相当于找到当前界面内第一个VIew,与第一个view的坐标点)
2、填充从底部开始堆叠
3、从顶部填充到端部
4、计算是否还有滚动,添加各种变量,创建布局。

其实原理还是挺简单的,循环判断是否超出边界,测量view,添加view 布局view,判定值改变 在跳到上面循环。好多人看不懂是因为google工程师将这些判断数据抽取封装了起来,而其中字段非常多,让人眼花缭乱。
一些总结:
开始调用 onLayoutChildren() 调用fill() 摆放位置
fill():判断相应规则 回收一部分view, 调用layoutChunk() 获取 view、填充、摆放view;
layoutChunk(): 获取view,计算 view的位置
layoutDecoratedWithMargins():拿到装饰器设置的偏移量,摆放view;

2、按需加载布局,位置摆放的规则

上面也可以知道 在fill()方法内我们先去判断位置摆放的规则 在决定是否加载下一个/上一个View(也就是调用 layoutChunk),那么我们就来看一下判断规则是怎么样的吧

   while((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
           int start = layoutState.mAvailable;
        if(layoutState.mScrollingOffset != -2147483648) {
            if(layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }

            this.recycleByLayoutState(recycler, layoutState);
        }

        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LinearLayoutManager.LayoutChunkResult layoutChunkResult = this.mLayoutChunkResult;
            layoutChunkResult.resetInternal();
            this.layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if(layoutChunkResult.mFinished) {
                break;
            }

            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            if(!layoutChunkResult.mIgnoreConsumed || this.mLayoutState.mScrapList != null || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                remainingSpace -= layoutChunkResult.mConsumed;
            }

            if(layoutState.mScrollingOffset != -2147483648) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if(layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }

                this.recycleByLayoutState(recycler, layoutState);
            }

            if(stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }

可以看到while关键字内,有lauoutState.mInfinite remainingspace layoutState.haseMore(state)变量,就是这三个变量控制着我们的摆放view的数量。
字面意思可以猜出来:无穷、可用空间>0; 是否有更多

那我们来一个一个看一下,这几个变量都经历了什么。

2.1、layoutState.mInfinite

字面意思 mInfinite是无穷的意思...

    this.mLayoutState.mInfinite = this.resolveIsInfinite();
    boolean resolveIsInfinite() {
    return this.mOrientationHelper.getMode() == 0 && this.mOrientationHelper.getEnd() == 0;
}
new OrientationHelper(layoutManager) {
         public int getEnd() {
                return this.mLayoutManager.getHeight();
            }
         public int getMode() {
                return this.mLayoutManager.getHeightMode();
          }    
        }

可以看到 mInfinite 的值与 recyclerview的高度 与规格有关,判断==0 ? 这是什么鬼! 看起来一点卵用都没有 ಥ_ಥ(是在下输了)

2.1、remainingspace

  int remainingSpace = layoutState.mAvailable + layoutState.mExtra;

第一次 layoutState.mAvailable的取值

    private void updateAnchorInfoForLayout(Recycler recycler, State state, LinearLayoutManager.AnchorInfo anchorInfo) {
        if(!this.updateAnchorFromPendingData(state, anchorInfo)) {
        }
        
    private boolean updateAnchorFromPendingData(State state, LinearLayoutManager.AnchorInfo anchorInfo) {
    //.....
               if(anchorInfo.mLayoutFromEnd) {
            anchorInfo.mCoordinate = this.mOrientationHelper.getEndAfterPadding() - this.mPendingSavedState.mAnchorOffset;
        } else {
            anchorInfo.mCoordinate = this.mOrientationHelper.getStartAfterPadding() + this.mPendingSavedState.mAnchorOffset;
        }
        //.........
    }
    
    private void updateLayoutStateToFillStart(LinearLayoutManager.AnchorInfo anchorInfo) {
        this.updateLayoutStateToFillStart(anchorInfo.mPosition, anchorInfo.mCoordinate);
    }

    private void updateLayoutStateToFillStart(int itemPosition, int offset) {
        this.mLayoutState.mAvailable = offset - this.mOrientationHelper.getStartAfterPadding();
    }
    

可以看到 第一次 layoutState.mAvailable的值是通过 锚点 AnchorInfo来计算的。
其中还判断了mLayoutFromEnd ...

看到这里我是晕的,变量太多了。。。

第一次 layoutState.mExtra的值
extra意思是额外...

2.3、layoutstate.hasMore(state)

        boolean hasMore(State state) {
            return this.mCurrentPosition >= 0 && this.mCurrentPosition < state.getItemCount();
        }

就是判断 position;

2.4、摆完一个布局以后,干了些什么

布局是一个一个添加的,那么在加载完一个children view后在加载了什么呢?

    int fill(Recycler recycler, LinearLayoutManager.LayoutState layoutState, State state, boolean stopOnFocusable) {
    
        while((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            this.layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if(layoutChunkResult.mFinished) {
                break;
            }

    // 加上偏移量
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            if(!layoutChunkResult.mIgnoreConsumed || this.mLayoutState.mScrapList != null || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                //  减去剩余空间
                remainingSpace -= layoutChunkResult.mConsumed;
            }

            if(layoutState.mScrollingOffset != -2147483648) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if(layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }

                this.recycleByLayoutState(recycler, layoutState);
            }

            if(stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
            //....
        }
    }
void layoutChunk(Recycler recycler, State state, LinearLayoutManager.LayoutState layoutState, LinearLayoutManager.LayoutChunkResult result){
  //......
    //得到 消耗的高度/宽度
    result.mConsumed = this.mOrientationHelper.getDecoratedMeasurement(view);
    // 布局
    addView(view); 
    layout
    // 判断是否被隐藏忽略
     if(params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
     }
     // 焦点
     result.mFocusable = view.isFocusable();
}

mOrientationHelper.calss
        public int getDecoratedMeasurement(View view) {
            LayoutParams params = (LayoutParams)view.getLayoutParams();
            return this.mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin + params.bottomMargin;
        }

可以看到 layoutChunk()中赋值layoutChunkResult,在之后偏移量添加layoutState.mOffset+=...,剩余空间减去了view消耗的数量 remainingSpace-= layoutChunkResult.mConsumed。

还是回到了刚开始的注释: 先找到锚点,计算第一个坐标, 在循环添加view、计算偏移量确定摆放位置。

2.5 布局的一些总结

来一点总结吧:

涉及到的主要类
1、 LinearLayoutManager.LayoutState:布局状态类(有布局方向(向上向下)、结束布局、偏移量...)
2、Recycler 布局支持类、可回收view、创建view...
3、AnchorInfo 锚点信息类

布局流程:
1、以开始方向先更新锚点信息
2、在通过锚点信息 赋值layoutState 得到可用空间,偏移量等信息, while(剩余空间) 摆放childrenView 改变偏移量;
3、以底部更新锚点信息
4、重复第2步
5、结束

原理其实是很简单的,只不过其中变量太多了,代码也是很多所以看得云里雾里的,有时候还会跑偏找不到南北。
画一个流程图吧。

onChildren填充机制

其中填充规则 fill()方法流程如下

fill填充规则

3、 滑动时干了哪些事情

第一次布局看起来比较简单,我们就可以去猜测 滑动时应该加载布局也是一样的。 去看看源码吧。

    public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
        return this.mOrientation == 1?0:this.scrollBy(dx, recycler, state);
    }

    public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
        return this.mOrientation == 0?0:this.scrollBy(dy, recycler, state);
    }

有两个, 分析scrollVerticallyBy

    int scrollBy(int dy, Recycler recycler, State state) {
        if(this.getChildCount() != 0 && dy != 0) {
            this.mLayoutState.mRecycle = true;
            this.ensureLayoutState();
            int layoutDirection = dy > 0?1:-1;
            int absDy = Math.abs(dy);
            this.updateLayoutState(layoutDirection, absDy, true, state);
            // 滑动且 返回消耗的距离
            int consumed = this.mLayoutState.mScrollingOffset + this.fill(recycler, this.mLayoutState, state, false);
            if(consumed < 0) {
                return 0;
            } else {
            // 计算滑动的量 
                int scrolled = absDy > consumed?layoutDirection * consumed:dy;
                this.mOrientationHelper.offsetChildren(-scrolled);
                this.mLayoutState.mLastScrollDelta = scrolled;
                return scrolled;
            }
        } else {
            return 0;
        }
    }
    
      private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, State state) {
        this.mLayoutState.mInfinite = this.resolveIsInfinite();
        this.mLayoutState.mExtra = this.getExtraLayoutSpace(state);
        this.mLayoutState.mLayoutDirection = layoutDirection;
        int scrollingOffset;
        View child;
        if(layoutDirection == 1) {
            this.mLayoutState.mExtra += this.mOrientationHelper.getEndPadding();
            child = this.getChildClosestToEnd();
            this.mLayoutState.mItemDirection = this.mShouldReverseLayout?-1:1;
            this.mLayoutState.mCurrentPosition = this.getPosition(child) + this.mLayoutState.mItemDirection;
            this.mLayoutState.mOffset = this.mOrientationHelper.getDecoratedEnd(child);
            scrollingOffset = this.mOrientationHelper.getDecoratedEnd(child) - this.mOrientationHelper.getEndAfterPadding();
        } else {
            child = this.getChildClosestToStart();
            this.mLayoutState.mExtra += this.mOrientationHelper.getStartAfterPadding();
            this.mLayoutState.mItemDirection = this.mShouldReverseLayout?1:-1;
            this.mLayoutState.mCurrentPosition = this.getPosition(child) + this.mLayoutState.mItemDirection;
            this.mLayoutState.mOffset = this.mOrientationHelper.getDecoratedStart(child);
            scrollingOffset = -this.mOrientationHelper.getDecoratedStart(child) + this.mOrientationHelper.getStartAfterPadding();
        }
        // 可用距离  偏移距离 
        this.mLayoutState.mAvailable = requiredSpace;
        if(canUseExistingSpace) {
            this.mLayoutState.mAvailable -= scrollingOffset;
        }

        this.mLayoutState.mScrollingOffset = scrollingOffset;
    }

scrollVerticallyBy 的作用是 ,改变layoutState,调用fill,布局view,返回实际消耗的距离。

fill的流程我们在上面也已经做了说明了。
补充一点的是,当我们滑动的距离在一定范围内(没有超出页面上 第一个或最后一个child在屏幕上的范围)是不会重新

4、 锚点的位置是如何确定的

从以上流程中我们了解到了layoutManager的一些布局的规则,我们的布局都是通过一个基准点来进行上下布局,那么最重要的肯定就是这个基准点即锚点的位置。
我们知道锚点的信息 保存在 LinearLayoutManager.AnchorInfo中,进而layouState控制布局

1、第一次锚点得位置

void onLayoutChildren(Recycler recycler, State state) {
}
    private void updateAnchorInfoForLayout(Recycler recycler, State state, LinearLayoutManager.AnchorInfo anchorInfo) {
        if(!this.updateAnchorFromPendingData(state, anchorInfo)) {
            if(!this.updateAnchorFromChildren(recycler, state, anchorInfo)) {
            //分配坐标   
                anchorInfo.assignCoordinateFromPadding();
            //得到数据集位置
                anchorInfo.mPosition = this.mStackFromEnd?state.getItemCount() - 1:0;
            }
        }
    }
        class AnchorInfo {
            void assignCoordinateFromPadding() {
            this.mCoordinate = this.mLayoutFromEnd?LinearLayoutManager.this.mOrientationHelper.getEndAfterPadding():LinearLayoutManager.this.mOrientationHelper.getStartAfterPadding();
            }
        }

第一次特别简单,就判断布局方向获取mCoordinate 开始或结束的padding与itemView的数据集位置position。

2、 滑动位置的确定

    int scrollBy(int dy, Recycler recycler, State state) {
            this.updateLayoutState(layoutDirection, absDy, true, state);
    }
    private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, State state) {
        this.mLayoutState.mInfinite = this.resolveIsInfinite();
        this.mLayoutState.mExtra = this.getExtraLayoutSpace(state);
        this.mLayoutState.mLayoutDirection = layoutDirection;
        int scrollingOffset;
        View child;
        if(layoutDirection == 1) {
            this.mLayoutState.mExtra += this.mOrientationHelper.getEndPadding();
            child = this.getChildClosestToEnd();
            this.mLayoutState.mItemDirection = this.mShouldReverseLayout?-1:1;
            
            this.mLayoutState.mCurrentPosition = this.getPosition(child) + this.mLayoutState.mItemDirection; 
            this.mLayoutState.mOffset = this.mOrientationHelper.getDecoratedEnd(child);
            scrollingOffset = this.mOrientationHelper.getDecoratedEnd(child) - this.mOrientationHelper.getEndAfterPadding();
        } else {
            child = this.getChildClosestToStart();
            this.mLayoutState.mExtra += this.mOrientationHelper.getStartAfterPadding();
            this.mLayoutState.mItemDirection = this.mShouldReverseLayout?1:-1;
            this.mLayoutState.mCurrentPosition = this.getPosition(child) + this.mLayoutState.mItemDirection;
            this.mLayoutState.mOffset = this.mOrientationHelper.getDecoratedStart(child);
            scrollingOffset = -this.mOrientationHelper.getDecoratedStart(child) + this.mOrientationHelper.getStartAfterPadding();
        }

        this.mLayoutState.mAvailable = requiredSpace;
        if(canUseExistingSpace) {
            this.mLayoutState.mAvailable -= scrollingOffset;
        }

        this.mLayoutState.mScrollingOffset = scrollingOffset;
    }

给张图吧

文章视频地址:链接:http://pan.baidu.com/s/1o7Ai48E 密码:98pc

recyclerView滚动时布局流程

· RecyclerView(1)- Decoration源码解析
· RecyclerView(2)- 自定义Decoration打造时光轴效果
· RecyclerView(3)- LayoutMagager源码解析,LinearLayoutManager
· RecyclerView(4)- 核心、Recycler复用机制_1
· RecyclerView(4)- 核心、Recycler复用机制_2
· RecyclerView(5)- 自定义LayoutManager(布局、复用)
· RecyclerView(6)- 自定义ItemAnimator
· RecyclerView(7)- ItemTouchHelper
· RecyclerView(8)- MultiTypeAdapter文章MultiTypeAdapter Github地址
文章视频地址:链接:http://pan.baidu.com/s/1hssvXC4 密码:18v1


希望我的文章不会误导在观看的你,如果有异议的地方欢迎讨论和指正。
如果能给观看的你带来收获,那就是最好不过了。

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

推荐阅读更多精彩内容