React NativeでWallet風UIを実装する


この記事はレコチョク Advent Calendar 2021の10日目の記事となります。

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

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

作ったもの

今回作ったものはReact Nativeアプリケーションで動作する以下のようなアニメーションになります。
https://snack.expo.dev/@y-matsuki/wallet-like-ui

解説

今回実装するUIは react-native-snap-carousel をベースに実装しています。
コンポーネントの利用方法は以下です。

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}
  />
);

今回の実装で重要なポイントを以下に列挙します。
その他の属性についてはドキュメントを参照ください。

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

ドキュメントを参考にスクロールした際の挙動のカスタマイズします。 scrollInterpolator についてはほぼそのまま利用したため slideInterpolatedStyle の話をします。

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() 関数を用いてスクロール時のスタイルの挙動アニメーションを定義します。 inputRange がリスト内でのカードの位置を表しており outputRange がそれに対応するカードのスタイルを表します。
ちなみに outputRange を全て 0 にすると以下のようになります。(itemHeight = 54のため高さ54のリストとして描画される)

transform.translateYコンポーネントのY座標の位置を移動する ことを表しており cardHeight * 1.2カードの高さの1.2倍の長さ(画面下部に)移動する という意味になります。
アクティブなカードの前後を少しずつ上下に移動させてあげることで、アクティブなカードにアクセントを付けたUIを作ることができます。

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

zIndex: carouselProps.data.length - index

まとめ

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

明日のレコチョク Advent Calendar 2021は11日目「家計簿のデータをNode.jsでスプレッドシート連携してみた」です。お楽しみに!!


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