關于RecyclerView你知道的不知道的都在這了(上)

本篇文章已授權微信公眾號 dasu_Android(大蘇)獨家發布

最近打算花點精力來研究 RecyclerView 這個控件架構和原理,對我來說,難度很大,我不清楚最后能不能徹底搞清楚,這個系列的博客會不會被太監,但我會盡我最大努力,并將這整個過程分享出來。

第一篇打算從使用方面入手,力求將 RecyclerView 開放給開發人員的所有接口都體驗一番。

前言

雖然在日常開發中,大伙或多或少都會接觸到 RecyclerView,但通常,也就是寫寫 adapter,用個系統提供的 LayoutManager,寫寫點擊事件,處理處理復雜的 item 布局。

也就是說,大部分場景下,我們其實并不會去接觸到 RecyclerView 的大部分其他功能,比如自定義 LayoutManager ,自定義 Item 動畫,自定義邊界樣式,自定義滑動效果,自定義回收策略等等之類的功能。

那么,本篇就專門來試用下這些功能,力求將 RecyclerView 支持的所有功能都試一遍,只有清楚了這個控件都支持哪些功能效果,那么分析起它的架構、原理才會有一個比較清晰的脈絡。

目錄

由于本篇篇幅特長,特意做了個目錄,讓大伙對本篇內容先有個大概的了解。

另外,由于有些平臺可能不支持 [TOC] 解析,所以建議大伙可借助本篇目錄,或平臺的目錄索引進行快速查閱。

  1. LayoutManager

    1.1 LinearLayoutManager

    • 基本效果介紹
    • findFirstCompletelyVisibleItemPosition()
    • findFirstVisibleItemPosition()
    • findLastCompletelyVisibleItemPosition()
    • findLastVisibleItemPosition()
    • setRecycleChildrenOnDetach()

    1.2 GridLayoutManager

    • 基本效果介紹
    • setSpanSizeLookUp()

    1.3 StaggeredGridLayoutManager

    • 基本效果介紹
    • setFullSpan()
    • findXXX() 系列方法介紹
  2. ViewHolder

    • getAdapterPosition()
    • getLayoutPosition()
    • setIsRecyclable()
  3. LayoutParams

  4. Adapter

    • 基本用法介紹
    • onViewRecycled()
    • onViewAttachedFromWindow()
    • onViewDetachedFromWindow()
    • onAttachedToRecyclerView()
    • onDetachedFromRecyclerView()
    • registerAdapterDataObserver()
    • unregisterAdapterDataObserver()
  5. RecyclerView

    • addOnItemTouchListener()
    • addOnScrollListener()
    • setHasFixedSize()
    • setLayoutFrozen()
    • setPreserveFocusAfterLayout()
    • findChildViewUnder()
    • findContainingItemView()
    • findContainingViewHolder()
    • findViewHolderXXX()
  6. Recycler

    • setItemViewCacheSize()
    • setViewCacheExtension()
    • setRecycledViewPool()
    • setRecyclerListener()
  7. ItemAnimator

    7.1 SimpleItemAnimator

    7.2 DefaultItemAnimator

  8. ItemDecoration

    8.1 DividerItemDecoration

    8.2 ItemTouchHelper

    8.3 FastScroller

  9. OnFlingListener

    9.1 SnapHelper

    9.2 LinearSnapHelper

    9.3 PagerSnapHelper

正文

閱讀須知:

  • 本篇力求列舉 RecyclerView 所有功能的使用示例,由于篇幅原因,并不會將實現代碼全部貼出,只貼出關鍵部分的代碼。
  • 本篇所使用的 RecyclerView 的版本是 26.0.0。
  • 下列標題中,但凡是斜體字,表示該知識點目前暫時沒理清楚,留待后續繼續補充。
  • 第 6 章至第 9 章內容在下篇:關于RecyclerView你知道的不知道的都在這了(下)

1. LayoutManager

RecyclerView 的 support 包里默認提供了三個 LayoutManager,分別是下列三個,可用于實現大部分場景的布局需求:線性布局、網格布局、瀑布流布局等等。

1.1 LinearLayoutManager

線性布局,用它可以來實現橫豎自由切換的線性布局,先來看看它的構造函數:

public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
    
    public LinearLayoutManager(Context context) {
        this(context, VERTICAL, false);
    }

    public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
        ...
    }

    public LinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        ...
    }
}

總共三個,我們分別來看看它們各自的使用場景:

  • 第一個構造函數
//用法(在Activity里初始化控件后):
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(layoutManager);

很簡單,這種時候默認就是豎直方向的線性布局,效果圖:

豎直LinearLayoutManager示例.png

在 Tv 應用中,這種豎直方向的 LinearLayoutManager 使用場景大多都是用于顯示菜單項,使用頻率并不是特別高,但在手機應用中,這種的使用頻率算是特別高的了,幾乎每個 app 都會有豎直方向的滑動列表控件。

  • 第二個構造函數
//用法(在Activity里初始化控件后):
//第二個參數就是用于指定方向是豎直還是水平,第三個參數用于指定是否從右到左布局,基本都是false,我們的習慣都是左到右的排列方式
LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
mRecyclerView.setLayoutManager(layoutManager);

第二個參數就是用于指定方向是豎直還是水平,第三個參數用于指定是否從右到左布局,基本都是false,我們的習慣都是左到右的排列方式,來看看效果:

水平LinearLayoutManager.png

在 Tv 應用中,這種布局就比較常見了,常見的還有網格布局,多行布局等等;而在手機應用中,水平滑動的列表控件也還是有,但會比豎直的少見一些。

  • 第三個構造函數
//xml文件:
<android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_main"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layoutManager="LinearLayoutManager"
        />

