技術メモ・実装記録
「ネイティブアプリっぽい滑らかなフリック操作」を実装したくて、react-native-gesture-handlerとreact-native-reanimatedを使いました。
この記事では、フリックジェスチャーの実装から、アニメーション完了後の処理、setTimeoutを使わない設計まで、実コードをもとに書きます。
対象読者React Nativeでスワイプ・フリック操作を実装しようとしているエンジニア向けです。
キラ
キラ
「Instagramのストーリーみたいにスワイプで切り替えたい」から始まった実装。思ったより深かった。
Contents
- Gesture HandlerとReanimatedの関係
- 基本のフリック実装
- アニメーション完了後に処理する(setTimeoutを使わない)
- フリックと判定する速度の閾値
- まとめ
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を見てみる
※ アフィリエイトリンクです


コメント