Reanimatedとの連携
Reanimatedの共有値(Shared Value)をprops経由でSkiaコンポーネントに直接渡す仕組みと、そのメリットを解説します。ブリッジも再レンダリングも不要です。
ジェスチャーや時間が共有値を更新し、その値が複数の描画propsへ直接流れます。
前章のuseClockは、実はReanimatedの共有値(Shared Value)そのものを返していました。この章では、その共有値がなぜSkiaコンポーネントのpropsにそのまま渡せるのか、という仕組みを掘り下げます。
共有値(Shared Value)とは
共有値は、.valueで読み書きできるミュータブルな値のコンテナです。
const r = useSharedValue(0);
r.value = 100; // 読み書きは.valueプロパティ経由
通常のReact Nativeコンポーネントで共有値をアニメーションに使う場合、Animated.createAnimatedComponent()でラップしたコンポーネントに対してuseAnimatedStyleやuseAnimatedPropsを介す必要があります。共有値の変更をネイティブ側のUIスレッドに橋渡しする、専用の仕組みが必要になるからです。
React Native Skiaでは、この橋渡しが不要です。Skiaのコンポーネントには、共有値をそのままpropに渡せます。
import { useEffect } from "react";
import { Canvas, Circle, Group } from "@shopify/react-native-skia";
import {
useDerivedValue,
useSharedValue,
withRepeat,
withTiming,
} from "react-native-reanimated";
const HelloWorld = () => {
const size = 256;
const r = useSharedValue(0);
const c = useDerivedValue(() => size - r.value);
useEffect(() => {
r.value = withRepeat(withTiming(size * 0.33, { duration: 1000 }), -1);
}, [r, size]);
return (
<Canvas style={{ flex: 1 }}>
<Group blendMode="multiply">
<Circle cx={r} cy={r} r={r} color="cyan" />
<Circle cx={c} cy={r} r={r} color="magenta" />
<Circle cx={size / 2} cy={c} r={r} color="yellow" />
</Group>
</Canvas>
);
};
rは共有値、cはそこから導出したuseDerivedValueですが、どちらも<Circle>のcx・cy・rにそのまま渡っています。createAnimatedComponentもuseAnimatedPropsも出てきません。Skiaのレンダラーは、propに渡された値が共有値かどうかを判定し、共有値であればUIスレッド上でその変更を直接読み取って再描画します。JavaScriptスレッドを経由する再レンダリングは発生しません。
propsに直接渡せることの効果
このデモでは、ボタンを押すたびに円がwithSpringでランダムな位置へ移動します。注目してほしいのは画面下の「再レンダリング回数」の表示です。円は何度移動しても、この数字は1のまま増えません。
デモのコードを見ると、cx・cyはどちらもuseSharedValueで作った共有値で、<Circle cx={cx} cy={cy} .../>にそのまま渡されています。ボタンのハンドラーはcx.value = withSpring(...)と共有値の.valueを書き換えているだけで、setStateは一度も呼んでいません。コンポーネント自体は最初の1回しかレンダリングされておらず、円の移動はSkiaのレンダラーがUIスレッド上で直接反映しているわけです。
色の扱い: interpolateColors
ReanimatedのinterpolateColorは、Reanimated自身のカラー表現を前提にしているため、Skiaの色(内部的にはFloat32の配列)とはそのままでは噛み合いません。代わりにReact Native Skiaが提供するinterpolateColorsを使います。
import {
Canvas,
Fill,
LinearGradient,
interpolateColors,
vec,
} from "@shopify/react-native-skia";
import { useDerivedValue, useSharedValue } from "react-native-reanimated";
const startColors = ["rgba(34,193,195,0.4)", "rgba(63,94,251,1)"];
const endColors = ["rgba(0,212,255,0.4)", "rgba(252,70,107,1)"];
const AnimatedGradient = () => {
const progress = useSharedValue(0);
const colors = useDerivedValue(() => [
interpolateColors(progress.value, [0, 1], startColors),
interpolateColors(progress.value, [0, 1], endColors),
]);
return (
<Canvas style={{ flex: 1 }}>
<Fill>
<LinearGradient start={vec(0, 0)} end={vec(256, 256)} colors={colors} />
</Fill>
</Canvas>
);
};
colorspropに渡す配列自体も、useDerivedValueで作った共有値です。配列やオブジェクトを返す共有値も、これまでと同じように直接propへ渡せます。
下のデモでは、useClockから作ったprogressをinterpolateColorsに渡し、グラデーションの2色を継続的に往復させています。
注意点
useDerivedValueやuseAnimatedReactionに渡す関数の先頭には"worklet"という文字列が必要です。Reanimatedがこの文字列を目印に、UIスレッドで実行可能な関数として扱います。書き忘れても動くことがありますが(Reanimatedのバージョンやビルド設定によって挙動が変わる)、意図しない場所で処理がJSスレッドに落ちる原因になるため、習慣として必ず書いておくのが安全です。
Web、かつBabel/SWCプラグイン無しの場合はuseDerivedValueの第2引数が必須: useDerivedValue(updater)は本来、updaterが参照している共有値をreact-native-workletsのBabel/SWCプラグインが自動的にクロージャとして検出し、それらの値が変わるたびにupdaterを再実行します。ところがこのプロジェクトのようにMetro/Babelを経由しないVite製のWebビルドでは、そのプラグインが一切動いていません。結果としてupdaterのクロージャ検出は常に空になり、useDerivedValueが返す共有値はマウント時の初回計算のまま二度と更新されなくなります — .valueを直接読むと確かに変化しているように見えても(clock自体は正しく毎フレーム更新される)、そこから導出した値だけが固まってしまうという、気づきにくい落とし穴です。Reanimated自身がこのケース向けに用意している回避策が、useDerivedValue(updater, dependencies)の第2引数です。updaterが参照している共有値をdependencies配列に明示的に渡せば、Babelプラグインが無くても正しく再計算されるようになります(本章・前章のサンプルコードもすべてこの形で書いています)。ネイティブ(iOS/Android)ではBabel/SWCプラグインが動くため、dependenciesを省略しても問題は起きません。
共有値を渡すpropの種類によっては、Web版のレンダラーが再描画を検知しないことがあります: dependenciesを正しく指定しても、<Group transform={...}><Image .../></Group>のように<Group>のtransformで<Image>を回転・拡大縮小するパターンや、<Skottie frame={...}>のようにSkottieアニメーションのフレームを共有値で駆動するパターンでは、共有値自体は正しく更新されているのに画面に反映されないケースを確認しています(このプロジェクトのWeb版CanvasKitレンダラー側の制約とみられます)。これらのケースでは、requestAnimationFrameでReanimatedの値をポーリングしてuseStateに反映し、通常のprops経由で渡す(またはPictureを毎フレーム作り直す)方が確実です。本書のテクスチャアニメーション・Skottieの章のデモコードが実例です。単純な図形のprops(<Circle cx={...} cy={...}>など)への直接代入は、この章で見た通り問題なく動作します。
前章のuseClockやusePathValueなど、Skia専用のアニメーションフックも、内部の仕組みはこの章で見た「共有値を直接propに渡す」という考え方の上に成り立っています。次章では、この仕組みをジェスチャーと組み合わせて、指で触って動かせるアニメーションを作ります。
公式ドキュメントのReanimated 3も参照してください。