這種方式基本沒見過吧,我也是看了 LinearLayoutManager 源碼的構造函數,才發現,原來還有這種方式,可以直接在 xml 布局文件中指定 RecyclerView 的 LayoutManager,這時候,android:orientation 就是用來指定 LinearLayoutManager 的布局方向了。

那么使用這種 xml 方式時,還有哪些屬性可以配置呢?直接去看對應的 LayoutManager 的源碼就清楚了,比如:

    //LinearLayoutManager.java
    /**
     * Constructor used when layout manager is set in XML by RecyclerView attribute
     * "layoutManager". Defaults to vertical orientation.
     *
     * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_android_orientation
     * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_reverseLayout
     * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_stackFromEnd
     */
    //上面是源碼的注釋,當在 xml 中通過 app:layoutManager="LinearLayoutManager" 之后,那么此時就還可以再使用三個屬性來配置 LinearLayoutManager,如下:
    //android:orientation="horizontal"
    //app:reverseLayout="false"
    //app:stackFromEnd="false"
    public LinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes);
        setOrientation(properties.orientation);
        setReverseLayout(properties.reverseLayout);
        setStackFromEnd(properties.stackFromEnd);
        setAutoMeasureEnabled(true);
    }

另外兩個 LayoutManager 同理。

以上,僅僅就是 LinearLayoutManager 支持的布局樣式,我們只需要設置布局方向后,其他都不用管了。那么,LinearLayoutManager 是否還有提供其他一些可選功能來讓我們使用呢?接下去就一起再看看:

  • setOrientation()

用于設置布局方向,如果不通過構造函數來指定,也可以通過該方法指定,就兩個值:
LinearLayoutManager.HORIZONTAL
LinearLayoutManager.VERTICAL

  • findFirstCompletelyVisibleItemPosition()
  • findFirstVisibleItemPosition()
  • findLastCompletelyVisibleItemPosition()
  • findLastVisibleItemPosition()
findItem示例.png
findItem日志.png

上述四個方法作用從方法命名就可以很直觀的理解了,但有些細節需要注意一下:

兩個查找全部可見的 item 方法并不是我們正常意義上的全部可見,而是指在布局方向上是否已全部可見。說得白點,如果是 HORIZONTAL 水平樣式,如上圖,那么它只會去計算左右方向上是否全部可見來判定,比如我們特意在代碼中通過 layout_marginTop="-100dp" 來將控件移出屏幕一部分,如下:

部分可見.png

此時,按照我們正常意義上來理解是沒有一個 item 處于全部可見的,因為每個 item 的上半部分都被移出屏幕了。但是調用那兩個查找全部可見的 item 方法,仍然會返回 0 和 4,因為它只去判斷水平方向是否全部可見。

findFirst 就是判斷左邊第一個 item 的左邊界是否可見,findLast 就是判斷右邊最后一個 item 的右邊界是否可見。如果布局方向是豎直的,那么同樣的道理。這點細節需要注意一下。

還有另外兩個查找第一個或最后一個可見的 item 方法也有個細節需要注意一下,如果這個 item 是有設置了 ItemDecoration,那么如果 ItemDecoration 這部分區域是可見的,也會判定該 item 是可見的。

  • setRecycleChildrenOnDetach()
    /**
     * Set whether LayoutManager will recycle its children when it is detached from
     * RecyclerView.
     * <p>
     * If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set
     * this flag to <code>true</code> so that views will be available to other RecyclerViews
     * immediately.
     * <p>
     * Note that, setting this flag will result in a performance drop if RecyclerView
     * is restored.
     *
     * @param recycleChildrenOnDetach Whether children should be recycled in detach or not.
     */
    public void setRecycleChildrenOnDetach(boolean recycleChildrenOnDetach) {
        mRecycleChildrenOnDetach = recycleChildrenOnDetach;
    }

先來看看源碼注釋,注釋里說了,這個方法是用來設置,當它(LinearLayoutManager)從 RecyclerView 上面 detached 時是否要回收所有的 item。而且,它還建議我們,如果我們項目里有復用 RecyclerViewPool 的話,那么開啟這個功能會是一個很好的輔助,它可以將這些 item 回收起來給其他 RecyclerView 用。最后,還指明了一點,開啟這個功能的話,當 RecyclerView 恢復時,也就是從 detached 又變回 attached,那么會消耗一定的性能來繪制。

兩種場景會導致 LinearLayoutManager 從 RecyclerView 上被 detached,一種是:setLayoutManager(),而另外一種是:RecyclerView 從視圖樹上被 remove 掉。

但經過測試(你也可以去看源碼),setLayoutManager() 時,如果之前有設置過 LayoutManger,那么內部會自動先去將之前 LayoutManager 的所有 item 回收,然后再給新的 LayoutManager 復用。此時,這個方法并沒有什么卵用。

也就是說,上面說了有兩種場景會觸發到該方法開啟的回收工作,但實際上,第一種場景內部默認的工作中就包含了回收工作,那么有沒有通過這個方法來開啟并沒有任何影響。只有第二種場景下,要不要去處理回收工作才是由該方法來控制。

所以我懷疑是不是 Google 工程師太懶了,沒有同步更新這個方法的注釋。注釋的第一句 when 后面應該改成:

Set whether LayoutManager will recycle its children when RecyclerView is detached from Window.

我覺得這樣才比較合理一點,但純屬個人觀點哈,也許是我某個地方理解錯了。

那么這個方法開啟的回收工作到底有什么使用場景呢?

這類場景還是有的,我舉個例子,比如當前頁面是通過 ViewPager + Fragment 來實現的,每個 Fragment 里又有 RecyclerView 控件,那么如果當頁面布局信息需要更新時,有時候是直接暴力的通過 ViewPager 的 setAdapter() 來刷新,那么此時,舊的 fragment 其實就全被移除掉了,然后 new 了新的 fragment 繪制新的布局信息。

