自定義LayoutManager之復(fù)用與回收一

轉(zhuǎn)自RecyclerView系列之四實現(xiàn)回收復(fù)用
在上面文章自定義LayoutManager中講解了LayoutManager的自定義,實現(xiàn)了界面的展示和滑動。熟悉RecyclerView的都知道其緩存機(jī)制,在添加緩存的回收與復(fù)用之前,先簡單介紹下RecyclerView的緩存機(jī)制。

1、RecyclerView的回收復(fù)用原理

1.1、RecyclerView的回收

  • 在滑動過程中,RecyclerView會將即將與之分離的ViewHolder放到mCachedViews和mRecyclerPool,可使用removeAndRecycleView(View child, Recycler recycler)函數(shù)進(jìn)行回收。
    這兩級緩存的區(qū)別是:mCachedViews是第一級緩存,大小默認(rèn)為2,數(shù)據(jù)結(jié)構(gòu)是ArrayList。當(dāng)其中數(shù)量超過2時,會根據(jù)FIFO的原則移除元素,并將移除的元素添加到mRecyclerPool中。
    mRecyclerPool是一個緩存池,本質(zhì)上它是SparseArray,key是itemViewType,value是一個ArrayList,value的大小默認(rèn)為5。

  • 除了上面說到兩級緩存還有mAttachedScrap,在onLayoutChildren中會調(diào)用函數(shù)detachAndScrapAttachedViews(recycler);將屏幕上ViewHolder進(jìn)行detach,并暫存到mAttachedScrap,再重新布局時從mAttachedScrap中取出,attach到RecyclerView上。

1.2、RecyclerView的復(fù)用

通過View view = recycler.getViewForPosition(position)可以實現(xiàn)復(fù)用,根據(jù)源碼可知,在RecyclerView中,總共有四級緩存,優(yōu)先級:mAttachedScrap>mCachedViews>mViewCacheExtension>mRecyclerPool。

  • mAttachedScrap:只保存當(dāng)前屏幕中detach的ViewHolder,在重新布局時復(fù)用。
  • mCachedViews:緩存的是剛從RecyclerView中移除的ViewHolder(通過removeAndRecycleView(view, recycler)方法),在復(fù)用時需要position或id匹配才能復(fù)用,所以只有在來回滑動過程中才會復(fù)用mCachedViews中的ViewHolder。如果不能匹配就需要從mRecyclerPool中取出ViewHolder并重新綁定數(shù)據(jù)。
  • 復(fù)用mAttachedScrap、mCachedViews中的ViewHolder是需要精確匹配的,如果能匹配上可直接使用不需綁定數(shù)據(jù),如果不能精確匹配,即使mAttachedScrap、mCachedViews中有緩存也不能取出使用,只能從mRecyclerPool中取出使用,并且需重綁數(shù)據(jù)。如果mRecyclerPool中沒有緩存就需要調(diào)用onCreateViewHolder進(jìn)行創(chuàng)建。

2、幾個函數(shù)

  • public void detachAndScrapAttachedViews(Recycler recycler)
    僅用于onLayoutChildren中,在布局前將屏幕上的ViewHolder從RecyclerView中detach掉,將其放在mAttachedScrap中,以供重新布局時使用。

  • View view = recycler.getViewForPosition(position)
    當(dāng)我們需要填充布局時,就可以調(diào)用該方法,從四個緩存容器中取出合適的View,然后添加到RecyclerView中。

  • removeAndRecycleView(child, recycler)
    該函數(shù)僅用于在滑動過程中,在滾動時,將滾出屏幕的ViewHolder進(jìn)行remove并添加到mCachedViews或mRecyclerPool中。
    可以看到,正是這三個函數(shù)的使用,可以讓我們自定義的LayoutManager具有復(fù)用功能。
    另外,還有幾個常用,但經(jīng)常出錯的函數(shù):

  • int getItemCount()
    得到的是Adapter中總共有多少數(shù)據(jù)要顯示,也就是總共有多少個item

  • int getChildCount()
    得到的是當(dāng)前RecyclerView在顯示的item的個數(shù),所以這就是getChildCount()與 getItemCount()的區(qū)別

  • View getChildAt(int position)
    獲取某個可見位置的View,需要非常注意的是,它的位置索引并不是Adapter中的位置索引,而是當(dāng)前在屏幕上的位置的索引。也就是說,要獲取當(dāng)前屏幕上在顯示的第一個item的View,應(yīng)該用getChidAt(0),同樣,如果要得到當(dāng)前屏幕上在顯示的最后一個item的View,應(yīng)該用getChildAt(getChildCount()-1)

  • int getPosition(View view)
    這個函數(shù)用于得到某個View在Adapter中的索引位置,我們經(jīng)常將它與getChildAt(int position)聯(lián)合使用,得到某個當(dāng)前屏幕上在顯示的View在Adapter中的位置。

