📘

React Nativeで画像をぐりぐり動かす

2022/10/10に公開

こういうやつです。

はじめに

せっかくReact Nativeでアプリを開発しているんだから、モダンでイケイケなアプリを作りたいですよね。
モダンというとアニメーションかなぁという安直な発想から、ユーザーの操作に応じてアニメーションするアプリを作りたいと思いました。

React Nativeはデフォルトでアニメーション機能が付属していますが、わりと自由度が低そうに見えました。
一方で、以前からReact系列のアニメーションライブラリとしてreact-springの名前を聞いていたので、そちらを使っていい感じにやりたいなーと思いました。

というわけで、react-springでアニメーション作成をする入門的な記事になればと思いつつ、本記事を執筆させていただきます。
なお、本記事に記載したコマンドはReact Native CLIで作成したプロジェクトを前提としています。
Expoで作成している場合は適宜読み替えてください。

雛形の作成

とりあえずは画像を表示させましょう。
React Native CLIreact-native-template-typescriptテンプレートを使って作成したプロジェクトの中身をきれいにして、画像を表示させます。
実際に動かすのはAndroid Studioのエミュレータでいいでしょう。
(画像はこちらで公開されているものを利用しています。)

import React from 'react';
import { Image, StyleSheet, View } from 'react-native';

export default () => {
  return (
    <View style={styles.container}>
      <Image style={styles.image} source={require('{画像のパス}')} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  image: {
    height: '50%',
    width: '50%',
  },
});

ヨシ!
次に必要なコンポーネントを導入しましょう。
アニメーション用にreact-spring、ジェスチャー検知用にReact Native Gesture Handlerを導入します。

npm i @react-spring/native react-native-gesture-handler

React Native Gesture Handlerを導入するにはアプリ全体をラップするコンポーネントを追加する必要があります。

import { GestureHandlerRootView } from 'react-native-gesture-handler';

export default () => {
  return (
    <GestureHandlerRootView style={styles.container}>
      <View style={styles.container}>
        <Image style={styles.image} source={require('{画像のパス}')} />
      </View>
    </GestureHandlerRootView>
  );
};

アニメーションの設定

それでは画像に対してアニメーションを設定していきましょう。
react-springでアニメーション動作を設定するには、「アニメーション前の状態」と「アニメーション後の状態」の設定を切り替えるように関数を発火させるといい感じに動いてくれるように思います。

言葉で説明されてもわかりにくいと思うので、実際にコードを書いていきましょう。
まず、Imageコンポーネントをアニメーションできるようにreact-springのコンポーネントで置き換えます。
ついでにアニメーション用の値も設定していきましょう。今回はtransformを利用してアニメーションさせることにします。

import { animated } from '@react-spring/native';

export default () => {
  const [{ mx, my }, setTransform] = useState({ mx: 0, my: 0 });

  return (
    <GestureHandlerRootView style={styles.container}>
      <View style={styles.container}>
        <animated.Image
          style={[styles.image, { transform: [{ translateX: mx, translateY: my }] }]}
          source={require('{画像のパス}')}
        />
      </View>
    </GestureHandlerRootView>
  );
};

さて、アニメーションを考慮しないのであれば、後はsetTransformで値を変更することで画像の位置が調整できるように見えますね。
しかし今回はアニメーションさせたいので、useStateuseSpringに置き換えます。
ついでにタップした時にアニメーションするようにPressableでラップしてタップ時イベントを持たせましょう。
出来上がったものがこちらです。

import { animated, useSpring } from '@react-spring/native';

