diff --git a/docs/docs/gestures/fling-gesture.md b/docs/docs/gestures/fling-gesture.md index 6ae8ba9f8f..86733b8732 100644 --- a/docs/docs/gestures/fling-gesture.md +++ b/docs/docs/gestures/fling-gesture.md @@ -5,12 +5,24 @@ sidebar_label: Fling gesture sidebar_position: 8 --- +import { vanishOnMobile, appearOnMobile, webContainer } from '@site/src/utils/getGestureStyles'; + import useBaseUrl from '@docusaurus/useBaseUrl'; -
- +import FlingGestureBasic from '@site/static/examples/FlingGestureBasic'; +import FlingGestureBasicSrc from '!!raw-loader!@site/static/examples/FlingGestureBasicSrc'; + +
+
+ +
+ } + src={FlingGestureBasicSrc} + disableMarginBottom={true} + />
import BaseEventData from './\_shared/base-gesture-event-data.md'; @@ -22,6 +34,14 @@ Gesture gets [ACTIVE](/docs/fundamentals/states-events#active) when movement is When gesture gets activated it will turn into [END](/docs/fundamentals/states-events#end) state when finger is released. The gesture will fail to recognize if the finger is lifted before being activated. +
+ +
+ +Fling Gesture + ## Reference ```jsx diff --git a/docs/docs/gestures/hover-gesture.md b/docs/docs/gestures/hover-gesture.md index b0db6b327e..3135cbf2b5 100644 --- a/docs/docs/gestures/hover-gesture.md +++ b/docs/docs/gestures/hover-gesture.md @@ -5,12 +5,25 @@ sidebar_label: Hover gesture sidebar_position: 9 --- +import { vanishOnMobile, appearOnMobile, webContainer } from '@site/src/utils/getGestureStyles'; + import useBaseUrl from '@docusaurus/useBaseUrl'; -
- +import HoverGestureBasic from '@site/static/examples/HoverGestureBasic'; +import HoverGestureBasicSrc from '!!raw-loader!@site/static/examples/HoverGestureBasic'; + +
+ +
+ +
+ } + src={HoverGestureBasicSrc} + disableMarginBottom={true} + />
import BaseEventData from './\_shared/base-gesture-event-data.md'; @@ -22,6 +35,14 @@ A continuous gesture that can recognize hovering above the view it's attached to On iOS additional visual effects may be configured. +
+ +
+ +Hover Gesture + ## Reference ```jsx diff --git a/docs/docs/gestures/long-press-gesture.md b/docs/docs/gestures/long-press-gesture.md index 2f8d99b68e..eb3e4f5c0f 100644 --- a/docs/docs/gestures/long-press-gesture.md +++ b/docs/docs/gestures/long-press-gesture.md @@ -5,12 +5,24 @@ sidebar_label: Long press gesture sidebar_position: 5 --- +import { vanishOnMobile, appearOnMobile, webContainer } from '@site/src/utils/getGestureStyles'; + import useBaseUrl from '@docusaurus/useBaseUrl'; -
- +import LongPressGestureBasic from '@site/static/examples/LongPressGestureBasic'; +import LongPressGestureBasicSrc from '!!raw-loader!@site/static/examples/LongPressGestureBasic'; + +
+
+ +
+ } + src={LongPressGestureBasicSrc} + disableMarginBottom={true} + />
import BaseEventData from './\_shared/base-gesture-event-data.md'; @@ -21,6 +33,14 @@ A discrete gesture that activates when the corresponding view is pressed for a s This gesture's state will turn into [END](/docs/fundamentals/states-events#end) immediately after the finger is released. The gesture will fail to recognize a touch event if the finger is lifted before the [minimum required time](/docs/gestures/long-press-gesture#mindurationvalue-number) or if the finger is moved further than the [allowable distance](/docs/gestures/long-press-gesture#maxdistancevalue-number). +
+ +
+ +Long Press Gesture + ## Reference ```jsx diff --git a/docs/docs/gestures/pan-gesture.md b/docs/docs/gestures/pan-gesture.md index ac86d3067c..4123e1b591 100644 --- a/docs/docs/gestures/pan-gesture.md +++ b/docs/docs/gestures/pan-gesture.md @@ -5,12 +5,24 @@ sidebar_label: Pan gesture sidebar_position: 3 --- +import { vanishOnMobile, appearOnMobile, webContainer } from '@site/src/utils/getGestureStyles'; + import useBaseUrl from '@docusaurus/useBaseUrl'; -
- +import PanGestureBasic from '@site/static/examples/PanGestureBasic'; +import PanGestureBasicSrc from '!!raw-loader!@site/static/examples/PanGestureBasicSrc'; + +
+
+ +
+ } + src={PanGestureBasicSrc} + disableMarginBottom={true} + />
import BaseEventData from './\_shared/base-gesture-event-data.md'; @@ -27,6 +39,14 @@ Configurations such as a minimum initial distance, specific vertical or horizont Gesture callback can be used for continuous tracking of the pan gesture. It provides information about the gesture such as its XY translation from the starting point as well as its instantaneous velocity. +
+ +
+ +Pan Gesture + ## Reference ```jsx diff --git a/docs/docs/gestures/pinch-gesture.md b/docs/docs/gestures/pinch-gesture.md index 9530e4a522..bfeda8d81a 100644 --- a/docs/docs/gestures/pinch-gesture.md +++ b/docs/docs/gestures/pinch-gesture.md @@ -5,12 +5,24 @@ sidebar_label: Pinch gesture sidebar_position: 7 --- +import { vanishOnMobile, appearOnMobile, webContainer } from '@site/src/utils/getGestureStyles'; + import useBaseUrl from '@docusaurus/useBaseUrl'; -
- +import PinchGestureBasic from '@site/static/examples/PinchGestureBasic'; +import PinchGestureBasicSrc from '!!raw-loader!@site/static/examples/PinchGestureBasicSrc'; + +
+
+ +
+ } + src={PinchGestureBasicSrc} + disableMarginBottom={true} + />
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. +
+ +
+ +Pinch Gesture + ## 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);