diff --git a/packages/react-native/Libraries/Animated/AnimatedImplementation.js b/packages/react-native/Libraries/Animated/AnimatedImplementation.js index 46a08d2e8954..d14425499cab 100644 --- a/packages/react-native/Libraries/Animated/AnimatedImplementation.js +++ b/packages/react-native/Libraries/Animated/AnimatedImplementation.js @@ -20,6 +20,7 @@ import type {DecayAnimationConfig} from './animations/DecayAnimation'; import type {SpringAnimationConfig} from './animations/SpringAnimation'; import type {TimingAnimationConfig} from './animations/TimingAnimation'; +import NativeAnimatedHelper from '../../src/private/animated/NativeAnimatedHelper'; import {AnimatedEvent, attachNativeEventImpl} from './AnimatedEvent'; import DecayAnimation from './animations/DecayAnimation'; import SpringAnimation from './animations/SpringAnimation'; @@ -200,7 +201,11 @@ const springImpl = function ( }, _isUsingNativeDriver: function (): boolean { - return config.useNativeDriver || false; + return ( + NativeAnimatedHelper.isNativeDriverForced() || + config.useNativeDriver || + false + ); }, } ); @@ -254,7 +259,11 @@ const timingImpl = function ( }, _isUsingNativeDriver: function (): boolean { - return config.useNativeDriver || false; + return ( + NativeAnimatedHelper.isNativeDriverForced() || + config.useNativeDriver || + false + ); }, } ); @@ -296,7 +305,11 @@ const decayImpl = function ( }, _isUsingNativeDriver: function (): boolean { - return config.useNativeDriver || false; + return ( + NativeAnimatedHelper.isNativeDriverForced() || + config.useNativeDriver || + false + ); }, } ); diff --git a/packages/react-native/Libraries/Animated/NativeAnimatedAllowlist.js b/packages/react-native/Libraries/Animated/NativeAnimatedAllowlist.js index c5cecfc828c6..c91017f23fc0 100644 --- a/packages/react-native/Libraries/Animated/NativeAnimatedAllowlist.js +++ b/packages/react-native/Libraries/Animated/NativeAnimatedAllowlist.js @@ -78,6 +78,42 @@ const SUPPORTED_STYLES: {[string]: true} = { top: true, /* flex */ flex: true, + flexGrow: true, + flexShrink: true, + flexBasis: true, + aspectRatio: true, + /* margin */ + margin: true, + marginLeft: true, + marginRight: true, + marginTop: true, + marginBottom: true, + marginStart: true, + marginEnd: true, + marginHorizontal: true, + marginVertical: true, + /* padding */ + padding: true, + paddingLeft: true, + paddingRight: true, + paddingTop: true, + paddingBottom: true, + paddingStart: true, + paddingEnd: true, + paddingHorizontal: true, + paddingVertical: true, + /* border width */ + borderWidth: true, + borderLeftWidth: true, + borderRightWidth: true, + borderTopWidth: true, + borderBottomWidth: true, + borderStartWidth: true, + borderEndWidth: true, + /* gap */ + gap: true, + rowGap: true, + columnGap: true, } : {}), }; diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js index adeaf7e485cf..57b72ea67715 100644 --- a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js @@ -21,6 +21,65 @@ import {Animated, View, useAnimatedValue} from 'react-native'; import {allowStyleProp} from 'react-native/Libraries/Animated/NativeAnimatedAllowlist'; import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; +// marginLeft (and the other margin props) are only on the native animated +// allowlist when the shared backend is enabled. This test deliberately does NOT +// call allowStyleProp('marginLeft') — it verifies the prop is supported natively +// out of the box under useSharedAnimatedBackend. +test('animate marginLeft layout prop', () => { + const viewRef = createRef(); + + let _animatedMarginLeft; + let _marginLeftAnimation; + + function MyApp() { + const animatedMarginLeft = useAnimatedValue(0); + _animatedMarginLeft = animatedMarginLeft; + return ( + + ); + } + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + Fantom.runTask(() => { + _marginLeftAnimation = Animated.timing(_animatedMarginLeft, { + toValue: 100, + duration: 200, + useNativeDriver: true, + }).start(); + }); + + Fantom.unstable_produceFramesForDuration(100); + + expect(root.getRenderedOutput({props: ['marginLeft']}).toJSX()).toEqual( + , + ); + + Fantom.unstable_produceFramesForDuration(100); + + // TODO: this shouldn't be necessary since animation should be stopped after duration + Fantom.runTask(() => { + _marginLeftAnimation?.stop(); + }); + + expect(root.getRenderedOutput({props: ['marginLeft']}).toJSX()).toEqual( + , + ); +}); + test('animated opacity', () => { let _opacity; let _opacityAnimation; diff --git a/packages/react-native/Libraries/Animated/animations/Animation.js b/packages/react-native/Libraries/Animated/animations/Animation.js index 7322ec03c6b3..83e1a715379a 100644 --- a/packages/react-native/Libraries/Animated/animations/Animation.js +++ b/packages/react-native/Libraries/Animated/animations/Animation.js @@ -70,6 +70,7 @@ export default class Animation { previousAnimation: ?Animation, animatedValue: AnimatedValue, ): void { + // TODO: T274006331 - Remove js-only animation once shared backend is fully rolled out if (!this._useNativeDriver && animatedValue.__isNative === true) { throw new Error( 'Attempting to run JS driven animation on animated node ' + diff --git a/packages/react-native/Libraries/Animated/animations/DecayAnimation.js b/packages/react-native/Libraries/Animated/animations/DecayAnimation.js index 35eb106f5a2b..d6b834b032ee 100644 --- a/packages/react-native/Libraries/Animated/animations/DecayAnimation.js +++ b/packages/react-native/Libraries/Animated/animations/DecayAnimation.js @@ -85,6 +85,7 @@ export default class DecayAnimation extends Animation { this._startTime = Date.now(); const useNativeDriver = this.__startAnimationIfNative(animatedValue); + // TODO: T274006331 - Remove js-only animation once shared backend is fully rolled out if (!useNativeDriver) { this._animationFrame = requestAnimationFrame(() => this.onUpdate()); } diff --git a/packages/react-native/Libraries/Animated/animations/SpringAnimation.js b/packages/react-native/Libraries/Animated/animations/SpringAnimation.js index cb70e4454117..f04a527469b3 100644 --- a/packages/react-native/Libraries/Animated/animations/SpringAnimation.js +++ b/packages/react-native/Libraries/Animated/animations/SpringAnimation.js @@ -225,6 +225,7 @@ export default class SpringAnimation extends Animation { const start = () => { const useNativeDriver = this.__startAnimationIfNative(animatedValue); + // TODO: T274006331 - Remove js-only animation once shared backend is fully rolled out if (!useNativeDriver) { this.onUpdate(); } diff --git a/packages/react-native/Libraries/Animated/animations/TimingAnimation.js b/packages/react-native/Libraries/Animated/animations/TimingAnimation.js index c464334cc376..dffb737a9882 100644 --- a/packages/react-native/Libraries/Animated/animations/TimingAnimation.js +++ b/packages/react-native/Libraries/Animated/animations/TimingAnimation.js @@ -129,6 +129,7 @@ export default class TimingAnimation extends Animation { this._startTime = Date.now(); const useNativeDriver = this.__startAnimationIfNative(animatedValue); + // TODO: T274006331 - Remove js-only animation once shared backend is fully rolled out if (!useNativeDriver) { // Animations that sometimes have 0 duration and sometimes do not // still need to use the native driver when duration is 0 so as to diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index be1206256523..d6ceaed2129e 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -981,6 +981,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + animatedForceNativeDriver: { + defaultValue: false, + metadata: { + dateAdded: '2026-06-10', + description: + '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.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, animatedShouldDebounceQueueFlush: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/animated/NativeAnimatedHelper.js b/packages/react-native/src/private/animated/NativeAnimatedHelper.js index f27650b3d327..8035f6cde926 100644 --- a/packages/react-native/src/private/animated/NativeAnimatedHelper.js +++ b/packages/react-native/src/private/animated/NativeAnimatedHelper.js @@ -406,17 +406,35 @@ function assertNativeAnimatedModule(): void { let _warnedMissingNativeAnimated = false; +// Whether the native driver should be forced on for every animation, overriding +// the config (including an explicit `useNativeDriver: false`). This is only safe +// when the shared animated backend is enabled — that backend is what makes every +// prop drivable natively. Forcing native without it would break animations of +// props the legacy native driver doesn't support. +function isNativeDriverForced(): boolean { + return ( + ReactNativeFeatureFlags.animatedForceNativeDriver() && + ReactNativeFeatureFlags.cxxNativeAnimatedEnabled() && + // eslint-disable-next-line + ReactNativeFeatureFlags.useSharedAnimatedBackend() + ); +} + function shouldUseNativeDriver( config: Readonly<{...AnimationConfig, ...}> | EventConfig, ): boolean { - if (config.useNativeDriver == null) { + const forceNativeDriver = isNativeDriverForced(); + + if (config.useNativeDriver == null && !forceNativeDriver) { console.warn( 'Animated: `useNativeDriver` was not specified. This is a required ' + 'option and must be explicitly set to `true` or `false`', ); } - if (config.useNativeDriver === true && !NativeAnimatedModule) { + const useNativeDriver = forceNativeDriver || config.useNativeDriver === true; + + if (useNativeDriver === true && !NativeAnimatedModule) { if (process.env.NODE_ENV !== 'test') { if (!_warnedMissingNativeAnimated) { console.warn( @@ -432,7 +450,7 @@ function shouldUseNativeDriver( return false; } - return config.useNativeDriver || false; + return useNativeDriver; } function transformDataType(value: number | string): number | string { @@ -458,6 +476,7 @@ export default { assertNativeAnimatedModule, generateNewAnimationId, generateNewNodeTag, + isNativeDriverForced, // $FlowExpectedError[unsafe-getters-setters] - unsafe getter lint suppression // $FlowExpectedError[missing-type-arg] - unsafe getter lint suppression get nativeEventEmitter(): NativeEventEmitter { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index b3b3f6a2eca2..e031eb36061f 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<785d84f617e6b1870c3ff1eeed9f1c66>> + * @generated SignedSource<<2aaa9f49f9d072aca935862bf5da1630>> * @flow strict * @noformat */ @@ -30,6 +30,7 @@ import { export type ReactNativeFeatureFlagsJsOnly = Readonly<{ jsOnlyTestFlag: Getter, animatedDeferStartOfTimingAnimations: Getter, + animatedForceNativeDriver: Getter, animatedShouldDebounceQueueFlush: Getter, animatedShouldSyncValueBeforeStartCallback: Getter, animatedShouldUseSingleOp: Getter, @@ -147,6 +148,11 @@ export const jsOnlyTestFlag: Getter = createJavaScriptFlagGetter('jsOnl */ export const animatedDeferStartOfTimingAnimations: Getter = createJavaScriptFlagGetter('animatedDeferStartOfTimingAnimations', false); +/** + * 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. + */ +export const animatedForceNativeDriver: Getter = createJavaScriptFlagGetter('animatedForceNativeDriver', false); + /** * Enables an experimental flush-queue debouncing in Animated.js. */