這樣,新的 fragment 里新的 RecyclerView 的 item 就又需要全部重新創建了,如果用這個方法開啟了回收工作,那么當舊的 fragment 被移除時會觸發到 RecyclerView 的 detachedFromWindow 的回調,那么此時這個回收工作就會去將 item 回收到 RecyclerViewPool 中,如果新的 fragment 里的 RecyclerView 復用了這個 RecyclerViewPool,就可以省掉重新創建 item 的消耗,達到直接復用 item 的效果。

小結一下,其實也就是 RecyclerView 有更換新的實例對象時,這個方法開啟的回收工作是有一定的好處的。但如果同一個 RecyclerView 實例對象存在從 attached 到 detached 又到 attached 的場景,默認沒有開啟回收工作時,由于 item 一直都附著在 RecyclerView 上,所以當重新 attached 時就可以直接顯示出來了。但如果用該方法開啟了回收工作,等于是要重新在 onBind 一次了,這點也是在注釋中有提到的。

所以,這是一把雙刃劍,有好有壞,有符合的場景下再去開啟使用吧。

  • RecyclerView 內嵌 RecyclerView

另外,LayoutManager 里還有許多 public 的接口,這些方法涉及的方面是 RecyclerView 內嵌 RecyclerView 的場景,比如:
collectInitialPrefetchPositions()
setInitialPrefetchItemCount()
等等,但目前還沒搞懂這些相關方法的用法及效果,等待后續補充。

1.2 GridLayoutManager

網格樣式的布局管理器,同樣,先來看看它的構造函數:

//注意看,GridLayoutManager 是繼承的 LinearLayoutManger 的
public class GridLayoutManager extends LinearLayoutManager {  
    
    public GridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        ...
    }

    public GridLayoutManager(Context context, int spanCount) {
        super(context);
        setSpanCount(spanCount);
    }

    public GridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) {
        super(context, orientation, reverseLayout);
        setSpanCount(spanCount);
    }   
}

GridLayoutManager 繼承自 LinearLayoutManager, 并在它的繼承上補充了 spanCount 的概念,也就是說 LinearLayoutManager 是只支持線性布局,要么一行,要么一列。而 GridLayoutManager 補充了 spanCount 概念后,支持多行或者多列,這就是網格布局了。

使用方面跟 LinearLayoutManager 基本一樣,只是在構造函數內需要多傳一個 spanCount 參數,來指定多少行或多少列,來看看效果圖:

  • 2 行
GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 2, LinearLayoutManager.HORIZONTAL, false);
mRecyclerView.setLayoutManager(gridLayoutManager);
兩行.png
  • 4 列
GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 4);
mRecyclerView.setLayoutManager(gridLayoutManager);
四列.png

這種網格布局不管是 Tv 應用還是手機應用都挺常見的,Tv 上經常需要有多行或多列的形式來展示各個卡位信息,而手機上一些類似于九宮格之類的布局也可以用這個實現。

但有一些細節同樣需要注意一下:

如果指定 HORIZONTAL 樣式,即以多行形式進行布局,那么 item 布局的順序則是以豎直方向來進行,如上圖中標注的 item 序號,并且,此時的 RecyclerView 只支持水平方向的滑動,不支持豎直方向。如果指定 VERTICAL 樣式,則相反。

其實想想也很容易理解,GridLayoutManager 是繼承自 LinearLayoutManager,只是在它基礎上補充了 spanCount 概念,滑動的實現還是延用 LinearLayoutManager 的邏輯,那么如果指定水平樣式,自然就只有水平方向可滑動。

當設置成水平樣式,水平方向可滑動的話,那么水平方向的長度自然就是可根據 item 數量動態增加的,此時自然要按照豎直方向來進行 item 布局,否則還以行為優先的話,哪里知道盡頭是哪里,什么時候該換行布局了。

還有一點細節需要注意,當使用 GridLayoutManager 時,RecyclerView 的寬高在 match_parent 和 wrap_content 兩種情況下的表現完全不一樣,具體表現怎樣,有興趣的可以去試一下,這里就簡單舉個例子給大伙有個直觀印象:

  • 4 列,RecycerView 寬高為 wrap_content 模式,item 設置具體寬高數值
四列.png
  • 4 列,RecyclerView 寬高為 match_parent 模式,item 設置具體寬高數值
四列2.png

簡單點說,就是在 match_parent 模式下,如果指定了水平樣式,那么在豎直方向上,GridLayoutManager 會保證讓所有行都顯示出來,如果 item 指定了具體寬高,全部顯示出來還不足以鋪滿 RecyclerView,那么會自動將剩余空間平均分配到每個 item 之間的間隙。

如果 RecyclerView 高度不足以讓所有行都顯示出來,那么就會出現 item 重疊現象。這就是在 match_parent 下的表現,至于 wrap_content 則完全根據 item 設定的寬高來考慮了,不會再有自動分配剩余空間或者 Item 重疊之類的工作了。

所以,使用 GridLayoutManager 時,RecyclerView 的寬高模式需要注意一下。

  • setSpanCount()

通過構造函數指定了 spanCount 后也還可以繼續通過該方法進行修改

  • LinearLayoutManager 的方法

由于是繼承關系,所有 LinearLayoutManager 中的四個 findFirstCompletelyVisibleItemPosition() 方法一樣可以使用,但在 LinearLayoutManager 一節中對這四個方法所講的注意事項在這里就更加明顯了,使用時需要注意一下。

  • setSpanSizeLookup()

通常情況下,網格布局樣式下,每個小格的大小基本都是一樣的,但如果我們想實現如下的效果呢:

網格示例.png

區別于常見的網格布局,這里有的小格就占據了多個網格,這種效果就可以通過該方法來實現了。