3、自定義LayoutManager的回收和復(fù)用原理

從上面的原理中可以看到,回收復(fù)用主要有兩部分:
第一:在onLayoutChildren初始布局時:

  • 1、在布局前調(diào)用detachAndScrapAttachedViews(recycler)將所有可見的ViewHolder detach。
  • 2、通過調(diào)用recycler.getViewForPosition(position)申請一個View,并添加到RecyclerView中,直到填充滿整個屏幕。

第二:在scrollVerticallyBy滑動時

  • 1、判斷滾動dy后,那些ViewHolder需要回收,然后調(diào)用removeAndRecycleView(child, recycler)進(jìn)行回收。
  • 2、然后通過調(diào)用recycler.getViewForPosition(position)獲取View,填充空白區(qū)域。

4、為自定義LayoutManager添加回收復(fù)用

4.1、修改onLayoutChildren

上面已經(jīng)提到,在onLayoutChildren中,我們主要做兩件事:

  • 1、在布局前調(diào)用detachAndScrapAttachedViews(recycler)將所有可見的ViewHolder detach。
  • 2、通過調(diào)用recycler.getViewForPosition(position)申請一個View,并添加到RecyclerView中,直到填充滿整個屏幕。

關(guān)鍵就在于如何判斷一屏能顯示多少個item,在這里每個item高度相同,所以可以通過RecyclerView的高度處于item的高度即可

private int mItemWidth,mItemHeight;
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getItemCount() == 0) {//沒有Item,界面空著吧
        detachAndScrapAttachedViews(recycler);
        return;
    }
    detachAndScrapAttachedViews(recycler);

    View childView = recycler.getViewForPosition(0);
    measureChildWithMargins(childView, 0, 0);
    mItemWidth = getDecoratedMeasuredWidth(childView);
    mItemHeight = getDecoratedMeasuredHeight(childView);

    int visiableCount = (int) Math.ceil(getVerticalSpace() * 1.0f / mItemHeight);
if (visiableCount > itemCount)
            visiableCount = itemCount;
    …………
}       
//其中 getVerticalSpace()在上面已經(jīng)提到,得到的是RecyclerView用于顯示的高度,它的定義是:
private int getVerticalSpace() {
    return getHeight() - getPaddingBottom() - getPaddingTop();
}

一屏可見的item個數(shù)=(int) Math.ceil(getVerticalSpace() * 1.0f / mItemHeight);,這里使用Math.ceil進(jìn)行向上取整的原因就是:如果一屏內(nèi)可顯示1.5個item,此時可見的item應(yīng)該為2才對。

除此之外,由于item高度相同,為了布局方便,我們在初始化時,利用一個變量來保存在初始化時,在Adapter中每一個item的位置:

int offsetY = 0;
for (int i = 0; i < getItemCount(); i++) {
    Rect rect = new Rect(0, offsetY, mItemWidth, offsetY + mItemHeight);
    mItemRects.put(i, rect);
    offsetY += mItemHeight;
}

接下來布局可見的item,不可見的item不再布局

