モバイルアプリの操作感を左右する最大の要素はアニメーションです。ボタンをタップしたときの微細なフィードバック、画面遷移時のスムーズな動き、スクロールに連動するヘッダーの変化――こうした演出が「使いやすい」と感じるアプリを作ります。

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-skia2Dグラフィックス・描画GPUアクセラレーション2.4.x
lottie-react-nativeAfter 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.timingAnimated.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スレッドの負荷を軽減できます。ただしネイティブドライバーでは transformopacity のみ対応しており、widthheight といったレイアウトプロパティは使用できません。

複数アニメーションの制御

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 で作成した値を withTimingwithSpring で変更すると、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スレッドで実行されるため、指の動きとアニメーションの間にラグが発生しません。onEndwithSpring(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>
  );
}

enteringexitinglayout プロパティにプリセットアニメーションを渡すだけで、リストのアイテム追加・削除・並べ替え時の動きが自然になります。

パフォーマンス最適化の実践テクニック

アニメーションのカクつきを防ぐために、以下のポイントを押さえておくと効果的です。

ネイティブドライバーの活用

標準 Animated API を使う場合、useNativeDriver: true の設定は必須レベルの最適化です。対応プロパティは transformopacity に限られますが、この2つだけでもフェード・スライド・スケール・回転の大半をカバーできます。

JSスレッドの負荷を避ける

アニメーション中に setState を頻繁に呼び出すと、再レンダリングが発生してフレーム落ちの原因になります。react-native-reanimated の SharedValue はReactのレンダリングサイクルを経由せずにスタイルを更新するため、この問題を回避できます。

不要な再レンダリングの抑制

React.memouseCallback でコンポーネントの再レンダリングを最小限に抑えることも重要です。特にリスト内でアニメーションを使う場合、各アイテムのメモ化が効果的です。

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.jsplugins 配列の末尾'react-native-worklets/plugin' を追加してください。後方互換として 'react-native-reanimated/plugin' も動作しますが、新しい記法への移行が推奨されています。

よくあるトラブルと対処法

アニメーションがカクつく

JSスレッドの過負荷が原因です。以下を確認してください。

  1. useNativeDriver: true が設定されているか
  2. アニメーション中に setState を呼んでいないか
  3. 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スレッドで処理できるため、ネイティブアプリと遜色ないアニメーション体験を実現できます。