Skip to content

Commit d424e28

Browse files
Bartlomiej Bloniarzmeta-codesync[bot]
authored andcommitted
Add rn-tester example exercising every batched-animated prop
Summary: Adds a new RNTester example under the Animation Backend section that runs a looping animation per each property handled by BatchedAnimatedPropsMountItem: - Top-level numeric props: opacity, elevation, zIndex, shadowOpacity, shadowRadius - Color props: backgroundColor, color (Text), tintColor (Image), and all borderColor / borderTopColor / borderBottomColor / borderLeftColor / borderRightColor / borderStartColor / borderEndColor variants - All 13 border-radius props (borderRadius and per-corner) in px units, plus borderRadius in percent units - All transforms: translateX/Y in px and percent, scale/scaleX/scaleY, rotate/rotateX/rotateY/rotateZ in deg and rad, skewX/skewY, perspective Each row exercises one distinct command id from the buffer protocol decoded by BatchedAnimatedPropsMountItem, making it easy to visually verify that every supported animated prop survives the C++ to Java buffer round-trip. Changelog: [General][Added] - Add All Animated Props example to the rn-tester Animation Backend section Differential Revision: D102800169
1 parent 445f18b commit d424e28

2 files changed

Lines changed: 374 additions & 1 deletion

File tree

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
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+
*/
10+
11+
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
12+
13+
import * as React from 'react';
14+
import {useMemo} from 'react';
15+
import {
16+
Animated,
17+
ScrollView,
18+
StyleSheet,
19+
Text,
20+
View,
21+
useAnimatedValue,
22+
} from 'react-native';
23+
import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags';
24+
25+
const optimizedAnimatedPropUpdatesEnabled =
26+
ReactNativeFeatureFlags.optimizedAnimatedPropUpdates();
27+
28+
// 1x1 white PNG embedded as a data URI so the `tintColor` row works without
29+
// network access.
30+
const TINT_SOURCE = {
31+
uri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=',
32+
};
33+
34+
function useLoop(): Animated.Value {
35+
const value = useAnimatedValue(0);
36+
37+
React.useEffect(() => {
38+
const animation = Animated.loop(
39+
Animated.sequence([
40+
Animated.timing(value, {
41+
toValue: 1,
42+
duration: 1500,
43+
useNativeDriver: true,
44+
}),
45+
Animated.timing(value, {
46+
toValue: 0,
47+
duration: 1500,
48+
useNativeDriver: true,
49+
}),
50+
]),
51+
);
52+
animation.start();
53+
return () => {
54+
animation.stop();
55+
};
56+
}, [value]);
57+
58+
return value;
59+
}
60+
61+
function Row({
62+
label,
63+
children,
64+
}: {
65+
label: string,
66+
children: React.Node,
67+
}): React.Node {
68+
return (
69+
<View style={styles.row}>
70+
<Text style={styles.label} numberOfLines={2}>
71+
{label}
72+
</Text>
73+
<View style={styles.demo}>{children}</View>
74+
</View>
75+
);
76+
}
77+
78+
function SectionHeader({title}: {title: string}): React.Node {
79+
return <Text style={styles.section}>{title}</Text>;
80+
}
81+
82+
function FlagIndicator(): React.Node {
83+
return (
84+
<View style={styles.flagRow}>
85+
<Text style={styles.flagLabel}>optimizedAnimatedPropUpdates:</Text>
86+
<Text
87+
style={[
88+
styles.flagValue,
89+
optimizedAnimatedPropUpdatesEnabled ? styles.flagOn : styles.flagOff,
90+
]}>
91+
{optimizedAnimatedPropUpdatesEnabled ? 'ON' : 'OFF'}
92+
</Text>
93+
</View>
94+
);
95+
}
96+
97+
function AllAnimatedPropsExample(): React.Node {
98+
const t = useLoop();
99+
100+
// All interpolators are derived from a single shared driver. Building them
101+
// in a useMemo keeps the underlying Animated nodes stable across renders.
102+
const rows = useMemo(() => {
103+
const num = (a: number, b: number) =>
104+
t.interpolate({inputRange: [0, 1], outputRange: [a, b]});
105+
106+
const color = (a: string, b: string) =>
107+
t.interpolate({inputRange: [0, 1], outputRange: [a, b]});
108+
109+
// NOTE: percent-based interpolators (outputRange like ["0%", "50%"]) are
110+
// intentionally omitted. InterpolationAnimatedNode in the C++ renderer
111+
// doesn't parse string outputRange entries with unit suffixes (it crashes
112+
// on `asDouble()` for values like "50%"), so the wire-side
113+
// CMD_UNIT_PERCENT path can't be exercised end-to-end from this example
114+
// until that's fixed. See InterpolationAnimatedNode.cpp:50.
115+
116+
const deg = (a: number, b: number) =>
117+
t.interpolate({
118+
inputRange: [0, 1],
119+
outputRange: [`${a}deg`, `${b}deg`],
120+
});
121+
122+
const rad = (a: number, b: number) =>
123+
t.interpolate({
124+
inputRange: [0, 1],
125+
outputRange: [`${a}rad`, `${b}rad`],
126+
});
127+
128+
// Top-level numeric props (decoded as CMD_OPACITY..CMD_SHADOW_RADIUS).
129+
const numeric = [
130+
{label: 'opacity', style: {opacity: num(0.2, 1)}},
131+
{label: 'elevation (Android)', style: {elevation: num(0, 16)}},
132+
{label: 'zIndex', style: {zIndex: num(0, 10)}},
133+
{label: 'shadowOpacity (iOS)', style: {shadowOpacity: num(0, 1)}},
134+
{label: 'shadowRadius (iOS)', style: {shadowRadius: num(0, 12)}},
135+
];
136+
137+
// Color props (decoded as CMD_BACKGROUND_COLOR..CMD_TINT_COLOR and
138+
// CMD_BORDER_COLOR..CMD_BORDER_END_COLOR). `color` and `tintColor` are
139+
// rendered separately because they need Text and Image, respectively.
140+
const colorRows = [
141+
{
142+
label: 'backgroundColor',
143+
style: {backgroundColor: color('#fda4af', '#22c55e')},
144+
},
145+
{label: 'borderColor', style: {borderColor: color('red', 'blue')}},
146+
{label: 'borderTopColor', style: {borderTopColor: color('red', 'blue')}},
147+
{
148+
label: 'borderBottomColor',
149+
style: {borderBottomColor: color('red', 'blue')},
150+
},
151+
{
152+
label: 'borderLeftColor',
153+
style: {borderLeftColor: color('red', 'blue')},
154+
},
155+
{
156+
label: 'borderRightColor',
157+
style: {borderRightColor: color('red', 'blue')},
158+
},
159+
{
160+
label: 'borderStartColor',
161+
style: {borderStartColor: color('red', 'blue')},
162+
},
163+
{label: 'borderEndColor', style: {borderEndColor: color('red', 'blue')}},
164+
];
165+
166+
// Border radii (decoded as CMD_BORDER_RADIUS..CMD_BORDER_END_END_RADIUS).
167+
// Only the px unit path is exercised here; see note above for why the
168+
// percent path is currently disabled.
169+
const r = () => num(0, 24);
170+
const radiusPx = [
171+
{label: 'borderRadius (px)', style: {borderRadius: r()}},
172+
{label: 'borderTopLeftRadius (px)', style: {borderTopLeftRadius: r()}},
173+
{label: 'borderTopRightRadius (px)', style: {borderTopRightRadius: r()}},
174+
{label: 'borderTopStartRadius (px)', style: {borderTopStartRadius: r()}},
175+
{label: 'borderTopEndRadius (px)', style: {borderTopEndRadius: r()}},
176+
{
177+
label: 'borderBottomLeftRadius (px)',
178+
style: {borderBottomLeftRadius: r()},
179+
},
180+
{
181+
label: 'borderBottomRightRadius (px)',
182+
style: {borderBottomRightRadius: r()},
183+
},
184+
{
185+
label: 'borderBottomStartRadius (px)',
186+
style: {borderBottomStartRadius: r()},
187+
},
188+
{
189+
label: 'borderBottomEndRadius (px)',
190+
style: {borderBottomEndRadius: r()},
191+
},
192+
{
193+
label: 'borderStartStartRadius (px)',
194+
style: {borderStartStartRadius: r()},
195+
},
196+
{
197+
label: 'borderStartEndRadius (px)',
198+
style: {borderStartEndRadius: r()},
199+
},
200+
{
201+
label: 'borderEndStartRadius (px)',
202+
style: {borderEndStartRadius: r()},
203+
},
204+
{label: 'borderEndEndRadius (px)', style: {borderEndEndRadius: r()}},
205+
];
206+
207+
// Transforms (decoded under CMD_START_OF_TRANSFORM). Rotations cover both
208+
// CMD_UNIT_DEG and CMD_UNIT_RAD; translations cover CMD_UNIT_PX. The
209+
// CMD_UNIT_PERCENT path for translations is currently disabled (see note
210+
// above).
211+
const transforms = [
212+
{label: 'translateX (px)', transform: [{translateX: num(-30, 30)}]},
213+
{label: 'translateY (px)', transform: [{translateY: num(-15, 15)}]},
214+
{label: 'scale', transform: [{scale: num(0.5, 1.5)}]},
215+
{label: 'scaleX', transform: [{scaleX: num(0.5, 1.5)}]},
216+
{label: 'scaleY', transform: [{scaleY: num(0.5, 1.5)}]},
217+
{label: 'rotate (deg)', transform: [{rotate: deg(0, 360)}]},
218+
{label: 'rotate (rad)', transform: [{rotate: rad(0, Math.PI * 2)}]},
219+
{label: 'rotateX (deg)', transform: [{rotateX: deg(0, 360)}]},
220+
{label: 'rotateY (deg)', transform: [{rotateY: deg(0, 360)}]},
221+
{label: 'rotateZ (deg)', transform: [{rotateZ: deg(0, 360)}]},
222+
{label: 'skewX (deg)', transform: [{skewX: deg(0, 30)}]},
223+
{label: 'skewY (deg)', transform: [{skewY: deg(0, 30)}]},
224+
{
225+
label: 'perspective + rotateY',
226+
transform: [{perspective: num(200, 800)}, {rotateY: deg(0, 360)}],
227+
},
228+
];
229+
230+
return {
231+
numeric,
232+
textColor: color('red', 'blue'),
233+
tintColor: color('red', 'blue'),
234+
colorRows,
235+
radiusPx,
236+
transforms,
237+
};
238+
}, [t]);
239+
240+
return (
241+
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
242+
<FlagIndicator />
243+
<SectionHeader title="Top-level numeric props" />
244+
{rows.numeric.map(({label, style}) => (
245+
<Row key={label} label={label}>
246+
<Animated.View style={[styles.box, styles.colored, style]} />
247+
</Row>
248+
))}
249+
250+
<SectionHeader title="Color props" />
251+
{rows.colorRows.map(({label, style}) => (
252+
<Row key={label} label={label}>
253+
<Animated.View style={[styles.box, styles.thickBorder, style]} />
254+
</Row>
255+
))}
256+
<Row label="color (Text)">
257+
<Animated.Text style={[styles.text, {color: rows.textColor}]}>
258+
Animated text color
259+
</Animated.Text>
260+
</Row>
261+
<Row label="tintColor (Image)">
262+
<Animated.Image
263+
source={TINT_SOURCE}
264+
style={[styles.tintImage, {tintColor: rows.tintColor}]}
265+
/>
266+
</Row>
267+
268+
<SectionHeader title="Border radius (px)" />
269+
{rows.radiusPx.map(({label, style}) => (
270+
<Row key={label} label={label}>
271+
<Animated.View style={[styles.box, styles.colored, style]} />
272+
</Row>
273+
))}
274+
275+
<SectionHeader title="Transforms" />
276+
{rows.transforms.map(({label, transform}) => (
277+
<Row key={label} label={label}>
278+
<Animated.View style={[styles.box, styles.colored, {transform}]} />
279+
</Row>
280+
))}
281+
</ScrollView>
282+
);
283+
}
284+
285+
const styles = StyleSheet.create({
286+
container: {
287+
flex: 1,
288+
},
289+
content: {
290+
padding: 8,
291+
},
292+
section: {
293+
fontWeight: 'bold',
294+
fontSize: 14,
295+
marginTop: 16,
296+
marginBottom: 4,
297+
},
298+
row: {
299+
flexDirection: 'row',
300+
alignItems: 'center',
301+
paddingVertical: 6,
302+
minHeight: 70,
303+
},
304+
label: {
305+
width: 170,
306+
fontSize: 12,
307+
paddingRight: 8,
308+
},
309+
demo: {
310+
flex: 1,
311+
alignItems: 'center',
312+
justifyContent: 'center',
313+
minHeight: 70,
314+
},
315+
box: {
316+
width: 50,
317+
height: 50,
318+
},
319+
colored: {
320+
backgroundColor: '#3b82f6',
321+
},
322+
thickBorder: {
323+
borderWidth: 4,
324+
borderColor: 'red',
325+
backgroundColor: '#e5e7eb',
326+
},
327+
text: {
328+
fontSize: 16,
329+
fontWeight: 'bold',
330+
},
331+
tintImage: {
332+
width: 40,
333+
height: 40,
334+
},
335+
flagRow: {
336+
flexDirection: 'row',
337+
alignItems: 'center',
338+
paddingVertical: 8,
339+
paddingHorizontal: 8,
340+
marginBottom: 4,
341+
backgroundColor: '#f3f4f6',
342+
borderRadius: 4,
343+
},
344+
flagLabel: {
345+
fontSize: 12,
346+
fontWeight: 'bold',
347+
marginRight: 8,
348+
},
349+
flagValue: {
350+
fontSize: 12,
351+
fontWeight: 'bold',
352+
},
353+
flagOn: {
354+
color: '#16a34a',
355+
},
356+
flagOff: {
357+
color: '#dc2626',
358+
},
359+
});
360+
361+
export default {
362+
title: 'All Animated Props',
363+
name: 'allAnimatedProps',
364+
description:
365+
'Runs an animation per each property handled by BatchedAnimatedPropsMountItem (opacity, elevation, colors, border radii, transforms, etc.).',
366+
render: (): React.Node => <AllAnimatedPropsExample />,
367+
} as RNTesterModuleExample;

packages/rn-tester/js/examples/AnimationBackend/AnimationBackendIndex.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import type {RNTesterModule} from '../../types/RNTesterTypes';
1212

13+
import AllAnimatedPropsExample from './AllAnimatedPropsExample';
1314
import PlaygroundExample from './ChessboardExample';
1415
import PerformanceTestExample from './PerformanceTestExample';
1516
import SwipeableListExample from './SwipeableListExample';
@@ -27,6 +28,11 @@ export default {
2728
description: `Examples demonstrating the Animation Backend for layout-updating animations. ${canUseBackend ? '' : 'You need to enable c++ Animated and the Animation Backend to see these examples.'}`,
2829
showIndividualExamples: true,
2930
examples: canUseBackend
30-
? [PlaygroundExample, SwipeableListExample, PerformanceTestExample]
31+
? [
32+
AllAnimatedPropsExample,
33+
PlaygroundExample,
34+
SwipeableListExample,
35+
PerformanceTestExample,
36+
]
3137
: [],
3238
} as RNTesterModule;

0 commit comments

Comments
 (0)