📱

React Nativeのアニメーションを理解する(Animated API編)

2024/12/08に公開

この記事はReact Native 全部俺 Advent Calendar 8目の記事です。

https://adventar.org/calendars/10741

このアドベントカレンダーについて

このアドベントカレンダーは @itome が全て書いています。

基本的にReact NativeおよびExpoの公式ドキュメントとソースコードを参照しながら書いていきます。誤植や編集依頼はXにお願いします。

React Nativeのアニメーションを理解する(Animated API編)

今回はReact Nativeのアニメーション機能について解説します。アプリの見た目の完成度を上げるのに欠かせないアニメーションですが、React Nativeでは少し特殊な実装が必要になります。

TL;DR

  • React NativeのAnimated APIは3つのステップでアニメーションを作る
    1. アニメーションさせたい値をAnimated.Valueで管理する
    2. アニメーション時の値の変化をAnimated.timingなどで定義する
    3. アニメーション対象のコンポーネントをAnimated.Viewなどでラップする
  • アニメーション中の値の変化はNative Driverを使うことでJavaScriptスレッドから独立して動作する
  • 複雑なアニメーションはAnimated.sequenceAnimated.parallelを使って組み合わせることができる

Animated APIとは

React NativeのAnimated APIは、UIコンポーネントのアニメーションを実装するための公式APIです。以下のようなアニメーションを簡単に実装することができます。

  • フェードイン・フェードアウト
  • スケーリング
  • 移動
  • 回転

また、これらのアニメーションを組み合わせることで、より複雑なアニメーションを作ることもできます。例えばカルーセル形式の画像ビューワーでユーザーが指を離したときに画像を中央にスナップするアニメーションや、ボタンが押されたときにわかりやすいようにボタンの大きさを一瞬変えるアニメーションなど、さまざまなことが今回の記事の応用でできるようになります。

アニメーションの基本的な実装方法

まずは、最も基本的なフェードインアニメーションを例に実装方法を見ていきましょう。

import React, { useEffect } from 'react';
import { Animated } from 'react-native';

const FadeInView = () => {
  // 1. アニメーションさせたい値をAnimated.Valueで管理
  const fadeAnim = new Animated.Value(0);

  useEffect(() => {
    // 2. アニメーションの定義
    Animated.timing(fadeAnim, {
      toValue: 1,
      duration: 1000,
      useNativeDriver: true,
    }).start();
  }, []);

  // 3. アニメーション対象のコンポーネントをAnimated.Viewでラップ
  return (
    <Animated.View style={{ opacity: fadeAnim }}>
      <Text>フェードインするテキスト</Text>
    </Animated.View>
  );
};

上記のコードでは、透明度を0から1に1秒かけて変化させることで、フェードインアニメーションを実現しています。

1. Animated.Valueの作成

アニメーションさせたい値は必ずAnimated.Valueで管理する必要があります。これは、アニメーション中の値の変化をReact Nativeが追跡できるようにするためです。

Animated.Valueは数値を扱うAnimated.Valueと、色やパーセンテージなどの文字列を扱うAnimated.ValueXYの2種類があります。

// 数値の場合
const fadeAnim = new Animated.Value(0);

// XY座標の場合
const position = new Animated.ValueXY({ x: 0, y: 0 });

2. アニメーションの定義

アニメーションの定義には、Animated.timingを使います。これは、時間経過とともに値を変化させるアニメーションを作成するための関数です。

主なパラメータは以下の通りです。イージングについては章を分けて後述します。

Animated.timing(animatedValue, {
  toValue: 1, // 目標値
  duration: 1000, // アニメーションの時間(ミリ秒)
  delay: 0, // アニメーション開始までの遅延時間(ミリ秒)
  easing: Easing.linear, // イージング関数
  useNativeDriver: true, // ネイティブドライバーの使用
}).start();

useNativeDriverについて

useNativeDriver: trueを指定すると、アニメーションがネイティブスレッドで実行されます。これにより、JavaScriptスレッドの負荷を軽減し、パフォーマンスを向上させることができます。

ただし、全てのプロパティでネイティブドライバーが使用できるわけではありません。使用できるプロパティは以下の通りです:

  • opacity
  • transform
    • translateX
    • translateY
    • scale
    • rotate
    • perspective
  • backgroundColor (iOS only)

3. アニメーション対象のコンポーネントの指定

