今回はRecyclerViewについてです
Androidでリスト形式のデータを表示させる場合にはListView
やRecyclerView
などがあると思いますが、複雑なリストが作成できたりする点でRecyclerViewを選択することがほとんどだと思います
ただ、当たり前のように使っていて仕組みをあまり理解していない気がしないので調べたことをまとめようと思います
RecyclerViewについて
RecyclerViewはリストを表示させる際に使います。リサイクルという名前のとおり、個々の要素をリサイクルします。アイテムが画面外にスクロールされても、RecyclerViewはビューを破棄せず、画面上にスクロールされた新しいアイテムのビューを再利用します。再利用をすることによってパフォーマンスの大幅な改善がされるのがListViewとの違いです
RecyclerViewでリストを表示させるために必要なもの
今回はRecyclerViewのライブラリ追加は省きますが、前提としてbuild.gradleにRecyclerViewのバージョン等の設定を追加しないと使えないです
RecyclerViewを使用するためにはまず、Adapter
とViewHolder
が必要になります
Adapter
Adapterについて軽く説明します。AdapterはデザインパターンのAdapterパターンを元に作られています
理解しやすくするために例えられるものでコンセントを例にしたものがあります。電気のコンセントは日本と海外では異なるので、日本のコンセントを海外で使うときは変換アダプターを使いプラグの形状を変えて使います。このように家電自体のコンセントを変えずに、別の場所でコンセントを使えるように変換するのがアダプターの役割です
Adapterパターンも変換アダプターと同じように、異なるクラス同士のインターフェースを変更せずに使用するための役割を担います
RecyclerViewのAdapterは、表示するデータをレイアウトファイルのRecyclerViewに表示させるために使います(1行分のデータを1行分のViewに設定して生成するもの)
ViewHolder
ViewHolderは1行分のView(ウィジェット)の参照を保持するものです。Adapterに指示されてセルを作ったり保持したりします
RecyclerViewやAdapterやViewHolderの関連性を簡易的な図にまとめたものは下記です
次に実際にAdapterクラスを見てみましょう
実際のAdapterクラス
下記にAdapterクラスの一例です
class SimpleRecyclerAdapter(private val titleList: List<String>): RecyclerView.Adapter<SimpleRecyclerAdapter.SimpleListViewHolder>() { // セルのレイアウトを読み込んでViewHolderと紐付ける (1セルごとに毎回呼び出される) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleListViewHolder { val inflater = LayoutInflater.from(parent.context) // セルのレイアウト val view = inflater.inflate(R.layout.simple_list, parent, false) return SimpleListViewHolder(view) } // セルの個数の定義 override fun getItemCount(): Int { return titleList.size } // 取得したセルデータをViewHolderが参照してきたViewにセットする (1セルごとに毎回呼び出される) // positionは現在のセルが何番目かを定義しているもの override fun onBindViewHolder(holder: SimpleListViewHolder, position: Int) { holder.titleTextView.text = titleList[position] } class SimpleListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var titleTextView: TextView = itemView.findViewById(R.id.simpleListTitleTextView) } }
Adapterクラスを作成する場合には必ず親元のRecyclerView.Adapter<自作ViewHolder>を継承させることと次の3つのメソッドをOverrideする必要があります
- onCreateViewHolder()
- onBindViewHolder()
- getItemCount()
それぞれの処理の概要は上記の一例のコードにコメントとして記載したので確認してください
今回はセルのレイアウトにsimpleListTitleTextViewというidのTextViewを設定しています
ListViewとの大きな違いとして、ListViewではgetItem()内でレイアウトファイルからインフレート・データの取得・Viewに格納を行っていますが、RecyclerViewではそれぞれが独立した処理(メソッドやクラス)として定義する必要があります。
これでAdapterクラスを作成できたのですが、RecyclerViewに渡すために紐付けを行うのとレイアウトを調整する必要があります。
LayoutManager
LayoutManager
はセル1つ分のデータのサイズなどを考慮して、レスポンシブにレイアウトを管理するクラスになります
LayoutManagerは標準で下記3パターンのレイアウトを提供しています
- LinearLayoutManager→リストで表示する
- GridLayoutManager→それぞれのセルの高さ(幅)がそろった格子状に表示する
- StaggeredGridLayoutManager→格子状に表示するが、高さ(幅)がそれぞれのセルで異なる
一般的にLinearLayoutManagerを使用することが多いと思います。リストで表示する場合には縦にスクロールするか横にスクロールするかを設定することができます
実際にLayoutManagerの使い方の一例は下記になります
class SimpleRecyclerViewActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_simple_recycler) setupUI() } private fun setupUI() { setupAdapter() } private fun setupAdapter() { val titleList = listOf("title1", "title2", "title3", "title4", "title5", "title6", "title7", "title8") val adapter = SimpleRecyclerAdapter(titleList) // RecyclerViewとAdapterの紐付けを行う simpleRecyclerView.adapter = adapter // RecyclerViewの表示形式の設定 simpleRecyclerView.layoutManager = LinearLayoutManager(this) // 区切り線を追加する場合には必要 val separate = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) simpleRecyclerView.addItemDecoration(separate) } }
今回はリストの縦スクロールで表示するパターンなのですが、横スクロールをする場合にはval layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
で可能です
上記のsetupAdapterメソッドの処理をActivityやFragmentなどで行うのが一般的です
区切り線を追加する場合にはDividerItemDecoration
を追加する必要があります。今回は標準のクラスを使っていますが、背景色を変更する場合にはカスタマイズしたクラスを作成してRecyclerViewと紐づけることができます
ここまで行うとリストが表示されるはずです
複数のAdapterを使う場合
場合によっては複数のAdapterを使う場合があると思います。例えばセルのレイアウトが異なるリストを作成する場合などです
そんなときはConcatAdapter
という機能を使えます。ConcatAdapterはRecyclerviewの1.2以降で使える機能なので古いバージョンを使っている方はバージョンをあげて利用できるようにする必要があります
下記のようにConcatAdapterにaddAdapter
することでセルのレイアウトが異なる場合でも簡単に表示できるようになります
class SimpleRecyclerViewActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_simple_recycler) setupUI() } private fun setupUI() { setupAdapter() } private fun setupAdapter() { val titleList = listOf("title1", "title2", "title3", "title4", "title5", "title6", "title7", "title8") val headerAdapter = HeaderAdapter(titleList) val contentsAdapter = ContentsAdapter() val concatAdapter = ConcatAdapter().apply { addAdapter(headerAdapter) addAdapter(contentsAdapter) } simpleRecyclerView.adapter = concatAdapter simpleRecyclerView.layoutManager = LinearLayoutManager(this) } }
スクロール位置の取得・復元
スクロール位置の取得と復元ができます
// 保持 val state: Parcelable = recyclerView.layoutManager?.onSaveInstanceState() // 復元 recyclerView.layoutManager?.onRestoreInstanceState(state)
リストの先頭や末尾かどうかを調べる
あまり使うことはないかもしれませんが、リストが先頭や末尾にいるかどうかを判定する方法です。 次で判定することができます
// 先頭かどうか recyclerView.canScrollVertically(-1).not() // 末尾かどうか recyclerView.canScrollVertically(1).not()
主に使うのはonScrolledで判定するケースだと思いますが、大体以下のような実装になるかと思います
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if (recyclerView.canScrollVertically(-1).not()) { // リストの先頭に来た時の処理 } if (recyclerView.canScrollVertically(1).not()) { // リストの末尾に来た時の処理 } } })