RecyclerView一定是我從事Android app開發工作以來最常用到的元件之一了。雖然RecyclerView在2014的I/O大會就推出了,不過有鑒於這陣子常有人向我請教RecyclerView的用法,便決定將RecyclerView的使用方法及原理整理成文章。
What is RecyclerView?
RecyclerView可以說是ListView的改良版,和ListView相同的是,RecyclerView一樣是由配置器(Adapter)來handle item view呈現的內容。不同的是它較ListView更有彈性、容易客製化,且具官方說法,效能方面有很大的提升。
Why RecyclerView?
Stack overflow上一致認為Google推出RecyclerView正是為了取代ListView。我個人認為RecyclerView最大的優點之一是,它直接基於view recycle邏輯來運作。相較於此,ListView則沒有強制實作recycle機制,如果你熟悉Android慣用的list recycle機制,就會知道沒有實作recycle是多嚴重的事!在RecyclerView中,當你定義一個新的Adapter時。必須定義好對應的ViewHolder class,接著實作onCreateViewHolder, onBindViewHolder以運行recycle機制。雖然recycle機制對初學者來說可能有點抽象,但這項優點可以大大避免初學者忽略在adapter中實作recycle邏輯,並且更簡單地實作出來。
除了更方便實現view的recycle,還有許多比ListView更方便的優點,如:更容易管理資料新增刪除的UI變化、更方便定義ViewType、可以客製化動畫(ItemAnimator)、客製化layout offset (ItemDecoration)、客製化item view的排列(LayoutManager)等,好東西,不用嘛?
屁話就到這邊,馬上看扣
下載,在gradle加入library dependency 後按下sync
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
XML Component使用方法如下
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
接下來進入比較複雜的部分,開始定義Adapter
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
private Context mContext;
public MyAdapter(Context context) {
this.mContext = context;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
return null;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
}
@Override
public int getItemCount() {
return 0;
}
class ViewHolder extends RecyclerView.ViewHolder {
public ImageView ivPosterThumbnail;
public TextView tvPosterName;
public TextView tvContent;
public ImageButton btnLike;
public ImageButton btnComment;
public ViewHolder(View itemView) {
super(itemView);
}
}
}
上面的MyAdapter.class中,我們除了實作了onCreateVeiwHolder(), onBindViewHolder(), getItemCount() 之外,還定義了一個叫ViewHolder的inner class。
而在這邊的ViewHolder同樣是繼承RecyclerView裡的ViewHolder class,需要留意的是,這個子類constructor必需呼叫super constructor。接下來,你可以在ViewHolder內定義每個list cell上會有的view components,並在待會的onCreateViewHolder中初始化這些view components。再來,我們可以開始實作adapter裡的程式邏輯了。
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
View view = LayoutInflater.from(mContext)
.inflate(R.layout.cell_post, parent, false);
ViewHolder holder = new ViewHolder(view);
holder.ivPosterThumbnail = (ImageView) view.findViewById(R.id.ivPosterThumbnail);
holder.tvPosterName = (TextView) view.findViewById(R.id.tvPosterName);
holder.tvContent = (TextView) view.findViewById(R.id.tvContent);
holder.btnLike = (ImageButton) view.findViewById(R.id.btnLike);
holder.btnComment = (ImageButton) view.findViewById(R.id.btnComment);
return holder;
}
在onCreateViewHolder中,我們必須inflate要作為ViewHolder的layout,並將layout裡的view component與ViewHolder中的元件屬性綁定,以便後續item view在recycle時可以直接進行view & data的綁定,不必再重新初始化這些元件。邁向下個步驟之前,我們可先實作一資料結構 — Post,並將存放Post的ArrayList加入MyAdapter的constructor中
public class Post {
public String posterName;
public String posterThumbnailUrl;
public String content;
public Post(String posterName,
String posterThumbnailUrl, String content) {
this.posterName = posterName;
this.posterThumbnailUrl = posterThumbnailUrl;
this.content = content;
}
}
這是更新後的constructor與MyAdapter中的attributes
private Context mContext;
private ArrayList<Post> mData;
public MyAdapter(Context context, ArrayList<Post> data) {
this.mContext = context;
this.mData = data;
}
除此之外,不要忘了將getItemCount的return value改為你的data list的長度,RecyclerView將依據getItemCount決定顯示幾筆資料。
@Override
public int getItemCount() {
return mData.size();
}
接下來,我們即可開始在onBindViewHolder實作binding data的邏輯,將list的每筆資料帶入cell item
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Post post = mData.get(position);
holder.tvPosterName.setText(post.posterName);
holder.tvContent.setText(post.content);
Glide.with(mContext)
.load(post.posterThumbnailUrl)
.into(holder.ivPosterThumbnail);
}
當程式運行過程中,list item在畫面中出現時,會觸發onBindViewHolder,在這個函式我們可以拿到recycle的ViewHolder、以及該item的position,於是我們就能將位於該順序的data與view component進行綁定。在這裡我用了一個叫Glide的第三方套件方便url圖片載入,Glide操作方便、支援同步與非同步下載、快取,對提升開發效率有很大幫助,是我常用的第三方套件之一。
需要特別注意的是,由於input ViewHolder是recycle來的,如果你的ui會針對資料做不同樣式的改變,記得在onBindViewHolder回復view component的狀態,例如,我要讓content == null時隱藏content的TextView
if (post.content == null) {
holder.tvContent.setVisibility(View.GONE);
}
如果只寫這樣,那麼當TextView被隱藏後,這個ViewHolder被recycle到下一個item的時候TextView也會是被隱藏的,因此這裡最好完整處理好if/else的狀態,才不會讓畫面和想像中不一樣,例如
holder.tvContent.setVisibility(post.content == null ?
View.GONE : View.VISIBLE);
到此為止我們總算定義完Adapter class了,終於可以進入最後一個步驟:在Activity裡初始化RecyclerView & Adapter
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
ArrayList<Post> data = new ArrayList<>();
data.add(new Post("Marshmallow", "http://i.imgur.com/mVpDmzc.jpg", "Android 66666666666666"));
data.add(new Post("Lollipop", "http://i.imgur.com/kyVfpYh.png", "Android 55555555555555"));
MyAdapter adapter = new MyAdapter(this, data);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
RecyclerView和ListView在使用上有個不同的地方,除了adapter外,它還強制你必須attach 一個LayoutManager,否則運行後會跳No layout manager attached的exception
LayoutManager和RecyclerView一樣包含在android.support.v7 library,預設的LayoutManager有LinearLayoutManager用來呈現直式列表、GridLayoutManager用來呈現格狀列表,以及StaggeredGridLayoutManager用來呈現類似Pinterest瀑布狀列表,如果這些還不能滿足規格需求,也可以繼承LayoutManager進行客製化。這邊我先用LinearLayoutManager作為範例。
在這裡我先用mock data做demo,如果你的list data來自非同步api撈取,別忘了在資料變動後,對adapter呼叫notifyDataSetChanged(),才會觸發adapter更新畫面。
Build and Run it!
這是上面的code run起來的樣子,打了好長一篇,其他的使用技巧只能留到之後再講了zzz
source code: https://github.com/EvanHou/RecyclerViewDemo