モバイルアプリの操作感を左右する最大の要素はアニメーションです。ボタンをタップしたときの微細なフィードバック、画面遷移時のスムーズな動き、スクロールに連動するヘッダーの変化――こうした演出が「使いやすい」と感じるアプリを作ります。
React Nativeには標準の Animated API が用意されていますが、要件に応じて react-native-reanimated や React Native Skia、Lottie など複数の選択肢があります。それぞれ設計思想もパフォーマンス特性も異なるため、プロジェクトに合ったライブラリを選ぶことが重要です。
React Nativeで使えるアニメーションライブラリ一覧
React Nativeのアニメーション周辺には多くのライブラリが存在します。主要なものを整理すると以下のとおりです。
| ライブラリ | 用途 | 動作スレッド | 学習コスト | 最新バージョン |
|---|---|---|---|---|
| Animated API(標準) | 基本的なアニメーション | JSスレッド/ネイティブドライバー | 低 | React Native 0.84同梱 |
| react-native-reanimated | 高性能アニメーション全般 | UIスレッド(ワークレット) | 中 | 4.2.x |
| React Native Gesture Handler | タッチ・ジェスチャー操作 | ネイティブスレッド | 中 | 2.30.x |
| @shopify/react-native-skia | 2Dグラフィックス・描画 | GPUアクセラレーション | 高 | 2.4.x |
| lottie-react-native | After Effectsアニメーション再生 | ネイティブレンダラー | 低 | 7.3.x |
| react-native-animatable | 宣言的な定型アニメーション | JSスレッド | 低 | 1.4.0(更新停止) |
react-native-reanimated は週間約280万ダウンロード、React Native Gesture Handler は約235万ダウンロードと、React Nativeエコシステムで広く採用されています(出典: npm)。
標準Animated APIの基礎
React Native に組み込まれている Animated API は、追加ライブラリなしで利用できます。Animated.Value で状態を管理し、Animated.timing や Animated.spring でアニメーションを定義する仕組みです。
Animated.Valueとアニメーションの定義
import { useRef, useEffect } from 'react';
import { Animated, View, StyleSheet } from 'react-native';
function FadeInBox() {
const opacity = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(opacity, {
toValue: 1,
duration: 800,
useNativeDriver: true,
}).start();
}, [opacity]);
return (
<Animated.View style={[styles.box, { opacity }]}>
<View />
</Animated.View>
);
}
const styles = StyleSheet.create({
box: {
width: 100,
height: 100,
backgroundColor: '#3b82f6',
borderRadius: 8,
},
});
useNativeDriver: true を指定すると、アニメーション処理がネイティブ側に委譲され、JSスレッドの負荷を軽減できます。ただしネイティブドライバーでは transform と opacity のみ対応しており、width や height といったレイアウトプロパティは使用できません。
複数アニメーションの制御
Animated API には、複数アニメーションを組み合わせるためのメソッドが用意されています。
// 順番に実行
Animated.sequence([
Animated.timing(scaleValue, { toValue: 1.2, duration: 200, useNativeDriver: true }),
Animated.timing(scaleValue, { toValue: 1.0, duration: 200, useNativeDriver: true }),
]).start();
// 同時に実行
Animated.parallel([
Animated.timing(opacity, { toValue: 1, duration: 300, useNativeDriver: true }),
Animated.spring(translateY, { toValue: 0, useNativeDriver: true }),
]).start();
Animated.sequence は配列内のアニメーションを直列実行し、Animated.parallel は並列実行します。Animated.stagger を使えば、一定間隔で順次開始するアニメーションも表現できます。
interpolate による値の変換
interpolate メソッドで、ひとつの Animated.Value から複数のスタイル値を導出できます。
const rotation = opacity.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
});
const backgroundColor = opacity.interpolate({
inputRange: [0, 0.5, 1],
outputRange: ['#ef4444', '#eab308', '#22c55e'],
});
数値だけでなく、角度や色の補間にも対応しています。
react-native-reanimatedによる高性能アニメーション
標準の Animated API はJSスレッド上で動作するため、重い計算が走るとフレーム落ちが起きやすくなります。react-native-reanimated はこの課題を解決するライブラリで、「ワークレット」と呼ばれる仕組みでアニメーション処理をUIスレッド上で実行します。
ワークレットとSharedValueの仕組み
react-native-reanimated の核となるのが SharedValue と ワークレット です。
- SharedValue: JSスレッドとUIスレッドの両方からアクセスできる共有状態
- ワークレット: UIスレッド上で直接実行されるJavaScript関数
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
} from 'react-native-reanimated';
function ScaleButton() {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const handlePressIn = () => {
scale.value = withTiming(0.95, { duration: 100 });
};
const handlePressOut = () => {
scale.value = withTiming(1, { duration: 100 });
};
return (
<Animated.View style={[styles.button, animatedStyle]}>
<Pressable onPressIn={handlePressIn} onPressOut={handlePressOut}>
<Text>タップ</Text>
</Pressable>
</Animated.View>
);
}
useSharedValue で作成した値を withTiming や withSpring で変更すると、UIスレッド上でフレームごとにスタイルが更新されます。JSスレッドのブロッキングを受けないため、API通信や状態管理の処理中でも滑らかな60fpsアニメーションが維持されます。
withSpringとwithTimingの使い分け
| 関数 | 特徴 | 適した場面 |
|---|---|---|
withTiming | 指定時間で目標値へ到達(イージング指定可) | フェードイン、スライドなど時間制御が必要な場面 |
withSpring | バネの物理法則で自然に収束 | ボタン押下、ドラッグ後のスナップ |
withDecay | 速度を与えて徐々に減速 | フリックスクロール、慣性スクロール |
withSequence | 複数のアニメーションを順番に実行 | バウンス効果、段階的な変化 |
withRepeat | アニメーションをN回または無限に繰り返す | ローディング、パルスアニメーション |
// バネアニメーション
scale.value = withSpring(1.2, {
damping: 10,
stiffness: 100,
});
// 繰り返しアニメーション
rotation.value = withRepeat(
withTiming(360, { duration: 2000, easing: Easing.linear }),
-1, // 無限ループ
false
);
Reanimated v4とNew Architecture
react-native-reanimated 4.x は React Native の New Architecture 専用です。React Native 0.82以降では New Architecture が標準で有効になっているため、新規プロジェクトではそのまま利用できます(出典: React Native公式)。
旧アーキテクチャのプロジェクトでは 3.x 系を引き続き使用する必要があります。
ジェスチャー連動アニメーションの実装
ドラッグやピンチといったジェスチャーとアニメーションを組み合わせるには、react-native-gesture-handler と react-native-reanimated を併用します。
ドラッグ可能なカードの実装例
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
function DraggableCard() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const savedX = useSharedValue(0);
const savedY = useSharedValue(0);
const panGesture = Gesture.Pan()
.onStart(() => {
savedX.value = translateX.value;
savedY.value = translateY.value;
})
.onUpdate((event) => {
translateX.value = savedX.value + event.translationX;
translateY.value = savedY.value + event.translationY;
})
.onEnd(() => {
translateX.value = withSpring(0);
translateY.value = withSpring(0);
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
}));
return (
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.card, animatedStyle]}>
<Text>ドラッグできるカード</Text>
</Animated.View>
</GestureDetector>
);
}
ジェスチャーのコールバック(onUpdate など)はUIスレッドで実行されるため、指の動きとアニメーションの間にラグが発生しません。onEnd で withSpring(0) を呼び出すことで、リリース後に元の位置へ自然に戻る演出を実現しています。
スクロールに連動するヘッダーアニメーション
スクロール位置に応じてヘッダーの透明度やサイズを変化させるパターンは、多くのアプリで見られます。
import Animated, {
useSharedValue,
useAnimatedStyle,
useAnimatedScrollHandler,
interpolate,
Extrapolation,
} from 'react-native-reanimated';
function CollapsibleHeader() {
const scrollY = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y;
},
});
const headerStyle = useAnimatedStyle(() => ({
height: interpolate(
scrollY.value,
[0, 150],
[200, 60],
Extrapolation.CLAMP
),
opacity: interpolate(
scrollY.value,
[0, 100],
[1, 0.8],
Extrapolation.CLAMP
),
}));
return (
<View style={{ flex: 1 }}>
<Animated.View style={[styles.header, headerStyle]}>
<Text>ヘッダー</Text>
</Animated.View>
<Animated.ScrollView onScroll={scrollHandler} scrollEventThrottle={16}>
{/* コンテンツ */}
</Animated.ScrollView>
</View>
);
}
useAnimatedScrollHandler はスクロールイベントをUIスレッドで処理するフックです。interpolate で値を範囲指定に変換し、Extrapolation.CLAMP で範囲外の値を制限しています。
React Native Skiaで描く高度なグラフィックス
@shopify/react-native-skia は Shopify が開発するライブラリで、GoogleのグラフィックスエンジンSkiaをReact Nativeから直接利用できます。GitHub Star数は約8,200で、週間ダウンロード数は約51万と急速に普及が進んでいます(出典: GitHub)。
通常のアニメーションライブラリとは異なり、Canvasベースの描画を行うため、円グラフ・折れ線グラフ・パーティクルエフェクト・シェーダーといった複雑なビジュアル表現に強みがあります。
Skiaで円形プログレスバーを描画
import { Canvas, Circle, Path, Skia } from '@shopify/react-native-skia';
import { useSharedValue, withTiming } from 'react-native-reanimated';
function CircularProgress({ progress }: { progress: number }) {
const animatedProgress = useSharedValue(0);
useEffect(() => {
animatedProgress.value = withTiming(progress, { duration: 1000 });
}, [progress]);
const radius = 80;
const strokeWidth = 10;
const center = radius + strokeWidth;
const size = center * 2;
return (
<Canvas style={{ width: size, height: size }}>
{/* 背景の円 */}
<Circle
cx={center}
cy={center}
r={radius}
color="#e5e7eb"
style="stroke"
strokeWidth={strokeWidth}
/>
{/* プログレスの円弧 */}
<Circle
cx={center}
cy={center}
r={radius}
color="#3b82f6"
style="stroke"
strokeWidth={strokeWidth}
strokeCap="round"
start={0}
end={animatedProgress}
/>
</Canvas>
);
}
Skia と react-native-reanimated の SharedValue を組み合わせることで、描画のアニメーションも UIスレッドで60fps を維持しながら実行できます。
Lottieで複雑なモーションを手軽に導入
After EffectsやFigmaで作成したアニメーションをJSON形式でエクスポートし、アプリ内で再生できるのが Lottie です。ローディングスピナー、成功・エラー通知、オンボーディング画面など、デザイナーが作成した複雑なモーションをコード不要で組み込めます。
import LottieView from 'lottie-react-native';
function SuccessAnimation() {
return (
<LottieView
source={require('./assets/success.json')}
autoPlay
loop={false}
style={{ width: 200, height: 200 }}
/>
);
}
Lottie のJSONファイルは LottieFiles で多数公開されており、無料素材も豊富です。ファイルサイズはGIFやMP4と比べて極めて小さく、ベクターベースのためどの解像度でもシャープに表示されます。
LayoutAnimationで画面遷移を滑らかにする
React Native 標準の LayoutAnimation は、レイアウト変更時に自動でアニメーションを付与する仕組みです。react-native-reanimated にも同等の機能が Layout トランジションとして実装されています。
標準LayoutAnimationの使い方
import { LayoutAnimation, UIManager, Platform } from 'react-native';
// Android では事前に有効化が必要
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental?.(true);
}
function ExpandableList() {
const [expanded, setExpanded] = useState(false);
const toggleExpand = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setExpanded(!expanded);
};
return (
<View>
<Pressable onPress={toggleExpand}>
<Text>タップして展開</Text>
</Pressable>
{expanded && (
<View style={{ height: 200, backgroundColor: '#dbeafe' }}>
<Text>展開されたコンテンツ</Text>
</View>
)}
</View>
);
}
Reanimatedの Layout Transition
react-native-reanimated では、コンポーネントの出現・退出・レイアウト変更にアニメーションを宣言的に指定できます。
import Animated, { FadeIn, FadeOut, Layout } from 'react-native-reanimated';
function AnimatedList({ items }: { items: string[] }) {
return (
<View>
{items.map((item, index) => (
<Animated.View
key={item}
entering={FadeIn.delay(index * 100)}
exiting={FadeOut}
layout={Layout.springify()}
style={styles.listItem}
>
<Text>{item}</Text>
</Animated.View>
))}
</View>
);
}
entering・exiting・layout プロパティにプリセットアニメーションを渡すだけで、リストのアイテム追加・削除・並べ替え時の動きが自然になります。
パフォーマンス最適化の実践テクニック
アニメーションのカクつきを防ぐために、以下のポイントを押さえておくと効果的です。
ネイティブドライバーの活用
標準 Animated API を使う場合、useNativeDriver: true の設定は必須レベルの最適化です。対応プロパティは transform と opacity に限られますが、この2つだけでもフェード・スライド・スケール・回転の大半をカバーできます。
JSスレッドの負荷を避ける
アニメーション中に setState を頻繁に呼び出すと、再レンダリングが発生してフレーム落ちの原因になります。react-native-reanimated の SharedValue はReactのレンダリングサイクルを経由せずにスタイルを更新するため、この問題を回避できます。
不要な再レンダリングの抑制
React.memo や useCallback でコンポーネントの再レンダリングを最小限に抑えることも重要です。特にリスト内でアニメーションを使う場合、各アイテムのメモ化が効果的です。
const AnimatedItem = React.memo(({ item }: { item: ItemType }) => {
const animatedStyle = useAnimatedStyle(() => ({
// スタイル定義
}));
return (
<Animated.View style={animatedStyle}>
<Text>{item.name}</Text>
</Animated.View>
);
});
Hermes V1 エンジンとの相性
React Native 0.84 では Hermes V1 がデフォルトの JavaScript エンジンとなりました(出典: React Native公式ブログ)。Hermes V1 はバイトコードプリコンパイルの改善により、複雑なViewのTime to Interactive(TTI)が10〜15%向上しています。メモリ消費の削減効果もあるため、アニメーションのパフォーマンスにも間接的に貢献します。
ライブラリ選定フローチャート
プロジェクトの要件に応じたライブラリ選択の目安をまとめます。
標準 Animated API が適するケース
- 外部依存を増やしたくない
- フェード・スライドなど単純なアニメーションのみ
- 既存プロジェクトへの影響を最小限にしたい
react-native-reanimated が適するケース
- ジェスチャー連動やスクロール連動のアニメーション
- 60fps を安定して維持したい
- 複数のアニメーションを組み合わせる
- New Architecture を採用済み(v4利用時)
React Native Skia が適するケース
- カスタムチャート・グラフの描画
- パーティクルやシェーダーを使った視覚効果
- After Effects に頼らず、コードでグラフィックスを制御したい
Lottie が適するケース
- デザイナーが After Effects / Figma でアニメーションを作成している
- ローディング・オンボーディングなどの定型的なモーション
- アニメーションのプログラミングに時間をかけたくない
開発環境のセットアップ手順
主要ライブラリのインストール方法をまとめます。
Expo プロジェクトの場合
# react-native-reanimated
npx expo install react-native-reanimated
# Gesture Handler
npx expo install react-native-gesture-handler
# React Native Skia
npx expo install @shopify/react-native-skia
# Lottie
npx expo install lottie-react-native
Expo SDK 54 以降では、react-native-reanimated と Gesture Handler はプリインストール済みのケースが多くなっています。
React Native CLI プロジェクトの場合
# react-native-reanimated
npm install react-native-reanimated
# babel.config.js にプラグイン追加(配列の末尾に記載)
# plugins: ['react-native-worklets/plugin']
# Gesture Handler
npm install react-native-gesture-handler
# iOS のネイティブ依存をインストール
cd ios && pod install && cd ..
react-native-reanimated 4.x では Babel プラグインが react-native-worklets パッケージに移動しました。babel.config.js の plugins 配列の末尾に 'react-native-worklets/plugin' を追加してください。後方互換として 'react-native-reanimated/plugin' も動作しますが、新しい記法への移行が推奨されています。
よくあるトラブルと対処法
アニメーションがカクつく
JSスレッドの過負荷が原因です。以下を確認してください。
useNativeDriver: trueが設定されているか- アニメーション中に
setStateを呼んでいないか console.logをアニメーションのコールバック内に残していないか
react-native-reanimated に移行すると、UIスレッド実行になるためほぼ解消されます。
Reanimated のワークレットエラー
「Tried to synchronously call a non-worklet function」というエラーが出る場合、useAnimatedStyle 内で通常のJavaScript関数を呼んでいることが原因です。ワークレット内からはワークレット関数のみ呼び出せます。
// NG: 通常関数をワークレット内で呼ぶ
const format = (v: number) => `${v}px`;
const style = useAnimatedStyle(() => ({
width: format(width.value), // エラー
}));
// OK: ワークレット内で完結させる
const style = useAnimatedStyle(() => ({
width: width.value,
}));
Android で LayoutAnimation が効かない
Android では UIManager.setLayoutAnimationEnabledExperimental(true) の呼び出しが必要です。React Native の New Architecture を利用している場合、Fabric レンダラーでは標準で対応しているため追加設定は不要です。
まとめ
React Nativeのアニメーションは、標準 Animated API から始めて、パフォーマンスやインタラクションの要件が高まったら react-native-reanimated へ移行するのが現実的なステップです。グラフ描画やカスタムシェーダーが必要なら React Native Skia、デザイナーとの協業が多いなら Lottie が最適な選択肢になります。
2026年2月現在、React Native 0.84 と New Architecture の標準化により、react-native-reanimated 4.x(最新4.2.2)が事実上のスタンダードとなっています。ジェスチャー連動・スクロール連動・レイアウトトランジションまで一貫してUIスレッドで処理できるため、ネイティブアプリと遜色ないアニメーション体験を実現できます。