for (int i = 0; i < visibleCount; i++) {
    Rect rect = mItemRects.get(i);
    View view = recycler.getViewForPosition(i);
    addView(view);
    //addView后一定要measure,先measure再layout
    measureChildWithMargins(view, 0, 0);
    layoutDecorated(view, rect.left, rect.top, rect.right, rect.bottom);
}

mTotalHeight = Math.max(offsetY, getVerticalVisibleHeight());

因為,在上面我們已經(jīng)從保存了初始化狀態(tài)下每個Item的位置,所以在初始化時,直接從mItemRects中取出當(dāng)前要顯示的Item的位置,直接將它擺放在這個位置就可以了。需要注意的是,因為我們在之前已經(jīng)使用detachAndScrapAttachedViews(recycler);將所有view從RecyclerView中剝離,所以,我們需要重新通過addView(view)添加進(jìn)來。在添加進(jìn)來以后,需要走一個這個View的測量和layout邏輯,先經(jīng)過測量,再將它layout到指定位置。如果我們沒有測量直接layout,會什么都出不來,因為任何view的layout都是依賴measure出來的位置信息的。

到此,完整的onLayoutChildren的代碼如下:

private int mItemWidth, mItemHeight;
private SparseArray<Rect> mItemRects = new SparseArray<>();;
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getItemCount() == 0) {//沒有Item,界面空著吧
        detachAndScrapAttachedViews(recycler);
        return;
    }
    detachAndScrapAttachedViews(recycler);

    //將item的位置存儲起來
    View childView = recycler.getViewForPosition(0);
    measureChildWithMargins(childView, 0, 0);
    mItemWidth = getDecoratedMeasuredWidth(childView);
    mItemHeight = getDecoratedMeasuredHeight(childView);

    int visiableCount = (int) Math.ceil(getVerticalSpace() * 1.0f / mItemHeight);
    if (visiableCount > itemCount)
        visiableCount = itemCount;


    //定義豎直方向的偏移量
    int offsetY = 0;

    for (int i = 0; i < getItemCount(); i++) {
        Rect rect = new Rect(0, offsetY, mItemWidth, offsetY + mItemHeight);
        mItemRects.put(i, rect);
        offsetY += mItemHeight;
    }


    for (int i = 0; i < visibleCount; i++) {
        Rect rect = mItemRects.get(i);
        View view = recycler.getViewForPosition(i);
        addView(view);
        //addView后一定要measure,先measure再layout
        measureChildWithMargins(view, 0, 0);
        layoutDecorated(view, rect.left, rect.top, rect.right, rect.bottom);
    }

    //如果所有子View的高度和沒有填滿RecyclerView的高度,
    // 則將高度設(shè)置為RecyclerView的高度
    mTotalHeight = Math.max(offsetY, getVerticalSpace());
}

4.2、處理滾動

經(jīng)過上面的分析可知,我們可知,我們首先回收滾出屏幕的ViewHolder,然后再填充滾動后的空白區(qū)域。向上滾動和向下滾動雖然都是回收滾出屏幕的ViewHolder,但是處理邏輯還是有區(qū)別的,下面就按照滾動方向分為兩種情況進(jìn)行分析。

4.2.1、處理向上滾動

向上滾動時dy>0,這里先假設(shè)向上滾動dy,然后判斷哪些ViewHolder需要回收,需要新增哪些item,然后再執(zhí)行offsetChildrenVertical(-travel)進(jìn)行滑動。

因為在開始移動之前,我們對dy做了到頂、到底的邊界判斷并進(jìn)行了修正。

int travel = dy;
//如果滑動到最頂部
if (mSumDy + dy < 0) {
    travel = -mSumDy;
} else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
    //如果滑動到最底部
    travel = mTotalHeight - getVerticalSpace() - mSumDy;
}

所以真正移動的距離,是修正后的travel。所以在進(jìn)行處理回收和填充item,應(yīng)該以travel進(jìn)行判斷。

1、判斷回收item

