search
LoginSignup
5

posted at

updated at

React Native で Liquid Swipe を実装するための4つの技術

この記事は React Native Advent Calendar 2021 の21日目の記事です。

少し前に Twitter で Liquid Swipe の映像を見て興味を持ったのですが、 React Native での実装を YouTube で紹介してくださっている方がいらっしゃいました。

この動画を見ながら実装すれば、 Liquid Swipe を実現できるのですが、仕組みがあまりわかリませんでした。
この記事では Liquid Swipe の実装に使用されている技術について、紹介しようと思います。

ちなみに動画を見ながら実装したものがこちらになります

採用されている技術

React Native SVG

この Liquid Swipe では、 SVG の Paths が使用されています。
SVG の Paths を使えば、直線、曲線などを表現し、図形を作成することができます。
以下のコードで、画面上に正方形を表示することができます。

import Svg, {Path} from 'react-native-svg';
import {StyleSheet} from 'react-native';

const Example = () => {
  return (
    <Svg style={StyleSheet.absoluteFill}>
      <Path d="M 0 0 H 200 V 200 H 0 L 0 0" fill="#55C500" />
    </Svg>
  );
};

Dimensions を使用すれば、図形を画面いっぱいに表示することができます。

import Svg, {Path} from 'react-native-svg';
import {Dimensions, StyleSheet} from 'react-native';

const Example = () => {
  const {width, height} = Dimensions.get('screen');

  return (
    <Svg style={StyleSheet.absoluteFill}>
      <Path d={`M 0 0 H ${width} V ${height} H 0 L 0 0`} fill="#55C500" />
    </Svg>
  );
};

Liquid Swipe ではベジェ曲線を使用しています。
MDN web docs にベジェ曲線の使い方など詳しく書かれているので、ご参考ください。

React Native MaskedView

この Liquid Swipe では、 SVG を使用してマスクする実装がなされています。
README のサンプルコードにもありますが、背景色が設定されている画面を、文字列が書かれた画面でマスクすることで、背景に設定した色で文字列が浮かび上がります。

import {StyleSheet, Text, View} from 'react-native';
import MaskedView from '@react-native-masked-view/masked-view';

const Example = () => {
  const maskElement = (
    <View
      style={
        (StyleSheet.absoluteFill,
        {flex: 1, alignItems: 'center', justifyContent: 'center'})
      }>
      <Text style={{fontSize: 156, fontWeight: 'bold'}}>Qiita</Text>
    </View>
  );

  return (
    <MaskedView style={StyleSheet.absoluteFill} maskElement={maskElement}>
      <View style={[StyleSheet.absoluteFill, {backgroundColor: '#55C500'}]} />
    </MaskedView>
  );
};

maskElement には SVG で作成した図形も設定することができます。
以下のコードで、下にある画像を半分だけマスクして、別の画像を表示させることができます。

import Svg, {Path} from 'react-native-svg';
import {Dimensions, Image, StyleSheet} from 'react-native';
import MaskedView from '@react-native-masked-view/masked-view';

const Example = () => {
  const {width, height} = Dimensions.get('screen');

  const maskElement = (
    <Svg style={StyleSheet.absoluteFill}>
      <Path d={`M 0 0 H ${width / 2} V ${height} H 0 L 0 0`} fill="black" />
    </Svg>
  );

  return (
    <>
      <Image
        style={{width: '100%'}}
        source={require('../assets/2.png')}
        resizeMode="contain"
      />
      <MaskedView style={StyleSheet.absoluteFill} maskElement={maskElement}>
        <Image
          style={{width: '100%'}}
          source={require('../assets/1.png')}
          resizeMode="contain"
        />
      </MaskedView>
    </>
  );
};

React Native Reanimated

この Liquid Swipe では、 SVG の Path に Reanimated が使用されていて、マスク範囲を動的に変更しています。
以下のコードで、 Button を押すたびに Path の横幅を動的に変更できます。

