RecyclerView で Section を表示する
アクトインディ Advent Calendar 2016 2日目の記事になります。2015年の11月にアクトインディに入社して 1 年が経ちました。 入社してからは iOS アプリの開発に携わらせてもらい、現在は鋭意 Android アプリの開発をしています。
子どもとのお出かけ先に困っている方は、是非いこーよアプリを使ってみてください ^^
アドベントカレンダーの本題ですが、今回は Android の RecyclerView で Section をどうやって表現するかの話を書きたいと思います。アプリの設定画面など、表示する項目が決まっていて、セクションで区切られたリストを表示したいケースがあると思います。iOS では TableView に Section が用意されているので容易に実装できますが Android では List にセクションといった要素がない為、セクション用のレイアウトとリストアイテム用のレイアウトファイルを項目で切り替えて実装する必要が出てきます。今回は Enum を使った実装を紹介したいと思います。実装のサンプルは Kotlin 1.0.5 になります。
やっていることは簡単で ヘッダーには setOnClickListener を設定せず、リストアイテムの場合に setOnClickListener を設定するといった事をしています。
1. リストで表示する Enum クラスを作成する
enum class ListItem(val title: String) { HeaderFruits(title = "果物"), Apple(title = "りんご"), Orange(title = "オレンジ"), HeaderVegetables(title = "野菜"), Carrot(title = "人参"), Onion(title = "玉ねぎ"), HeaderDrinks(title = "飲み物"), Milk(title = "牛乳"), Water(title = "水") }
2. Adapter を作成する
Adapter の雛形は以下になります。コメントアウトの箇所をそれぞれ実装していきたいと思います。
class ItemsAdapter(val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { private val items = ListItem.values() override fun getItemViewType(position: Int): Int { // 1. ヘッダーかリストアイテムかで異なる viewType を返すように実装する } override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder { // 2. viewType によって読み込む layout ファイルを指定する } override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) { // 3. text のセットとアイテムクリック時のイベントをセットする } override fun getItemCount(): Int { return this.items.count() } }
1. ヘッダーかリストアイテムかで異なる viewType を返すように実装する
ヘッダー用かリストアイテム用のレイアウトを返すか判別する為の viewType を返却するように設定します。
class ItemsAdapter(val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { private val items = ListItem.values() private val headerType = 0 private val itemType = 1 override fun getItemViewType(position: Int): Int { val item = this.items[position] when (item) { ListItem.HeaderFruits, ListItem.HeaderVegetables, ListItem.HeaderDrinks -> return headerType else -> return itemType } }
2. viewType によって読み込む layout ファイルを指定する
設定した viewTyep を利用してヘッダー用とリストアイテム用の Header を指定した ViewHolder を返却するようにします。
class ItemsAdapter(val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ... 省略 ... override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(context) when (viewType) { headerType -> return ViewHolderItem(inflater.inflate(ViewHolderItem.headerLayoutId, parent, false)) itemType -> return ViewHolderItem(inflater.inflate(ViewHolderItem.itemLayoutId, parent, false)) else -> return ViewHolderItem(inflater.inflate(ViewHolderItem.itemLayoutId, parent, false)) } } ... 省略 ... class ViewHolderItem(view: View) : RecyclerView.ViewHolder(view) { companion object { val headerLayoutId = R.layout.header val itemLayoutId = R.layout.item } }
3. text のセットとアイテムクリック時のイベントをセットする
あとは Enum で定義した各アイテム毎に行いたいアクションを定義します。
class ItemsAdapter(val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ... 省略 ... override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) { holder ?: return val item = this.items[position] val textView = holder.itemView.findViewById(R.id.item_text_view) as TextView textView.text = item.title holder.itemView.setOnClickListener { this.action(item = item) } } ... 省略 ... private fun action(item: ListItem) { when (item) { ListItem.Apple -> { Toast.makeText(context, "りんご", Toast.LENGTH_LONG).show() } ListItem.Orange -> { Toast.makeText(context, "オレンジ", Toast.LENGTH_SHORT).show() } ListItem.Carrot -> { Toast.makeText(context, "人参", Toast.LENGTH_SHORT).show() } // ... else -> {} } }
adapter をセットする
最後に activity で adapter をセットします。
class MainActivity : AppCompatActivity() { private val recyclerView: RecyclerView by bindView(R.id.recycler_view) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) this.recyclerView.setHasFixedSize(true) this.recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) val settingAdapter = ItemsAdapter(context = this) recyclerView.adapter = settingAdapter } }
Enum で定義した順番がリストに表示される順番という気持ち悪さはありますが、ある程度はアイテムが増減しても、そこまで改修が大きくなることはない実装かと思います。各レイアウトファイルは以下のような感じです。
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="test.sectionrecyclerview.MainActivity"> <android.support.v7.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent"/> </RelativeLayout>
item.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/item_text_view" android:padding="16dp" android:layout_width="match_parent" android:layout_height="wrap_content"/> </FrameLayout>
header.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:background="@android:color/darker_gray" android:id="@+id/item_text_view" android:padding="16dp" android:layout_width="match_parent" android:layout_height="wrap_content"/> </FrameLayout>