はじめに
React Nativeで、以下のgifのようなカルーセルを自力で実装したので共有します。
このgifだと指の動きがついていないのでわかりにくいのですが、ポイントとしては以下3点です
- 画面内に複数のアイテムが表示されており、スワイプする毎に1つずつズレる(アニメーション)
- 両端の矢印をタップするとスキップする(1~4が表示されてたら、5~8を表示する)
- 各アイテムへのタップに対応(gifでは 「{タップした数字} was tapped」 と表示)
※ここでの「アイテム」は、gifでいう「1」「2」などの各要素を示しています。
なぜ自作なのか
カルーセルのライブラリではreact-native-snap-carouselが人気のようですが、どうやらこのライブラリでは複数アイテムの表示に対応していないようなので自作しました。
どうやるのか
まず、この記事ではコードは出てきません。ソースコードはgithubにあげています。
実装方法は色々とあるのでしょうが、今回の実装での基本的な考え方は次の6ステップです。
- 各アイテムをFlatListで囲う
- FlatList(ScrollView)の
scrollEnabled
をfalseにしてスクロールを防ぐ - Gesture Responder Systemを使ってスワイプとタップを判定
- スワイプ時はFlatListのscrollToIndexでアイテムを1つズラす
- タップ時は任意の関数を呼び出す
- 両端の矢印をタップ時はscrollToIndexで任意のindexまでスキップさせる
1. 各アイテムをFlatListで囲う
これは、4.に出てくるように、scrollToIndexを使うためです。
scrollToIndex
はアニメーションつきで任意のindexまでスクロールしてくれるので、カルーセル的な動きはこれを使いました。
2. FlatList(ScrollView)のscrollEnabled
をfalseにしてスクロールを防ぐ
普通にFlatListを使っていると、FlatList上をスワイプした時に指の動きに合わせてスクロールされてしまい、カルーセル的な動きを実現できませんでした。そのため、scrollEnabled
プロパティをfalse
にして、スクロールできないようにしました。
この時点でFlatListは、明示的にscrollToIndex
で中身を動かすためだけの存在になります。
3. Gesture Responder Systemを使ってスワイプとタップを判定
「スワイプ」しているのか、或いは各アイテムを「タップ」しているのかを判定するために、Gesture Responder Systemというのを使います。
これにより、
- 指が触れた(ジェスチャーが始まった)時点でのX座標 (
onStartShouldSetResponder
) - 指が触れ続けている間のX座標 (
onResponderMove
) - 指が離れた時のX座標(
onResponderRelease
)
が分かります。
これを利用し、
- 「指が触れた時点でのX座標」と「指が触れ続けている間のX座標」が異なった時点で「スワイプ」と判定
- 「指が触れた時点でのX座標」と「指が触れ続けている間のX座標」と「指が離れた時のX座標」が全て一緒なら「タップ」と判定
というロジックにしました。
さらに、スワイプの場合はそのX座標の推移から「右」なのか「左」なのかを判定できます。
4. スワイプ時はFlatListのscrollToIndexでアイテムを1つズラす
ここからはもう想像通りです。
3で「右にスワイプ」と判定されれば、scrollToIndex
で一個右にズラします。
「左にスワイプ」と判定されれば一個左にズラします。
5. タップ時は任意の関数を呼び出す
3で「タップ」と判定されたら、任意の関数を呼び出します。
実際にはタップされたアイテムの透過度をアニメーションさせてTouchableOpacity
ぽくする実装が必要かと思いますが、今回はそれは実装していません。
アニメーションに関してはReactNativeでAnimated.Valueの挙動をグラフ化してみたという記事も書いたので興味があれば読んでみてください。
6.両端の矢印をタップ時はscrollToIndexで任意のindexまでスキップさせる
これはもう明快だと思うので割愛します。
課題
今回の実装の場合、 Gesture Responder System
を使った判定は各アイテムで行なっています。
そのため、各アイテム間のレイアウトにmargin
が使われると、アイテム同士の隙間に指を触れた時に反応しません。
この課題を解決するため、今回は各アイテムにpadding
を使い見かけ上隙間があるように見えるようにしました。
ただ、その分レイアウトに制限がかかってしまいます。
以上です。
最後に繰り返しですが、ソースコードはgithubを参考にしてください。