アニメーション対象のコンポーネントは、必ずAnimatedのコンポーネントでラップする必要があります。React Nativeでは以下のコンポーネントが提供されています:

  • Animated.View
  • Animated.Text
  • Animated.Image
  • Animated.ScrollView
  • Animated.FlatList

カスタムコンポーネントをアニメーション対象にする場合は、Animated.createAnimatedComponentを使用します:

const AnimatedButton = Animated.createAnimatedComponent(Button);

イージング関数について

アニメーションをより自然に見せるために、イージング関数を使用することができます。イージング関数は、時間の経過に対する値の変化の仕方を定義する関数です。

React NativeではEasingモジュールで以下のイージング関数が提供されています:

import { Easing } from 'react-native';

// 基本的なイージング関数
Animated.timing(animation, {
  toValue: 1,
  duration: 1000,
  easing: Easing.linear, // 線形の変化
  useNativeDriver: true,
}).start();

主なイージング関数

基本的なイージング

  • Easing.linear: 一定の速度で変化します
  • Easing.ease: 緩やかに加速して緩やかに減速します
  • Easing.quad: 二次関数的な変化をします
  • Easing.cubic: 三次関数的な変化をします
linear ease
quad cubic

https://api.flutter.dev/flutter/animation/Curves-class.html

加速・減速

  • Easing.in: 徐々に加速します
  • Easing.out: 徐々に減速します
  • Easing.inOut: 加速してから減速します

これらは他のイージング関数と組み合わせて使用できます:

// cubicを使った加速
Easing.in(Easing.cubic)

// quadを使った加速→減速
Easing.inOut(Easing.quad)

バウンス効果

  • Easing.bounce: ボールが跳ねるような効果
  • Easing.elastic(bounciness): ゴムのように伸び縮みする効果
bounce elastic

https://api.flutter.dev/flutter/animation/Curves-class.html

// バウンス効果のあるアニメーション
Animated.timing(animation, {
  toValue: 1,
  duration: 1000,
  easing: Easing.bounce,
  useNativeDriver: true,
}).start();

// 弾性のあるアニメーション
Animated.timing(animation, {
  toValue: 1,
  duration: 1000,
  easing: Easing.elastic(2), // 弾性の強さを指定
  useNativeDriver: true,
}).start();

ベジェ曲線

より細かい制御が必要な場合は、ベジェ曲線を使用してカスタムのイージング関数を作成できます:

// カスタムのイージング関数
const customEasing = Easing.bezier(0.42, 0, 0.58, 1);

Animated.timing(animation, {
  toValue: 1,
  duration: 1000,
  easing: customEasing,
  useNativeDriver: true,
}).start();

コードで見るとよくわからない数値の羅列に見えてしまうので、以下のサイトで実際にぐりぐり動かしながら確認すると直感的です。
https://cubic-bezier.com/#0,0,1,1

イージングの使い分け

アニメーションの目的によって適切なイージングを選択することで、より自然な動きを実現できます:

  • UIの移動: Easing.easeEasing.inOut(Easing.quad)
    • 自然な加速と減速で違和感のない動きに
  • ポップアップ: Easing.elastic(1)
    • 少し跳ねる動きで注目を集める
  • ボタンのタップ: Easing.bounce
    • 押した感触をフィードバック
  • フェードイン/アウト: Easing.ease
    • 急激な変化を避けて目に優しく
// ボタンのタップアニメーション
const animatePress = () => {
  Animated.sequence([
    // 押し込み
    Animated.timing(scale, {
      toValue: 0.9,
      duration: 100,
      easing: Easing.inOut(Easing.quad),
      useNativeDriver: true,
    }),
    // 跳ね返り
    Animated.timing(scale, {
      toValue: 1,
      duration: 200,
      easing: Easing.bounce,
      useNativeDriver: true,
    }),
  ]).start();
};

イージングは、アニメーションをより魅力的にするための重要な要素です。適切なイージングを選択することで、ユーザー体験を大きく向上させることができます。

複雑なアニメーションの実装

基本的なアニメーションを組み合わせることで、より複雑なアニメーションを作ることができます。

複数のアニメーションを順番に実行

Animated.sequenceを使うと、複数のアニメーションを順番に実行することができます。

