Skip to content

Commit ff5c44a

Browse files
zeyapfacebook-github-bot
authored andcommitted
Add pan gesture animation examples to rntester (facebook#50851)
Summary: Pull Request resolved: facebook#50851 ## Changelog: [General] [Added] - Add pan gesture animation example to rntester Including examples of * using native driven Animated.event + touch event (which will not be interrupted by busy js thread, and is potentially a boost to performance) - the code requires some hacks but it's doable * using js PanResponder to drive pan gesture animation Reviewed By: sammy-SC Differential Revision: D68909931
1 parent ec43150 commit ff5c44a

File tree

2 files changed

+255
-0
lines changed

2 files changed

+255
-0
lines changed

packages/rn-tester/js/examples/Animated/AnimatedIndex.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import EasingExample from './EasingExample';
1919
import FadeInViewExample from './FadeInViewExample';
2020
import LoopingExample from './LoopingExample';
2121
import MovingBoxExample from './MovingBoxExample';
22+
import PanGestureExample from './PanGestureExample';
2223
import PressabilityWithNativeDrivers from './PressabilityWithNativeDrivers';
2324
import RotatingImagesExample from './RotatingImagesExample';
2425
import TransformBounceExample from './TransformBounceExample';
@@ -47,5 +48,6 @@ export default ({
4748
ContinuousInteractionsExample,
4849
CombineExample,
4950
PressabilityWithNativeDrivers,
51+
PanGestureExample,
5052
],
5153
}: RNTesterModule);
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
* @oncall react_native
10+
*/
11+
12+
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
13+
14+
import ToggleNativeDriver from './utils/ToggleNativeDriver';
15+
import * as React from 'react';
16+
import {useRef, useState, useLayoutEffect} from 'react';
17+
import {
18+
Animated,
19+
Button,
20+
PanResponder,
21+
StyleSheet,
22+
Text,
23+
View,
24+
} from 'react-native';
25+
26+
function TextBox({children}: $ReadOnly<{children: React.Node}>): React.Node {
27+
// Prevent touch from being hijacked by Text
28+
return (
29+
<View pointerEvents="none">
30+
<Text style={styles.text}>{children}</Text>
31+
</View>
32+
);
33+
}
34+
35+
function AnimatedEventExample({
36+
containerPageXY,
37+
useNativeDriver,
38+
}: $ReadOnly<{
39+
containerPageXY: $ReadOnly<{x: number, y: number}>,
40+
useNativeDriver: boolean,
41+
}>): React.Node {
42+
const boxRef = useRef<?React.ElementRef<typeof Animated.View>>();
43+
44+
const pointerPageXY = useRef(
45+
new Animated.ValueXY(
46+
{
47+
x: containerPageXY.x,
48+
y: containerPageXY.y,
49+
},
50+
{useNativeDriver},
51+
),
52+
).current;
53+
54+
const dragStartOffsetXY = useRef(
55+
new Animated.ValueXY({x: 0, y: 0}, {useNativeDriver}),
56+
).current;
57+
58+
const finalOffsetX = Animated.subtract(
59+
Animated.subtract(pointerPageXY.x, dragStartOffsetXY.x),
60+
containerPageXY.x,
61+
);
62+
const finalOffsetY = Animated.subtract(
63+
Animated.subtract(pointerPageXY.y, dragStartOffsetXY.y),
64+
containerPageXY.y,
65+
);
66+
67+
const syncAnimationToHostView = () => {
68+
boxRef.current?.setNativeProps({
69+
transform: [
70+
{translateX: finalOffsetX.__getValue()},
71+
{translateY: finalOffsetY.__getValue()},
72+
],
73+
});
74+
};
75+
76+
return (
77+
<Animated.View
78+
ref={boxRef}
79+
onTouchMove={Animated.event(
80+
[
81+
{
82+
nativeEvent: {
83+
pageX: pointerPageXY.x,
84+
pageY: pointerPageXY.y,
85+
},
86+
},
87+
],
88+
{useNativeDriver},
89+
)}
90+
onTouchStart={Animated.event(
91+
[
92+
{
93+
nativeEvent: {
94+
pageX: pointerPageXY.x,
95+
pageY: pointerPageXY.y,
96+
locationX: dragStartOffsetXY.x,
97+
locationY: dragStartOffsetXY.y,
98+
},
99+
},
100+
],
101+
{useNativeDriver},
102+
)}
103+
onTouchEnd={() => {
104+
// Animated change sometimes doesn't commit to Fabric, and box will jump back to offset before animation
105+
// This is to make sure that finalOffsetX/Y are synced to native host view
106+
syncAnimationToHostView();
107+
}}
108+
style={[
109+
styles.box,
110+
{
111+
backgroundColor: useNativeDriver ? 'orange' : 'violet',
112+
transform: [
113+
{
114+
translateX: finalOffsetX,
115+
},
116+
{
117+
translateY: finalOffsetY,
118+
},
119+
],
120+
},
121+
]}>
122+
<TextBox>Use {useNativeDriver ? 'Native' : 'JS'} Animated.event</TextBox>
123+
</Animated.View>
124+
);
125+
}
126+
127+
function PanResponderExample({
128+
useNativeDriver,
129+
}: $ReadOnly<{useNativeDriver: boolean}>): React.Node {
130+
const finalOffsetXY = useRef(
131+
new Animated.ValueXY({x: 0, y: 0}, {useNativeDriver}),
132+
).current;
133+
const dragStartOffsetXY = useRef({x: 0, y: 0}).current;
134+
const panResponder = useRef(
135+
PanResponder.create({
136+
onMoveShouldSetPanResponder: (pressEvent, gestureState) => {
137+
dragStartOffsetXY.x = finalOffsetXY.x.__getValue();
138+
dragStartOffsetXY.y = finalOffsetXY.y.__getValue();
139+
return true;
140+
},
141+
onPanResponderMove: (pressEvent, gestureState) => {
142+
if (gestureState.dx !== 0) {
143+
finalOffsetXY.x.setValue(dragStartOffsetXY.x + gestureState.dx);
144+
}
145+
146+
if (gestureState.dy !== 0) {
147+
finalOffsetXY.y.setValue(dragStartOffsetXY.y + gestureState.dy);
148+
}
149+
},
150+
}),
151+
).current;
152+
153+
return (
154+
<Animated.View
155+
{...panResponder.panHandlers}
156+
style={[
157+
styles.box,
158+
{
159+
backgroundColor: useNativeDriver ? 'pink' : 'cyan',
160+
transform: [
161+
{
162+
translateX: finalOffsetXY.x,
163+
},
164+
{
165+
translateY: finalOffsetXY.y,
166+
},
167+
],
168+
},
169+
]}>
170+
<TextBox>
171+
Use PanResponder{' '}
172+
{`+ ${useNativeDriver ? 'Native Animated value' : 'JS Animated value'}`}
173+
</TextBox>
174+
</Animated.View>
175+
);
176+
}
177+
178+
function PanGestureExample(): React.Node {
179+
const [busy, setBusy] = useState(false);
180+
const [useNativeDriver, setUseNativeDriver] = useState(false);
181+
182+
const containerRef = useRef<?React.ElementRef<typeof View>>();
183+
const [containerPageXY, setContainerPageXY] =
184+
useState<?{x: number, y: number}>(null);
185+
186+
useLayoutEffect(() => {
187+
containerRef.current?.measure((x, y, width, height, pageX, pageY) => {
188+
setContainerPageXY({x: pageX, y: pageY});
189+
});
190+
}, []);
191+
192+
function sleep(t: number) {
193+
setBusy(true);
194+
setTimeout(() => {
195+
const start = Date.now();
196+
while (Date.now() - start < t) {
197+
// sleeping
198+
}
199+
setBusy(false);
200+
}, 1000);
201+
}
202+
203+
return (
204+
<View style={styles.container}>
205+
<ToggleNativeDriver
206+
value={useNativeDriver}
207+
onValueChange={setUseNativeDriver}
208+
/>
209+
<Button
210+
title={busy ? 'js thread blocked...' : 'Block js thread for 5s'}
211+
onPress={() => {
212+
sleep(5000);
213+
}}
214+
/>
215+
<View style={styles.examplesContainer} ref={containerRef}>
216+
{containerPageXY != null ? (
217+
<AnimatedEventExample
218+
useNativeDriver={useNativeDriver}
219+
containerPageXY={containerPageXY}
220+
/>
221+
) : null}
222+
<PanResponderExample useNativeDriver={useNativeDriver} />
223+
</View>
224+
</View>
225+
);
226+
}
227+
228+
const styles = StyleSheet.create({
229+
container: {
230+
alignItems: 'stretch',
231+
justifyContent: 'flex-start',
232+
},
233+
examplesContainer: {
234+
flex: 1,
235+
},
236+
box: {
237+
width: 160,
238+
height: 160,
239+
alignItems: 'center',
240+
justifyContent: 'center',
241+
padding: 12,
242+
},
243+
text: {
244+
pointerEvents: 'none',
245+
},
246+
});
247+
248+
export default ({
249+
title: 'Pan Gesture',
250+
name: 'panGesture',
251+
description: 'Animations driven by pan gesture.',
252+
render: () => <PanGestureExample />,
253+
}: RNTesterModuleExample);

0 commit comments

Comments
 (0)