Skip to content

Commit cffe14f

Browse files
zeyapmeta-codesync[bot]
authored andcommitted
remove useNativeDriver under featureflag animatedForceNativeDriver (#57211)
Summary: Pull Request resolved: #57211 ## Changelog: [General] [Added] - remove `useNativeDriver` under featureflag animatedForceNativeDriver When `animatedForceNativeDriver` is enabled, it forces `useNativeDriver` to `true` for all Animated animations and events, overriding the config (explicit `false` set by user will be no-op). Has no effect unless the shared animated backend is enabled, which is required to support native driver for all props. Also using this flag to gate the js animation logic that could be cleaned up when this path is fully working. Reviewed By: javache Differential Revision: D108193641 fbshipit-source-id: fa2c7332742435309fc00831d9644ca2cf2f6ab9
1 parent fc7dc74 commit cffe14f

10 files changed

Lines changed: 155 additions & 7 deletions

File tree

packages/react-native/Libraries/Animated/AnimatedImplementation.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {DecayAnimationConfig} from './animations/DecayAnimation';
2020
import type {SpringAnimationConfig} from './animations/SpringAnimation';
2121
import type {TimingAnimationConfig} from './animations/TimingAnimation';
2222

23+
import NativeAnimatedHelper from '../../src/private/animated/NativeAnimatedHelper';
2324
import {AnimatedEvent, attachNativeEventImpl} from './AnimatedEvent';
2425
import DecayAnimation from './animations/DecayAnimation';
2526
import SpringAnimation from './animations/SpringAnimation';
@@ -200,7 +201,11 @@ const springImpl = function (
200201
},
201202

202203
_isUsingNativeDriver: function (): boolean {
203-
return config.useNativeDriver || false;
204+
return (
205+
NativeAnimatedHelper.isNativeDriverForced() ||
206+
config.useNativeDriver ||
207+
false
208+
);
204209
},
205210
}
206211
);
@@ -254,7 +259,11 @@ const timingImpl = function (
254259
},
255260

256261
_isUsingNativeDriver: function (): boolean {
257-
return config.useNativeDriver || false;
262+
return (
263+
NativeAnimatedHelper.isNativeDriverForced() ||
264+
config.useNativeDriver ||
265+
false
266+
);
258267
},
259268
}
260269
);
@@ -296,7 +305,11 @@ const decayImpl = function (
296305
},
297306

298307
_isUsingNativeDriver: function (): boolean {
299-
return config.useNativeDriver || false;
308+
return (
309+
NativeAnimatedHelper.isNativeDriverForced() ||
310+
config.useNativeDriver ||
311+
false
312+
);
300313
},
301314
}
302315
);

packages/react-native/Libraries/Animated/NativeAnimatedAllowlist.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,42 @@ const SUPPORTED_STYLES: {[string]: true} = {
7878
top: true,
7979
/* flex */
8080
flex: true,
81+
flexGrow: true,
82+
flexShrink: true,
83+
flexBasis: true,
84+
aspectRatio: true,
85+
/* margin */
86+
margin: true,
87+
marginLeft: true,
88+
marginRight: true,
89+
marginTop: true,
90+
marginBottom: true,
91+
marginStart: true,
92+
marginEnd: true,
93+
marginHorizontal: true,
94+
marginVertical: true,
95+
/* padding */
96+
padding: true,
97+
paddingLeft: true,
98+
paddingRight: true,
99+
paddingTop: true,
100+
paddingBottom: true,
101+
paddingStart: true,
102+
paddingEnd: true,
103+
paddingHorizontal: true,
104+
paddingVertical: true,
105+
/* border width */
106+
borderWidth: true,
107+
borderLeftWidth: true,
108+
borderRightWidth: true,
109+
borderTopWidth: true,
110+
borderBottomWidth: true,
111+
borderStartWidth: true,
112+
borderEndWidth: true,
113+
/* gap */
114+
gap: true,
115+
rowGap: true,
116+
columnGap: true,
81117
}
82118
: {}),
83119
};

packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,65 @@ import {Animated, View, useAnimatedValue} from 'react-native';
2121
import {allowStyleProp} from 'react-native/Libraries/Animated/NativeAnimatedAllowlist';
2222
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
2323

