React NativeでGesture Handler + Reanimedを使ってフリック実装した話

技術メモ・実装記録

「ネイティブアプリっぽい滑らかなフリック操作」を実装したくて、react-native-gesture-handlerとreact-native-reanimatedを使いました。

この記事では、フリックジェスチャーの実装から、アニメーション完了後の処理、setTimeoutを使わない設計まで、実コードをもとに書きます。

対象読者

React Nativeでスワイプ・フリック操作を実装しようとしているエンジニア向けです。

PROFILE 20代ITエンジニア|React Native(Expo)でアプリ開発中|Raw Ambition 運営
キラ
キラ
「Instagramのストーリーみたいにスワイプで切り替えたい」から始まった実装。思ったより深かった。
Contents
  1. Gesture HandlerとReanimatedの関係
  2. 基本のフリック実装
  3. アニメーション完了後に処理する(setTimeoutを使わない)
  4. フリックと判定する速度の閾値
  5. まとめ

Gesture HandlerとReanimatedの関係

まずこの2つのライブラリの役割を整理します。

# ライブラリの役割

react-native-gesture-handler
→ ジェスチャー(タップ・スワイプ・フリック)を検出する
→ 「どの方向にどの速さで動いたか」を教えてくれる

react-native-reanimated
→ 滑らかなアニメーションを実行する
→ JavaScriptスレッドではなくUIスレッドで動くので
  「ネイティブっぽい滑らかさ」になる

# インストール
npx expo install react-native-gesture-handler react-native-reanimated

? POINT

React NativeのデフォルトのAnimated APIはJavaScriptスレッドで動くため、重い処理と競合すると動きがカクつきます。Reanimatedを使うとUIスレッドで直接アニメーションが動くため、滑らかな動きが実現できます。

基本のフリック実装

import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  runOnJS,
} from 'react-native-reanimated';

const FLICK_VELOCITY_THRESHOLD = 500; // フリックと判定する速度(px/s)
const SCREEN_WIDTH = Dimensions.get('window').width;

const FlickCard = ({ onFlickLeft, onFlickRight }) => {
  const translateX = useSharedValue(0);

  // パンジェスチャーの定義
  const panGesture = Gesture.Pan()
    .onUpdate((event) => {
      // 指の動きに合わせてカードを移動
      translateX.value = event.translationX;
    })
    .onEnd((event) => {
      const { velocityX, translationX } = event;

      // 右フリック判定
      if (velocityX > FLICK_VELOCITY_THRESHOLD || translationX > SCREEN_WIDTH / 3) {
        translateX.value = withSpring(SCREEN_WIDTH, {}, () => {
          runOnJS(onFlickRight)(); // アニメーション完了後に呼ぶ
        });
        return;
      }

      // 左フリック判定
      if (velocityX < -FLICK_VELOCITY_THRESHOLD || translationX < -SCREEN_WIDTH / 3) {
        translateX.value = withSpring(-SCREEN_WIDTH, {}, () => {
          runOnJS(onFlickLeft)(); // アニメーション完了後に呼ぶ
        });
        return;
      }

      // フリックでない場合は元の位置に戻す
      translateX.value = withSpring(0);
    });

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: translateX.value }],
  }));

  return (
    <GestureDetector gesture={panGesture}>
      <Animated.View style={[styles.card, animatedStyle]}>
        {/* カードの中身 */}
      </Animated.View>
    </GestureDetector>
  );
};
スカイ
スカイ
withSpringの第3引数にコールバックを渡すとアニメーション完了後に実行される。これがポイント。

アニメーション完了後に処理する(setTimeoutを使わない)

フリック後にページを切り替える処理をするとき、最初はsetTimeoutで遅延させていました。でもこれはアニメーションの長さとタイマーがずれるとUXが崩れる問題があります。

// ❌ setTimeoutで遅延させる(アニメーションとずれることがある)
.onEnd((event) => {
  if (isFlick) {
    translateX.value = withSpring(SCREEN_WIDTH);
    setTimeout(() => {
      onFlickRight(); // 500ms後に実行
                      // でもアニメーションが終わってるとは限らない
    }, 500);
  }
});

// ✅ withSpringのコールバックで完了後に実行
.onEnd((event) => {
  if (isFlick) {
    translateX.value = withSpring(SCREEN_WIDTH, {}, () => {
      runOnJS(onFlickRight)(); // アニメーション完了後に確実に実行される
    });
  }
});

? runOnJSとは

Reanimatedのアニメーションはワークレット(UIスレッド)で動きます。コールバック内でReactのstateを更新したり、通常のJavaScript関数を呼ぶにはrunOnJSでラップする必要があります。これを忘れると「Tried to synchronously call a non-worklet function」系のエラーが出ます。
キラ
キラ
runOnJSを最初知らなくてエラーで詰まった。「アニメーション内でJS関数を呼ぶときはrunOnJSでラップ」これを覚えておくだけでいい。

フリックと判定する速度の閾値

「どの速さからフリックと判定するか」の閾値設定も重要です。

// 速度だけで判定する場合
const FLICK_VELOCITY_THRESHOLD = 500; // px/s
if (velocityX > FLICK_VELOCITY_THRESHOLD) { /* フリック */ }

// 速度 OR 移動距離で判定する場合(より自然なUX)
const isFlickRight =
  velocityX > FLICK_VELOCITY_THRESHOLD ||   // 速く動かした
  translationX > SCREEN_WIDTH / 3;          // 画面の1/3以上移動した

// これにより「ゆっくり大きく動かした場合」もフリックと判定できる

速度だけで判定すると「ゆっくり大きく動かした場合」がフリックと判定されず、戻ってしまいます。速度と移動距離の両方を組み合わせるとより自然なUXになります。

⚠️ 閾値の調整が必要

500px/sという閾値はデバイスや用途によって変わります。実機で試しながら「これくらいの速さでフリックしたら切り替わってほしい」という感覚で調整します。シミュレーターとの差も大きいので必ず実機で確認。

? この記事のまとめ
  • Gesture Handlerでジェスチャー検出、Reanimatedで滑らかなアニメーションを担当
  • withSpringの第3引数にコールバックを渡すとアニメーション完了後に処理できる(setTimeoutは不要)
  • アニメーション内でJS関数を呼ぶにはrunOnJSでラップが必要
  • フリック判定は「速度 OR 移動距離」の組み合わせでより自然なUXになる

Reanimatedは最初の学習コストが高いですが、一度仕組みを理解すれば「ネイティブアプリっぽい滑らかな動き」を自由に実装できます。

Recommended
ブログを始めるならConoHa WINGが最速
エンジニアの技術メモをブログで発信して収益化。月1,000円以下で始められます。
▶ ConoHa WINGを見てみる
※ アフィリエイトリンクです
Next
次回:「コードレビューで並列フェッチのレースコンディションを指摘された話」
Raw Ambition 始動から22日目

コメント

タイトルとURLをコピーしました