React NativeでWallet風UIを実装する

この記事は[レコチョク Advent Calendar 2021](https://qiita.com/advent-calendar/2021/recochoku)の10日目の記事となります。

レコチョク・ラボの松木です。
仕事では主にレコチョク・ラボの取り組みをサポートしたり検証のためのプロトタイプを作ったりしています。

2021年も残り少しですが、おうち時間で楽器を楽しむ方が増えたというデータもあり、今年から新しく楽器をはじめた方もいるのではないでしょうか。
私は以前からサンプリングというものに興味があったので、サンプラーとマイクを使った遊びをはじめました。

## 作ったもの
今回作ったものはReact Nativeアプリケーションで動作する以下のようなアニメーションになります。
[https://snack.expo.dev/@y-matsuki/wallet-like-ui](https://snack.expo.dev/@y-matsuki/wallet-like-ui)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67231/82e3125a-7892-e32b-4f0e-3c7bc3994c1a.gif" width="200"/>

## 解説
今回実装するUIは [react-native-snap-carousel](https://github.com/meliorence/react-native-snap-carousel) をベースに実装しています。
コンポーネントの利用方法は以下です。

```jsx
const { height } = Dimensions.get('window');
return (
  <Carousel
    ref={carouselRef}
    data={items}
    renderItem={renderItem}
    vertical={true}
    enableSnap={false}
    layout={'default'}
    activeSlideAlignment={'start'}
    inactiveSlideScale={1}
    inactiveSlideOpacity={1}
    sliderHeight={height}
    itemHeight={54}
    scrollInterpolator={scrollInterpolator}
    slideInterpolatedStyle={animatedStyles}
    useScrollView={true}
  />
);
```

今回の実装で重要なポイントを以下に列挙します。
その他の属性については[ドキュメント](https://github.com/meliorence/react-native-snap-carousel/blob/master/doc/PROPS_METHODS_AND_GETTERS.md)を参照ください。

| 名称 | 説明 |
|--|--|
| data | リストのデータ |
| renderItem | データを元にReactコンポーネントを返す関数(今回はカードを返す) |
| itemHeight | 今回はカードの高さ300に対して54を指定することで重なりを表現する |
| scrollInterpolator | スクロール位置に応じた挙動をカスタマイズする |
| slideInterpolatedStyle | スクロール位置に応じたスタイルをカスタマイズする |

[ドキュメント](https://github.com/meliorence/react-native-snap-carousel/blob/master/doc/CUSTOM_INTERPOLATIONS.md)を参考にスクロールした際の挙動のカスタマイズします。 `scrollInterpolator` についてはほぼそのまま利用したため `slideInterpolatedStyle` の話をします。

```jsx
const animatedStyles = (
  index: number,
  animatedValue: Animated.Value,
  carouselProps: CarouselProps<any>
): StyleProp<ViewStyle> => {
  const cardHeight = Math.floor((width / 16) * 9);
  return {
    zIndex: carouselProps.data.length - index,
    transform: [
      {
        translateY: animatedValue.interpolate({
          inputRange: [-1, 0, 1, 2],
          outputRange: [
            -cardHeight * 0.2,  // <- アクティブなカードの前のカード
            0,                  // <- アクティブなカード(今回は一番上のカード)
            cardHeight * 1,     // <- アクティブなカードの次のカード
            cardHeight * 1.2,   // <- アクティブなカードの次の次のカード
          ],
        }),
      },
    ],
  };
};
```

[animatedValue.interpolate()](https://reactnative.dev/docs/animatedvalue#interpolate) 関数を用いてスクロール時のスタイルの挙動アニメーションを定義します。 `inputRange` がリスト内でのカードの位置を表しており `outputRange` がそれに対応するカードのスタイルを表します。
ちなみに `outputRange` を全て `0` にすると以下のようになります。(itemHeight = 54のため高さ54のリストとして描画される)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67231/74325361-28ae-2e8f-295f-b4a7340efefc.png" width="200"/>

`transform.translateY` は **コンポーネントのY座標の位置を移動する** ことを表しており `cardHeight * 1.2` は **カードの高さの1.2倍の長さ(画面下部に)移動する** という意味になります。
アクティブなカードの前後を少しずつ上下に移動させてあげることで、アクティブなカードにアクセントを付けたUIを作ることができます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67231/999868e8-1a33-c933-2220-c9a42df72f9c.png" width="200"/>

また、 `zIndex` を以下のよう指定すると、カードの重なり順を逆にできます。

```jsx
zIndex: carouselProps.data.length - index
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67231/1b161899-52b4-1f2e-9886-ddadb20ab38a.png" width="200"/>

## まとめ
iOSだとヌルヌル動くのですがAndroidの場合はshadowが効かなかったり動きが硬い感じでややイマイチでした。
今回の実装方法だとカードの枚数が多いとメモリを食うので、RecyclerView的な実装に改善するとより実用的と思います。

明日の[レコチョク Advent Calendar 2021](https://qiita.com/advent-calendar/2021/recochoku)は11日目「家計簿のデータをNode.jsでスプレッドシート連携してみた」です。お楽しみに！!

---

この記事はレコチョクのエンジニアブログの記事を転載したものとなります。

https://techblog.recochoku.jp/7993