【React Native】アニメーションの基本(追加ライブラリなし)


はじめに

WebならCSSでお手軽に実装できる簡素なアニメーションでも、
React Nativeの場合には、少し複雑になってきます。

とはいえ、アプリ全体のうち、ほんの少しだけ使用するアニメーションのために依存関係を増やしたくない場合もあるかと思います。

React Native純正のアニメーションAPIを使うことで、簡単なアニメーションであれば問題なく実装できます。

Like!ボタンを作る

今回実装するのは、以下のようなボタンです。
タップ時に♡マークが一瞬大きくなり、色が変化します。

実は最初グッと大きくなってから、少しスピードを落として縮小しています。
CSSでいう、@keyframesのようなイメージで、フレームごとに大きさを変える制御を実装します。

必要なインポートを行う

AnimatedInteractionManagerIconをインポートします。

import React, { useRef } from 'react';
import { Animated, InteractionManager } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';

Animatedコンポーネント

<View><Text>コンポーネントは頭にAnimated.を付加することで、アニメーションに対応したコンポーネント化することができます。

しかし、<Icon>は同じようにするとエラーになってしまいます。(<Text>を継承してるのに...)

// Animatedコンポーネント
<Animated.View />
<Animated.Text />

// エラー!
<Animated.Icon />

Animated.createAnimatedComponent()

以下のように記述することで<Icon>もアニメーション対応させることができます。

const AnimatedIcon = Animated.createAnimatedComponent(Icon);

マークアップする

まずはアニメーションなしのボタンをマークアップします。


const AnimatedIcon = Animated.createAnimatedComponent(Icon);

const LikeButton = (props) => {
  return (
      <TouchableOpacity
        style={styles.container}
        onPress={_onPress}
        activeOpacity={1}
      >
        <View style={styles.circle}>
          <Text>
            <Text>
              <AnimatedIcon name="md-heart" />
            </Text>
          </Text>
        </View>
      </TouchableOpacity>
  )
}

アニメーションのフレーム数を定義する

今回の♡マークは、バウンス感を出すために、
実は最初グッと大きくなってから、少しスピードを落として縮小しています。

こうした動きを実装するために、フレームごとの制御を行います。

CSSでいう、@keyframesのようなイメージです。

今回は最終的に、0 ~ 150フレームの間でアニメーションを定義していきます

new Animated.Value()

new Animated.Value()は、数値を引数にとり、フレームの初期値を定義します。
今回は「0」からスタートします。

const animatedValue = useRef(new Animated.Value(0)).current;

フレームにスタイルを割り振る(アイコンの色)

♡マークの色が

  • 初期値:「灰色」
  • ボタン押下:「赤色」

となるように設定します。

interpolate()

interpolate()はフレーム毎にスタイルを割り当てます。

今回は、

  • 初期値「0」に「#999」(灰色)
  • (最終フレームとなる)「150」に「#B11」(赤色)

を割り当てます。

アニメーションを実行すると、フレーム数値が「0」から「150」に増えるにしたがって、色も変化していくというわけです。

// 0 ~ 150 のフレーム間でアニメーションするアイコン色
  const interPolateColor = animatedValue.interpolate({
    inputRange: [0, 150],
    outputRange: ['#999', '#B11'],
  });

コンポーネントで使いやすいようオブジェクトにしておく(任意)

最終的に<Animated.Text style={animatedIconStyle}>のようにしたいので、オブジェクトにしておきます。
別にやらなくてもOKです。

const animatedIconStyle = {
  color: interPolateColor,
};

フレームにスタイルを割り振る(アイコンのサイズ)

今度は、アイコンのサイズにもフレームを設定します。

最初グッと大きくなってから、少しスピードを落として縮小させるために、今度は4つのフレームを設定します。

先ほどと同じく、初期値「0」、最終フレームは「150」ですが、その間の「50」と「100」にも設定値を割り振ります。

// 0 ~ 150 のフレーム間でアニメーションするスタイル(アイコンサイズ)
  const interPolateSize = animatedValue.interpolate({
    inputRange: [0, 50, 100, 150],
    outputRange: [28, 38, 34, 28],
  });
  const animatedIconSize = {
    fontSize: interPolateSize,
  };

アイコンサイズが「28」から「38」に一気に大きくなり、

その後「34」を経て、初期値「28」に戻ります。

これで、最初グッと大きくなってから、少しスピードを落として縮小させる動きができました。

コンポーネントの styleに適用させる

先ほどのマークアップにstyleを設定します。

アニメーションさせる<Text>コンポーネントは、Animated.つけてアニメーション対応させます。

const AnimatedIcon = Animated.createAnimatedComponent(Icon);

const LikeButton = (props) => {
  return (
      <TouchableOpacity
        style={styles.container}
        onPress={_onPress} // アニメーションスタート
        activeOpacity={1}
      >
        <View style={styles.circle}>
          <Text>
            // 色のアニメーションスタイル追加
            <Animated.Text style={animatedIconStyle}>
              // サイズのアニメーションスタイル追加
              <AnimatedIcon name="md-heart" style={animatedIconSize} />
            </Animated.Text>
          </Text>
        </View>
      </TouchableOpacity>
  )
}

アニメーションを開始する

ボタンを押した時に、アニメーションが開始するように_onPress関数を実装します。

  const _onPress = () => {
    Animated.timing(animatedValue, {
      toValue: 150,
      duration: 200,
    }).start();
  };

Animated.timing()

Animated.timing()は以下のように引数をとります。

  • 第一引数:フレーム値
  • 第二引数:フレーム値をどのように変化させるか

ここでは設定値を以下のように設定しています。

  • toValue: フレーム値をどこまで変化させるか
  • duration: アニメーション完了までの時間(ミリ秒)

.start()することで、スタートします。

アニメーション完了を待って、次の処理をする場合

例えば、「イイね」したら画面遷移させたい!
など、アニメーションと連動して何かしらイベントを起こしたいことがあるかもしれません。

しかし何も考えずに実装すると、アニメーションがまだ途中なのに次の処理が走ってしまいます。

InteractionManager.runAfterInteractions()

InteractionManager.runAfterInteractions()に処理を記述することで、

アニメーションの完了を待ってから処理実行することができます。

const _onPress = () => {
  Animated.timing(animatedValue, {
    toValue: props.isClip ? 0 : 150,
    duration: 200,
  }).start();
  // アニメーション終了を待って行う処理
  InteractionManager.runAfterInteractions(() => {
    // someFunc();
  });
};

まとめ

簡単なアニメーションであれば、標準APIも十分機能します。
また、interpolateを使用することで、CSSでいう@keyframesのような少し複雑な動きも実装できます。