1. 程式人生 > >關於RecyclerView你知道的不知道的都在這了

關於RecyclerView你知道的不知道的都在這了

https://www.cnblogs.com/dasusu/p/9159904.html

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

目錄

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

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

前言

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

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

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

目錄

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

另外,由於有些平臺可能不支援 `` 解析,所以建議大夥可藉助本篇目錄,或平臺的目錄索引進行快速查閱。

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。
  • 下列標題中,但凡是斜體字,表示該知識點目前暫時沒理清楚,留待後續繼續補充。

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:以下內容留待下篇介紹~

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),如果你覺得本篇內容有幫助到你,可以轉載但記得要關注,要標明原文哦,謝謝支援~