ジェスチャーと組み合わせる
react-native-gesture-handlerのGestureDetectorとタップ・パンジェスチャーを組み合わせ、指の動きに追従するアニメーションとwithDecayによる慣性を実装します。
ここまでのアニメーションは、ボタンを押すと再生されるものが中心でした。第3部では、指の動きそのものにアニメーションを追従させます。ドラッグ量やスワイプの勢いといった「生きた値」を共有値に流し込み、コアAPIで動きを組み立てる。これがジェスチャー連携の基本です。
Reanimated自体はジェスチャーを検出しません。そのかわり、同じSoftware Mansionが開発するReact Native Gesture Handler(以下RNGH)と緊密に連携します。RNGHがジェスチャーを検出し、そのコールバックの中で共有値を書き換え、Reanimatedがそれをアニメーションへ変換する、という役割分担です。
RNGHにはTapやPanのほか、Pinch(2本指の拡大縮小)やFling(素早いはじき)など多くのジェスチャーが用意されています。この章ではそのうち、もっとも使うTapとPanを取り上げ、第2部で保留したwithDecayをジェスチャーと組み合わせて動かすところまで進めます。

ドラッグ中は位置を共有値へ流し、リリース時には速度をwithDecayへ渡して勢いを引き継ぎます。
ルートビューでラップする
RNGHを使うには、アプリのルートをGestureHandlerRootViewで包む必要があります。実際のルートビューにできるだけ近い位置に置くのが推奨です。ここが複数のジェスチャーを正しく協調させる土台になります。
import { GestureHandlerRootView } from "react-native-gesture-handler";
function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
{/* アプリの中身 */}
</GestureHandlerRootView>
);
}
タップジェスチャー
いちばん単純なジェスチャーがタップです。画面に指が短く触れたことを検出します。自作のボタンや押せる要素を、一から組み立てたいときに使えます。
まずは、触れると少し大きくなって色が変わる円を作ってみます。ジェスチャーはコンポーネントの本体でGesture.Tap()を呼んで定義します。振る舞いはonBegin・onStart・onEnd・onFinalizeといったメソッドをチェーンして組み立てます。ここでは触れ始めた直後に共有値をtrueにし、ジェスチャーが終わったらfalseへ戻します。
const pressed = useSharedValue(false);
const tap = Gesture.Tap()
.onBegin(() => {
pressed.value = true;
})
.onFinalize(() => {
pressed.value = false;
});
ジェスチャーに渡すコールバックは自動的にworklet化されるので、その中では共有値に安全にアクセスできます。特別なおまじないは要りません。
次に、pressedの状態に応じて色とスケールを変えるアニメーションをuseAnimatedStyleで定義します。バイオレットからイエローへ変わり、withTimingで20%だけなめらかに拡大します。
const animatedStyles = useAnimatedStyle(() => ({
backgroundColor: pressed.value ? "#FFE04B" : "#B58DF1",
transform: [{ scale: withTiming(pressed.value ? 1.2 : 1) }],
}));
定義したジェスチャーは、GestureDetectorのgestureプロパティに渡します。GestureDetectorは、ジェスチャーを検出したいビューを包むコンポーネントです。アニメーションさせたいビューには、先ほどのanimatedStylesを渡しておきます。
<GestureDetector gesture={tap}>
<Animated.View style={[styles.circle, animatedStyles]} />
</GestureDetector>
これで完成です。円に触れると、色が変わりながら少しふくらみます。
パンジェスチャー
今度は円をドラッグできるようにし、指を離すと元の位置に戻るようにしてみます。触れたときの色とスケールの変化はそのまま残します。この動きはタップでは表現できないので、パンジェスチャーに持ち替えます。
うれしいことに、ジェスチャーはどれも似たAPIを持っています。そのためTapをPanに置き換え、onChangeメソッドを1つ足すだけでほぼ実装できます。
const pressed = useSharedValue(false);
const offset = useSharedValue(0);
const pan = Gesture.Pan()
.onBegin(() => {
pressed.value = true;
})
.onChange((event) => {
offset.value = event.translationX;
})
.onFinalize(() => {
offset.value = withSpring(0);
pressed.value = false;
});
onChangeに渡されるコールバックは、便利なプロパティを持つイベントデータを受け取ります。そのうちのtranslationXは、X軸方向にどれだけ移動したかを表します。これを共有値offsetに保存し、円を横に動かします。指を離したときに元の位置へ戻すには、onFinalizeの中でoffset.valueをリセットするだけです。ここではwithSpringで包み、バネのように戻します。
あとはuseAnimatedStyleでoffsetをtranslateXに反映すれば完成です。
const animatedStyles = useAnimatedStyle(() => ({
transform: [
{ translateX: offset.value },
{ scale: withTiming(pressed.value ? 1.2 : 1) },
],
backgroundColor: pressed.value ? "#FFE04B" : "#b58df1",
}));
円をつかんで左右にドラッグし、離してみてください。バネで中央へ戻ります。
勢いを引き継ぐ
第2部のモディファイアの章で、withDecayは後であらためて扱うと述べました。ここがその場面です。
withDecayは、ジェスチャーの速度を引き継いで、そこから減速しながら止まる動きを作ります。指を離した瞬間の速度をそのまま初速として渡すと、スワイプの勢いを受け継いだまま、摩擦のある物体のように滑って止まります。
やることは、onFinalizeが受け取るイベントのvelocityXをwithDecayのvelocityに渡すだけです。ドラッグ中の位置はonChangeのchangeX(前フレームからの変化量)をoffsetに足し込んで追従させます。
const pan = Gesture.Pan()
.onChange((event) => {
offset.value += event.changeX;
})
.onFinalize((event) => {
offset.value = withDecay({
velocity: event.velocityX,
rubberBandEffect: true,
clamp: [
-(width.value / 2) + SIZE / 2 + BOUNDARY_OFFSET,
width.value / 2 - SIZE / 2 - BOUNDARY_OFFSET,
],
});
});
clampで四角形が動ける範囲を区切り、rubberBandEffectをtrueにすることで、範囲の端に達したときゴムのように跳ね返らせています。残りのコードは、四角形が画面の外へ出ないよう幅を測っているだけです。
勢いをつけて離したとき、四角形がどう減速するか試してみてください。
withDecayの設定オプションは、公式のAPIリファレンスにひととおりまとまっています。
この章のまとめ
- Reanimatedはジェスチャーを検出しません。検出はReact Native Gesture Handlerが担い、そのコールバックの中で共有値を書き換えます
Gesture.Tap()やGesture.Pan()でジェスチャーを作り、GestureDetectorに渡します。GestureDetectorは操作したい要素を包みます- ジェスチャーのコールバックは自動でworklet化されるので、共有値にそのままアクセスできます
withDecayにジェスチャーの速度を渡すと、勢いを引き継いで減速する動きになります
ここで扱ったのはRNGHのごく一部です。PinchやFlingをはじめ、多くのジェスチャーやそれらを組み合わせる仕組みが用意されています。詳しくはReact Native Gesture Handlerのドキュメントを見てください。
次章では、ジェスチャーと並ぶもう一つの「生きた値」であるスクロール位置を扱い、スクロールに連動するアニメーションを組み立てます。