【ReactNative】Animated.eventを使って上にスクロールしたときだけ、タブやロゴを表示する


スマホアプリでよくある、上スクロールしたときだけ、上からタブとかロゴが出てくる実装をReactNativeで書きました。

参考にした記事
https://reactnative.dev/docs/animated
https://hrk-ys.blogspot.com/2017/07/react-nativeanimation.html
https://qiita.com/rrrroji/items/35b659728a953307c040

完成イメージ

完成コード

import React from 'react';
import {Animated, StyleSheet, Text, ScrollView, Dimensions} from 'react-native';

const TestScreen = () => {
  const scrollY = React.useRef(new Animated.Value(0)).current;

  return (
    <>
      <Animated.View
        style={[
          styles.box,
          {
            transform: [{
              translateY: Animated.diffClamp( Animated.multiply(scrollY, -1), -60, 0)
            }],
          },
        ]}
      >
        <Text style={styles.text}>ロゴ</Text>
      </Animated.View>
      <ScrollView
        contentContainerStyle={{ marginTop: 60 }}
        scrollEventThrottle={16}
        onScroll={Animated.event(
          [{ nativeEvent: { contentOffset: { y: scrollY } } }],
          {
            useNativeDriver: false,
          },
        )}
      >
        <Text style={styles.text}>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
          eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
          minim veniam, quis nostrud exercitation ullamco laboris nisi ut
          aliquip ex ea commodo consequat. Duis aute irure dolor in
          reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
          pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
          culpa qui officia deserunt mollit anim id est laborum.
        </Text>
      </ScrollView>
    </>
  );
}

const { width } = Dimensions.get('window');

const styles = StyleSheet.create({
  box: {
    position: 'absolute',
    top: 0,
    right: 0,
    zIndex: 10,
    backgroundColor: '#FFF',
    height: 60,
    width: width,
    justifyContent: 'center',
    alignItems: 'center',
  },
  text: {
    fontSize: 42,
    fontWeight: 'bold',
  },
});

export default TestScreen;

ポイント

浅い知識で解説するので、興味ある方だけ見てください。

変数宣言

const scrollY = React.useRef(new Animated.Value(0)).current

イベント発火

<ScrollView
  onScroll={Animated.event(
    [{ nativeEvent: { contentOffset: { y: scrollY } } }],
    {
      useNativeDriver: false,
    },
  )}
>

スクロールすると現在の位置に合わせてscrollYの値が変化する。
Animated.event([{nativeEvent:{contentOffset:{y:scrollY}}}])}は決まった書き方らしい。言われるがままに書く。
https://reactnative.dev/docs/animated#handling-gestures-and-other-events
useNativeDriverはtrueだと、スマホでの操作性が上がるらしいが開発環境similatorでエラーが出たので、falseにした。何も書かないと警告がでる。
https://reactnative.dev/docs/animations#using-the-native-driver

タブのstyle

<Animated.View
  style={{
    position: 'absolute',
    top: 0,
    right: 0,
    transform: [{
      translateY: Animated.diffClamp( Animated.multiply(scrollY, -1), -60, 0)
    }],
  }}
>

絶対位置で指定する。
scrollYの値に合わせて移動するようにtransformで位置を動かす。Animated.multiplyは第一引数と第二引数の乗算を返す(scrolly × (-1))ので、これを使いスクロール方向と値の正負を逆転させる。Animated.diffClampは第二引数で最小値を、第三引数で最大値を設定できるので、これでタブの動ける範囲を指定する。
https://reactnative.dev/docs/animated#multiply

スクロールのオプション

<ScrollView
  contentContainerStyle={{ marginTop: 60 }}
  scrollEventThrottle={16}
>

この2つのオプションは必須ではない。contentContainerStyleを指定することで、ページを最初に表示したときに、タブとコンテンツがかぶらないようになる。
https://reactnative.dev/docs/scrollview#contentcontainerstyle
scrollEventThrottle={16}を指定することで、操作性が上がるらしい。