在判斷向上滑動回收哪些item時,應(yīng)該遍歷當(dāng)前屏幕所有可見的item,假設(shè)讓它們向上滑動travel,然后判斷是否已經(jīng)超出了上邊界(y=0),如果超出就進(jìn)行回收。

for (int i = getChildCount() - 1; i >= 0; i--) {
    View child = getChildAt(i);
    if (travel > 0) {//需要回收當(dāng)前屏幕,上越界的View
        if (getDecoratedBottom(child) - travel< 0) {
            removeAndRecycleView(child, recycler);
        }
    }
}

在上面代碼中:

  • 首先遍歷屏幕上所有可見的item,這里getChildCount()-1表示屏幕上可見的最后一個item,注意和getItemCount()的區(qū)別。
  • 由于是獲取屏幕上可見的item,所以可以調(diào)用getChildAt(i)直接獲取。開始我不注意使用了recycler.getViewForPosition(i)去獲取了,這是從四個緩存池中獲取,很明顯不對。
  • getDecoratedBottom(child) - travel< 0表示向上移動travel后超出了上邊界,故對進(jìn)入該判斷的item進(jìn)行回收。
  • 注意這里使用removeAndRecycleView(child, recycler)方法進(jìn)行回收,而不是detachAndScrapAttachedViews(recycler)方法。在滾動時,滾出屏幕的ViewHolder應(yīng)該remove掉,而不是detach掉。在onLayoutChildren中進(jìn)行布局時,需要暫存屏幕上的ViewHolder,在再次布局時使用,此時就需要使用detachAndScrapAttachedViews(recycler)方法。
2、填充空白區(qū)域

假設(shè)向上滾動了travel后,屏幕的位置如下圖,左邊是初始狀態(tài),右邊是移動后的情況,其中綠色框表示屏幕。其實RecyclerView的滑動只是其中的內(nèi)容在滑動,這里假設(shè)內(nèi)容的位置不動,那么屏幕相對于內(nèi)容就發(fā)生滑動。

image.png

在滾動travel后,屏幕此時所在的區(qū)域如下:

private Rect getVisibleArea(int travel) {
    Rect result = new Rect(getPaddingLeft(), getPaddingTop() + mSumDy + travel, getWidth() - getPaddingRight(), getHeight()-getPaddingBottom() + mSumDy + travel);
    return result;
}

mSumDy表示已經(jīng)滑動的距離,travel表示即將滑動的距離。所以mSumDy + travel表示此時滑動后,屏幕的位置。
由于在onLayoutChildren中初始化布局時,已經(jīng)記錄每個item的初始位置,在拿到屏幕移動后的位置后,只需要和初始化item的位置進(jìn)行比對,如果存在交集就表示在屏幕內(nèi),否則表示已滑出了屏幕。

分析到這里,我們還是不知道哪些item要滑入屏幕,再回看下上圖不難看出,滑入屏幕的item無非就是在當(dāng)前屏幕中可見的最后一個item的下一個item一直到第itemCount個item中一些item將要滑入屏幕。

