中央でピタッととまるカルーセルの作り方の解説します。ハマりどころがあるので記事にしました。
実装手順
Step 1. 依存ライブラリの記述
RecyclerViewとGravitySnapHelperというライブラリを使います。build.gradleに以下を記述します。
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.github.rubensousa:gravitysnaphelper:2.2.1'
Step 2. 水平方向にスクロール可能なRecyclerViewを追加する
水平方向にスクロール可能なRecyclerViewでカルーセルは大体実現できます。
val carousel = findViewById<RecyclerView>(R.id.carousel)
carousel.apply {
// RecyclerViewを水平方向にスクロールできるように設定
layoutManager =
LinearLayoutManager(this@MainActivity, LinearLayoutManager.HORIZONTAL, false)
// サンプルAdapter
adapter = MyAdapter((0..30).map { it.toString() }.toTypedArray())
// カルーセルの両端が中央に表示されるようにRecyclerViewの両端にPaddingを入れる。
val itemWidthDp = 96 // 各項目の幅。サンプル値。
val itemWidthPixels = (itemWidthDp * resources.displayMetrics.density + 0.5).toInt()
val paddingH = (resources.displayMetrics.widthPixels - itemWidthPixels) / 2
setPadding(paddingH, 0, paddingH, 0)
clipToPadding = false
setHasFixedSize(true)
}
ポイントは2つです。
- 左右両端にPaddingを入れつつclipToPaddingをfalseにすることで、左右両端の項目を中央に表示させる
- setHasFixedSize(true)を設定する
Web検索をすると、左右両端の項目にItemDecoratorを使って余白を入れる方法をしばしば見かけますが、それだと項目の削除がある場合には上手く対処できません。末尾項目を削除したとき、中間状態として末尾のItemDecoratorが消えます。その状態で表示位置の調整が行われることで、表示位置が不正になる問題が起きます。なお、画面の表示中にカルーセルの項目を変化させないなら、ItemDecoratorの方法も使えます。
setHasFixedSize(true)を設定するのも、カルーセルの項目数が変化したときに表示位置が不正になる問題を回避するのためです1。
Step 3. カルーセルの項目が丁度良い位置に止まるようにする
GravitySnapHelperを使います。RecyclerViewに付属しているLinearSnapHelperは、中央に表示されている位置(この記事では以下、Snap位置と呼びます)の変化を検知するAPIがないので不便だからです。
val snapHelper = GravitySnapHelper(Gravity.CENTER, true)
snapHelper.attachToRecyclerView(carousel)
これでカルーセルの項目が丁度良い位置に止まるようになります。
Step 4. Snap位置の変化を検知する
GraivitySnapHelperのAPIに位置変化リスナーがあるので、それを使います。カルーセルの位置をTextViewに表示するサンプルコードを示します。
val textView = findViewById<TextView>(R.id.textView)
textView.text = "0" // 最初はリスナー呼ばれないので、初期表示は自分で設定する必要がある
snapHelper.setSnapListener {
textView.text = it.toString() // it = Snap位置
}
番外. プログラムからSnap位置を設定する
GraivitySnapHelper#smoothScrollToPositionを使います。ここまでの手順だけだと、各項目をタップしたときにその項目が中央に表示されません。項目のクリックイベントを拾う + smoothScrollToPositionを呼ぶことで、タップした項目を中央に表示できます。
snapHelper.smoothScrollToPosition(position)
smoothでない方のSnap位置設定のAPIもありますが、期待どおりに動作しない場面がしばしばあるので、smoothの方を使うのが無難だと思います(動作しない理由は不明)。
サンプルコード
-
カルーセルを作る場合、項目のサイズに応じてRecyclerViewの幅が変化することはないと思うので、基本的に設定できると思います。描画パフォーマンスも上がります。 ↩