Skip to content
React Native Reanimated ガイドブック
Esc
navigateopen⌘Jpreview
On this page

リアクションとフレームコールバック

共有値の変化に反応するuseAnimatedReactionと、毎フレーム関数を走らせるuseFrameCallback、その薄いラッパーであるuseTimestampを扱います。UIスレッド上で「値を監視する」「時間で駆動する」ための道具です。

これまでは、共有値をスタイルに結びつけて動きを作ってきました。この章で扱うのは、少し毛色の違う2つの道具です。ひとつは共有値の変化を監視して何かを起こすuseAnimatedReaction、もうひとつは毎フレーム関数を実行するuseFrameCallbackです。どちらもUIスレッド上で動き、これまで学んだしくみの延長線上にあります。

useAnimatedReaction — 値の変化に反応する

useAnimatedReactionは、共有値の変化に反応するためのフックです。特に、前回の値と今回の値を比べて「変わったときだけ何かする」といった処理に向いています。

値の変化に反応するuseAnimatedReaction、毎フレーム動くuseFrameCallback、時刻を返すuseTimestampの比較図

値が変わった瞬間、各フレーム、経過時間という3つの異なるタイミングで処理を組み立てられます。

2つの関数を渡します。第1引数のprepareは、反応したい値を返す関数です。第2引数のreactは、その値が更新されたときに呼ばれ、現在の値と前回の値の2つを受け取ります。前回の値は初回はnullです。

import { useAnimatedReaction } from "react-native-reanimated";

function App() {
  useAnimatedReaction(
    () => {
      return sv.value;
    },
    (currentValue, previousValue) => {
      if (currentValue !== previousValue) {
        // 変化したときの処理
      }
    }
  );
}

preparereactはどちらも自動でworklet化され、UIスレッドで動きます。つまり、フレームごとに更新される共有値を、UIスレッド上でそのまま監視できます。JS側に結果を反映したいときは、これまでどおりrunOnJS(第6部1章で見たscheduleOnRNの従来名)で渡します。

次のデモでは、左右に往復する箱の位置を「左・中央・右」の3つのゾーンに畳み込み、ゾーンが変わった瞬間だけJS側のラベルを更新しています。prepareが返すゾーン番号が前回と変わったかをreactで比べ、変わったときだけrunOnJSする形です。位置そのものは毎フレーム変わりますが、ラベルの更新はゾーンをまたぐ数回に抑えられます。

デモを読み込み中…

useAnimatedReactionを使ううえで、いちばん気をつけたいのが無限ループです。prepareで読んだ共有値をreactの中で書き換えると、その書き換えがまたprepareを発火させ、止まらなくなります。

useAnimatedReaction(
  () => {
    return width.value;
  },
  (currentValue) => {
    // 🚨 無限ループ
    width.value += currentValue;
  }
);

もうひとつ。技術的にはReactのstateなど普通の値にも反応できますが、それはuseEffectの仕事です。useAnimatedReactionは、あくまでUIスレッド上の共有値を監視するための道具だと考えてください。

なお、本書のデモではBabelプラグインがないため、prepareが読む共有値を第3引数の依存配列で明示しています。

useFrameCallback — 毎フレーム実行する

useFrameCallbackは、フレーム更新のたびに関数を実行します。コールバックにはframeInfoオブジェクトが渡され、時間に関する3つの情報が入っています。

  • timestamp: 直近のフレームが描画されたシステム時刻(ミリ秒)
  • timeSincePreviousFrame: 前のフレームからの経過時間(ミリ秒)。有効化後の最初のフレームではnull。2フレーム目以降は60Hzで約16ms、120Hzで約8ms
  • timeSinceFirstFrame: 有効化されてからの経過時間(ミリ秒)

戻り値のオブジェクトで、コールバックの開始と停止を制御できます。setActive(true / false)で切り替え、isActiveで現在動いているかを確認できます。第2引数のautostart(既定はtrue)で、最初から動かすかどうかを指定します。

import { useFrameCallback } from "react-native-reanimated";

function App() {
  const frameCallback = useFrameCallback((frameInfo) => {
    // 毎フレーム、共有値を1増やす
    sv.value += 1;
  });

  return (
    <Button
      title="開始 / 停止"
      onPress={() => frameCallback.setActive(!frameCallback.isActive)}
    />
  );
}

次のデモは、毎フレーム回転を少しずつ進めながらフレーム数を数え、timeSincePreviousFrameからFPSを計算して表示します。表示は毎フレームだと重いので、15フレームごとにまとめてJS側へ渡しています。停止ボタンでsetActiveを切り替えると、回転もカウントも止まります。

デモを読み込み中…

コールバックはUIスレッドで動くので、毎フレームの重い計算をJSスレッドに載せずに済みます。ただし本当に毎フレーム呼ばれるので、中身は軽く保つのが鉄則です。

useTimestamp — 時間で駆動する

useTimestampは、現在のフレームのタイムスタンプを共有値として返すフックです。中身はuseFrameCallbackの薄いラッパーで、「今の時間からアニメーションを組み立てたい」というよくあるケースを手軽にするためのものです。

import { useTimestamp } from "react-native-reanimated";

function PulsingDot({ visible }) {
  const timestamp = useTimestamp(visible);

  const animatedStyle = useAnimatedStyle(() => ({
    opacity: 0.5 + 0.5 * Math.sin(timestamp.value / 300),
  }));

  return <Animated.View style={[styles.dot, animatedStyle]} />;
}

timestamp.valueは最初のフレームからの経過時間(ミリ秒)で、毎フレーム更新されます。引数で更新のオン・オフを切り替えられます。上の例のように、時間を三角関数に通せば、脈打つような周期的な動きが素直に書けます。

パフォーマンスのため、複数のコンポーネントでそれぞれuseTimestampを呼ぶより、1つのインスタンスを上位で作ってpropsやコンテキストで配るほうが望ましい、という点だけ覚えておいてください。

この章のまとめ

useAnimatedReactionは共有値の変化を監視する道具、useFrameCallbackは毎フレーム実行する道具、useTimestampはその時間版の近道です。いずれもUIスレッド上で動き、共有値を軸にしている点は共通しています。宣言的なuseAnimatedStyleでは表現しづらい「変化に応じた分岐」や「時間で駆動する動き」を書きたくなったら、この章のフックを思い出してください。

この章のもとになった公式ドキュメント