export default () => {
  const [{ mx, my }, api] = useSpring(() => ({ mx: 0, my: 0 }));

  const switchStyle = useCallback(() => {
    const newMx = mx.get() !== 0 ? 0 : 100;
    const newMy = my.get() !== 0 ? 0 : 100;
    api.start({ mx: newMx, my: newMy });
  }, [mx, my, api]);

  return (
    <GestureHandlerRootView style={styles.container}>
      <View style={styles.container}>
        <Pressable style={styles.container} onPress={switchStyle}>
          <animated.Image
            style={[styles.image, { transform: [{ translateX: mx }, { translateY: my }] }]}
            source={require('{画像のパス}')}
          />
        </Pressable>
      </View>
    </GestureHandlerRootView>
  );
};

ヨシ!
translateXtranslateYの値を0100で切り替えてアニメーションするように設定しています。
タップするごとに画像がアニメーションするようになりましたね。

ジェスチャーの設定

ここからドラッグ動作を検知できるようにしていきます。今回はManualジェスチャーを利用します。
onTouchesDownonTouchesMoveonTouchesUpはそれぞれ指が触れたとき、動いたとき、離れたときに発火する処理を定義できます。
いずれもタッチイベントを引数にとるコールバック関数を設定できます。設定したジェスチャーはGestureDetectorに渡すことで検知できるようになります。
というわけで、まずはPressableGestureDetectorに置き換え、適当な関数を持たせたジェスチャーを設定します。

import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';

export default () => {
  const [{ mx, my }, api] = useSpring(() => ({ mx: 0, my: 0 }));

  const gesture = Gesture.Manual()
    .onTouchesDown(e => console.log(e))
    .onTouchesMove(e => console.log(e))
    .onTouchesUp(e => console.log(e));

  return (
    <GestureHandlerRootView style={styles.container}>
      <View style={styles.container}>
        <GestureDetector gesture={gesture}>
          <animated.Image
            style={[styles.image, { transform: [{ translateX: mx }, { translateY: my }] }]}
            source={require('{画像のパス}')}
          />
        </GestureDetector>
      </View>
    </GestureHandlerRootView>
  );
};

なお、GestureDetectorは基本的に直下のコンポーネントに対する操作しか検知できません。
厳密にはちょっと異なるのですが、詳しいところはドキュメントに記載されているのでそちらをご参照ください。

あとはジェスチャーに適切なコールバック関数を設定すればOKですね。
ドラッグ操作に合わせて画像が移動するように設定したいので、最初に指が触れた座標から現在の座標との差分に応じた補正をし、指が離れた時は元の座標に戻るように設定すればいいですね。
最終的に出来上がったものがこちらです。

import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
import { animated, useSpring } from '@react-spring/native';

export default () => {
  const [{ initialX, initialY }, setInitial] = useState({ initialX: 0, initialY: 0 }); // 指が触れた座標
  const [{ mx, my }, api] = useSpring(() => ({ mx: 0, my: 0 }));

  const gesture = Gesture.Manual()
    .onTouchesDown(e => setInitial({ initialX: e.allTouches[0].absoluteX, initialY: e.allTouches[0].absoluteY })) // 指が触れた座標を記録
    .onTouchesMove(e =>
      api.start({ mx: e.allTouches[0].absoluteX - initialX, my: e.allTouches[0].absoluteY - initialY }), // 現在座標との差分で補正
    )
    .onTouchesUp(() => api.start({ mx: 0, my: 0 })); // 元の座標に戻す

  return (
    <GestureHandlerRootView style={styles.container}>
      <View style={styles.container}>
        <GestureDetector gesture={gesture}>
          <animated.Image
            style={[styles.image, { transform: [{ translateX: mx }, { translateY: my }] }]}
            source={require('{画像のパス}')}
          />
        </GestureDetector>
      </View>
    </GestureHandlerRootView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  image: {
    height: '50%',
    width: '50%',
  },
});

ヨシ!

おわりに

今回くらいのアニメーションであれば、React Nativeのデフォルトで付属しているアニメーションで十分かと思います。
一方でreact-springReact Native Gesture Handlerを組み合わせるといろいろ遊べそうなので、より良いアプリ開発に活かしていきたいですね。

GitHubで編集を提案

Discussion