React Nativeで画像をぐりぐり動かす
こういうやつです。
はじめに
せっかくReact Native
でアプリを開発しているんだから、モダンでイケイケなアプリを作りたいですよね。
モダンというとアニメーションかなぁという安直な発想から、ユーザーの操作に応じてアニメーションするアプリを作りたいと思いました。
React Nativeはデフォルトでアニメーション機能が付属していますが、わりと自由度が低そうに見えました。
一方で、以前からReact系列のアニメーションライブラリとしてreact-spring
の名前を聞いていたので、そちらを使っていい感じにやりたいなーと思いました。
というわけで、react-spring
でアニメーション作成をする入門的な記事になればと思いつつ、本記事を執筆させていただきます。
なお、本記事に記載したコマンドはReact Native CLI
で作成したプロジェクトを前提としています。
Expo
で作成している場合は適宜読み替えてください。
雛形の作成
とりあえずは画像を表示させましょう。
React Native CLI
でreact-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
で値を変更することで画像の位置が調整できるように見えますね。
しかし今回はアニメーションさせたいので、useState
をuseSpring
に置き換えます。
ついでにタップした時にアニメーションするように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>
);
};
ヨシ!
translateX
とtranslateY
の値を0
と100
で切り替えてアニメーションするように設定しています。
タップするごとに画像がアニメーションするようになりましたね。
ジェスチャーの設定
ここからドラッグ動作を検知できるようにしていきます。今回はManualジェスチャー
を利用します。
onTouchesDown
、onTouchesMove
、onTouchesUp
はそれぞれ指が触れたとき、動いたとき、離れたときに発火する処理を定義できます。
いずれもタッチイベントを引数にとるコールバック関数を設定できます。設定したジェスチャーはGestureDetector
に渡すことで検知できるようになります。
というわけで、まずはPressable
をGestureDetector
に置き換え、適当な関数を持たせたジェスチャーを設定します。
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-spring
とReact Native Gesture Handler
を組み合わせるといろいろ遊べそうなので、より良いアプリ開発に活かしていきたいですね。
Discussion