一、概述
對於RecyclerView
的學習,主要是需要掌握以下幾點:
- 資料:
Adapter
- 使用:RecyclerView - Adapter
- 進階:BaseRecyclerViewAdapterHelper
- 佈局:
LayoutManager
- 使用:RecyclerView - LayoutManager
- 進階:自定義
- 動畫:
ItemAnimator
- 使用
- 進階:RecyclerViewItemAnimators
- 裝飾:
ItemDecorator
- 使用:RecyclerView - ItemDecoration
- 手勢:
ItemTouchHelper
- 使用:RecyclerView - ItemTouchHelper
要理解整個RecyclerView
的思想,有一個視訊是一定要看的:RecyclerView ins and outs - Google I/O 2016。今天,我們就通過這個視訊,把上面所學到的東西串聯起來。
二、為什麼要使用RecyclerView
RecyclerView
誕生的目的就是為了替代ListView
,我們先總結一下在使用ListView
過程當中所遇到的問題:
- 複用
Item
需要編寫很多的程式碼 在使用ListView
的時候,有經驗的程式設計師一定會告訴你在getView
中要這麼寫,如果忘了,那麼會產生很嚴重的效能問題。
if (convertView == null) {
//通過LayoutInflator生成convertView,併產生一個ViewHolder,通過setTag關聯起來.
} else {
//通過getTag獲取ViewHolder,進行更新操作.
}
複製程式碼
- 焦點衝突問題
當
Item
有焦點時,Item
的子控制元件就無法獲取到焦點;而如果子控制元件搶奪了焦點,那麼Item
的點選事件又不能響應,這個相信大家都遇到過。 - 重複的
API
ListView
中提供了很多的API
,但是這些API
又和View
的一些API
重複了,例如我們可以給ListView
設定setOnItemClickListener
,也可以在getView
中給某個View
設定setOnClickListener
,這就讓人很疑惑,到底應當選用哪個。 - 動畫
當我們需要在
ListView
中進行新增、刪除、移動等操作的時候,如果希望加上動畫,那麼是很困難的,根本原因是我們是通過Adapter
通知ListView
進行更新,然而ListView
根本就沒法確定到底是哪些View
發生了變化。 - 更加複雜的佈局需求
ListView
在佈局是規整的列表的時候能滿足大多數人的使用,然而如果想要實現像瀑布流這種複雜的佈局,並且保證View
能夠複用,那麼需要編寫很多的程式碼。
如果之前有了解過RecyclerView
的基本用法,那麼你會發現,對於上述這些問題,它都給出了自己的解決方案:
- 強制使用開發者使用
ViewHolder
,提供了onCreateViewHolder
和onBindViewHolder
這兩個方法,把建立View
和繫結View
的操作分離開。 - 把焦點交給系統處理。
- 去掉了
onItemClickListener
,以及一些重複的API
。 - 在
Adapter
中增加了notifyItemChanged()
等方法,讓我們可以指定變化的型別和範圍,並且提供了setItemAnimator()
方法,讓開發者能夠方便地定義新增、刪除、移動的動畫。 - 把佈局的工作抽象出來,放到了
LayoutManager
當中,並預製了瀑布流佈局。
瞭解了這些,我們就能知道RecyclerView
能幫我們解決什麼問題,也就能更好地理解它為什麼要這麼設計,下面就開始進入真正的RecyclerView
的學習。
三、RecyclerView
架構
整個RecyclerView
體系包含三大元件:
LayoutManager
:position the view
ItemAnimator
:animate the view
Adapter
:provide the view
這三大元件各司其職,而RecyclerView
負責管理,就組成了整個RecyclerView
的架構。
3.1 LayoutManager
LayoutManager
需要負責以下幾部分的工作:
Position
它負責View
的擺放,可以是線性、宮格、瀑布流式或者任意型別,而RecyclerView
不知道也不關心這些,這是LayoutManager
的職責。Scroll
對於滾動事件的處理,RecyclerView
負責接收事件,但是最終還是由LayoutManager
進行處理滾動後的邏輯,因為只有它在知道View
具體擺放的位置。Focus traversal
當焦點轉移導致需要一個新的Item
出現在可視區域中時,也是由LayoutManager
處理的。
3.2 Adapter
Adapter
需要負責以下幾部分的工作:
- 建立
View
和ViewHolder
,後者作為整個複用機制的跟蹤單元。 - 把具體位置的
Item
和ViewHolder
進行繫結,並儲存相關的資訊。 - 通知
RecyclerView
資料變化,支援區域性的更新,在提高效率的同時也有效地支援了動畫。 Item
點選事件的處理。- 多型別佈局的支援。
四、ViewHolder
的生命週期
4.1 LayoutManager
請求RecyclerView
提供指定position
的View
ViewHolder
是和View
相繫結的,同時它也是整個複用框架的跟蹤單元。在RecyclerView
體系中,對ViewHolder
採用了二級快取,分為Cache
和Recycled Pool
,當LayoutManager
向RecyclerView
請求位於某個Position
的View
時,Recycled View
會先去Cache
中尋找,如果找到,那麼直接返回;如果找不到,那麼再去Recycled Pool
中尋找,下面就是整個尋找過程的幾種情況:
- 命中
Cache
這種情況下,不會呼叫Adapter
的onCreateViewHolder
或者onBindViewHolder
方法: Cache
不存在,Recycled Pool
也不存在 這種情況下,會呼叫Adapter
的onCreateViewHolder
方法,讓它提供一個對應viewType
的ViewHolder
,我們在其中建立ViewHolder
和View
之間的關聯。Cache
不存在,Recycled Pool
存在 這種情況下,會回撥Adapter
的onBindViewHolder
方法,我們在其中使用當前的資料集合來更新ViewHolder
所繫結的itemView
的狀態。
4.2 LayoutManager
找到對應位置的View
LayoutManager
通過addView
方法把之前找到的View
新增進RecyclerView
,RecyclerView
通過onViewAttachToWindow(VH viewHolder)
方法,通知Adapter
這個viewHolder
所關聯的itemView
已經被新增到了佈局當中,
4.3 LayoutManager
請求RecyclerView
移除某一個位置的View
4.3.1 普通情況
當LayoutManager
發現不再需要某一個position
的View
時,它會通知RecyclerView
,RecyclerView
通過onViewDetachFromWindow(VH viewHolder)
通知Adapter
和它繫結的itemView
被移出了。同時,RecyclerView
判斷它是否能夠被快取,假設能夠被快取,那麼它會先被放到Cache
當中,在Cache
中又會判斷它內部是否有需要轉移到Recycled Pool
中的ViewHolder
,在放入之後回收池後,通過onViewRecycled(VH viewHolder)
方法通知Adapter
它被回收了。
4.3.2 特殊情況
在上面的普通的情況中,onViewDetachFromWindow(VH viewHolder)
是立即被回撥的。然而在實際當中,由於我們需要對View
的新增、刪除做一些過度動畫,這時候,我們需要等待ItemAnimator
進行完動畫操作之後,才做detach
和recycle
的邏輯,這一過程對於LayoutManager
是不可見的。
4.4 ViewHolder
的銷燬
在一般情況下,我們不會去銷燬ViewHolder
,而是把它放入到快取當中,除非出現以下兩種情況。
4.4.1 ViewHolder
所繫結的itemView
當前狀態異常
在放入Recycled Pool
時,會去檢查itemView
的狀態是否正常。這一操作的目的主要是為了避免出現諸如此類的情況:當前itemView
正在執行動畫,此時它可能呈現半透明的狀態,如果此時把它放入到回收池中,那麼當另一個位置的position
需要複用它時就可能會出現問題。
當出現上面的情況後,Recycled Pool
會先通過Adapter
的onFailedToRecycled(VH viewHolder)
告訴它我們現在出現了異常的情況,由Adapter
的實現者通過返回值來決定是否仍然要把它放入到Recycled Pool
,預設是返回false
,也就是不放入,那麼這個ViewHolder
就會被銷燬了。
4.4.2 Recycled Pool
中已經沒有足夠的空間
Recycled Pool
的空間並不是無限大的,因此,如果沒有足夠的空間存放要被回收的ViewHolder
,那麼它也會被銷燬。
notifyItemRangeChanged(0, getItemCount())
方法,這時候為了進行漸出漸進的動畫,那麼我們就需要建立兩倍的ViewHolder
,出現這種情況時一般有兩種解決方法:
- 只通知具體發生變化的
Item
- 通過
pool.setMaxRecycledViews(type, count)
改變回收池的大小。
五、ItemAnimator
對於Item
的動畫,主要有以下幾種情況:
- 新增:
Fade In
- 刪除:
Fade Out
- 移動:
Translate
- 更新:
Cross Fade
RecyclerView
對於動畫的處理採用了Predictive
的方式,除了當前已經在RecyclerView
佈局中的View
(實線框部分),它還需要知道在螢幕意外的資訊(虛線框部分),這樣在H
被刪除的時候,它才能夠對J-K
進行上移動畫,並把原來不在螢幕內的L
上移到可視範圍之內。
六、ChildHelper
和AdapterHelper
6.1 ChildHelper
對於ChildHelper
的作用是:Provide a virtual children list to layoutmanager
,下面我們就首先看一下為什麼需要它。
6.1.1 解決什麼問題
我們看下面這種情況,假如LayoutManager
想要移除一個View
,而ItemAnimator
又希望給這一移除的操作增加一個動畫,那麼這時候就會產生衝突,到底應該怎麼辦,為此,RecyclerView
通過ChildHelper
來把它們隔離開。
6.1.2 解決問題的方法
當RecyclerView
收到LayoutManager
要求改變佈局的請求時,它並不是直接去更改ViewGroup
,而是讓ChildHelper
和ItemAnimator
去協調,並由它來操作ViewGroup
。
0,1,2,3
,此時我們移除了position=0
的Item
,這時候假如刪除的動畫還沒有完成,那麼LayoutManager
和RecyclerView
的getChildAt(0)
返回值將會不同,因為在LayoutManager
並不清楚ChildHelper
的存在,在它看來,position=0
的Item
已經被移除了。
layoutManager.getChildAt(0); //return 1;
recyclerView.getChildAt(0); //return 0;
複製程式碼
6.2 AdapterHelper
而AdapterHelper
所解決的問題和ChildHelper
類似,ChildHelper
是處理View
的,而AdapterHelper
用來跟蹤ViewHolder
的,其作用為:
Tracks ViewHolder positions
Virtual Adapter for LayoutManager
說起來可能比較抽象,我們用下面這種圖理解一下,當我們移動某個Item
並且它的onLayout
方法還沒有完成,那麼Adapter
和Layout
的postion
是不相同的:
七、ItemDecoration
ItemDecoration
用來在RecyclerView
的Canvas
上進行額外的繪製操作,我們不僅可以在單個Item
(例如給每個Item
新增分割線)的Canvas
上進行繪製,也可以在整個RecyclerView
的Canvas
上進行繪製,此外,我們還可以指定Item
之間的間隔:
Custom Drawing on RecyclerViews Canvas
Add offset to View bounds
Have multiple ItemDecoration
需要注意的點:
Do not try to access to adapter
Keep necessary information in viewHolder
General onDraw rules apply
recyclerView.getChildViewHolder(View view)
參考文章:Android RecyclerView 使用完全解析 體驗藝術般的控制元件。
八、RecycledViewPool
RecyclerViewPool
用來快取那些回收的View
,這些快取不僅可以提供給單個RecyclerView
使用,還可以提供和別的自定義控制元件共享。
Sanctuary for reserve ViewHolders
Can be shared between RecyclerViews or Custom ViewGroups
PerActivity Context
九、ItemTouchHelper
之前使用ListView
的時候,如果需要支援側滑刪除、拖動排序這種操作,那麼我們一般用引入一些開源庫,現在RecyclerView
已經幫我們提供了實現的介面,通過重寫ItemTouchHelper
的方法,就可以實現上面提到的那些操作。
Drag & Drop
Swipe to dismiss
參考文章:RecyclerView 進階:使用 ItemTouchHelper 實現拖拽和側滑刪除
十、Tips
onBind Position != final
,use holder.getAdapterPostion()
如果我們像下面這樣,在onBindViewHolder
中繫結了監聽:
public void onBindViewHolder(final ViewHolder, final int position) {
holder.itemView.setOnClickListener(new View.onClickListener) {
@Override
public void onClick(View view) {
removeAtPostion(position);
}
}
}
複製程式碼
由於Item
會被新增、刪除、移動,因此,我們在onBindViewHolder
中獲得位置,並不一定是當前的位置,例如像下面這樣:
onBindViewHolder(holder, 5);
notifyItemMoved(5, 15);
holder.itemView.callOnClick();
複製程式碼
那麼就會得到錯誤的位置,這時候應當使用holder.getAdapterPostion()
來保證能夠得到預期的結果。
Payloads
通過onBindViewHolder
中的List payloads
,我們可以指定在bind
的時候只更新某一部分的資訊,而不是全部更新。onCreate means create
在onCreateViewHolder
中,始終應當返回一個新的ViewHolder
,而不是返回一個快取的ViewHolder
。Adapter position and Layout position
就像我們前面在AdapterHelper
中討論的那樣,在某些時刻,Adapter Position
和Layout Position
並不相等,我們應當根據情況選擇需要使用哪個,Adapter Position
資料所處的位置,而Layout Position
則對應當前**View
的所處的位置**。
更多文章,歡迎訪問我的 Android 知識梳理系列:
- Android 知識梳理目錄:www.jianshu.com/p/fd82d1899…
- 個人主頁:lizejun.cn
- 個人知識總結目錄:lizejun.cn/categories/