上述布局是設定了 HORIZONTAL 水平方向的 GridLayoutManager,并且設定為 3 行,默認情況下每個 item 占據一個小格,按照豎直方向依次布局。

通過 setSpanSizeLookup() 方法就可以自定義為每個 item 指定它在豎直方向要占據多少個小格,最多不超過設定的行數,上述例子中每個 item 最多就只能占據 3 行的高度。如果在該列的剩余空間不足 item 設定占據的行數,那么會將該列剩余的空間空閑出來,將該 item 移到下列進行布局。

同樣的道理,當設定為 VERTICAL 豎直方向的樣式時,那么可以自定義為每個 item 設定要占據的列數,最多不超過指定的列數。

示例

GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 3, LinearLayoutManager.HORIZONTAL, false);
//自定義item占據的小格大小時需要重寫 getSpanSize(),返回值就是占據的小格數量
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    @Override
    public int getSpanSize(int position) {    
        //以下代碼僅為上圖示例為寫,具體場景中應該根據需求具體編寫
        if (position == 3) {
            return 2;
        }
        if (position == 7) {
            return 3;
        }
         return 1;
    }
    
    //這個方法也很重要,但我還沒搞清楚它的具體效果,從注釋上來看,該方法是用于指定 item 在該行或該列上具體哪個位置,比如將GridLayoutManager設置為3行水平樣式,那么第1個卡位就是在第一列的 0 位置,第2個卡位 1,一次類推。但該方法具體被調用的場景還沒理清
    @Override
    public int getSpanIndex(int position, int spanCount) {
          return super.getSpanIndex(position, spanCount);
    }
});
//官方建議說,如果延用默認的 getSpanIndxe() 的實現邏輯的話,那么建議調用下述方法來進行優化,否則每次布局計算時會很耗性能。 
gridLayoutManager.getSpanSizeLookup().setSpanIndexCacheEnabled(true);
mRecyclerView.setLayoutManager(gridLayoutManager);

雖然提供了該方法讓網格布局可以更加多樣化布局,但仍然無法滿足一些場景,比如當設定為多行的樣式時,此時就只支持自定義每個 item 占據的行數,只有行數!也就是說,所有的卡位頂多只會在高度方面不一樣,同一列的卡位的寬度都是一樣的。那么,如果需求是五花八門的網格布局,每個卡位都有可能占據多行的情況下又占據多列,用這個就沒法實現了。

1.3 StaggeredGridLayoutManager

英文直譯過來是:交錯式的網格布局管理者,不過我還是喜歡網上大伙的說法:瀑布流。

首先,也還是來看看它的構造方法:

public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager implements RecyclerView.SmoothScroller.ScrollVectorProvider {  
    
    public StaggeredGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        ...
    }

    public StaggeredGridLayoutManager(int spanCount, int orientation) {
        ...
    }
}

只有兩個構造方法,第一個跟 LinearLayoutManager 一樣,用于在 xml 布局文件中直接指定 LayoutManager 時用的。

第二個構造方法才是我們經常使用它的入口,兩個參數,說白點就是用來設置成多行的瀑布流或者多列的瀑布流樣式。

這里順便提一點不怎么重要的,注意到沒有,這里的構造方法是不需要 Context,那么為啥另外兩個 LayoutManager 卻需要呢?它們之間有什么不同么?

哈哈哈,答案是沒啥不同,LinearLayoutManager 實際上也是不需要 Context 的,看看它的源碼就會發現它根本沒使用這個參數,可能是早期版本有需要用到,然后新版不需要了,為了讓開發者兼容舊代碼,就一直留著的吧。

  • 豎直方向瀑布流
StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(staggeredGridLayoutManager);
瀑布流.png

瀑布流的樣式在手機應用上比較常見,尤其圖片查看相關的應用,在 Tv 應用上這種瀑布流布局就比較少見了。

瀑布流的方向可以選擇水平或者豎直,兩者只是方向上的區別而已,水平方向的效果圖就不貼了。

有點細節需要注意一下,瀑布流樣式在布局 item 時,并不是說一定按照某個方向某個順序來布局。當設定為豎直方向時,以水平方向為順序,尋找水平方向上最靠近頂端的位置來布局 item,所以并不是說一定按照第 1 列、第 2 列、第 3 列這種順序來布局。

  • 瀑布流樣式和網格樣式的區別

也許有人會疑惑,瀑布流就是設置下幾行或者幾列,然后設定下方向而已。網格樣式時不也一樣是設置下幾行或幾列,也一樣是要再設置個方向。那么為什么瀑布流不可以直接用網格樣式來實現呢?它們兩者有什么區別么?

有去嘗試過的就清楚了,這是兩種完全不一樣的布局樣式。下面以兩者都設置為豎直方向多列的樣式來區分:

  1. 網格樣式每一行中的所有 item 高度是一致的,不同行可以不一樣,但同行的都是一樣的,因此它就實現不了瀑布流的樣式了;瀑布流所有的 item 高度都允許不一樣,所有能實現瀑布流樣式。
  2. 網格樣式支持 item 占據多列的寬度;瀑布流支持 item 占據總列數的寬度,不支持只占據其中幾列。
  3. 當設置為水平方向樣式時,以上結論中行列對調,寬度高度對調。
  • setFullSpan()

該方法是 StaggeredGridLayoutManager 內部類 LayoutParams 的方法,用這個方法可以設置 item 是否要占據總寬度或總高度,當瀑布流中有某個 item 需要橫穿的場景時,可以使用這個方法,效果如下:

瀑布流示例.png
  • setOrientation()
  • setSpanCount()

不解釋,上面兩個 LayoutManager 中介紹過了。

  • findFirstCompletelyVisibleItemPositions()
  • findFirstVisibleItemPositions()
  • findLastCompletelyVisibleItemPositions()
  • findLastVisibleItemPositions()

