カスタムコンポーネントとイベント
createAnimatedComponentで任意のコンポーネントをアニメ化する方法と、cancelAnimationによる中断、そしてuseAnimatedScrollHandlerなどの土台になっている低レベルなイベントフックのしくみを扱います。
これまでAnimated.ViewやAnimated.ScrollViewを当たり前のように使ってきました。この章では、そのAnimated.〜がどこから来るのかを見たうえで、任意のコンポーネントをアニメ化する方法、走っているアニメーションを止める方法、そしてスクロールハンドラーの内側で動いている低レベルなイベントのしくみを扱います。
createAnimatedComponent — 任意のコンポーネントをアニメ化する
Reanimatedには、最初から5つのAnimatedコンポーネントが用意されています。
Animated.FlatListAnimated.ImageAnimated.ViewAnimated.ScrollViewAnimated.Text
これ以外のコンポーネント、たとえばTextInputや自作のコンポーネントをアニメ化したいときに使うのがcreateAnimatedComponentです。ラップすると、そのコンポーネントのpropやスタイルをReanimatedがアニメーションできるようになります。
import Animated from "react-native-reanimated";
import { TextInput } from "react-native";
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
関数コンポーネントをラップするときは、React.forwardRef()でくるんで、refとスタイルを内側のネイティブコンポーネントまで渡す必要があります。Reanimatedがアニメーションを適用するには、実体のビューへの参照とスタイルの受け渡し口が要るためです。
次のデモは、自作のBadgeコンポーネント(中に文字を表示するだけの小さなView)をcreateAnimatedComponentでラップし、回転アニメーションを適用しています。組み込みのAnimatedコンポーネントでなくても、同じように動かせることが分かります。
cancelAnimation — アニメーションを止める
デモの「停止」ボタンが呼んでいるのがcancelAnimationです。共有値に紐づいて走っているアニメーションを中断します。
import { cancelAnimation } from "react-native-reanimated";
function App() {
const offset = useSharedValue(100);
const handleCancel = () => {
cancelAnimation(offset);
};
}
止めたあと、値はその時点の位置で保持されます。そして同じアニメーション(withTimingやwithSpring)をもう一度共有値に割り当てれば、そこから再開できます。デモの「再開」ボタンは、まさにこれをやっています。無限アニメーションを扱うときは、不要になったらcancelAnimationで明示的に止める習慣をつけておくと安心です。
イベントハンドラーの内側 — useEventとuseHandler
ここからは、普段は直接触らないけれど、useAnimatedScrollHandlerやuseScrollOffsetといったおなじみのフックを支えている低レベルなAPIの話です。自分でカスタムのイベントハンドラーフックを作りたくなったときに登場します。
useEventは、ネイティブイベントが発生したときに呼ばれるイベントハンドラーを返す低レベルなフックです。useHandlerは、ハンドラー間で共有するコンテキストオブジェクトと、workletを作り直すべきかを示す値を返します。この2つを組み合わせて、独自のイベントハンドラーフックを組み立てます。
import { useEvent, useHandler } from "react-native-reanimated";
function useAnimatedPagerScrollHandler(handlers, dependencies) {
const { context, doDependenciesDiffer } = useHandler(handlers, dependencies);
return useEvent(
(event) => {
"worklet";
const { onPageScroll } = handlers;
if (onPageScroll && event.eventName.endsWith("onPageScroll")) {
onPageScroll(event, context);
}
},
["onPageScroll"],
doDependenciesDiffer
);
}
useHandlerが返すcontextは、イベントの発生をまたいで保持される素のJSオブジェクトです。複数のworkletで共有され、状態のやりとりに使えます。doDependenciesDifferは依存が変わったかどうかを表し、useEventの第3引数に渡してハンドラーの作り直しを制御します。
普段のアプリ開発でこれらを直接書くことはまずありません。useAnimatedScrollHandlerのようなReanimatedが用意したフックが、内部でこの組み合わせを使っています。「スクロールハンドラーはこうやって組み立てられているのだ」という視点を持っておくと、既存のフックの挙動が読みやすくなります。
useComposedEventHandler — 複数のハンドラーをまとめる
useComposedEventHandlerは、useEventベースのイベントハンドラー(useAnimatedScrollHandlerや自作のもの)を1つにまとめるフックです。同じイベントに複数のハンドラーで反応させたいときに使います。
import Animated, {
useAnimatedScrollHandler,
useComposedEventHandler,
} from "react-native-reanimated";
function ComposedEventHandlerExample() {
const onScrollHandler1 = useAnimatedScrollHandler({
onScroll(e) {
console.log("ハンドラー1");
},
});
const onScrollHandler2 = useAnimatedScrollHandler({
onScroll(e) {
console.log("ハンドラー2");
},
});
const composedHandler = useComposedEventHandler([
onScrollHandler1,
onScrollHandler2,
]);
return (
<Animated.ScrollView onScroll={composedHandler}>
{/* ... */}
</Animated.ScrollView>
);
}
まとめたハンドラーを対応するonEventプロパティ(スクロール系ならonScroll)に渡すだけです。1つのイベントに複数の処理をぶら下げたり、種類の違うイベントを1つのオブジェクトで扱ったりできます。
makeMutable — フックを使わずに共有値を作る
最後にmakeMutableです。これはuseSharedValueが内部で使っている関数で、フックを使わずに共有値(正確にはミュータブルな値)を作ります。
import { makeMutable } from "react-native-reanimated";
const mv = makeMutable(100);
グローバルスコープで共有値を持ちたい、可変長の配列として共有値を並べたい、といったフックでは書けないケースのための道具です。作られるオブジェクトはuseSharedValueが返すものと同じなので、以降の使い方は変わりません。
useSharedValueとの違いを一言でいえば、useSharedValueは「同じオブジェクトを再利用し、アンマウント時にアニメーションを自動でキャンセルする」フック版であり、makeMutableはその自動管理がない素のバージョンです。使いどころが分かっていて、その結果を引き受けられる場合にだけ手を伸ばしてください。
この章のまとめ
createAnimatedComponentは、組み込み以外のコンポーネントをアニメ化するための入り口です。cancelAnimationは走っているアニメーションを止める道具でした。useEvent / useHandler / useComposedEventHandlerは、普段使うスクロールハンドラーの内側を支える低レベルなAPIで、makeMutableはuseSharedValueの素のバージョンです。後半の4つは日々のコードで直接書くことは少ないものの、既存のフックがどう組み立てられているかを理解する手がかりになります。