Material Design Patterns 教學 (4) - RecyclerView
本來想寫 AppBarLayout
,不過發現會牽涉到 RecyclerView
,所以決定先寫 RecyclerView
。
RecyclerView
就像 ListView
,都是透過 scrolling 的動作來顯示一個清單,不過它更具彈性更自由。RecyclerView
可以很簡單的將它設為橫向或直向,或者以格仔形式顯示,而且設定加減項目的動畫也很容易。它的定位是 ListView
的後繼者,之前介紹過的 CoordinatorLayout
也只支援 RecyclerView
而不支援 LisView
,所以大家還是用一用 RecyclerView
吧。
此教學分為以下幾步:
- 安裝
- 設定 layout
- 編寫
adapter
- 給合
adapter
和RecyclerView
- 更多設定
1. 安裝
安裝 RecyclerView
,在 gradle 加入以下設定即可。
compile 'com.android.support:recyclerview-v7:+'
2. 設定 layout
RecyclerView
的 layout 是這樣。
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
將其加進 layout_activity.xml
即可。
新增 item_contact.xml
,它將會是 RecyclerView
中每一個項目的 view
。為示範,我們只用一個 TextView
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="30dp"
/>
</LinearLayout>
3. 編寫 Adapter
RecyclerView
需要一個 Adapter
,但它不像 ListView
有內建的 adapter
(如 ArrayAdapter
) 可供使用,每次也要自行 extends RecyclerView.ViewHolder
。
為示範,我們先建立一個 model Contract
,
public class Contact {
private String name;
public Contact() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static List<Contact> generateSampleList(){
List<Contact> list = new ArrayList<>();
for(int i=0; i < 30; i++){
Contact contact = new Contact();
contact.setName("Name - " + i);
list.add(contact);
}
return list;
}
}
最後的 generateSmapleList()
只是隨便建位一個 list
來模擬資料。
接著開始建立 ContactsAdapter
。
先在 ContactsAdapter
中加入 ViewHolder
public class ContactsAdapter extends RecyclerView.Adapter<ContactsAdapter.ViewHolder> {
public static class ViewHolder extends RecyclerView.ViewHolder{
public TextView nameTextView;
public ViewHolder(View itemView){
super(itemView);
nameTextView = (TextView) itemView.findViewById(R.id.tv_name);
}
}
}
其實 ViewHolder
pattern 在 ListView
時已有,不過可選擇不使用,而 RecyclerView.Adapter
強制使用。
接著加入 constructor 和 getItemCount()
。
public class ContactsAdapter extends RecyclerView.Adapter<ContactsAdapter.ViewHolder> {
//View holder code
private List<Contact> mContacts;
public ContactsAdapter(List<Contact> contacts){
mContacts = contacts;
}
@Override
public int getItemCount() {
return mContacts.size();
}
}
然後是戲肉:onCreateViewHolder()
和 onBindViewHolder()
。前者建立 view
,並將 view
轉成 ViewHolder
,後者將 Contact
顯示在 view
中。
public class ContactsAdapter extends RecyclerView.Adapter<ContactsAdapter.ViewHolder> {
//view holder code
// constructor & getItemCount()
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
Context context = viewGroup.getContext();
View contactView = LayoutInflater.from(context).inflate(R.layout.item_contact, viewGroup, false);
ViewHolder viewHolder = new ViewHolder(contactView);
return viewHolder;
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
Contact contact = mContacts.get(position);
TextView nameTextView = viewHolder.nameTextView;
nameTextView.setText(contact.getName());
}
}
這樣 ContactsAdapter
便完成。
細心的你可以發現,以往在 ArrayAdapter
的 getview()
,現在 RecyclerView
中拆散為 onCreateViewHolder()
和 onBindViewHolder()
。建立 view
和 更新 view
的動作分為兩個 methods ,code 變得更易讀。
4. 給合 adapter
和 RecyclerView
最後在 Activity
中將 RecyclerView
和 ContactsAdapter
結合:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_appbar);
RecyclerView rvContacts = (RecyclerView) findViewById(R.id.recyclerView);
ContactsAdapter adapter = new ContactsAdapter(Contact.generateSampleList());
rvContacts.setAdapter(adapter);
rvContacts.setLayoutManager(new LinearLayoutManager(this));
}
這樣便大功告成。
5. 更多設定
LayoutManager
RecyclerView
不一定要垂直一個個的顯示項目。透過設定不同的 LayoutManager
, 我們可以改變它的 layout。要用 GridLayoutManager
便可變成 GridView
一樣:
recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
Support library 已內建三款不同的 LayoutManager
:LinearLayoutManager
, GridLayoutManager
和 StaggeredGridLayoutManager
。當然,自行編寫一個也絕對沒有問題。
想了解如何行編寫的 ItemDecoration
的話請看最底的相關連結吧。
click, click, click
處理 click 是 RecyclerView
比 ListView
麻煩的地方,因為 RecyclerView
沒有如 ListView.setItemClickListener()
一樣的 method,要處理 click event 變得很自由,也很麻煩。
方法一:使用 OnClickListener
可以在 ViewHolder
加入 OnClickListener
去處理:
public class ContactsAdapter extends RecyclerView.Adapter<ContactsAdapter.ViewHolder> {
public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
public TextView nameTextView;
public MyViewHolderClick mListener;
public ViewHolder(View itemView, MyViewHolderClick listener){
super(itemView);
mListener = listener;
nameTextView = (TextView) itemView.findViewById(R.id.tv_name);
itemView.setOnClickListener(this);
}
@Override
public void onClick(View v) {
mListener.clickOnView(v, getLayoutPosition());
}
public interface MyViewHolderClick {
void clickOnView(View v, int position);
}
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
Context context = viewGroup.getContext();
View contactView = LayoutInflater.from(context).inflate(R.layout.item_contact, viewGroup, false);
ViewHolder viewHolder = new ViewHolder(contactView, new ViewHolder.MyViewHolderClick() {
@Override
public void clickOnView(View v, int position) {
Contact contact = mContacts.get(position);
Snackbar.make(v, contact.getName(), Snackbar.LENGTH_LONG).show();
}
});
return viewHolder;
}
還有,要將 item_contact
的 background 改為 ?android:attr/selectableItemBackground
,才會有 click 下去變色的效果。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackground"
>
<!--.... -->
</LinearLayout>
注意的是,以上的 code 是在 ViewHolder
中將整個 itemView
執行 setOnClickListener()
的,所以 background 要設在整個 layout 最外層,則 LinearLayout
上。若只需要某一個 child view
處理 click 的話,要將對應的 view
background 設為 ?android:attr/selectableItemBackground
才行。
使用 OnClickListener
的好處是:若你有多於一個 view
需要處理 click,可以在 MyViewHolderClick
加進新的 method ,並在 onClick
中根據 view
來決定執行那個 method。
方法二:使用 3rd party library
若你純綷只有一個 view
又不想麻煩的話,可以用別人寫的 library ,然後便可以:
ItemClickSupport.addTo(mRecyclerView).setOnItemClickListener(new ItemClickSupport.OnItemClickListener() {
@Override
public void onItemClicked(RecyclerView recyclerView, int position, View v) {
// do it
}
});
方法三:使用 onItemTouchListener
當然,想自行完全控制的話,可使用 onItemTouchListener()
處理所有 touch event
recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
@Override
public void onTouchEvent(RecyclerView recycler, MotionEvent event) {
// Handle on touch events here
}
@Override
public boolean onInterceptTouchEvent(RecyclerView recycler, MotionEvent event) {
return false;
}
});
為什麼會這麼麻煩?
其實 ListView
的做法反而是有問題,因為你可以在 ArrayAdapter.getView()
中自行對各 child view
的 setOnClickListner()
,同時在 ListView
執行 setItemClickListener()
,那麼結果會如何呢? touch event 會如何處埋? 這方面沒有什麼文件說明 Android 會怎樣處理。這是一個容易引起誤會的地方。
加減項目
加減項目可以直接修改 data source ,然後通知 adapter
mContacts.add(0, newContact);
adapter.notifyItemInserted(0);
mContacts.remove(0);
adapter.notifyItemRemoved(0);
為了效能,不建議如 listView
一樣執行 notifyDataSetChanged()
。RecyclerView.Adapter
提供 notifyItemInserted()
, notifyItemChanged()
和 notifyItemRemoved()
等以處理加減項目。而且需要使用相關的 notify method 才會有加減項目的動畫。另外,在 data source 的頭尾加新減項目是不會有動畫的。
動畫
RecyclerView
其中一個強項是動畫的支援,我們可改變捲動時或加減項目的動畫。不過最簡的做法使用 open source library:RecyclerView-animators。
要改變加減項目的動畫,只需使用 recyclerView.setItemAnimator()
便可:
recyclerView.setItemAnimator(new SlideInUpAnimator())
有多種動畫可供選擇:
如想捲動時有動畫,便要用它的 AnimationAdapter
包著原本的 adapter
:
recyclerView.setAdapter(new AlphaInAnimationAapter(adapter));
項目分隔線
RecyclerView
是沒有內建項目之間的分隔線的。要加上的話,需要自行使用 ItemDecoration
,編寫對應的 class
,這樣你可以自行決定項目間的間距和分隔線的外觀等。就算只想如 ListView
般加一條橫線作分隔,也需自行寫一段 code 去做。幸好,我們活在共享的世代,來使用 Android Support Demo 的 DivierItemDecoration
:
RecyclerView.ItemDecoration itemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST);
recyclerView.addItemDecoration(itemDecoration);
多項選擇
支援多項選擇不是這麼簡單,請自行查閱。
嗯,我承認,到這裏我已經不想再寫下去了。
結語
有了 RecyclerView
,我們可以完全放棄 ListView
了嗎?不,上面也可看到, RecyclerView
雖然很強大,但同時也有點複雜。在 ListView
很簡單的設定,到 RecyclerView
便需要編寫一大段 code 或使用 3rd party library。缺少內建 default 設定,實在是 Google 的失職。
用不用 RecyclerView
要看你的需要。若想要動畫,或要動態地轉變顯示格式的話,便用 RecyclerView
,只需直排或橫排簡單顯示的話,便用 ListView
吧。畢竟雖然 RecyclerView
不算複雜,但也比不上 ListView
的簡單。
不過為了美好的 Material Design ,建議大家儘量使用。
以上的程式碼已放上 Github:
https://github.com/goofyz/android-material-design-tutorial/tree/part4_recyclerview
相關連結
- RecyclerView 官方文件
- RecyclerView-animators - 為 RecyclerView 加進生動的動畫
- Getting your clicks on RecyclerView - 加進點擊的動作
- DividerItemDecoration.java - Android Support Demo 的分隔線
- How to add dividers and spaces between items in RecyclerView? - 自行編寫分隔線
- RecyclerView Part 2: Choice Modes - 設定多項選擇
- Material Design Patterns 教學