作用跟 LinearLayoutManager 的一樣,但有些許區別,因為這里需要傳入 int[] 類型的參數,返回的結果也是 int[] 類型的。

就以上上圖的布局為例,來看下打出來的日志:

日志.png

得到的結果是個數組,數組的大小就是構造方法中傳入的 spanCount。

簡單點說,上面四個方法的作用,是以每行或每列為單位來尋找相對應的首個(末個)可見或完全可見的 item。

為什么要這么做呢?

我想了想,還是想不出比較合理的解釋,大概硬套了下,感覺也許是因為瀑布流的布局下是沒辦法確定 item 的大小的,如果還像 LinearLayoutManager 只尋找首個或末個完全可見的 item 時,也許它并不是處于當前屏的最頂部或最底部,就像上圖日志中的 position=7 的 item,它雖然是最后完全可見的 item,但并不是位于最底部,最底部是 6 的 item。

在這種場景下,如果我們的需求是要找到處于最底部的 item 時,如果還只是像 LinearLayoutManager 只尋找最后完全可見的 item 時,就沒辦法做到了。那么,如果你想說,那干脆將尋找最后一個完全可見 item 改成尋找位于最底部的完全可見的 item,不就好了。那如果這時我的需求是要尋找最后一個 item 而不是最底部的呢?

所以,瀑布流它直接以每行或每列為單位,將該行/列的首(末)個可見或完全可見的 item 信息都全部給我們,我們需要哪些數據,是最后一個,還是最底部一個,就自行去處理這些信息好了。

以上,純屬個人觀點。

  • setGapStrategy()
  • invalidateSpanAssignments()

這兩個方法還沒理清它們是干嘛用的,網上有資料說是用于解決滑動時 item 自動變換位置以及頂部留白問題,但我不是很清楚,后續有時間再繼續查證。

2. ViewHolder

ViewHolder 大伙也不陌生了,但沒想到我會單獨開個小節來講吧,也是,平時使用時頂多就是繼承它,然后重寫一下構造方法而已,但其實,它本身攜帶著很多信息,利用得當的話,可以方便我們處理很多事情。

  • getAdapterPosition()
  • getLayoutPosition()

將這兩個放在一起講,因為這兩個很類似,不理清它們之間的區別的話,很容易搞亂,源碼中的注釋其實已經說得很清楚了。

在大部分場景下,這兩個的值都是一樣的,但在涉及到刷新時,由于 Android 是每隔 16.6 ms 刷新一次屏幕,如果在某一幀開始時,adapter 關聯的數據源發生的變化,item 被移除或者新增了,我們一般都會調用 notifyDataSetChanged() 或者 notifyItem系列() 方法來刷新,但 RecyclerView 會直到下個幀來的時候才會去刷新界面。

那么,從調用了 notifyDataSetChanged() 到界面刷新這之間就會存在一定的時間差,在這段時間內,數據源與界面呈現的 Item 就不是一致性的了,如果這時候有需要區分實際數據源的 Item 和界面呈現 Item 的需求,那么這兩個方法就派上用場了。

getLayoutPosition():返回的一直是界面上呈現的 Item 的位置信息,即使某個 Item 已經從數據源中被移除。

getAdapterPosition():當數據源發生變化,且界面已經刷新過后即 onBindViewHolder() 已經被調用了后,返回的值跟 getLayoutPosition() 一致;但當數據源發生變化,且在 onBindViewHolder() 被調用之前,如果調用了 notifyDataSetChanged(), 那么將返回無效的位置標志 -1;如果調用了 notifyItem系列(),那么將返回 Item 在數據源中的位置信息。

示例場景:

mDataList.remove(0);
//1. 場景1
mAdapter.notifyDataSetChanged();
logPosition();

//2. 場景2
mAdapter.notifyItemRemove(0);
logPosition();

//3. 場景3
mAdapter.notifyItemRemove(0);
mRecyclerView.post(new Runnable() {
    @Override
    public void run() {
        logPosition();
    }
})

private void logPosition() {
    for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
        View view = mRecyclerView.getChildAt(i);
        int layPosi = mRecyclerView.findContainingViewHolder(view).getLayoutPosition();
        int adapterPosi = mRecyclerView.findContainingViewHolder(view).getAdapterPosition();
        int oldPosi = mRecyclerView.findContainingViewHolder(view).getOldPosition();
        LogUtils.d(TAG, "getLayoutPosition = " + layPosi);
        LogUtils.d(TAG, "getAdapterPosition = " + adapterPosi);
    }
}

場景1:由于數據源發生變化后,調用了 notifyDataSetChanged(),在這之后馬上去遍歷界面上的 Item 元素,分別輸出 ViewHolder 的幾個方法,那么打日志的時間點肯定是在界面刷新之前,所以可以看到這些方法的區別:

場景1日志.png

0 position 的 Item 明明已經從數據源中被移除掉了,但由于日志打印的時機是在界面刷新之前,因此可以看到通過 getLayoutPosition() 獲取到的是界面上還未刷新之前的 Item 的信息,而由于是調用了 notifyDataSetChanged() 去通知,因此 getAdapterPosition() 對于所有 Item 都返回無效的位置標志 -1。

場景2:同理,這次也是在數據源發生變化,界面刷新之前就去打印日志了,但是是通過 notifyItemRemove() 通知,這個時候 getAdapterPosition() 方法返回的值跟上面就有所差別了:

場景2日志.png

由于這次是通過 notifyItemRemove() 方法來通知的,因此,此時可以通過 getAdapterPositon() 來獲取到界面還未刷新之前的 Item 的實際在數據源中的 position 信息。position = 0 的 Item 由于已經從數據源中移除,因此返回 -1,之后的所有 Item 位置自動向前移 1 位。

