Reanimated徹底解説:React Nativeアニメーションの未来か?
software-mansion/react-native-reanimated
さあ、今日は「Reanimated」という素晴らしいライブラリについてお話ししましょう!
React Nativeでアニメーションを実装する際に、標準のAnimatedライブラリを使うことが多いですよね。でも、「もっと滑らかに動かないかな…」「UIスレッドがブロックされてカクつく…」なんて感じたことはありませんか?そんな悩みを解決してくれるかもしれないのが、このReanimatedなんです!
でも、このReanimated、実は開発者の間では熱い議論が繰り広げられているんですよ。「信じる派」と「信じない派」に分かれて、まるでコントのように激しい討論が日々行われています。
信じる派リーダー
情熱的なアニメーション愛好家
信じない派リーダー
現実主義のパフォーマンス至上主義者
信じる派リーダー(腕組みをしてニヤリ) 「いやー、Reanimatedは本当に素晴らしい!これを使えば、今まで諦めていたような複雑なアニメーションも、ヌルヌル動くんだよ!UIスレッドに負荷をかけずに、ネイティブ側でアニメーションを処理してくれるんだから、もう最高!」
信じない派リーダー(冷めた目で腕を組み返す)
「ふん、夢ばかり見てるな。確かにパフォーマンスは向上するかもしれない。だが、それだけのために学習コストをかけて、新たなAPIを覚える必要があるのか?既存のAnimatedで十分なケースも多いだろう。それに、デバッグが複雑になる可能性だってあるんだぞ?」
信じる派リーダー(熱弁) 「何を言ってるんだ!その学習コストを払ってでも得られる恩恵は計り知れない!例えば、ジェスチャーとの連携なんて、Reanimatedを使えば驚くほど簡単に、そして滑らかに実現できるんだ!ユーザー体験が格段に向上するんだから、それはもう、投資だよ、投資!」
信じない派リーダー(ため息) 「投資ねぇ…。ちょっとしたアニメーションのためだけに、大掛かりなライブラリを導入するのは、オーバーキルじゃないのか?バンドルサイズも大きくなるし、依存関係が増えることで、将来的なメンテナンスコストだって上がる可能性がある。シンプル・イズ・ベストだろ?」
信じる派リーダー(さらに熱くなる) 「シンプルに見えて、実は複雑なことを裏でやってくれているんだよ!例えば、値の補間やアニメーションの連鎖、ループ処理なども、宣言的に書けるから、コードが分かりやすくなるんだ!それに、Hot Reloadingも強力だし、開発効率だって上がるんだ!」
信じる派リーダー(さらに熱くなる) 「何より、彼らが目指しているのは、React Nativeにおけるアニメーションの究極形なんだ!将来的に、もっと多くの機能が追加され、より簡単に、より高性能なアニメーションが実現できるようになるはずだ!未来に目を向けろ!」
信じない派リーダー(頭を抱える) 「未来はいいが、今をどうするかだ!既存プロジェクトへの導入は?移行コストは?新しいバージョンのたびに、また変更に追われるのか?安定性だって重要だぞ!」
信じる派リーダー(キリッとした表情で) 「大丈夫!公式ドキュメントも充実しているし、コミュニティも活発だ!それに、彼らは常に最高のパフォーマンスと開発体験を提供しようと努力している。信じてみろ!きっと、君のReact Nativeライフが変わるはずだ!」
信じる派リーダー(笑顔で) 「さあ、まずは触ってみることから始めようじゃないか!百聞は一見に如かずだよ!」
信じない派リーダー(渋々ながらも、少し興味を持った様子) 「…まあ、そこまで言うなら、少しは話を聞いてやるか…。」
このコントのように、色々な意見があるReanimatedですが、ソフトウェアエンジニアとしての視点から見ると、以下のような点で非常に役に立ちます。
ネイティブスレッドでのアニメーション実行(UIスレッド解放)
ここが一番のキモです。従来のAnimatedライブラリは、JavaScriptスレッドでアニメーションの計算を行っていました。JavaScriptスレッドはUIのレンダリングやその他のロジックも担当しているため、複雑なアニメーションや頻繁な更新があると、UIスレッドがブロックされてしまい、カクつき(フレーム落ち)が発生することがありました。
Reanimatedは、アニメーションのロジックをネイティブ(C++)側で実行するように最適化されています。これにより、JavaScriptスレッドの負荷が軽減され、UIスレッドがスムーズに動作し、非常に滑らかなアニメーションを実現できます。これは特に、高速なジェスチャー操作や多数の要素が同時に動くようなケースで威力を発揮します。
宣言的なAPIと強力なフック
Reanimatedは、useSharedValue、useAnimatedStyle、useAnimatedGestureHandlerなどのフックを提供しており、これらを使うことで、宣言的にアニメーションを記述できます。これはReactのコンポーネント指向の考え方と非常に相性が良く、コードの可読性と保守性を高めます。
ジェスチャーハンドリングとの連携のしやすさ
react-native-gesture-handlerと組み合わせて使用することで、ユーザーのタッチ操作やスワイプ、ピンチなどのジェスチャーに完璧に同期したアニメーションを簡単に実装できます。例えば、ドラッグ可能な要素や、スワイプで開閉するパネルなど、リッチなUIを少ないコード量で実現可能です。
高度なアニメーション制御
値の補間(interpolate)、アニメーションの連鎖、ループ、条件分岐など、複雑なアニメーションロジックをより直感的に記述できます。また、アニメーションの途中で値を取得したり、イベントを発生させたりすることも可能です。
デバッグのしやすさ(最近は特に向上)
以前はデバッグが難しいという声もありましたが、最近のバージョンでは、デバッグツールが充実してきており、より開発しやすくなっています。
Reanimatedの導入は、いくつかのステップを踏む必要がありますが、公式ドキュメントが非常に丁寧に書かれているので、それに沿って進めれば問題ありません。
まずは、必要なパッケージをインストールします。
yarn add react-native-reanimated react-native-gesture-handler
# または npm install react-native-reanimated react-native-gesture-handler
react-native-gesture-handlerもReanimatedと組み合わせて使うことが多いので、一緒にインストールすることをおすすめします。
babel.config.jsファイルにReanimatedのプラグインを追加します。
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
'react-native-reanimated/plugin', // この行を追加
],
};
重要
'react-native-reanimated/plugin'は、他のプラグイン(特に 'react-native-dotenv' や 'react-native-paper' など)よりも最後に配置する必要があります。これは、プラグインの処理順序が重要だからです。
iOSプロジェクトの場合、Podfileを更新し、Podをインストールする必要があります。
ios/Podfileを開き、一番上に以下の行を追加します。
require_relative '../node_modules/react-native-reanimated/scripts/configure_gradle_for_turbo_modules.rb'
# ... 他のPodfileの内容 ...
次に、ターミナルでiOSディレクトリに移動してPodをインストールします。
cd ios
pod install
App.jsやindex.jsなど、アプリケーションのエントリポイントで、react-native-gesture-handlerのインポートを一番上に追加します。
import 'react-native-gesture-handler'; // この行を一番上に!
import React from 'react';
import { View, Text } from 'react-native';
const App = () => {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Reanimated App</Text>
</View>
);
};
export default App;
これで、基本的な導入は完了です。一度アプリを再ビルド(yarn react-native run-ios または yarn react-native run-android)してみてください。
では、実際にReanimatedを使ってシンプルなアニメーションを実装してみましょう。今回は、ボタンをタップすると四角が左右に動くアニメーションと、ドラッグで動かせる四角の例を見てみましょう。
import React from 'react';
import { Button, StyleSheet } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring, // スプリングアニメーション
} from 'react-native-reanimated';
const SIZE = 100;
function BasicMovement() {
const offset = useSharedValue(0); // アニメーションさせる値を管理するSharedValue
const animatedStyles = useAnimatedStyle(() => {
return {
transform: [{ translateX: offset.value }], // offset.valueに基づいてX座標を変換
};
});
const handlePress = () => {
offset.value = withSpring(Math.random() * 200 - 100); // ランダムな位置へスプリングアニメーション
};
return (
<>
<Animated.View style={[styles.box, animatedStyles]} />
<Button onPress={handlePress} title="動かす!" />
</>
);
}
const styles = StyleSheet.create({
box: {
width: SIZE,
height: SIZE,
backgroundColor: 'hotpink',
borderRadius: 10,
marginVertical: 50,
},
});
export default BasicMovement;
解説
useSharedValue(0)
offsetというShared Valueを初期値0で作成します。これがアニメーションさせる値になります。offset.valueで値にアクセスします。
useAnimatedStyle(() => { ... })
アニメーションされるスタイルを定義するためのフックです。この中の関数はUIスレッドで実行され、offset.valueの変化に応じてスタイルの計算が行われます。
transform: [{ translateX: offset.value }]
offset.valueが変化すると、四角がX軸方向に移動します。
withSpring(Math.random() * 200 - 100)
offset.valueに新しい値を代入する際に、withSpringを使うことで、スプリングアニメーションで滑らかに移動させます。
この例ではreact-native-gesture-handlerも使います。
import React from 'react';
import { StyleSheet } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
useAnimatedGestureHandler,
withSpring,
} from 'react-native-reanimated';
import { PanGestureHandler } from 'react-native-gesture-handler';
const SIZE = 100;
function DraggableBox() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
// ジェスチャーイベントを処理するハンドラー
const onGestureEvent = useAnimatedGestureHandler({
onStart: (event, ctx) => {
// ジェスチャー開始時の現在の位置を保存
ctx.startX = translateX.value;
ctx.startY = translateY.value;
},
onActive: (event, ctx) => {
// 指の移動量に開始時の位置を足して、現在の位置を計算
translateX.value = ctx.startX + event.translationX;
translateY.value = ctx.startY + event.translationY;
},
onEnd: (event) => {
// 指を離した時に、初期位置に戻す(例として)
translateX.value = withSpring(0);
translateY.value = withSpring(0);
},
});
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
};
});
return (
<PanGestureHandler onGestureEvent={onGestureEvent}>
<Animated.View style={[styles.box, animatedStyle]} />
</PanGestureHandler>
);
}
const styles = StyleSheet.create({
box: {
width: SIZE,
height: SIZE,
backgroundColor: 'purple',
borderRadius: 10,
marginTop: 50, // 上からの余白
},
});
export default DraggableBox;
解説
PanGestureHandler
react-native-gesture-handlerから提供されるコンポーネントで、パン(ドラッグ)ジェスチャーを検出します。
useAnimatedGestureHandler({ ... })
ジェスチャーイベント(onStart、onActive、onEndなど)が発生した際に実行されるロジックを定義します。この中もUIスレッドで実行されます。
ctx
contextオブジェクトで、ジェスチャーの開始時などの状態を保持するために使えます。
event.translationX, event.translationY
指がどれだけ移動したか(翻訳量)を示します。
onEnd
ジェスチャーが終了した際に、スプリングアニメーションで初期位置(0, 0)に戻るようにしています。
Reanimatedは、React Nativeでより高度で滑らかなアニメーションを実装するための強力なツールです。確かに学習コストや導入の手間はありますが、その恩恵は大きく、特にパフォーマンスが重視されるアプリケーションや、リッチなUIアニメーションを必要とする場合に、その真価を発揮します。
「信じる派」の意見のように、未来のReact Nativeアニメーションを担う可能性を秘めたライブラリだと私は思います。まずは触ってみて、そのスムーズさを体験してみてください!きっと、「信じる派」の気持ちが少しは理解できるはずです!