+import PinchGestureBasic from '@site/static/examples/PinchGestureBasic';
+import PinchGestureBasicSrc from '!!raw-loader!@site/static/examples/PinchGestureBasicSrc';
+
+
import BaseEventData from './\_shared/base-gesture-event-data.md';
@@ -28,6 +40,14 @@ Similarly, the scale factor decreases as the distance between the fingers decrea
Pinch gestures are used most commonly to change the size of objects or content onscreen.
For example, map views use pinch gestures to change the zoom level of the map.
+
+
## Reference
```jsx
diff --git a/docs/docs/gestures/rotation-gesture.md b/docs/docs/gestures/rotation-gesture.md
index be4ab87ad0..bedab4064c 100644
--- a/docs/docs/gestures/rotation-gesture.md
+++ b/docs/docs/gestures/rotation-gesture.md
@@ -5,12 +5,24 @@ sidebar_label: Rotation gesture
sidebar_position: 6
---
+import { vanishOnMobile, appearOnMobile, webContainer } from '@site/src/utils/getGestureStyles';
+
import useBaseUrl from '@docusaurus/useBaseUrl';
-
-
-
-
+import RotationGestureBasic from '@site/static/examples/RotationGestureBasic';
+import RotationGestureBasicSrc from '!!raw-loader!@site/static/examples/RotationGestureBasicSrc';
+
+
+
+
+
+
+
+
}
+ src={RotationGestureBasicSrc}
+ disableMarginBottom={true}
+ />
import BaseEventData from './\_shared/base-gesture-event-data.md';
@@ -25,6 +37,14 @@ The gesture [activates](/docs/fundamentals/states-events#active) when fingers ar
Gesture callback can be used for continuous tracking of the rotation gesture. It provides information about the gesture such as the amount rotated, the focal point of the rotation (anchor), and its instantaneous velocity.
+
+
+
+
+
+
+
Rotation Gesture
+
## Reference
```jsx
diff --git a/docs/docs/gestures/tap-gesture.md b/docs/docs/gestures/tap-gesture.md
index dbb8ab7bee..b718f4f1e7 100644
--- a/docs/docs/gestures/tap-gesture.md
+++ b/docs/docs/gestures/tap-gesture.md
@@ -5,12 +5,24 @@ sidebar_label: Tap gesture
sidebar_position: 4
---
+import { vanishOnMobile, appearOnMobile, webContainer } from '@site/src/utils/getGestureStyles';
+
import useBaseUrl from '@docusaurus/useBaseUrl';
-
-
-
-
+import TapGestureBasic from '@site/static/examples/TapGestureBasic';
+import TapGestureBasicSrc from '!!raw-loader!@site/static/examples/TapGestureBasic';
+
+
+
+
+
+
+
+
}
+ src={TapGestureBasicSrc}
+ disableMarginBottom={true}
+ />
import BaseEventData from './\_shared/base-gesture-event-data.md';
@@ -26,6 +38,14 @@ For example, you might configure tap gesture recognizers to detect single taps,
In order for a gesture to [activate](/docs/fundamentals/states-events#active), specified gesture requirements such as minPointers, numberOfTaps, maxDist, maxDuration, and maxDelayMs (explained below) must be met. Immediately after the gesture [activates](/docs/fundamentals/states-events#active), it will [end](/docs/fundamentals/states-events#end).
+
+
+
+
+
+
+
Tap Gesture
+
## Reference
```jsx
diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js
index 405e40e2d4..f4b50e0c09 100644
--- a/docs/docusaurus.config.js
+++ b/docs/docusaurus.config.js
@@ -21,6 +21,14 @@ const config = {
customFields: {
shortTitle: 'Gesture Handler',
},
+
+ scripts: [
+ {
+ src: '/react-native-gesture-handler/js/snack-helpers.js',
+ async: true,
+ },
+ ],
+
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
diff --git a/docs/src/components/InteractiveExample/index.tsx b/docs/src/components/InteractiveExample/index.tsx
index 8a89da64ed..19468ce3f9 100644
--- a/docs/src/components/InteractiveExample/index.tsx
+++ b/docs/src/components/InteractiveExample/index.tsx
@@ -21,6 +21,7 @@ interface Props {
label?: string;
showCode?: boolean; // whether to show code by default
larger?: boolean; // should the view be enlarged?
+ disableMarginBottom?: boolean;
}
export default function InteractiveExample({
@@ -29,6 +30,7 @@ export default function InteractiveExample({
label,
showCode = false,
larger = false,
+ disableMarginBottom
}: Props) {
const [_, copy] = useCopyToClipboard();
const [key, setKey] = React.useState(0);
@@ -47,7 +49,8 @@ export default function InteractiveExample({
className={clsx(
styles.container,
larger && styles.largerContainer,
- !showPreview ? styles.code : ''
+ !showPreview ? styles.code : '',
+ !disableMarginBottom ? styles.marginBottom : ''
)}
data-ispreview={showPreview}>
{showPreview && prefersReducedMotion &&
}
@@ -88,37 +91,35 @@ export default function InteractiveExample({
/>
)}
-
- {showPreview ? (
- <>
-
{component}
+ {showPreview ? (
+ <>
+
{component}
-
-
- {label &&
{label}
}
-
}
- iconDark={
}
- animation={Animation.FADE_IN_OUT}
- onClick={(actionPerformed, setActionPerformed) => {
- if (!actionPerformed) {
- resetExample();
- setActionPerformed(true);
- }
- }}
- />
-
- >
- ) : (
-
-
{src}
+
+
+ {label &&
{label}
}
+
}
+ iconDark={
}
+ animation={Animation.FADE_IN_OUT}
+ onClick={(actionPerformed, setActionPerformed) => {
+ if (!actionPerformed) {
+ resetExample();
+ setActionPerformed(true);
+ }
+ }}
+ />
- )}
-
+ >
+ ) : (
+
+ {src}
+
+ )}
)}
diff --git a/docs/src/components/InteractiveExample/styles.module.css b/docs/src/components/InteractiveExample/styles.module.css
index db85c38b84..245e1171ee 100644
--- a/docs/src/components/InteractiveExample/styles.module.css
+++ b/docs/src/components/InteractiveExample/styles.module.css
@@ -6,6 +6,12 @@
background-color: var(--swm-off-background);
border: 1px solid var(--swm-border);
+
+ width: 100%;
+ align-self: stretch;
+}
+
+.marginBottom {
margin-bottom: var(--ifm-leading);
}
@@ -15,7 +21,7 @@
/* Preferred height in code section of container. */
.container[data-ispreview='false'] {
- height: 400px;
+ height: 360px;
}
/* Classes used to omit default docusaurus styling. */
@@ -23,8 +29,13 @@
box-shadow: none;
}
+.interactiveCodeBlock {
+ overflow-y: auto;
+}
+
.interactiveCodeBlock [class*='codeBlockContent'] pre {
border: none;
+ padding: 16px 20px;
}
.interactiveCodeBlock [class*='codeBlockContent'] code {
@@ -32,6 +43,7 @@
width: 100%;
padding: 0;
border: none;
+ text-wrap: wrap;
}
/* Hide default action buttons, displayed by Docusaurus */
@@ -93,9 +105,9 @@
}
}
-.previewContainer {
+/* .previewContainer {
flex: 1 1 auto;
-}
+} */
/* Style preview only when user is in the 'code' section. */
.container[data-ispreview='false'] .previewContainer {
diff --git a/docs/src/utils/getGestureStyles.tsx b/docs/src/utils/getGestureStyles.tsx
new file mode 100644
index 0000000000..567d35a7f6
--- /dev/null
+++ b/docs/src/utils/getGestureStyles.tsx
@@ -0,0 +1,7 @@
+import styles from './style.module.css';
+
+export const [appearOnMobile, vanishOnMobile, webContainer] = [
+ styles.appearOnMobile,
+ styles.vanishOnMobile,
+ styles.container,
+];
diff --git a/docs/src/utils/style.module.css b/docs/src/utils/style.module.css
new file mode 100644
index 0000000000..6dbd29d9e0
--- /dev/null
+++ b/docs/src/utils/style.module.css
@@ -0,0 +1,26 @@
+.vanishOnMobile {
+ display: flex !important;
+}
+
+.appearOnMobile {
+ display: none !important;
+}
+
+@media (max-width: 1440px) {
+ .vanishOnMobile {
+ display: none !important;
+ }
+
+ .appearOnMobile {
+ display: flex !important;
+ }
+}
+
+.container {
+ display: flex;
+ gap: 1rem;
+ justify-content: stretch;
+ width: 100%;
+ align-items: stretch;
+ margin-bottom: 1rem;
+}
diff --git a/docs/static/examples/FlingGestureBasic.js b/docs/static/examples/FlingGestureBasic.js
new file mode 100644
index 0000000000..333b681aae
--- /dev/null
+++ b/docs/static/examples/FlingGestureBasic.js
@@ -0,0 +1,97 @@
+import React from 'react';
+import {
+ Directions,
+ Gesture,
+ GestureDetector,
+ GestureHandlerRootView,
+} from 'react-native-gesture-handler';
+import { StyleSheet, View } from 'react-native';
+import Animated, {
+ withTiming,
+ useSharedValue,
+ useAnimatedStyle,
+} from 'react-native-reanimated';
+
+function clamp(val, min, max) {
+ return Math.min(Math.max(val, min), max);
+}
+
+export default function App() {
+ const translateX = useSharedValue(0);
+ const startTranslateX = useSharedValue(0);
+ const containerWidth = useSharedValue(0);
+
+ const containerRef = React.useRef(null);
+
+ const updateContainerWidth = () => {
+ if (!containerRef.current) return;
+
+ containerRef.current.measure((x, y, width, height) => {
+ containerWidth.value = width;
+
+ translateX.value = clamp(
+ translateX.value,
+ containerWidth.value / -2 + 50,
+ containerWidth.value / 2 - 50
+ );
+ });
+ };
+
+ React.useEffect(() => {
+ updateContainerWidth();
+ }, [containerRef.current]);
+
+ React.useEffect(() => {
+ window.addEventListener('resize', updateContainerWidth);
+
+ return () => {
+ window.removeEventListener('resize', updateContainerWidth);
+ };
+ }, []);
+
+ const fling = Gesture.Fling()
+ .direction(Directions.LEFT | Directions.RIGHT)
+ .onBegin((event) => {
+ startTranslateX.value = event.x;
+ })
+ .onStart((event) => {
+ translateX.value = withTiming(
+ clamp(
+ translateX.value + event.x - startTranslateX.value,
+ containerWidth.value / -2 + 50,
+ containerWidth.value / 2 - 50
+ ),
+ { duration: 200 }
+ );
+ });
+
+ const boxAnimatedStyles = useAnimatedStyle(() => ({
+ transform: [{ translateX: translateX.value }],
+ }));
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ box: {
+ width: 100,
+ height: 100,
+ borderRadius: 20,
+ backgroundColor: '#b58df1',
+ cursor: 'grab',
+ },
+});
diff --git a/docs/static/examples/FlingGestureBasicSrc.js b/docs/static/examples/FlingGestureBasicSrc.js
new file mode 100644
index 0000000000..19bda17850
--- /dev/null
+++ b/docs/static/examples/FlingGestureBasicSrc.js
@@ -0,0 +1,68 @@
+import React from 'react';
+import {
+ Directions,
+ Gesture,
+ GestureDetector,
+ GestureHandlerRootView,
+} from 'react-native-gesture-handler';
+import { Dimensions, StyleSheet } from 'react-native';
+import Animated, {
+ withTiming,
+ useSharedValue,
+ useAnimatedStyle,
+} from 'react-native-reanimated';
+
+const { width } = Dimensions.get('screen');
+
+function clamp(val, min, max) {
+ return Math.min(Math.max(val, min), max);
+}
+
+export default function App() {
+ const translateX = useSharedValue(0);
+ const startTranslateX = useSharedValue(0);
+
+ const fling = Gesture.Fling()
+ .direction(Directions.LEFT | Directions.RIGHT)
+ .onBegin((event) => {
+ startTranslateX.value = event.x;
+ })
+ .onStart((event) => {
+ translateX.value = withTiming(
+ clamp(
+ translateX.value + event.x - startTranslateX.value,
+ width / -2 + 50,
+ width / 2 - 50
+ ),
+ { duration: 200 }
+ );
+ })
+ .runOnJS(true);
+
+ const boxAnimatedStyles = useAnimatedStyle(() => ({
+ transform: [{ translateX: translateX.value }],
+ }));
+
+ return (
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ box: {
+ width: 100,
+ height: 100,
+ borderRadius: 20,
+ backgroundColor: '#b58df1',
+ cursor: 'grab',
+ },
+});
diff --git a/docs/static/examples/HoverGestureBasic.js b/docs/static/examples/HoverGestureBasic.js
new file mode 100644
index 0000000000..3f7b110161
--- /dev/null
+++ b/docs/static/examples/HoverGestureBasic.js
@@ -0,0 +1,88 @@
+import React from 'react';
+import {
+ Gesture,
+ GestureDetector,
+ GestureHandlerRootView,
+} from 'react-native-gesture-handler';
+import { StyleSheet } from 'react-native';
+import Animated, {
+ Easing,
+ interpolateColor,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from 'react-native-reanimated';
+
+const EASING = Easing.bezier(1, -1, 0.3, 1.43);
+
+export default function App() {
+ const translateX = useSharedValue(0);
+ const translateY = useSharedValue(0);
+
+ const progress = useSharedValue(0);
+
+ const startX = useSharedValue(0);
+ const startY = useSharedValue(0);
+
+ const hover = Gesture.Hover()
+ .onStart((event) => {
+ startX.value = event.x;
+ startY.value = event.y;
+ })
+ .onUpdate((event) => {
+ translateX.value = (event.x - startX.value) * 0.3;
+ translateY.value = (event.y - startY.value) * 0.3;
+
+ const distance = Math.sqrt(Math.pow(translateX.value, 2) + Math.pow(translateY.value, 2));
+
+ progress.value = distance / 35;
+ })
+ .onEnd(() => {
+ translateX.value = withTiming(0, {
+ duration: 400,
+ easing: EASING,
+ });
+ translateY.value = withTiming(0, {
+ duration: 400,
+ easing: EASING,
+ });
+ progress.value = withTiming(0, {
+ duration: 400,
+ easing: EASING,
+ });
+ });
+
+ const boxAnimatedStyle = useAnimatedStyle(() => ({
+ transform: [
+ { translateX: translateX.value },
+ { translateY: translateY.value },
+ ],
+ backgroundColor: interpolateColor(
+ progress.value,
+ [0, 1],
+ ['#b58df1', '#fa7f7c']
+ )
+ }));
+
+ return (
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ box: {
+ width: 100,
+ height: 100,
+ borderRadius: 20,
+ cursor: 'pointer',
+ },
+});
diff --git a/docs/static/examples/LongPressGestureBasic.js b/docs/static/examples/LongPressGestureBasic.js
new file mode 100644
index 0000000000..6df3275205
--- /dev/null
+++ b/docs/static/examples/LongPressGestureBasic.js
@@ -0,0 +1,76 @@
+import React from 'react';
+import {
+ Gesture,
+ GestureDetector,
+ GestureHandlerRootView,
+} from 'react-native-gesture-handler';
+import { Easing, StyleSheet } from 'react-native';
+import Animated, {
+ interpolateColor,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from 'react-native-reanimated';
+
+const COLORS = ['#b58df1', '#fa7f7c', '#ffe780', '#82cab2'];
+
+export default function App() {
+ const colorIndex = useSharedValue(0);
+ const scale = useSharedValue(1);
+
+ const longPress = Gesture.LongPress()
+ .onBegin(() => {
+ scale.value = withTiming(1.2, {
+ duration: 500,
+ easing: Easing.bezier(0.31, 0.04, 0.03, 1.04),
+ });
+ })
+ .onStart(() => {
+ colorIndex.value = withTiming(
+ (colorIndex.value + 1) % (COLORS.length + 1),
+ { duration: 200 },
+ () => {
+ if (colorIndex.value === COLORS.length) {
+ colorIndex.value = 0;
+ }
+ }
+ );
+ })
+ .onFinalize(() => {
+ scale.value = withTiming(1, {
+ duration: 250,
+ easing: Easing.bezier(0.82, 0.06, 0.42, 1.01),
+ });
+ });
+
+ const animatedStyle = useAnimatedStyle(() => ({
+ backgroundColor: interpolateColor(
+ colorIndex.value,
+ [...COLORS.map((_, i) => i), COLORS.length],
+ [...COLORS, COLORS[0]]
+ ),
+ transform: [{ scale: scale.value }],
+ }));
+
+ return (
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ box: {
+ width: 100,
+ height: 100,
+ borderRadius: 20,
+ cursor: 'pointer',
+ },
+});
diff --git a/docs/static/examples/PanGestureBasic.js b/docs/static/examples/PanGestureBasic.js
new file mode 100644
index 0000000000..2d2c7b74eb
--- /dev/null
+++ b/docs/static/examples/PanGestureBasic.js
@@ -0,0 +1,106 @@
+import React from 'react';
+import Animated, {
+ useSharedValue,
+ useAnimatedStyle,
+} from 'react-native-reanimated';
+import {
+ Gesture,
+ GestureDetector,
+ GestureHandlerRootView,
+} from 'react-native-gesture-handler';
+import { StyleSheet, View } from 'react-native';
+
+function clamp(val, min, max) {
+ return Math.min(Math.max(val, min), max);
+}
+
+export default function App() {
+ const translationX = useSharedValue(0);
+ const translationY = useSharedValue(0);
+ const prevTranslationX = useSharedValue(0);
+ const prevTranslationY = useSharedValue(0);
+ const grabbing = useSharedValue(false);
+ const maxTranslateX = useSharedValue(0);
+ const maxTranslateY = useSharedValue(0);
+
+ const containerRef = React.useRef(null);
+
+ const animatedStyles = useAnimatedStyle(() => ({
+ transform: [
+ { translateX: translationX.value },
+ { translateY: translationY.value },
+ ],
+ cursor: grabbing.value ? 'grabbing' : 'grab',
+ }));
+
+ const updateWidthAndHeight = () => {
+ if (!containerRef.current) return;
+
+ containerRef.current.measureInWindow((x, y, width, height) => {
+ maxTranslateX.value = width / 2 - 50;
+ maxTranslateY.value = height / 2 - 50;
+ });
+ };
+
+ React.useEffect(() => {
+ updateWidthAndHeight();
+ }, [containerRef.current]);
+
+ React.useEffect(() => {
+ window.addEventListener('resize', updateWidthAndHeight);
+ window.addEventListener('scroll', updateWidthAndHeight);
+
+ return () => {
+ window.removeEventListener('resize', updateWidthAndHeight);
+ window.removeEventListener('scroll', updateWidthAndHeight);
+ };
+ }, []);
+
+ const pan = Gesture.Pan()
+ .minDistance(1)
+ .onBegin(() => {
+ grabbing.value = true;
+ prevTranslationX.value = translationX.value;
+ prevTranslationY.value = translationY.value;
+ })
+ .onUpdate((event) => {
+ translationX.value = clamp(
+ prevTranslationX.value + event.translationX,
+ -maxTranslateX.value,
+ maxTranslateX.value
+ );
+ translationY.value = clamp(
+ prevTranslationY.value + event.translationY,
+ -maxTranslateY.value,
+ maxTranslateY.value
+ );
+ })
+ .onFinalize(() => {
+ grabbing.value = false;
+ });
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ aspectRatio: 3,
+ },
+ box: {
+ width: 100,
+ height: 100,
+ backgroundColor: '#b58df1',
+ borderRadius: 20,
+ },
+});
diff --git a/docs/static/examples/PanGestureBasicSrc.js b/docs/static/examples/PanGestureBasicSrc.js
new file mode 100644
index 0000000000..9331b2eabe
--- /dev/null
+++ b/docs/static/examples/PanGestureBasicSrc.js
@@ -0,0 +1,76 @@
+import React from 'react';
+import Animated, {
+ useSharedValue,
+ useAnimatedStyle,
+} from 'react-native-reanimated';
+import {
+ Gesture,
+ GestureDetector,
+ GestureHandlerRootView,
+} from 'react-native-gesture-handler';
+import { StyleSheet, Dimensions } from 'react-native';
+
+function clamp(val, min, max) {
+ return Math.min(Math.max(val, min), max);
+}
+
+const { width, height } = Dimensions.get('screen');
+
+export default function App() {
+ const translationX = useSharedValue(0);
+ const translationY = useSharedValue(0);
+ const prevTranslationX = useSharedValue(0);
+ const prevTranslationY = useSharedValue(0);
+
+ const animatedStyles = useAnimatedStyle(() => ({
+ transform: [
+ { translateX: translationX.value },
+ { translateY: translationY.value },
+ ],
+ }));
+
+ const pan = Gesture.Pan()
+ .minDistance(1)
+ .onStart(() => {
+ prevTranslationX.value = translationX.value;
+ prevTranslationY.value = translationY.value;
+ })
+ .onUpdate((event) => {
+ const maxTranslateX = width / 2 - 50;
+ const maxTranslateY = height / 2 - 50;
+
+ translationX.value = clamp(
+ prevTranslationX.value + event.translationX,
+ -maxTranslateX,
+ maxTranslateX
+ );
+ translationY.value = clamp(
+ prevTranslationY.value + event.translationY,
+ -maxTranslateY,
+ maxTranslateY
+ );
+ })
+ .runOnJS(true);
+
+ return (
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ box: {
+ width: 100,
+ height: 100,
+ backgroundColor: '#b58df1',
+ borderRadius: 20,
+ },
+});
diff --git a/docs/static/examples/PinchGestureBasic.js b/docs/static/examples/PinchGestureBasic.js
new file mode 100644
index 0000000000..8b87072dff
--- /dev/null
+++ b/docs/static/examples/PinchGestureBasic.js
@@ -0,0 +1,160 @@
+import React from 'react';
+import {
+ Gesture,
+ GestureDetector,
+ GestureHandlerRootView,
+} from 'react-native-gesture-handler';
+import { StyleSheet, View } from 'react-native';
+import Animated, {
+ useSharedValue,
+ useAnimatedStyle,
+ withTiming,
+} from 'react-native-reanimated';
+
+const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
+
+export default function App() {
+ const boxWidth = useSharedValue(100);
+ const distanceDifference = useSharedValue(0);
+
+ const centerX = useSharedValue(0);
+ const centerY = useSharedValue(0);
+ const width = useSharedValue(0);
+ const height = useSharedValue(0);
+
+ const pointerPositionX = useSharedValue(0);
+ const pointerPositionY = useSharedValue(0);
+ const negativePointerPositionX = useSharedValue(0);
+ const negativePointerPositionY = useSharedValue(0);
+
+ const touchOpacity = useSharedValue(0);
+
+ const containerRef = React.useRef(null);
+ const boxRef = React.useRef(null);
+
+ function updateCenterAndDimensions() {
+ if (boxRef.current) {
+ boxRef.current.measureInWindow((x, y, width, height) => {
+ centerX.value = x + width / 2;
+ centerY.value = y + height / 2;
+ });
+ }
+
+ if (containerRef.current) {
+ containerRef.current.measureInWindow((x, y, w, h) => {
+ width.value = w;
+ height.value = h;
+
+ boxWidth.value = clamp(
+ boxWidth.value,
+ 100,
+ Math.min(w, h)
+ );
+ });
+ }
+ }
+
+ React.useEffect(() => {
+ updateCenterAndDimensions();
+ }, [boxRef.current, containerRef.current]);
+
+ React.useEffect(() => {
+ window.addEventListener('resize', updateCenterAndDimensions);
+ window.addEventListener('scroll', updateCenterAndDimensions);
+
+ return () => {
+ window.removeEventListener('resize', updateCenterAndDimensions);
+ window.removeEventListener('scroll', updateCenterAndDimensions);
+ };
+ }, []);
+
+ const pan = Gesture.Pan()
+ .minDistance(1)
+ .onStart((event) => {
+ const distanceX = Math.abs(event.absoluteX - centerX.value);
+ const distanceY = Math.abs(event.absoluteY - centerY.value);
+ const width = Math.max(distanceX, distanceY) * 2;
+ distanceDifference.value = boxWidth.value - width;
+
+ touchOpacity.value = withTiming(0.4, { duration: 200 });
+ })
+ .onUpdate((event) => {
+ const distanceX = Math.abs(event.absoluteX - centerX.value);
+ const distanceY = Math.abs(event.absoluteY - centerY.value);
+ boxWidth.value = clamp(
+ Math.max(distanceX, distanceY) * 2 + distanceDifference.value,
+ 100,
+ Math.min(width.value, height.value)
+ );
+
+ pointerPositionX.value = event.absoluteX - centerX.value - 12;
+ pointerPositionY.value = event.absoluteY - centerY.value - 12;
+ negativePointerPositionX.value = centerX.value - event.absoluteX - 12;
+ negativePointerPositionY.value = centerY.value - event.absoluteY - 12;
+ })
+ .onEnd(() => {
+ touchOpacity.value = withTiming(0, { duration: 200 });
+ });
+
+ const boxAnimatedStyles = useAnimatedStyle(() => ({
+ width: boxWidth.value,
+ }));
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ aspectRatio: 3,
+ },
+ box: {
+ aspectRatio: 1,
+ borderRadius: 20,
+ backgroundColor: '#b58df1',
+ cursor: 'pointer',
+ },
+ dot: {
+ width: 24,
+ height: 24,
+ borderRadius: 12,
+ backgroundColor: '#ccc',
+ position: 'absolute',
+ left: '50%',
+ top: '50%',
+ },
+});
diff --git a/docs/static/examples/PinchGestureBasicSrc.js b/docs/static/examples/PinchGestureBasicSrc.js
new file mode 100644
index 0000000000..34a94a8a2a
--- /dev/null
+++ b/docs/static/examples/PinchGestureBasicSrc.js
@@ -0,0 +1,71 @@
+import React from 'react';
+import { Dimensions, StyleSheet } from 'react-native';
+import {
+ Gesture,
+ GestureDetector,
+ GestureHandlerRootView,
+} from 'react-native-gesture-handler';
+import Animated, {
+ useSharedValue,
+ useAnimatedStyle,
+} from 'react-native-reanimated';
+
+const { width, height } = Dimensions.get('screen');
+
+function clamp(val, min, max) {
+ return Math.min(Math.max(val, min), max);
+}
+
+export default function App() {
+ const scale = useSharedValue(1);
+ const startScale = useSharedValue(0);
+
+ const pinch = Gesture.Pinch()
+ .onStart(() => {
+ startScale.value = scale.value;
+ })
+ .onUpdate((event) => {
+ scale.value = clamp(
+ startScale.value * event.scale,
+ 0.5,
+ Math.min(width / 100, height / 100)
+ );
+ })
+ .runOnJS(true);
+
+ const boxAnimatedStyles = useAnimatedStyle(() => ({
+ transform: [{ scale: scale.value }],
+ }));
+
+ return (
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ box: {
+ width: 100,
+ height: 100,
+ borderRadius: 20,
+ backgroundColor: '#b58df1',
+ },
+ dot: {
+ width: 24,
+ height: 24,
+ borderRadius: 12,
+ backgroundColor: '#ccc',
+ position: 'absolute',
+ left: '50%',
+ top: '50%',
+ pointerEvents: 'none',
+ },
+});
diff --git a/docs/static/examples/RotationGestureBasic.js b/docs/static/examples/RotationGestureBasic.js
new file mode 100644
index 0000000000..8f0bc9370b
--- /dev/null
+++ b/docs/static/examples/RotationGestureBasic.js
@@ -0,0 +1,145 @@
+import React from 'react';
+import {
+ Gesture,
+ GestureDetector,
+ GestureHandlerRootView,
+} from 'react-native-gesture-handler';
+import { StyleSheet } from 'react-native';
+import Animated, {
+ useSharedValue,
+ useAnimatedStyle,
+ withTiming,
+} from 'react-native-reanimated';
+
+export default function App() {
+ const angle = useSharedValue(0);
+ const startAngle = useSharedValue(0);
+ const centerX = useSharedValue(0);
+ const centerY = useSharedValue(0);
+ const pointerPositionX = useSharedValue(0);
+ const pointerPositionY = useSharedValue(0);
+ const negativePointerPositionX = useSharedValue(0);
+ const negativePointerPositionY = useSharedValue(0);
+ const touchOpacity = useSharedValue(0);
+ const grabbing = useSharedValue(false);
+
+ const boxRef = React.useRef(null);
+
+ function updateCenter() {
+ if (!boxRef.current) return;
+
+ boxRef.current.measureInWindow((x, y, width, height) => {
+ centerX.value = x + width / 2;
+ centerY.value = y + height / 2;
+ });
+ }
+
+ React.useEffect(() => {
+ updateCenter();
+ }, [boxRef.current]);
+
+ React.useEffect(() => {
+ window.addEventListener('resize', updateCenter);
+ window.addEventListener('scroll', updateCenter);
+
+ return () => {
+ window.removeEventListener('resize', updateCenter);
+ window.removeEventListener('scroll', updateCenter);
+ };
+ }, []);
+
+ const pan = Gesture.Pan()
+ .minDistance(1)
+ .onBegin((event) => {
+ startAngle.value =
+ angle.value -
+ Math.atan2(
+ event.absoluteY - centerY.value,
+ event.absoluteX - centerX.value
+ );
+ touchOpacity.value = withTiming(0.4, { duration: 200 });
+ grabbing.value = true;
+
+ pointerPositionX.value = event.absoluteX - centerX.value - 12;
+ pointerPositionY.value = event.absoluteY - centerY.value - 12;
+ negativePointerPositionX.value = centerX.value - event.absoluteX - 12;
+ negativePointerPositionY.value = centerY.value - event.absoluteY - 12;
+ })
+ .onUpdate((event) => {
+ angle.value =
+ startAngle.value +
+ Math.atan2(
+ event.absoluteY - centerY.value,
+ event.absoluteX - centerX.value
+ );
+ pointerPositionX.value = event.absoluteX - centerX.value - 12;
+ pointerPositionY.value = event.absoluteY - centerY.value - 12;
+ negativePointerPositionX.value = centerX.value - event.absoluteX - 12;
+ negativePointerPositionY.value = centerY.value - event.absoluteY - 12;
+ })
+ .onFinalize(() => {
+ touchOpacity.value = withTiming(0, { duration: 200 });
+ grabbing.value = false;
+ });
+
+ const boxAnimatedStyles = useAnimatedStyle(() => ({
+ transform: [{ rotate: `${angle.value}rad` }],
+ cursor: grabbing.value ? 'grabbing' : 'grab',
+ }));
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ box: {
+ width: 100,
+ height: 100,
+ borderRadius: 20,
+ backgroundColor: '#b58df1',
+ },
+ dot: {
+ width: 24,
+ height: 24,
+ borderRadius: 12,
+ backgroundColor: '#ccc',
+ position: 'absolute',
+ left: '50%',
+ top: '50%',
+ pointerEvents: 'none',
+ },
+});
diff --git a/docs/static/examples/RotationGestureBasicSrc.js b/docs/static/examples/RotationGestureBasicSrc.js
new file mode 100644
index 0000000000..85dbdd9c78
--- /dev/null
+++ b/docs/static/examples/RotationGestureBasicSrc.js
@@ -0,0 +1,59 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import {
+ Gesture,
+ GestureDetector,
+ GestureHandlerRootView,
+} from 'react-native-gesture-handler';
+import Animated, {
+ useSharedValue,
+ useAnimatedStyle,
+} from 'react-native-reanimated';
+
+export default function App() {
+ const angle = useSharedValue(0);
+ const startAngle = useSharedValue(0);
+
+ const rotation = Gesture.Rotation()
+ .onStart(() => {
+ startAngle.value = angle.value;
+ })
+ .onUpdate((event) => {
+ angle.value = startAngle.value + event.rotation;
+ });
+
+ const boxAnimatedStyles = useAnimatedStyle(() => ({
+ transform: [{ rotate: `${angle.value}rad` }],
+ }));
+
+ return (
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ box: {
+ width: 100,
+ height: 100,
+ borderRadius: 20,
+ backgroundColor: '#b58df1',
+ },
+ dot: {
+ width: 24,
+ height: 24,
+ borderRadius: 12,
+ backgroundColor: '#ccc',
+ position: 'absolute',
+ left: '50%',
+ top: '50%',
+ },
+});
diff --git a/docs/static/examples/TapGestureBasic.js b/docs/static/examples/TapGestureBasic.js
new file mode 100644
index 0000000000..96f6657af5
--- /dev/null
+++ b/docs/static/examples/TapGestureBasic.js
@@ -0,0 +1,58 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import {
+ Gesture,
+ GestureDetector,
+ GestureHandlerRootView,
+} from 'react-native-gesture-handler';
+import Animated, {
+ interpolateColor,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from 'react-native-reanimated';
+
+const COLORS = ['#b58df1', '#fa7f7c', '#ffe780', '#82cab2'];
+
+export default function App() {
+ const colorIndex = useSharedValue(1);
+
+ const tap = Gesture.Tap().onEnd(() => {
+ if (colorIndex.value > COLORS.length) {
+ colorIndex.value = colorIndex.value % 1 === 0 ? 1 : colorIndex.value % 1;
+ }
+
+ const nextIndex = Math.ceil(colorIndex.value + 1);
+ colorIndex.value = withTiming(nextIndex, { duration: 250 });
+ });
+
+ const animatedStyle = useAnimatedStyle(() => ({
+ backgroundColor: interpolateColor(
+ colorIndex.value,
+ [0, ...COLORS.map((_, i) => i + 1), COLORS.length + 1],
+ [COLORS[COLORS.length - 1], ...COLORS, COLORS[0]]
+ ),
+ }));
+
+ return (
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ box: {
+ width: 100,
+ height: 100,
+ borderRadius: 20,
+ cursor: 'pointer',
+ },
+});
diff --git a/docs/static/js/snack-helpers.js b/docs/static/js/snack-helpers.js
new file mode 100644
index 0000000000..0e3bd71ca9
--- /dev/null
+++ b/docs/static/js/snack-helpers.js
@@ -0,0 +1,86 @@
+const DEFAULT_PLATFORM = 'android';
+const DEPENDENCIES = [
+ 'react-native-reanimated@*',
+ 'react-native-gesture-handler@*',
+ '@shopify/flash-list@*',
+];
+
+function getSnackUrl(options) {
+ let label = options.label || document.title;
+ let codeId = options.templateId;
+
+ let baseUrl =
+ `https://snack.expo.io?platform=${DEFAULT_PLATFORM}&name=` +
+ encodeURIComponent(label) +
+ '&dependencies=' +
+ encodeURIComponent(DEPENDENCIES.join(',')) +
+ '&hideQueryParams=true';
+
+ let templateUrl = `${document.location.origin}/react-native-gesture-handler/examples/${codeId}.js`;
+ return `${baseUrl}&sourceUrl=${encodeURIComponent(templateUrl)}`;
+}
+
+let openIcon =
+ '
';
+
+function appendSnackLink() {
+ let samples = document.querySelectorAll('samp');
+
+ if (!samples.length) {
+ return;
+ }
+
+ samples.forEach((samp) => {
+ var id = samp.getAttribute('id');
+
+ var snackLink = document.createElement('a');
+ snackLink.href = getSnackUrl({ templateId: id });
+ snackLink.target = '_blank';
+ snackLink.innerHTML = `Try this example on Snack ${openIcon}`;
+ snackLink.className = 'snack-link';
+
+ var nextDiv = samp.nextElementSibling;
+ while (
+ nextDiv &&
+ !Array.from(nextDiv.classList).some((className) =>
+ className.includes('codeBlockContainer')
+ )
+ ) {
+ nextDiv = nextDiv.nextElementSibling;
+ }
+
+ if (nextDiv) {
+ nextDiv.insertAdjacentElement('afterend', snackLink);
+ } else {
+ samp.parentNode.insertBefore(snackLink, samp.nextSibling);
+ }
+
+ samp.remove();
+ });
+}
+
+function transformExistingSnackLinks() {
+ document.querySelectorAll('a[href*="#example/"]').forEach((a) => {
+ let urlParts = a.href.split('#example/');
+ let templateId = urlParts[urlParts.length - 1];
+ a.href = getSnackUrl({ templateId });
+ a.target = '_blank';
+ });
+}
+
+function initializeSnackObservers() {
+ appendSnackLink();
+ transformExistingSnackLinks();
+
+ const mutationObserver = new MutationObserver((mutations) => {
+ mutations.forEach(appendSnackLink);
+ mutations.forEach(transformExistingSnackLinks);
+ });
+
+ mutationObserver.observe(document.documentElement, {
+ childList: true,
+ subtree: true,
+ });
+}
+
+document.addEventListener('DOMContentLoaded', initializeSnackObservers);