場景3:上面講解時一直強調說,只有在數據源發生變化且界面刷新之前,這兩個方法才會有所區別,所以場景 3 就來模擬一下,通過 mRecyclerView.post() 的工作由于消息隊列的同步屏障機制會被延遲到下一幀的屏幕刷新之后才執行(詳情翻看我的歷史博客),所以可以來比較下兩次日志的區別,你就清楚了:

場景3日志.png

左邊的日志是場景 2 所打的日志,右邊的日志是場景 3 下的日志。由于場景 3 將日志的執行時機延遲到下一幀的界面刷新之后,所有,可以看到,界面刷新之后,原本的第一個 Item 就被移除掉了。既然界面已經刷新了,那么數據源和界面的呈現其實就是一致的了,所以 getLayoutPosition() 返回的值就跟 getAdapterPosition() 是一致的了。

小結:說得白點,getLayoutPosition() 會返回 Item 在界面上呈現的位置信息,不管數據源有沒有發生變化,界面是否已刷新,總之你在界面上看到的 Item 在哪個位置,這個方法就會返回那個位置信息,注釋里也說了,我們大部分場景下,使用這個方法即可。

getAdapterPosition() 的使用場景是,當數據源發生變化,且界面刷新之前,你又需要獲取 Item 在數據源中的實際位置時才需要考慮使用該方法。另外,使用該方法時,還要注意你是用哪種 notifyXXX 來通知刷新。這個方法的實際應用場景我還沒遇到過,后續有用到再繼續補充。

  • getOldPosition()

這個看注釋說是用于處理動畫時用的,但還沒找到相關的場景,也沒理解具體有啥樣,后續再繼續研究。

  • getItemId()

返回在 adapter 中通過 getItemId(int position) 為該 item 生成的 id,沒有在 adapter 重寫那個方法的話,就返回 RecyclerView.NO_ID。

用途在 adapter 一節講解。

  • getItemViewType()

返回在 adapter 中通過 getItemViewType() 為該 item 設定的 type,沒有在 adapter 重寫那個方法的話,默認就是單一類型的 item type。

item type 是用于實現不同 item 樣式。

  • setIsRecyclable()

RecyclerView 最大的特性就是它內部實現了一套高效的回收復用機制,而回收復用是以 ViewHolder 為單位進行管理的,每個 item 都會對應一個 ViewHolder,默認都是會參與進回收復用機制中。

但可以通過該方法來標志該 ViewHolder 不會被回收

3. LayoutParams

RecyclerView 自定義了 LayoutParams 內部類,在每個 Item 的 LayoutParams 攜帶了一些額外的信息,需要的話,我們也可以通過這里來獲取這些信息。

public static class LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
    ...
    public boolean viewNeedsUpdate() {...}
    public boolean isViewInvalid() {...}
    public boolean isItemRemoved() {...}
    public boolean isItemChanged() {...}
    public int getViewLayoutPosition() {...}
    public int getViewAdapterPosition() {...}
}

公開的接口有以上幾個,也就是說,我們可以通過 LayoutParams 獲取到 item 的 position 信息、狀態信息,是否需要刷新,是否被移除等等。

更多的應用場景留待后續補充。

4. Adapter

adapter 大伙肯定是最熟悉的了,寫 RecyclerView 打交道最多的也就是 adapter 了,所以一些基本知識我就一筆帶過了,本節著重介紹各種可選功能。

  • onCreateViewHolder()
  • onBindViewHolder()
  • getItemCount()
  • RecyclerView.ViewHolder

以上是寫一個 adapter 時必須實現的四點,它們決定了 item 長啥樣,填充啥數據,以及有多少個 item,有了這些信息,一個 RecyclerView 列表也就出來了。

  • notifyDataSetChanged()
  • notifyItemChanged()
  • notifyItemXXX() 系列

以上是用于刷新 item,當數據源發生變化時,我們手動去刷新 item。官方說了, item 的更新分兩種,一種是數據需要更新,這類刷新不涉及到 item 的位置變化;而另一種屬于結構刷新,就是涉及到 item 的位置變化。

使用 notifyDataSetChanged() 時,它不管你分哪種形式的刷新,強制所有 item 重新綁定數據,重新布局操作。

以上都屬于常用的基本功能,一句話帶過,下面介紹一些可選功能:

  • onViewRecycled()
  • onViewAttachedFromWindow()
  • onViewDetachedFromWindow()
  • onAttachedToRecyclerView()
  • onDetachedFromRecyclerView()

這些方法基本都是 item 或 adapter 的一些生命周期的回調,所以分別來看看每個方法都是什么時候會被回調的,可以用來處理什么場景,做些啥工作:

onViewRecycled():當 ViewHolder 已經確認被回收,且要放進 RecyclerViewPool 中前,該方法會被回調。

首先需要明確,RecyclerView 的回收機制在工作時,會先將移出屏幕的 ViewHolder 放進一級緩存中,當一級緩存空間已滿時,才會考慮將一級緩存中已有的 ViewHolder 移到 RecyclerViewPool 中去。所以,并不是所有剛被移出屏幕的 ViewHoder 都會回調該方法。

另外,注釋中也說了,該方法的回調是在 ViewHolder 放進 RecyclerViewPool 中前,而 ViewHolder 在放進 Pool 中時會被 reset,因為上一節中也說過,其實 ViewHolder 本身攜帶著很多信息。那么,在該方法回調時,這些信息還沒被重置掉,官方建議我們可以在這里釋放一些耗內存資源的工作,如 bitmap 的釋放。

onViewAttachedFromWindow()
onViewDetachedFromWindow()

RecyclerView 本質上也是一個 ViewGroup,那么它的 Item 要顯示出來,自然要 addView() 進來,移出屏幕時,自然要 removeView() 出去,對應的就是這兩個方法的回調。