24+
// marginLeft (and the other margin props) are only on the native animated
25+
// allowlist when the shared backend is enabled. This test deliberately does NOT
26+
// call allowStyleProp('marginLeft') — it verifies the prop is supported natively
27+
// out of the box under useSharedAnimatedBackend.
28+
test('animate marginLeft layout prop', () => {
29+
const viewRef = createRef<HostInstance>();
30+
31+
let _animatedMarginLeft;
32+
let _marginLeftAnimation;
33+
34+
function MyApp() {
35+
const animatedMarginLeft = useAnimatedValue(0);
36+
_animatedMarginLeft = animatedMarginLeft;
37+
return (
38+
<Animated.View
39+
ref={viewRef}
40+
style={[
41+
{
42+
width: 100,
43+
height: 100,
44+
marginLeft: animatedMarginLeft,
45+
},
46+
]}
47+
/>
48+
);
49+
}
50+
51+
const root = Fantom.createRoot();
52+
53+
Fantom.runTask(() => {
54+
root.render(<MyApp />);
55+
});
56+
57+
Fantom.runTask(() => {
58+
_marginLeftAnimation = Animated.timing(_animatedMarginLeft, {
59+
toValue: 100,
60+
duration: 200,
61+
useNativeDriver: true,
62+
}).start();
63+
});
64+
65+
Fantom.unstable_produceFramesForDuration(100);
66+
67+
expect(root.getRenderedOutput({props: ['marginLeft']}).toJSX()).toEqual(
68+
<rn-view marginLeft="50" />,
69+
);
70+
71+
Fantom.unstable_produceFramesForDuration(100);
72+
73+
// TODO: this shouldn't be necessary since animation should be stopped after duration
74+
Fantom.runTask(() => {
75+
_marginLeftAnimation?.stop();
76+
});
77+
78+
expect(root.getRenderedOutput({props: ['marginLeft']}).toJSX()).toEqual(
79+
<rn-view marginLeft="100" />,
80+
);
81+
});
82+
2483
test('animated opacity', () => {
2584
let _opacity;
2685
let _opacityAnimation;

packages/react-native/Libraries/Animated/animations/Animation.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export default class Animation {
7070
previousAnimation: ?Animation,
7171
animatedValue: AnimatedValue,
7272
): void {
73+
// TODO: T274006331 - Remove js-only animation once shared backend is fully rolled out
7374
if (!this._useNativeDriver && animatedValue.__isNative === true) {
7475
throw new Error(
7576
'Attempting to run JS driven animation on animated node ' +

packages/react-native/Libraries/Animated/animations/DecayAnimation.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export default class DecayAnimation extends Animation {
8585
this._startTime = Date.now();
8686

8787
const useNativeDriver = this.__startAnimationIfNative(animatedValue);
88+
// TODO: T274006331 - Remove js-only animation once shared backend is fully rolled out
8889
if (!useNativeDriver) {
8990
this._animationFrame = requestAnimationFrame(() => this.onUpdate());
9091
}

packages/react-native/Libraries/Animated/animations/SpringAnimation.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ export default class SpringAnimation extends Animation {
225225

226226
const start = () => {
227227
const useNativeDriver = this.__startAnimationIfNative(animatedValue);
228+
// TODO: T274006331 - Remove js-only animation once shared backend is fully rolled out
228229
if (!useNativeDriver) {
229230
this.onUpdate();
230231
}

packages/react-native/Libraries/Animated/animations/TimingAnimation.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export default class TimingAnimation extends Animation {
129129
this._startTime = Date.now();
130130

131131
const useNativeDriver = this.__startAnimationIfNative(animatedValue);
132+
// TODO: T274006331 - Remove js-only animation once shared backend is fully rolled out
132133
if (!useNativeDriver) {
133134
// Animations that sometimes have 0 duration and sometimes do not
134135
// still need to use the native driver when duration is 0 so as to

packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,17 @@ const definitions: FeatureFlagDefinitions = {
970970
},
971971
ossReleaseStage: 'none',
972972
},
973+
animatedForceNativeDriver: {
974+
defaultValue: false,
975+
metadata: {
976+
dateAdded: '2026-06-10',
977+
description:
978+
'When enabled, forces `useNativeDriver` to `true` for all Animated animations and events, overriding the config (including an explicit `false`). Has no effect unless the shared animated backend is enabled, which is required to support native driver for all props.',
979+
expectedReleaseValue: true,
980+
purpose: 'experimentation',
981+
},
982+
ossReleaseStage: 'none',
983+
},
973984
animatedShouldDebounceQueueFlush: {
974985
defaultValue: false,
975986
metadata: {

packages/react-native/src/private/animated/NativeAnimatedHelper.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -417,17 +417,35 @@ function assertNativeAnimatedModule(): void {
417417

418418
let _warnedMissingNativeAnimated = false;
419419

420+
// Whether the native driver should be forced on for every animation, overriding
421+
// the config (including an explicit `useNativeDriver: false`). This is only safe
422+
// when the shared animated backend is enabled — that backend is what makes every
423+
// prop drivable natively. Forcing native without it would break animations of
424+
// props the legacy native driver doesn't support.
425+
function isNativeDriverForced(): boolean {
426+
return (
427+
ReactNativeFeatureFlags.animatedForceNativeDriver() &&
428+
ReactNativeFeatureFlags.cxxNativeAnimatedEnabled() &&
429+
// eslint-disable-next-line
430+
ReactNativeFeatureFlags.useSharedAnimatedBackend()
431+
);
432+
}
433+
420434
function shouldUseNativeDriver(
421435
config: Readonly<{...AnimationConfig, ...}> | EventConfig<unknown>,
422436
): boolean {
423-
if (config.useNativeDriver == null) {
437+
const forceNativeDriver = isNativeDriverForced();
438+
439+
if (config.useNativeDriver == null && !forceNativeDriver) {
424440
console.warn(
425441
'Animated: `useNativeDriver` was not specified. This is a required ' +
426442
'option and must be explicitly set to `true` or `false`',
427443
);
428444
}
429445

430-
if (config.useNativeDriver === true && !NativeAnimatedModule) {
446+
const useNativeDriver = forceNativeDriver || config.useNativeDriver === true;
447+
448+
if (useNativeDriver === true && !NativeAnimatedModule) {
431449
if (process.env.NODE_ENV !== 'test') {
432450
if (!_warnedMissingNativeAnimated) {
433451
console.warn(
@@ -443,7 +461,7 @@ function shouldUseNativeDriver(
443461
return false;
444462
}
445463

446-
return config.useNativeDriver || false;
464+
return useNativeDriver;
447465
}
448466

449467
function transformDataType(value: number | string): number | string {
@@ -469,6 +487,7 @@ export default {
469487
assertNativeAnimatedModule,
470488
generateNewAnimationId,
471489
generateNewNodeTag,
490+
isNativeDriverForced,
472491
// $FlowExpectedError[unsafe-getters-setters] - unsafe getter lint suppression
473492
// $FlowExpectedError[missing-type-arg] - unsafe getter lint suppression
474493
get nativeEventEmitter(): NativeEventEmitter {

packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<1f64eab49a337feb5c9b1b41faf92730>>
7+
* @generated SignedSource<<960e77b9abee222a2f1480870a33560a>>
88
* @flow strict
99
* @noformat
1010
*/
@@ -30,6 +30,7 @@ import {
3030
export type ReactNativeFeatureFlagsJsOnly = Readonly<{
3131
jsOnlyTestFlag: Getter<boolean>,
3232
animatedDeferStartOfTimingAnimations: Getter<boolean>,
33+
animatedForceNativeDriver: Getter<boolean>,
3334
animatedShouldDebounceQueueFlush: Getter<boolean>,
3435
animatedShouldSyncValueBeforeStartCallback: Getter<boolean>,
3536
animatedShouldUseSingleOp: Getter<boolean>,
@@ -146,6 +147,11 @@ export const jsOnlyTestFlag: Getter<boolean> = createJavaScriptFlagGetter('jsOnl
146147
*/
147148
export const animatedDeferStartOfTimingAnimations: Getter<boolean> = createJavaScriptFlagGetter('animatedDeferStartOfTimingAnimations', false);
148149

150+
/**
151+
* When enabled, forces `useNativeDriver` to `true` for all Animated animations and events, overriding the config (including an explicit `false`). Has no effect unless the shared animated backend is enabled, which is required to support native driver for all props.
152+
*/
153+
export const animatedForceNativeDriver: Getter<boolean> = createJavaScriptFlagGetter('animatedForceNativeDriver', false);
154+
149155
/**
150156
* Enables an experimental flush-queue debouncing in Animated.js.
151157
*/

0 commit comments

Comments
 (0)