Rect visibleRect = getVisibleArea(travel);
//布局子View階段
if (travel >= 0) {
    View lastView = getChildAt(getChildCount() - 1);
    int minPos = getPosition(lastView) + 1;//從最后一個View+1開始吧\

    //順序addChildView
    for (int i = minPos; i <= getItemCount() - 1; i++) {
        Rect rect = mItemRects.get(i);
        if (Rect.intersects(visibleRect, rect)) {
            View child = recycler.getViewForPosition(i);
            addView(child);
            measureChildWithMargins(child, 0, 0);
            layoutDecorated(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
        } else {
            break;
        }
    }
}

mSumDy += travel;
// 平移容器內(nèi)的item
offsetChildrenVertical(-travel);

我們來看看上面代碼:
首先獲取滑動后的屏幕的位置

Rect visibleRect = getVisibleArea(travel);

然后,找到移動前最后一個可見的View

View lastView = getChildAt(getChildCount() - 1);

然后,找到它之后的一個item:

int minPos = getPosition(lastView) + 1;

然后從這個item開始查詢,看它和它之后的每個item是不是都在可見區(qū)域內(nèi),之后就是判斷這個item是不是在顯示區(qū)域,如果在就加進(jìn)來并且布局,如果不在就退出循環(huán):

for (int i = minPos; i <= getItemCount() - 1; i++) {
    Rect rect = mItemRects.get(i);
    if (Rect.intersects(visibleRect, rect)) {
        View child = recycler.getViewForPosition(i);
        addView(child);
        measureChildWithMargins(child, 0, 0);
        layoutDecorated(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
    } else {
        break;
    }
}

需要注意的是:mItemRects中記錄的是item的位置是參考屏幕(0,0),在向上滾動時,我們需要把高度減去滑動的距離,這樣才能實現(xiàn)滾入屏幕。注意這個滑動距離并不包括即將滑動的距離travel,雖然我們判斷哪些item是新增顯示時,假設(shè)移動了travel,其實到目前為止并沒有發(fā)生滾動。所以我們在布局時,仍然需要按上次的移動距離來進(jìn)行布局,所以這里在布局時使用是layoutDecorated(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy),單純只是減去了mSumDy,并沒有同時減去mSumDy和travel,最后才調(diào)用offsetChildrenVertical(-travel)來整體移動布局好的item。這時才會把我們剛才新增布局上的item顯示出來。
所以,此時完整的scrollVerticallyBy的代碼如下:

public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getChildCount() <= 0) {
        return dy;
    }

    int travel = dy;
    //如果滑動到最頂部
    if (mSumDy + dy < 0) {
        travel = -mSumDy;
    } else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
        //如果滑動到最底部
        travel = mTotalHeight - getVerticalSpace() - mSumDy;
    }

    //回收越界子View
    for (int i = getChildCount() - 1; i >= 0; i--) {
        View child = getChildAt(i);
        if (travel > 0) {//需要回收當(dāng)前屏幕,上越界的View
            if (getDecoratedBottom(child) - travel < 0) {
                removeAndRecycleView(child, recycler);
            }
        }
    }
    
    Rect visibleRect = getVisibleArea(travel);
    //布局子View階段
    if (travel >= 0) {
        View lastView = getChildAt(getChildCount() - 1);
        int minPos = getPosition(lastView) + 1;//從最后一個View+1開始吧

        //順序addChildView
        for (int i = minPos; i <= getItemCount() - 1; i++) {
            Rect rect = mItemRects.get(i);
            if (Rect.intersects(visibleRect, rect)) {
                View child = recycler.getViewForPosition(i);
                addView(child);
                measureChildWithMargins(child, 0, 0);
                layoutDecorated(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
            } else {
                break;
            }
        }
    }

    mSumDy += travel;
    // 平移容器內(nèi)的item
    offsetChildrenVertical(-travel);
    return travel;
}

此時已經(jīng)實現(xiàn)了向上滾動的功能,并在向上滾動過程中,回收滑動屏幕的ViewHolder。

4.2.2、處理向下滾動

在分析完向上滾動的處理后,向下滾動的處理就很簡單了,和向上滾動是完全相反的。

1、判斷回收item

遍歷當(dāng)前屏幕可見的item,假設(shè)向下移動travel后,判斷哪些item滑出了屏幕的底部,回收滑出的item即可。

for (int i = getChildCount() - 1; i >= 0; i--) {
    View child = getChildAt(i);
    if (travel > 0) {//需要回收當(dāng)前屏幕,上越界的View
        …………
    }else if (travel < 0) {//回收當(dāng)前屏幕,下越界的View
        if (getDecoratedTop(child) - travel > getHeight() - getPaddingBottom()) {
            removeAndRecycleView(child, recycler);
        }
    }
}

getDecoratedTop(child) - travel得到移動travel距離后item的頂部位置,然后判斷是否大于屏幕底部的位置getHeight() - getPaddingBottom(),若大于則表示滑動了屏幕。

2、為滾動后的空白處填充Item