所以,當 Item 移出屏幕時,onViewRecycled() 不一定會回調,但 onViewDetachedFromWindow() 肯定會回調。相反,當 Item 移進屏幕內時,另一個方法則會回調。

那么,其實,在一定場景下,可以通過這兩個回調來處理一些 Item 移出屏幕,移進屏幕所需要的工作。為什么說一定場景下呢,因為如果調用了 notifyDataSetChanged() 的話,會觸發所有 Item 的 detached 回調先觸發再觸發 onAttached 回調。

onAttachedToRecyclerView()
onDetachedFromRecyclerView()

這兩個回調則是當 RecyclerView 調用了 setAdapter() 時會觸發,舊的 adapter 回調 onDetached,新的 adapter 回調 onAttached。

我們同樣可以在這里來做一些資源回收工作,更多其他應用場景留待后續補充。

  • registerAdapterDataObserver()
  • unregisterAdapterDataObserver()

用于注冊監聽 notifyXXX() 系列方法的事件,當調用了 notifyXXX() 系列的方法時,注冊監聽后就可以接收到回調。

  • setHasStableIds()
  • getItemId()

這兩方法看注釋是說用于回收復用機制中,給 ViewHoler 設置一個唯一的標識符,但具體的使用場景還不清楚,后續有用到,再補充。

另,setHasStableIds() 必須在 setAdapter() 方法之前調用,否則會拋異常。

5. RecyclerView

5.1 addOnItemTouchListener()

咋一看到這個方法,我還以為 RecyclerView 也把 item 的點擊事件封裝好了,終于不用我們自己去寫了呢。看了下源碼注釋才發現,這個方法的作用是用于根據情況是否攔截觸屏事件的分發。先看一下它的參數類型:OnItemTouchListener

public interface OnItemTouchListener {
    boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e);
    void onTouchEvent(RecyclerView rv, MotionEvent e);
    void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);
}

是不是感覺接口里的方法很熟悉,沒錯,就是觸屏事件分發流程中的攔截和處理的兩個方法。

通常我們都說在自定義 View 中重寫這幾個方法來將觸屏事件攔截,交由自己處理。RecyclerView 也是一個 View,如果你有 RecyclerView 需要攔截觸屏事件自己處理的需求,那么你可以選擇繼承 RecyclerView,也可以選擇調用這個方法。

5.2 addOnScrollListener()

RecyclerView 是一個列表控件,自然會涉及到滑動,所以它提供了滑動狀態的監聽接口,當我們需要在滑動狀態變化時相對應的工作時,可以調用該方法注冊滑動監聽。來看看它的參數:OnScrollListener

public abstract static class OnScrollListener {
    /**
    * Callback method to be invoked when RecyclerView's scroll state changes.
    *
    * @param recyclerView The RecyclerView whose scroll state has changed.
    * @param newState     The updated scroll state. One of {@link #SCROLL_STATE_IDLE},
    *                     {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}.
    */
    public void onScrollStateChanged(RecyclerView recyclerView, int newState){}
    public void onScrolled(RecyclerView recyclerView, int dx, int dy){}
}

onScrolled():滑動的實現本質上就是每一幀時要么通過動畫,要么通過修改屬性,一幀幀內處理一小段滑動,整個過程連起來就是一個流暢的滑動效果。這個方法就是每幀內處理的滑動距離,理想狀態下,每幀都會回調一次,直到滑動結束。

如果想得到滑動的距離,方向的話,可以在這個方法里做。

onScrollStateChanged():該方法則是滑動狀態變化時的回調,一共設置了三種狀態:

  • SCROLL_STATE_IDLE:停止滑動時的狀態
  • SCROLL_STATE_DRAGGING:手指拖動時的狀態
  • SCROLL_STATE_SETTLING:慣性滑動時的狀態(這是我的理解)

在手機應用上和 Tv 應用上,這些狀態的回調還是有所區別的,所以分開來說一下:

  • 手機應用:

手機上的 RecyclerView 列表控件,通常都是通常手指拖動來觸發滑動的,因此在手指觸摸并拖動的那個時刻,這個方法會被回調,參數傳入 SCROLL_STATE_DRAGGING 表示進入拖動狀態。

當手指放開的時候,分兩種情況,一是手指放開后 RecyclerView 又根據慣性滑動了一段距離,只要有稍微滑動就算,那么這個時候進入慣性滑動時該方法會被回調,參數傳入 SCROLL_STATE_SETTLING 表示進入了慣性滑動狀態。當最終停止滑動后,該方法還會被回調,參數傳入 SCROLL_STATE_IDLE。

另外一種情況是,手指放開后,RecyclerView 并沒有任何滑動了,通常是手指很慢的拖動情況下放開,這時候該方法就會只回調一次,參數傳入 SCROLL_STATE_IDLE,因為在手指還沒放開前就已經停止滑動了,放開后更不會滑動,所以直接進入停止滑動狀態。

所以,在手機應用上,ReyclerView 的滑動狀態變化有兩種,一是從 SCROLL_STATE_DRAGGING 到 SCROLL_STATE_SETTLING 再到 SCROLL_STATE_IDLE;另外一種是直接從 SCROLL_STATE_DRAGGING 到 SCROLL_STATE_IDLE。

  • Tv 應用:

由于 Tv 應用沒有觸摸事件,只有遙控器事件,因此 RecyclerView 滑動的觸發都是由遙控器方向鍵操作后由于焦點的變化來觸發的,所以在 Tv 應用上不會有 SCROLL_STATE_DRAGGING 這個狀態。

每次滑動都是從 SCROLL_STATE_SETTLING 到 SCROLL_STATE_IDLE。

兩者有所區別,需要注意一下,如果從事 Tv 應用開發的話。

5.3 setHasFixedSize()

