一、概述
當我們使用RecyclerView
時,第一件事就是要繼承于RecyclerView.Adapter
,實現其中的抽象方法,來處理數據的展示邏輯,今天,我們就來介紹一下Adapter
中的相關方法。
二、基礎用法
我們從一個簡單的線性列表布局開始,介紹RecyclerView.Adapter
的基礎用法。
首先,需要導入遠程依賴包:
compile'com.android.support:recyclerview-v7:25.3.1'
接著,繼承于RecyclerView.Adapter
來實現自定義的NormalAdapter
:
public class NormalAdapter extends RecyclerView.Adapter<NormalAdapter.NormalViewHolder> {
private List<String> mTitles = new ArrayList<>();
public NormalAdapter(List<String> titles) {
mTitles = titles;
}
@Override
public NormalViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_normal_item, parent, false);
return new NormalViewHolder(itemView);
}
@Override
public void onBindViewHolder(NormalViewHolder holder, int position) {
holder.setTitle(mTitles.get(position));
}
@Override
public int getItemCount() {
return mTitles.size();
}
class NormalViewHolder extends RecyclerView.ViewHolder {
private TextView mTextView;
NormalViewHolder(View itemView) {
super(itemView);
mTextView = (TextView) itemView.findViewById(R.id.tv_title);
}
void setTitle(String title) {
mTextView.setText(title);
}
}
}
當我們實現自己的Adapter
時,至少要做四個工作:
- 第一:繼承于
RecyclerView.ViewHolder
,編寫自己的ViewHolder
- 這個子類用來描述
RecyclerView
中每個Item
的布局以及和它關聯的數據,它同時也是RecyclerView.Adapter<VH>
中需要指定的VH
類型。 - 在構造方法中,除了需要調用
super(View view)
方法來傳入Item
的跟布局來給基類中itemView
變量賦值,還應當提前執行findViewById
來獲得其中的子View
以便我們之后對它們進行更新。 - 第二:實現
onCreateViewHolder(ViewGroup parent, int viewType)
- 當
RecyclerView
需要我們提供類型為viewType
的新ViewHolder
時,會回調這個方法。 - 在這里,我們實例化出了
Item
的根布局,并返回一個和它綁定的ViewHolder
。 - 第三:實現
onBindViewHolder(VH viewHolder, int position)
- 當
RecyclerView
需要展示對應position
位置的數據時會回調這個方法。 - 通過
viewHolder
中持有的對應position
上的View
,我們可以更新視圖。 - 第四:實現
getItemCount()
- 返回
Item
的總數。
在Activity
中,我們給Adapter
傳遞數據,使用方法和ListView
基本相同,只是多了一句在設置LayoutManager
的操作,這個我們后面再分析。
private void init() {
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rv_content);
mTitles = new ArrayList<>();
for (int i = 0; i < 20; i++) {
mTitles.add("My name is " + i);
}
NormalAdapter normalAdapter = new NormalAdapter(mTitles);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(normalAdapter);
}
這樣,一個RecyclerView
的例子就完成了:
三、只有一種ViewType
下的復用情況分析
下面,我們來分析一下兩個關鍵方法的調用時機:
onCreateViewHolder
onBindViewHolder
通過這兩個方法回調的時機,我們可以對RecyclerView
復用的機制有一個大概的了解。
3.1 初始進入
剛開始進入界面的時候,我們只展示了3
個Item
,此時這兩個方法的調用情況如下,可以看到,RecyclerView
只實例化了屏幕內可見的ViewHolder
,并且onBindViewHolder
是在對應的onCreateViewHolder
調用完后立即調用的:
3.2 開始滑動
當我們手指觸摸到屏幕,并開始向下滑動,我們會發現,雖然position=3
的Item
還沒有展示出來,但是這時候它的onCreateViewHolder
和onBindViewHolder
就被回調了,也就是說,我們會預加載一個屏幕以外的Item
:
3.3 繼續滑動
當我們繼續往下滑動,position=3
的Item
一被展示,那么position=4
的Item
的兩個方法就會被回調。
3.4 復用
當postion=6
的Item
被展示之后,按照前面的分析,這時候就應當回調position=7
的onCreateViewHolder
和onBindViewHolder
方法了,但是我們發現,這時候只回調了onBindViewHolder
方法,而傳入的ViewHolder
其實是position=0
的ViewHolder
,也就是我們所說的復用:
此時,屏幕中
Items
的展現情況為:目前不可見的
Item
為position=0,1,2
,所以,我們可以得出結論:在單一布局的情況,RecyclerView
在復用的時候,會取相反方向中超出顯示范圍的第3
個Item
來復用,而并不是超出顯示范圍的第一個Item
進行復用。
四、多種類型的布局
4.1 基本使用
當我們需要在列表當中展示不同類型的Item
時,我們一般需要重寫下面的方法,告訴RecyclerView
在對應的position
上需要展示什么類型的Item
。
public int getItemViewType(int position)
RecyclerView
在回調onCreateViewHolder
的時候,同時也會把viewType
傳遞進來,我們根據viewType
來創建不同的布局。
下面,我們就來演示一下它的用法,這里我們返回三種不同類型的item
:
public class NormalAdapter extends RecyclerView.Adapter<NormalAdapter.NormalViewHolder> {
private List<String> mTitles = new ArrayList<>();
public NormalAdapter(List<String> titles) {
mTitles = titles;
}
@Override
public NormalViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = null;
switch (viewType) {
case 0:
itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_normal_item_1, parent, false);
break;
case 1:
itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_normal_item_2, parent, false);
break;
case 2:
itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_normal_item_3, parent, false);
break;
}
NormalViewHolder viewHolder = new NormalViewHolder(itemView);
Log.d("NormalAdapter", "onCreateViewHolder, address=" + viewHolder.toString());
return viewHolder;
}
@Override
public void onBindViewHolder(NormalViewHolder holder, int position) {
Log.d("NormalAdapter", "onBindViewHolder, address=" + holder.toString() + ",position=" + position);
int viewType = getItemViewType(position);
String title = mTitles.get(position);
holder.setTitle1("title=" + title + ",viewType=" + viewType);
}
@Override
public int getItemCount() {
return mTitles.size();
}
@Override
public int getItemViewType(int position) {
return position % 3;
}
class NormalViewHolder extends RecyclerView.ViewHolder {
private TextView mTv1;
NormalViewHolder(View itemView) {
super(itemView);
mTv1 = (TextView) itemView.findViewById(R.id.tv_title_1);
}
void setTitle1(String title) {
mTv1.setText(title);
}
}
}
最終,會得到下面的界面:
4.2 多種viewType
下的復用情況分析
前面,我們已經研究過一種viewType
下的復用情況,現在,我們再來分析一下多種viewType
時候的復用情況。
4.2.1 初始進入
此時,我們屏幕中展示了postion=0~6
這七個Item
,onCreateViewHolder
和onBindViewHolder
的回調和之前相同,只會生成屏幕內可見的ViewHolder
4.2.2 開始滑動和繼續滑動
這兩種情況都和單個viewType
時相同,會預加載屏幕以外的一個Item
:
4.2.3 復用
關鍵,我們看一下何時會復用position=0/viewType=1
的Item
:
此時,屏幕內最上方的
Item
為position=4/viewType=1
,最下方的Item
為position=11/viewType=2
,按照之前的分析,RecyclerView
會保留相反方向的2
個ViewHolder
,也就是保留postion=2,3
的ViewHolder
,并復用position=1
的ViewHolder
,但是現在position=0
的ViewHolder
的viewType=1
,不可以復用,因此,會繼續往上尋找,這時候就找到了position=0
的ViewHolder
進行復用。
五、數據更新
5.1 更新方式
當數據源發生變化的時候,我們一般會通過Adatper. notifyDataSetChanged()
來進行界面的刷新,RecyclerView.Adapter
也提供了相同的方法:
public final void notifyDataSetChanged()
除此之外,它還提供了下面幾種方法,讓我們進行局部的刷新:
//position的數據變化
notifyItemChanged(int postion)
//在position的下方插入了一條數據
notifyItemInserted(int position)
//移除了position的數據
notifyItemRemoved(int postion)
//從position開始,往下n條數據發生了改變
notifyItemRangeChanged(int postion, int n)
//從position開始,插入了n條數據
notifyItemRangeInserted(int position, int n)
//從position開始,移除了n條數據
notifyItemRangeRemoved(int postion, int n)
下面是一些簡單的使用方法:
//在頭部添加多個數據.
public void addItems() {
mTitles.add(0, "add Items, name=0");
mTitles.add(0, "add Items, name=1");
mNormalAdapter.notifyItemRangeInserted(0, 2);
}
//移除頭部的多個數據.
public void removeItems() {
mTitles.remove(0);
mTitles.remove(0);
mNormalAdapter.notifyItemRangeRemoved(0, 2);
}
//移動數據.
public void moveItems() {
mTitles.remove(1);
mTitles.add(2, "move Items name=0");
mNormalAdapter.notifyItemMoved(1, 2);
}
5.2 比較
數據的更新分為兩種:
-
Item changes
:除了Item
所對應的數據被更新外,沒有其它的變化,對應notifyXXXChanged()
方法。 -
Structural changes
:Items
在數據集中被插入、刪除或者移動,對應notifyXXXInsert/Removed/Moved
方法。
notifyDataSetChanged
會把當前所有的Item
和結構都視為已經失效的,因此它會讓LayoutManager
重新綁定Items
,并對他們重新布局,這在我們知道已經需要更新某個Item
的時候,其實是不必要的,這時候就可以選擇進行局部更新來提高效率。
六、監聽ViewHolder
的狀態
RecyclerView.Adapter
中還提供了一些回調,讓我們能夠監聽某個ViewHolder
的變化:
@Override
public void onViewRecycled(NormalViewHolder holder) {
Log.d("NormalAdapter", "onViewRecycled=" + holder);
super.onViewRecycled(holder);
}
@Override
public void onViewDetachedFromWindow(NormalViewHolder holder) {
Log.d("NormalAdapter", "onViewDetachedFromWindow=" + holder);
super.onViewDetachedFromWindow(holder);
}
@Override
public void onViewAttachedToWindow(NormalViewHolder holder) {
Log.d("NormalAdapter", "onViewAttachedToWindow=" + holder);
super.onViewAttachedToWindow(holder);
}
下面,我們就從實例來講解這幾個方法的調用時機,初始時刻,我們的界面為:
- 初始進入時,
position=0~6
的onViewAttachedToWindow
被回調:
- 當滑動到
postion=7
可見時,它的onViewAttachedToWindow
被回調:
- 當
postion=0
被移出屏幕可視范圍內,它的onViewDetachedFromWindow
被回調:
- 而當我們繼續往下滑動,當
position=2
被移出屏幕之后,此時position=0
的onViewRecycled
被回調:
現在回憶一下之前我們對復用情況的分析,RecyclerView
最多會保留相反方向上的兩個ViewHolder
,此時雖然position=1,2
不可見,但是依然需要保留它們,這時候會回收position=0
的ViewHolder
以備之后被復用。
七、監聽RecyclerView
和RecyclerView.Adapter
的關系
RecyclerView
和Adapter
是通過setAdapter
方法來綁定的,因此在Adapter
中也通過了綁定的監聽:
public void onAttachedToRecyclerView(RecyclerView recyclerView) {}
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {}
八、小結
這篇文章,主要總結了一些RecyclerView.Adapter
中平時我們不常注意的細節問題,也通過實例了解到了關鍵方法的含義,最后,推薦一個Adapter
的開源庫:BaseRecyclerViewAdapterHelper
。
更多文章,歡迎訪問我的 Android 知識梳理系列:
- Android 知識梳理目錄:http://www.lxweimin.com/p/fd82d18994ce
- 個人主頁:http://lizejun.cn
- 個人知識總結目錄:http://lizejun.cn/categories/