Spin an object at a resting speed and let the user fling it faster with a horizontal swipe. On release it keeps turning and slows down. Everything runs on the UI thread; nothing re-renders.
Copy useSwipeRotation.ts into your app, then use it:
import { MoneyStack } from "@apricot/assets";
import { Group, Scene, Spin } from "@apricot/react";
import { GestureDetector } from "react-native-gesture-handler";
import { useSwipeRotation } from "./useSwipeRotation";
export function SpinningMoney() {
const { gesture, rotation } = useSwipeRotation();
return (
<GestureDetector gesture={gesture}>
<Scene style={{ flex: 1 }}>
<Spin speed={0.6}>
<Group rotation={rotation}>
<MoneyStack bills={40} />
</Group>
</Spin>
</Scene>
</GestureDetector>
);
}The app root must be wrapped in GestureHandlerRootView (see Installation).
The hook#
// Apricot recipe: swipe-to-spin
// Copy this file into your app. A horizontal swipe rotates whatever you wrap
// in <Group rotation={rotation}>; on release it keeps turning and slows down
// (Reanimated withDecay). Everything runs on the UI thread.
import { Gesture } from "react-native-gesture-handler";
import { cancelAnimation, useDerivedValue, useSharedValue, withDecay } from "react-native-reanimated";
export interface SwipeRotationOptions {
/** Radians per point of finger movement. */
sensitivity?: number;
/** withDecay deceleration: closer to 1 coasts longer. */
deceleration?: number;
}
export function useSwipeRotation({ sensitivity = 0.012, deceleration = 0.997 }: SwipeRotationOptions = {}) {
const angle = useSharedValue(0);
const rotation = useDerivedValue<readonly [number, number, number]>(() => [0, angle.value, 0]);
const gesture = Gesture.Pan()
.onBegin(() => {
cancelAnimation(angle);
})
.onChange((event) => {
angle.value += event.changeX * sensitivity;
})
.onEnd((event) => {
angle.value = withDecay({ velocity: event.velocityX * sensitivity, deceleration });
});
return { gesture, rotation, angle };
}It needs react-native-gesture-handler and react-native-reanimated.
Options and result#
| Option | Default | Meaning |
|---|---|---|
sensitivity |
0.012 |
Radians per point of finger movement |
deceleration |
0.997 |
withDecay deceleration: closer to 1 coasts longer |
| Returns | Type | Use |
|---|---|---|
gesture |
Pan gesture | Pass to <GestureDetector gesture> around the scene |
rotation |
SharedValue<[0, angle, 0]> |
Pass to <Group rotation> around the object |
angle |
SharedValue<number> |
The current angle in radians, to read or set (for example angle.value = withSpring(0) to reset) |
How it works#
<Spin> adds a constant rotation; the swipe drives a separate rotation shared value on the UI thread. Apricot mirrors shared values into the render loop without blocking the JS thread, and wakes the loop when they change, so with frameloop="demand" the scene sleeps again once everything stops. (Here <Spin> keeps it awake; remove it for an object that only turns when swiped.)
Variations#
- Swipe only, no idle spin: drop the
<Spin>wrapper. - Tilt as well: add a vertical pan that writes the x angle, and derive
[x, y, 0](see Gestures and interaction). - A model: put a
<Model>inside the<Group>, or passrotationto the<Model>directly.