看方法注釋,它是說,當你能夠確定后續通過 notifyItemXXX() 系列方法來刷新界面時,RecyclerView 控件的寬高不會因為 item 而發生變化,那么這時候可以通過該方法來讓 ReyclerView 每次刷新界面時不用去重新計算它本身的寬高。

從代碼層面上來看,也就是說,當調用該方法設置了后,之后通過 notifyItemXXX() 系列方法刷新界面時,RecyclerView 的 onMeasure(), onLayout() 就不會被調用了,而是直接調用 LayoutManager 的 onMeasure()

但這樣做具體有什么好處,提高性能一點,但其他的就不清楚了。想了想,當 ReyclerView 控件的寬高模式是 match_parent 時,其實這個方法可以使用,因為此時它的寬高就不會受到 item 的因素影響了。如果模式為 wrap_content,那這個方法就不要用了。

5.4 setLayoutFrozen()

這方法可以禁掉 RecyclerView 的布局請求操作,而 RecyclerView 的滑動,item 的添加或移除本質上都會觸發 RecyclerView 的重新測量、布局操作。

所以,調用該方法,其實等效于關閉了 ReyclerView 的刷新,不管數據源發生了何種變化,不管用戶滑動了多長距離,都不會去刷新界面,看起來就像是不響應一樣,但等到再次調用該方法參數傳入 false 后,就會立馬去根據變化后的數據源來刷新界面了。

使用場景還是有的,假如有些場景暫時不想讓 RecyclerView 去刷新,比如此時有其他動畫效果正在執行中,RecyclerView 刷新多少會有些耗時,萬一導致了當前動畫的卡頓,那么體驗就不好了。所以,這個時候可以暫時將 ReyclerView 的刷新關閉掉,但后面記得要重新開啟。

5.5 setPreserveFocusAfterLayout()

這個還沒搞清它的應用場景是什么,注釋是說,當在進行布局工作時,有些時候,會由于 item 的狀態發生改變,或者由于動畫等原因,導致焦點丟失。通過該方法可以再這些工作之后,再繼續保持之前 item 的焦點狀態。這個方法默認就是開啟的。

但我測試了下,不管有沒有開啟這個方法,notifyDataSetChanged() 時,焦點仍然會亂飄,后續再繼續查證。

5.6 findChildViewUnder()

方法參數是 (float x, float y),作用是查找指定坐標點 (x, y) 落于 RecyclerView 的哪個子 View 上面,這里的坐標點是以 RecyclerView 控件作為坐標軸,并不是以屏幕左上角作為坐標原點。

具體應用場景,目前還沒遇到過,后續補充。

5.7 findContainingItemView()

該方法參數是 (View view),作用正如命名上的理解,查找含有指定 View 的 ItemView,而 ItemView 是指 RecyclerView 的直接子 View。

通常,RecyclerView 的 Item 布局都不會簡單到直接就是一個具體的 TextView,往往都挺復雜的,比如:

Item布局.png

Item 布局的結構至少如下:

<RelativeLayout>
    <ImageView/>
    <TextView/>
</RelativeLayout>

這種 item 已經算是很簡單的了,那么如果我們當前拿到的是 TextView 對象,通過該方法就可以找到這個 TextView 的根布局,即 RecyclerView 的直接子 View,這里是 RelativeLayout 對象。

應用場景:

我想到一種應用場景,通常我們點擊事件都是作用于具體的某個 View,比如上面的 TextView,那我們在點擊事件的回調中就只能拿到 TextView 對象而已。而通過這個方法,我們可以拿到這個 TextView 所屬的 ItemView。拿到 ItemView 之后可以做些什么呢?

看需求場景,反正總有些場景是需要用到根布局的。還有一點就是,RecyclerView 內部其實自定義了一個 LayoutParams,作用于它的直接子 View。所以只要我們可以拿到 RecyclerView 的直接子 View,就可以拿到它對應的 LayoutParams,那么就可以通過 LayoutParams 拿到一些這個 item 的信息,比如 position 等等。

5.8 findContainingViewHolder()

該方法參數是 (View view),作用跟上述方法類似,用于查找含有指定 View 的 ItemView 所對應的 ViewHolder。

這里就不展開介紹了,該方法跟上述的方法基本一模一樣,區別就僅僅是一個用于查找 ItemView,一個用于查找 ItemView 對應的 ViewHoler。

至于應用場景,拿到 ViewHolder 能做的事就更多了,而是 LayoutParams 提供的信息其實內部也是去 ViewHolder 中拿的,所以實際上 Item 攜帶的各種信息基本都在 ViewHolder 上面了。

5.9 findViewHolderXXX()

既然 ViewHolder 攜帶著大量 Item 的相關信息,RecyclerView 自然也就提供了各種方式來獲取 ViewHolder,這個系列的方法如下:

  • findViewHolderForAdapterPosition()
  • findViewHolderForLayoutPosition()
  • findViewHolderForItemId()
  • findContainingViewHolder()

通過 position, id, view 都可以獲取到對應的 ViewHolder 對象。


ps:以下內容留待下篇介紹~

鏈接:關于RecyclerView你知道的不知道的都在這了(下)

6. Recycler

6.1 setItemViewCacheSize()

6.2 setViewCacheExtension()

6.3 setRecycledViewPool()

6.4 setRecyclerListener()

7. ItemAnimator

7.1 SimpleItemAnimator

7.2 DefaultItemAnimator

8. ItemDecoration

8.1 DividerItemDecoration

8.2 ItemTouchHelper

8.3 FastScroller

9. OnFlingListener

9.1 SnapHelper

9.2 LinearSnapHelper

9.3 PagerSnapHelper


大家好,我是 dasu,歡迎關注我的公眾號(dasuAndroidTv),如果你覺得本篇內容有幫助到你,可以轉載但記得要關注,要標明原文哦,謝謝支持~


QQ圖片20180316094923.jpg
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容