こんにちは。EntrezWORLDでモバイルアプリ・バックエンド開発をしている吉田です。
「react-nativeで開発していて、動きがもっとほしい・・・。でもアニメーションって難しそう・・・」
「なんかreact-nativeのアニメーション、カクカクしてね?」
そう思ったことはありませんか?
react-native-reanimatedを使えば、滑らかなアニメーションが超簡単に書けちゃうんです!(もちろん、複雑なアニメーションも可能)
そんな素敵なreact-native-reanimatedのインストール方法・基本的な使い方を、今回ご紹介していきたいと思います。
react-native-reanimatedとは?
react-native-reanimatedは、react-nativeで滑らかなアニメーションを表現するためのライブラリです。
react-nativeは、ネイティブのスレッドとは独立したスレッドで動きます。そのため、Native⇔JavaScriptの間を同期する必要があるため必ず遅延が生じてしまいます。
UIの動作を記述する分にはこれでいいのですが、アニメーションなどの滑らかな動作をさせようとするとどうしてもNative上のスレッドで実行する必要があります。
それを解決してくれるのがreact-native-reanimatedです。
react-native-reanimatedは、アニメーションを滑らかに実行させる機能はもちろんのこと、アニメーションに必要な様々な機能・ショートカットを搭載しています。
react-native-reanimatedを今使うべき理由
v2.3系から「Layout Animation」という、よく使うアニメーションを非常に簡単に追加できる機能が追加されました。
元々はシンプルでよく使われるアニメーションであっても一々記述しなければなりませんでした。
しかし、このLayout Animationを利用すればシンプルなアニメーションはもちろんのこと、
次のGIFのようなシンプルなアニメーションをちょっと加工したものも簡単に作れてしまうのです!
というわけでみんな使おう。
今回使用するコード(アニメーション追加前)
App.tsx:
import { StatusBar } from 'expo-status-bar';
import { useState } from 'react';
import { Button, GestureResponderEvent, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
interface NumberButtonProps {
text: string;
onPress?: (event: GestureResponderEvent) => void;
}
const NumberButton = (props: NumberButtonProps) => {
return (
<TouchableOpacity style={styles.buttonContainer} onPress={props.onPress}>
<Text style={styles.buttonFont}>{props.text}</Text>
</TouchableOpacity>
);
}
export default function App() {
const [isVisibleDialButtons, setIsVisibleDialButtons] = useState<boolean>(false);
return (
<View style={styles.container}>
{}
<View style={styles.dialButtonsContainer}>
{isVisibleDialButtons ? <>
<View style={styles.line}>
<NumberButton text="1" />
<NumberButton text="2" />
<NumberButton text="3" />
</View>
<View style={styles.line}>
<NumberButton text="4" />
<NumberButton text="5" />
<NumberButton text="6" />
</View>
<View style={styles.line}>
<NumberButton text="7" />
<NumberButton text="8" />
<NumberButton text="9" />
</View>
</> : undefined}
</View>
{}
<View style={styles.showDialButtonsContainer}>
<Button title='Show Dial' onPress={() => setIsVisibleDialButtons(!isVisibleDialButtons)} />
</View>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
dialButtonsContainer: {
flex: 4,
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 32,
},
showDialButtonsContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
line: {
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row'
},
buttonContainer: {
width: 72,
height: 72,
borderRadius: 72,
backgroundColor: '#000000',
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 16,
marginVertical: 24,
},
buttonFont: {
color: '#ffffff',
fontSize: 20,
}
});
react-native-reanimatedをインストール
expoを使用している場合
react-native-reanimatedの公式のドキュメントには記載されていませんが、以下の手順でインストールすることができます。
- react-native-reanimatedを追加
expo add react-native-reanimated
- babel.config.jsのpluginsの項目に「react-native-reanimated/plugin」を追加
plugins: [
......
'react-native-reanimated/plugin',
],
- v2.3系は特定の条件でバグが発生するため、v2.4系に変更
package.json
"dependencies": {
...
"react-native-reanimated": "~2.4.1",
...
},
Command Line:
yarn install
(バグの詳細: https://github.com/software-mansion/react-native-reanimated/issues/2777)
expo sdk 44で使用されているreact-native 0.64.4だと、表示がそのまま残り続けるバグがあるので、非推奨ですが0.66.4にしておくことをおすすめします。2022/02/28時点で最新である0.67系では、expoとの互換性の問題で動きません。
react-native-reanimatedを使えば、基本的なアニメーションがこんなに簡単に書ける
Entering Animation(Layout Animationの一つ)
Entering Animationは、コンポーネントが画面上に表示(マウント)された際に実行されるアニメーションを簡単に記述できます。
次のGIFのように、「Show Dial」ボタンを押した時に、ダイヤルボタンが下からふわっと出てくる感じのアニメーションを作ってみましょう。
- AnimatedとFadeInDownをインポートする
react-native-reanimatedから、AnimatedとFadeInDownをインポートしましょう。
import Animated, { FadeInDown } from 'react-native-reanimated';
- Enteringアニメーションを記述する
<View style={styles.line}>
{}
</View>
となっている所を
<Animated.View style={styles.line} entering={FadeInDown.springify().duration(200).delay(0)}>
{}
</Animated.View>
に変更しましょう。
delayの値はお好みで。
変更するとソースコードはこのようになります。
import { StatusBar } from 'expo-status-bar';
import { useState } from 'react';
import { Button, GestureResponderEvent, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Animated, { FadeInDown } from 'react-native-reanimated';
interface NumberButtonProps {
text: string;
onPress?: (event: GestureResponderEvent) => void;
}
const NumberButton = (props: NumberButtonProps) => {
return (
<TouchableOpacity style={styles.buttonContainer} onPress={props.onPress}>
<Text style={styles.buttonFont}>{props.text}</Text>
</TouchableOpacity>
);
}
export default function App() {
const [isVisibleDialButtons, setIsVisibleDialButtons] = useState<boolean>(false);
return (
<View style={styles.container}>
{}
<View style={styles.dialButtonsContainer}>
{isVisibleDialButtons ? <>
<Animated.View style={styles.line} entering={FadeInDown.springify().duration(200).delay(0)}>
<NumberButton text="1" />
<NumberButton text="2" />
<NumberButton text="3" />
</Animated.View>
<Animated.View style={styles.line} entering={FadeInDown.springify().duration(200).delay(50)}>
<NumberButton text="4" />
<NumberButton text="5" />
<NumberButton text="6" />
</Animated.View>
<Animated.View style={styles.line} entering={FadeInDown.springify().duration(200).delay(100)}>
<NumberButton text="7" />
<NumberButton text="8" />
<NumberButton text="9" />
</Animated.View>
</> : undefined}
</View>
{}
<View style={styles.showDialButtonsContainer}>
<Button title='Show Dial' onPress={() => setIsVisibleDialButtons(!isVisibleDialButtons)} />
</View>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
dialButtonsContainer: {
flex: 4,
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 32,
},
showDialButtonsContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
line: {
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row'
},
buttonContainer: {
width: 72,
height: 72,
borderRadius: 72,
backgroundColor: '#000000',
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 16,
marginVertical: 24,
},
buttonFont: {
color: '#ffffff',
fontSize: 20,
}
});
このように「Animation名.アニメーションの特徴1(args).アニメーションの特徴2(args). ・・・」と書くだけで、基本的なアニメーションは数行変更するだけでできてしまうのです・・・!
Exiting Animation(Layout Animationの一つ)
Entering Animationだけでは、非表示の際に一瞬で閉じてしまい気持ち悪いです。
Exiting Animationも記述してあげましょう。
Entering Animationの時とやることはほぼ変わらず、「exiting={...}」を追加するだけです。
実行結果は次のGIFのようになります。
- AnimatedとFadeOutDownをインポートする
react-native-reanimatedから、AnimatedとFadeOutDownをインポートしましょう。
import Animated, { FadeOutDown } from 'react-native-reanimated';
- Exitingアニメーションを記述する
<Animated.View style={styles.line} entering={FadeInDown.springify().duration(200).delay(0)}>
{}
</Animated.View>
となっている所を
<Animated.View style={styles.line} entering={FadeInDown.springify().duration(200).delay(0)} exiting={FadeOutDown.duration(200).delay(100)}>
{}
</Animated.View>
に変更しましょう。
delayの値はお好みで。
変更するとソースコードはこのようになります。
import { StatusBar } from 'expo-status-bar';
import { useState } from 'react';
import { Button, GestureResponderEvent, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Animated, { FadeInDown, FadeOutDown } from 'react-native-reanimated';
interface NumberButtonProps {
text: string;
onPress?: (event: GestureResponderEvent) => void;
}
const NumberButton = (props: NumberButtonProps) => {
return (
<TouchableOpacity style={styles.buttonContainer} onPress={props.onPress}>
<Text style={styles.buttonFont}>{props.text}</Text>
</TouchableOpacity>
);
}
export default function App() {
const [isVisibleDialButtons, setIsVisibleDialButtons] = useState<boolean>(false);
return (
<View style={styles.container}>
{}
<View style={styles.dialButtonsContainer}>
{isVisibleDialButtons ? <>
<Animated.View style={styles.line} entering={FadeInDown.springify().duration(200).delay(0)} exiting={FadeOutDown.duration(200).delay(100)}>
<NumberButton text="1" />
<NumberButton text="2" />
<NumberButton text="3" />
</Animated.View>
<Animated.View style={styles.line} entering={FadeInDown.springify().duration(200).delay(50)} exiting={FadeOutDown.duration(200).delay(50)}>
<NumberButton text="4" />
<NumberButton text="5" />
<NumberButton text="6" />
</Animated.View>
<Animated.View style={styles.line} entering={FadeInDown.springify().duration(200).delay(100)} exiting={FadeOutDown.duration(200).delay(0)}>
<NumberButton text="7" />
<NumberButton text="8" />
<NumberButton text="9" />
</Animated.View>
</> : undefined}
</View>
{}
<View style={styles.showDialButtonsContainer}>
<Button title='Show Dial' onPress={() => setIsVisibleDialButtons(!isVisibleDialButtons)} />
</View>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
dialButtonsContainer: {
flex: 4,
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 32,
},
showDialButtonsContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
line: {
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row'
},
buttonContainer: {
width: 72,
height: 72,
borderRadius: 72,
backgroundColor: '#000000',
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 16,
marginVertical: 24,
},
buttonFont: {
color: '#ffffff',
fontSize: 20,
}
});
超簡単です!
記事を公開した経緯
react-native-reanimatedは、日本語の記事が少なく、便利なのに知名度が少ないと感じたため記事を書かせていただきました。
react-native-reanimatedは現在バージョン2で、急いでバージョン2を出したようでバグがまだまだ残っているところはあります。
しかし、それを凌駕するほどの機能を備えていると筆者は考えています。
これを機に、react-native-reanimated・react-nativeが更に広まればいいなと思っています。(Flutterに負けるな!)
次回について
次回はLayout Animation(Entering/Exiting Animation)から抜け出して、自分でアニメーションを作成するためにはどうしたらいいか?という記事を書く予定です。