import Svg, {Path} from 'react-native-svg';
import {Button, Dimensions, Image, StyleSheet, View} from 'react-native';
import MaskedView from '@react-native-masked-view/masked-view';
import Animated, {
  useAnimatedProps,
  useSharedValue,
} from 'react-native-reanimated';

const Example = () => {
  const {width, height} = Dimensions.get('screen');
  const randomWidth = useSharedValue(100);
  const AnimatedPath = Animated.createAnimatedComponent(Path);
  const animatedProps = useAnimatedProps(() => {
    return {
      d: `M 0 0 H ${randomWidth.value} V ${height} H 0 L 0 0`,
    };
  });
  const maskElement = (
    <Svg style={StyleSheet.absoluteFill}>
      <AnimatedPath fill="black" animatedProps={animatedProps} />
    </Svg>
  );

  return (
    <>
      <Image
        resizeMode="contain"
        source={require('../assets/2.png')}
        style={{width: '100%'}}
      />
      <MaskedView style={StyleSheet.absoluteFill} maskElement={maskElement}>
        <Image
          resizeMode="contain"
          source={require('../assets/1.png')}
          style={{width: '100%'}}
        />
      </MaskedView>
      <View
        style={{
          position: 'absolute',
          top: 600,
          right: 0,
          bottom: 0,
          left: 0,
          justifyContent: 'center',
          alignItems: 'center',
        }}>
        <View style={{backgroundColor: '#55C500', borderRadius: 4, width: 200}}>
          <Button
            color="white"
            onPress={() => {
              randomWidth.value = Math.random() * width;
            }}
            title="Button"
          />
        </View>
      </View>
    </>
  );
};

React Native Gesture Handler

この Liquid Swipe では、 Gesture Handler をフックして、 Reanimated でラップした Path を動的に変更しています。
以下のコードで、スクロールした分だけマスク範囲を広げたり、狭めたりすることができます。

import Svg, {Path} from 'react-native-svg';
import {Dimensions, Image, StyleSheet} from 'react-native';
import MaskedView from '@react-native-masked-view/masked-view';
import Animated, {
  useAnimatedGestureHandler,
  useAnimatedProps,
  useSharedValue,
} from 'react-native-reanimated';
import {PanGestureHandler} from 'react-native-gesture-handler';

const Example = () => {
  const {height} = Dimensions.get('screen');
  const maskedWidth = useSharedValue(10);

  const AnimatedPath = Animated.createAnimatedComponent(Path);
  const animatedProps = useAnimatedProps(() => {
    return {
      d: `M 0 0 H ${maskedWidth.value} V ${height} H 0 L 0 0`,
    };
  });

  const onGestureEvent = useAnimatedGestureHandler({
    onStart: ({x}) => {
      maskedWidth.value = x;
    },
    onActive: ({x}) => {
      maskedWidth.value = x;
    },
    onEnd: ({x}) => {
      maskedWidth.value = x;
    },
  });

  const maskElement = (
    <Svg style={StyleSheet.absoluteFill}>
      <AnimatedPath fill="black" animatedProps={animatedProps} />
    </Svg>
  );

  return (
    <PanGestureHandler onGestureEvent={onGestureEvent}>
      <Animated.View style={StyleSheet.absoluteFill}>
        <Image
          resizeMode="contain"
          source={require('../assets/2.png')}
          style={{width: '100%'}}
        />
        <MaskedView style={StyleSheet.absoluteFill} maskElement={maskElement}>
          <Image
            resizeMode="contain"
            source={require('../assets/1.png')}
            style={{width: '100%'}}
          />
        </MaskedView>
      </Animated.View>
    </PanGestureHandler>
  );
};

最後に

基本的には以上4つの技術を使用して、 Liquid Swipe が実装されます。
アニメーションの効果を調整したり、 SVG の Path を曲線で表現したりすることで、 Liquid Swipe が実現します。

自分は上記の4つの技術を理解して、もう一度実装したことで理解が深まりました。
React Native で Liquid Swipe を実装するときに、ぜひ4つの技術について見てみてください。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
5