向下滾動,RecyclerView的頭部位置滾動后會有空白,故可以從當(dāng)前屏幕可見的第一個item的上一個開始遍歷,到第0個item結(jié)束,判斷哪些item在屏幕內(nèi),將在屏幕內(nèi)的item添加進(jìn)來。

Rect visibleRect = getVisibleArea(travel);
//布局子View階段
if (travel >= 0) {
    …………
} else {
    View firstView = getChildAt(0);
    int maxPos = getPosition(firstView) - 1;

    for (int i = maxPos; i >= 0; i--) {
        Rect rect = mItemRects.get(i);
        if (Rect.intersects(visibleRect, rect)) {
            View child = recycler.getViewForPosition(i);
            addView(child, 0);
            measureChildWithMargins(child, 0, 0);
            layoutDecoratedWithMargins(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
        } else {
            break;
        }
    }
}

下面來看看這段代碼:
在這里,先得到在滾動前顯示的第一個item的前一個item:

View firstView = getChildAt(0);
int maxPos = getPosition(firstView) - 1;

如果在顯示區(qū)域,那么,就將它插在第一的位置:

 addView(child, 0);

同樣,在布局Item時,由于還沒有移動,所以在布局時并不考慮travel的事:layoutDecoratedWithMargins(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy)
其它的代碼都很好理解了,這里就不再講了。
這樣就完整實現(xiàn)了滾動的回收和復(fù)用功能了,完整的scrollVerticallyBy代碼如下:

public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getChildCount() <= 0) {
        return dy;
    }

    int travel = dy;
    //如果滑動到最頂部
    if (mSumDy + dy < 0) {
        travel = -mSumDy;
    } else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
        //如果滑動到最底部
        travel = mTotalHeight - getVerticalSpace() - mSumDy;
    }

    //回收越界子View
    for (int i = getChildCount() - 1; i >= 0; i--) {
        View child = getChildAt(i);
        if (travel > 0) {//需要回收當(dāng)前屏幕,上越界的View
            if (getDecoratedBottom(child) - travel < 0) {
                removeAndRecycleView(child, recycler);
                continue;
            }
        } else if (travel < 0) {//回收當(dāng)前屏幕,下越界的View
            if (getDecoratedTop(child) - travel > getHeight() - getPaddingBottom()) {
                removeAndRecycleView(child, recycler);
            }
        }
    }

    Rect visibleRect = getVisibleArea(travel);
    //布局子View階段
    if (travel >= 0) {
        View lastView = getChildAt(getChildCount() - 1);
        int minPos = getPosition(lastView) + 1;//從最后一個View+1開始吧

        //順序addChildView
        for (int i = minPos; i <= getItemCount() - 1; i++) {
            Rect rect = mItemRects.get(i);
            if (Rect.intersects(visibleRect, rect)) {
                View child = recycler.getViewForPosition(i);
                addView(child);
                measureChildWithMargins(child, 0, 0);
                layoutDecorated(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
            } else {
                break;
            }
        }
    } else {
        View firstView = getChildAt(0);
        int maxPos = getPosition(firstView) - 1;

        for (int i = maxPos; i >= 0; i--) {
            Rect rect = mItemRects.get(i);
            if (Rect.intersects(visibleRect, rect)) {
                View child = recycler.getViewForPosition(i);
                addView(child, 0);//將View添加至RecyclerView中,childIndex為1,但是View的位置還是由layout的位置決定
                measureChildWithMargins(child, 0, 0);
                layoutDecoratedWithMargins(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
            } else {
                break;
            }
        }
    }

    mSumDy += travel;
    // 平移容器內(nèi)的item
    offsetChildrenVertical(-travel);
    return travel;
}

到此為止,我們已經(jīng)為LayoutManager增加了回收復(fù)用的功能,但是這里我們調(diào)用offsetChildrenVertical(-travel)來實現(xiàn)平移,當(dāng)需要實現(xiàn)在平移時,改變每個item的大小、角度等參數(shù)時,offsetChildrenVertical(-travel)就無法完成了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內(nèi)容