diff --git a/packages/rn-tester/js/examples/Animated/AnimatedIndex.js b/packages/rn-tester/js/examples/Animated/AnimatedIndex.js index 215efae074c..3fd82fc9c39 100644 --- a/packages/rn-tester/js/examples/Animated/AnimatedIndex.js +++ b/packages/rn-tester/js/examples/Animated/AnimatedIndex.js @@ -19,6 +19,7 @@ import EasingExample from './EasingExample'; import FadeInViewExample from './FadeInViewExample'; import LoopingExample from './LoopingExample'; import MovingBoxExample from './MovingBoxExample'; +import PanGestureExample from './PanGestureExample'; import PressabilityWithNativeDrivers from './PressabilityWithNativeDrivers'; import RotatingImagesExample from './RotatingImagesExample'; import TransformBounceExample from './TransformBounceExample'; @@ -47,5 +48,6 @@ export default ({ ContinuousInteractionsExample, CombineExample, PressabilityWithNativeDrivers, + PanGestureExample, ], }: RNTesterModule); diff --git a/packages/rn-tester/js/examples/Animated/PanGestureExample.js b/packages/rn-tester/js/examples/Animated/PanGestureExample.js new file mode 100644 index 00000000000..7c6dee65cd5 --- /dev/null +++ b/packages/rn-tester/js/examples/Animated/PanGestureExample.js @@ -0,0 +1,256 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; + +import ToggleNativeDriver from './utils/ToggleNativeDriver'; +import * as React from 'react'; +import {useLayoutEffect, useRef, useState} from 'react'; +import { + Animated, + Button, + PanResponder, + StyleSheet, + Text, + View, +} from 'react-native'; + +function TextBox({children}: $ReadOnly<{children: React.Node}>): React.Node { + // Prevent touch from being hijacked by Text + return ( + + {children} + + ); +} + +function AnimatedEventExample({ + containerPageXY, + useNativeDriver, +}: $ReadOnly<{ + containerPageXY: $ReadOnly<{x: number, y: number}>, + useNativeDriver: boolean, +}>): React.Node { + const boxRef = useRef>(); + + const pointerPageXY = useRef( + new Animated.ValueXY( + { + x: containerPageXY.x, + y: containerPageXY.y, + }, + {useNativeDriver}, + ), + ).current; + + const dragStartOffsetXY = useRef( + new Animated.ValueXY({x: 0, y: 0}, {useNativeDriver}), + ).current; + + // We'll no longer have to subtract containerPageXY.x/y from offset, if we can animate left/top props natively + // TODO: T222117268 Adopt fabric driven c++ native animated in RNTester + const finalOffsetX = Animated.subtract( + Animated.subtract(pointerPageXY.x, dragStartOffsetXY.x), + containerPageXY.x, + ); + const finalOffsetY = Animated.subtract( + Animated.subtract(pointerPageXY.y, dragStartOffsetXY.y), + containerPageXY.y, + ); + + const syncAnimationToHostView = () => { + boxRef.current?.setNativeProps({ + transform: [ + {translateX: finalOffsetX.__getValue()}, + {translateY: finalOffsetY.__getValue()}, + ], + }); + }; + + return ( + { + // Animated change sometimes doesn't commit to Fabric, and box will jump back to offset before animation + // This is to make sure that finalOffsetX/Y are synced to native host view + // TODO: T222117268 Adopt fabric driven c++ native animated in RNTester + syncAnimationToHostView(); + }} + style={[ + styles.box, + { + backgroundColor: useNativeDriver ? 'orange' : 'violet', + transform: [ + { + translateX: finalOffsetX, + }, + { + translateY: finalOffsetY, + }, + ], + }, + ]}> + Use {useNativeDriver ? 'Native' : 'JS'} Animated.event + + ); +} + +function PanResponderExample({ + useNativeDriver, +}: $ReadOnly<{useNativeDriver: boolean}>): React.Node { + const finalOffsetXY = useRef( + new Animated.ValueXY({x: 0, y: 0}, {useNativeDriver}), + ).current; + const dragStartOffsetXY = useRef({x: 0, y: 0}).current; + const panResponder = useRef( + PanResponder.create({ + onMoveShouldSetPanResponder: (pressEvent, gestureState) => { + dragStartOffsetXY.x = finalOffsetXY.x.__getValue(); + dragStartOffsetXY.y = finalOffsetXY.y.__getValue(); + return true; + }, + onPanResponderMove: (pressEvent, gestureState) => { + if (gestureState.dx !== 0) { + finalOffsetXY.x.setValue(dragStartOffsetXY.x + gestureState.dx); + } + + if (gestureState.dy !== 0) { + finalOffsetXY.y.setValue(dragStartOffsetXY.y + gestureState.dy); + } + }, + }), + ).current; + + return ( + + + Use PanResponder{' '} + {`+ ${useNativeDriver ? 'Native Animated value' : 'JS Animated value'}`} + + + ); +} + +function PanGestureExample(): React.Node { + const [busy, setBusy] = useState(false); + const [useNativeDriver, setUseNativeDriver] = useState(false); + + const containerRef = useRef>(); + const [containerPageXY, setContainerPageXY] = + useState(null); + + useLayoutEffect(() => { + containerRef.current?.measure((x, y, width, height, pageX, pageY) => { + setContainerPageXY({x: pageX, y: pageY}); + }); + }, []); + + function sleep(t: number) { + setBusy(true); + setTimeout(() => { + const start = Date.now(); + while (Date.now() - start < t) { + // sleeping + } + setBusy(false); + }, 1000); + } + + return ( + + +