const animateSequentially = () => {
  Animated.sequence([
    // まず透明度を1にする
    Animated.timing(fadeAnim, {
      toValue: 1,
      duration: 500,
      useNativeDriver: true,
    }),
    // その後、Y座標を100下に移動する
    Animated.timing(translateYAnim, {
      toValue: 100,
      duration: 500,
      useNativeDriver: true,
    }),
    // 最後に0.8倍に縮小する
    Animated.timing(scaleAnim, {
      toValue: 0.8,
      duration: 500,
      useNativeDriver: true,
    }),
  ]).start();
};

複数のアニメーションを同時に実行

Animated.parallelを使うと、複数のアニメーションを同時に実行することができます。

const animateParallelly = () => {
  Animated.parallel([
    // 透明度を1にする
    Animated.timing(fadeAnim, {
      toValue: 1,
      duration: 500,
      useNativeDriver: true,
    }),
    // 同時にY座標を100下に移動する
    Animated.timing(translateYAnim, {
      toValue: 100,
      duration: 500,
      useNativeDriver: true,
    }),
  ]).start();
};

スプリングアニメーション

Animated.springを使うと、物理的な動きのようなアニメーションを作ることができます。

Animated.spring(springAnim, {
  toValue: 1,
  friction: 1, // 摩擦係数(小さいほどバウンドする)
  tension: 40, // 張力(大きいほど素早く動く)
  useNativeDriver: true,
}).start();

繰り返しアニメーション

Animated.loopを使うと、アニメーションを繰り返し実行することができます。

const rotate = new Animated.Value(0);

// 360度回転するアニメーションを無限に繰り返す
const rotateData = rotate.interpolate({
  inputRange: [0, 1],
  outputRange: ['0deg', '360deg'],
});

Animated.loop(
  Animated.timing(rotate, {
    toValue: 1,
    duration: 2000,
    easing: Easing.linear,
    useNativeDriver: true,
  })
).start();

値の補間

interpolateを使うと、アニメーション中の値を別の値に変換することができます。以下は、0から1の値を0degから360degに変換する例です。

const rotateData = fadeAnim.interpolate({
  inputRange: [0, 1],
  outputRange: ['0deg', '360deg'],
});

より複雑な補間も可能です:

const colorInterpolation = progress.interpolate({
  inputRange: [0, 0.5, 1],
  outputRange: ['rgb(255,0,0)', 'rgb(0,255,0)', 'rgb(0,0,255)'],
});

パフォーマンスの最適化

React Nativeのアニメーションでパフォーマンスを最適化するためのポイントをいくつか紹介します。

ネイティブドライバーを使用する

既に説明した通り、useNativeDriver: trueを指定することで、アニメーションをネイティブスレッドで実行することができます。これにより、JavaScriptスレッドの負荷を軽減し、パフォーマンスを向上させることができます。

レイアウトアニメーションを避ける

widthheightなどのレイアウトプロパティのアニメーションは、パフォーマンスに大きな影響を与えます。代わりにtransformを使用することをお勧めします。

// 非推奨
<Animated.View style={{ width: widthAnim }}>

// 推奨
<Animated.View style={{ transform: [{ scaleX: scaleAnim }] }}>

アニメーション中のレンダリングを最小限に

アニメーション中は、できるだけ再レンダリングを避けるようにしましょう。特に、アニメーション対象のコンポーネントの親コンポーネントが再レンダリングされると、パフォーマンスに影響を与えます。

// 非推奨
const AnimatedComponent = () => {
  const [animate, setAnimate] = useState(false);
  const fadeAnim = new Animated.Value(0);

  // このuseEffectが実行されるたびにAnimated.Valueが再作成される
  useEffect(() => {
    if (animate) {
      Animated.timing(fadeAnim, {
        toValue: 1,
        duration: 1000,
        useNativeDriver: true,
      }).start();
    }
  }, [animate]);
};

// 推奨
const AnimatedComponent = () => {
  const [animate, setAnimate] = useState(false);
  // useRefを使ってAnimated.Valueを保持
  const fadeAnim = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    if (animate) {
      Animated.timing(fadeAnim, {
        toValue: 1,
        duration: 1000,
        useNativeDriver: true,
      }).start();
    }
  }, [animate]);
};

まとめ

React NativeのAnimated APIを使うことで、様々なアニメーションを実装することができます。基本的な使い方を理解すれば、複雑なアニメーションも組み合わせで実現できます。

ただし、より複雑なアニメーションや、よりパフォーマンスの高いアニメーションが必要な場合は、react-native-reanimatedの使用を検討することをお勧めします。次回の記事では、react-native-reanimatedの使い方について解説します。

Discussion