ReactNativeで複数アイテムを一度に表示するカルーセル

##はじめに

React Nativeで、以下のgifのようなカルーセルを自力で実装したので共有します。

![ezgif.com-resize.gif](https://qiita-image-store.s3.amazonaws.com/0/214755/2163391b-63f3-012d-318a-04abd6da3164.gif)

このgifだと指の動きがついていないのでわかりにくいのですが、ポイントとしては以下3点です

1. 画面内に**複数のアイテムが表示**されており、スワイプする毎に１つずつズレる(アニメーション)
2. 両端の矢印をタップすると**スキップ**する(1~4が表示されてたら、5~8を表示する)
3. 各アイテムへのタップに対応(gifでは 「{タップした数字} was tapped」　と表示)

※ここでの「アイテム」は、gifでいう「1」「2」などの各要素を示しています。

##なぜ自作なのか

カルーセルのライブラリでは[react-native-snap-carousel](https://github.com/archriss/react-native-snap-carousel)が人気のようですが、どうやらこのライブラリでは[複数アイテムの表示に対応していない](https://github.com/archriss/react-native-snap-carousel/issues/102)ようなので自作しました。

##どうやるのか

まず、この記事ではコードは出てきません。ソースコードは[github](https://github.com/nichiyoshi/react-native-multiple-carousel)にあげています。

実装方法は色々とあるのでしょうが、今回の実装での基本的な考え方は次の6ステップです。

1. 各アイテムを**[FlatList](https://facebook.github.io/react-native/docs/flatlist)**で囲う
2. FlatList(ScrollView)の`scrollEnabled`をfalseにしてスクロールを防ぐ
3. [Gesture Responder System](https://facebook.github.io/react-native/docs/gesture-responder-system)を使ってスワイプとタップを判定
4. スワイプ時はFlatListの[scrollToIndex](https://facebook.github.io/react-native/docs/flatlist#scrolltoindex)でアイテムを１つズラす
5. タップ時は任意の関数を呼び出す
6. 両端の矢印をタップ時はscrollToIndexで任意のindexまでスキップさせる

### 1. 各アイテムを**[FlatList](https://facebook.github.io/react-native/docs/flatlist)**で囲う

これは、4.に出てくるように、[scrollToIndex](https://facebook.github.io/react-native/docs/flatlist#scrolltoindex)を使うためです。

`scrollToIndex`は**アニメーションつき**で**任意のindexまでスクロール**してくれるので、カルーセル的な動きはこれを使いました。

### 2. FlatList(ScrollView)の`scrollEnabled`をfalseにしてスクロールを防ぐ

 普通にFlatListを使っていると、FlatList上をスワイプした時に指の動きに合わせてスクロールされてしまい、カルーセル的な動きを実現できませんでした。そのため、`scrollEnabled`プロパティを`false`にして、スクロールできないようにしました。

この時点でFlatListは、明示的に`scrollToIndex`で中身を動かすためだけの存在になります。

### 3. [Gesture Responder System](https://facebook.github.io/react-native/docs/gesture-responder-system)を使ってスワイプとタップを判定

「スワイプ」しているのか、或いは各アイテムを「タップ」しているのかを判定するために、[Gesture Responder System](https://facebook.github.io/react-native/docs/gesture-responder-system)というのを使います。

これにより、

1. 指が触れた(ジェスチャーが始まった)時点でのX座標 (`onStartShouldSetResponder`)
2. 指が触れ続けている間のX座標 (`onResponderMove`)
3. 指が離れた時のX座標(`onResponderRelease`)

が分かります。

これを利用し、

- 「指が触れた時点でのX座標」と「指が触れ続けている間のX座標」が異なった時点で「スワイプ」と判定
- 「指が触れた時点でのX座標」と「指が触れ続けている間のX座標」と「指が離れた時のX座標」が全て一緒なら「タップ」と判定

というロジックにしました。

さらに、スワイプの場合はそのX座標の推移から「右」なのか「左」なのかを判定できます。

### 4. スワイプ時はFlatListの[scrollToIndex](https://facebook.github.io/react-native/docs/flatlist#scrolltoindex)でアイテムを１つズラす

ここからはもう想像通りです。

3で「右にスワイプ」と判定されれば、`scrollToIndex`で一個右にズラします。
「左にスワイプ」と判定されれば一個左にズラします。


### 5. タップ時は任意の関数を呼び出す

3で「タップ」と判定されたら、任意の関数を呼び出します。

**実際にはタップされたアイテムの透過度をアニメーションさせて`TouchableOpacity`ぽくする実装が必要かと思いますが、今回はそれは実装していません。**

アニメーションに関しては[ReactNativeでAnimated.Valueの挙動をグラフ化してみた](https://qiita.com/nichi_yo/items/befb0a36c1a3bf55eb65)という記事も書いたので興味があれば読んでみてください。

###6.両端の矢印をタップ時はscrollToIndexで任意のindexまでスキップさせる

これはもう明快だと思うので割愛します。

##課題

今回の実装の場合、 **`Gesture Responder System`を使った判定は各アイテムで行なっています**。

そのため、各アイテム間のレイアウトに`margin`が使われると、**アイテム同士の隙間に指を触れた時に反応しません**。

この課題を解決するため、今回は**各アイテムに`padding`を使い見かけ上隙間があるように見える**ようにしました。

ただ、その分**レイアウトに制限**がかかってしまいます。


以上です。

最後に繰り返しですが、ソースコードは[github](https://github.com/nichiyoshi/react-native-multiple-carousel)を参考にしてください。