diff --git a/packages/react-native/Libraries/Animated/__tests__/Animated-itest.js b/packages/react-native/Libraries/Animated/__tests__/Animated-itest.js index 995bb369bd81..0e3782ba7789 100644 --- a/packages/react-native/Libraries/Animated/__tests__/Animated-itest.js +++ b/packages/react-native/Libraries/Animated/__tests__/Animated-itest.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. * - * @fantom_flags useSharedAnimatedBackend:* + * @fantom_flags useSharedAnimatedBackend:* animatedDeferStartOfTimingAnimations:* * @flow strict-local * @format */ @@ -21,6 +21,12 @@ 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'; +// Deferred start outputs the initial value on the first animation frame and +// re-anchors timing on the second. This delays animation progress by one +// frame interval (~16ms at 60 fps). +const DEFERRED_START_MS = + ReactNativeFeatureFlags.animatedDeferStartOfTimingAnimations() ? 16 : 0; + test('moving box by 100 points', () => { let _translateX; const viewRef = createRef(); @@ -60,7 +66,7 @@ test('moving box by 100 points', () => { }).start(); }); - Fantom.unstable_produceFramesForDuration(500); + Fantom.unstable_produceFramesForDuration(500 + DEFERRED_START_MS); // shadow tree is not synchronised yet, position X is still 0. expect(viewElement.getBoundingClientRect().x).toBe(0); @@ -81,6 +87,85 @@ test('moving box by 100 points', () => { expect(viewElement.getBoundingClientRect().x).toBe(100); }); +// Validate that a `useNativeDriver` timing animation does not begin progressing +// until the end of the event loop tick it was started in. +// +// Tested different behavior introduced by `animatedDeferStartOfTimingAnimations`, +// the behavioral difference is animated prop value on the first frame after the tick: +// flag ON -> deferred, not progressed yet, flag OFF -> already progressing. +function startTimingAnimationAndGetTranslateXAfterFirstFrame(): number { + let _translateX; + const viewRef = createRef(); + + function MyApp() { + const translateX = useAnimatedValue(0); + _translateX = translateX; + return ( + + ); + } + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const viewElement = ensureInstance(viewRef.current, ReactNativeElement); + + Fantom.runTask(() => { + Animated.timing(_translateX, { + toValue: 100, + duration: 1000, + useNativeDriver: true, + }).start(); + + Fantom.unstable_produceFramesForDuration(500); + + // The UI thread advances while we are still inside the js tick. The animation + // must not produce any direct manipulation yet, because its mount + // operations have not been flushed. This holds regardless of the flag. + expect(() => + Fantom.unstable_getDirectManipulationProps(viewElement), + ).toThrow(); + }); + + // Produce the first frame after the tick (~16ms rounds to frame 1). + Fantom.unstable_produceFramesForDuration(16); + const translateXAfterFirstFrame = + // $FlowFixMe[incompatible-use] + Fantom.unstable_getDirectManipulationProps(viewElement).transform[0] + .translateX; + + // Drain the animation so it completes and the message queue is empty for the + // next test. + Fantom.unstable_produceFramesForDuration(1000); + Fantom.runWorkLoop(); + expect(viewElement.getBoundingClientRect().x).toBe(100); + + return translateXAfterFirstFrame; +} + +if (ReactNativeFeatureFlags.animatedDeferStartOfTimingAnimations()) { + test('animation does not start before the end of the current event loop tick', () => { + // With deferred start, the first frame after the tick outputs the initial + // value and re-anchors timing, so the animation has not progressed yet — + // no frames were skipped despite the UI thread advancing inside the tick. + expect(startTimingAnimationAndGetTranslateXAfterFirstFrame()).toBe(0); + }); +} else { + test('animation might start before the end of the current event loop tick', () => { + // Without deferred start, the animation begins progressing immediately — it + // has effectively started before the end of the tick. + expect( + startTimingAnimationAndGetTranslateXAfterFirstFrame(), + ).toBeGreaterThan(0); + }); +} + test('animation driven by onScroll event', () => { const scrollViewRef = createRef(); const viewRef = createRef(); @@ -248,7 +333,7 @@ test('animated opacity', () => { }).start(); }); - Fantom.unstable_produceFramesForDuration(30); + Fantom.unstable_produceFramesForDuration(30 + DEFERRED_START_MS); expect(Fantom.unstable_getDirectManipulationProps(viewElement).opacity).toBe( 0, ); @@ -559,7 +644,7 @@ test('animate layout props', () => { }).start(); }); - Fantom.unstable_produceFramesForDuration(10); + Fantom.unstable_produceFramesForDuration(10 + DEFERRED_START_MS); // TODO: this shouldn't be necessary since animation should be stopped after duration Fantom.runTask(() => { @@ -712,7 +797,7 @@ test('Animated.sequence', () => { }); }); - Fantom.unstable_produceFramesForDuration(500); + Fantom.unstable_produceFramesForDuration(500 + DEFERRED_START_MS); expect( // $FlowFixMe[incompatible-use] diff --git a/packages/react-native/Libraries/Animated/animations/TimingAnimation.js b/packages/react-native/Libraries/Animated/animations/TimingAnimation.js index f872d098bdf7..c464334cc376 100644 --- a/packages/react-native/Libraries/Animated/animations/TimingAnimation.js +++ b/packages/react-native/Libraries/Animated/animations/TimingAnimation.js @@ -15,6 +15,7 @@ import type AnimatedValue from '../nodes/AnimatedValue'; import type AnimatedValueXY from '../nodes/AnimatedValueXY'; import type {AnimationConfig, EndCallback} from './Animation'; +import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags'; import AnimatedColor from '../nodes/AnimatedColor'; import Animation from './Animation'; @@ -69,6 +70,7 @@ export default class TimingAnimation extends Animation { _animationFrame: ?AnimationFrameID; _timeout: ?TimeoutID; _platformConfig: ?PlatformConfig; + _deferredStart: boolean; constructor(config: TimingAnimationConfigSingle) { super(config); @@ -78,6 +80,7 @@ export default class TimingAnimation extends Animation { this._duration = config.duration ?? 500; this._delay = config.delay ?? 0; this._platformConfig = config.platformConfig; + this._deferredStart = false; } __getNativeAnimationConfig(): Readonly<{ @@ -102,6 +105,7 @@ export default class TimingAnimation extends Animation { iterations: this.__iterations, platformConfig: this._platformConfig, debugID: this.__getDebugID(), + deferredStart: this._deferredStart, }; } @@ -116,6 +120,10 @@ export default class TimingAnimation extends Animation { this._fromValue = fromValue; this._onUpdate = onUpdate; + if (ReactNativeFeatureFlags.animatedDeferStartOfTimingAnimations()) { + this._deferredStart = animatedValue.__deferAnimationStart; + animatedValue.__deferAnimationStart = false; + } const start = () => { this._startTime = Date.now(); diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedValue.js b/packages/react-native/Libraries/Animated/nodes/AnimatedValue.js index 8650912edf6f..afe6693c6e16 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedValue.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedValue.js @@ -20,6 +20,7 @@ import type AnimatedNode from './AnimatedNode'; import type {AnimatedNodeConfig} from './AnimatedNode'; import type AnimatedTracking from './AnimatedTracking'; +import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags'; import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; import AnimatedInterpolation from './AnimatedInterpolation'; import AnimatedWithChildren from './AnimatedWithChildren'; @@ -95,6 +96,7 @@ export default class AnimatedValue extends AnimatedWithChildren { _offset: number; _animation: ?Animation; _tracking: ?AnimatedTracking; + __deferAnimationStart: boolean; constructor(value: number, config?: ?AnimatedValueConfig) { super(config); @@ -107,6 +109,8 @@ export default class AnimatedValue extends AnimatedWithChildren { this._startingValue = this._value = value; this._offset = 0; + this.__deferAnimationStart = + ReactNativeFeatureFlags.animatedDeferStartOfTimingAnimations(); this._animation = null; if (config && config.useNativeDriver) { this.__makeNative(); @@ -327,6 +331,10 @@ export default class AnimatedValue extends AnimatedWithChildren { result => { this._animation = null; callback && callback(result); + if (this._animation == null) { + this.__deferAnimationStart = + ReactNativeFeatureFlags.animatedDeferStartOfTimingAnimations(); + } }, previousAnimation, this, diff --git a/packages/react-native/ReactCommon/react/renderer/animated/drivers/FrameAnimationDriver.cpp b/packages/react-native/ReactCommon/react/renderer/animated/drivers/FrameAnimationDriver.cpp index 46af79a137bb..0994ec42f43d 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/drivers/FrameAnimationDriver.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/drivers/FrameAnimationDriver.cpp @@ -49,15 +49,34 @@ void FrameAnimationDriver::onConfigChanged() { frames_.push_back(frameValue); } toValue_ = config_["toValue"].asDouble(); + auto deferIt = config_.find("deferredStart"); + deferredStart_ = deferIt != config_.items().end() && deferIt->second.asBool(); } -bool FrameAnimationDriver::update(double timeDeltaMs, bool /*restarting*/) { +bool FrameAnimationDriver::update(double timeDeltaMs, bool restarting) { if (auto node = manager_->getAnimatedNode(animatedValueTag_)) { if (!startValue_) { startValue_ = node->getRawValue(); } + if (deferredStart_ && restarting) { + // On the very first update after start: output the starting value + // (frame 0) and defer the time anchor. The base class will re-anchor + // startFrameTimeMs_ on the next call, so elapsed time is measured + // from the first frame that has actually been rendered — not from + // when startAnimatingNode was dispatched. + // + // This prevents skipping initial frames when the UI thread is busy + // with layout/mount work between animation start and first composite. + node->setRawValue( + startValue_.value() + frames_[0] * (toValue_ - startValue_.value())); + markNodeUpdated(node->tag()); + startFrameTimeMs_ = -1; + deferredStart_ = false; + return false; + } + const auto startIndex = static_cast(std::round(timeDeltaMs / SingleFrameIntervalMs)); assert(startIndex >= 0); diff --git a/packages/react-native/ReactCommon/react/renderer/animated/drivers/FrameAnimationDriver.h b/packages/react-native/ReactCommon/react/renderer/animated/drivers/FrameAnimationDriver.h index 7bcbc4a04484..6efd986dd666 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/drivers/FrameAnimationDriver.h +++ b/packages/react-native/ReactCommon/react/renderer/animated/drivers/FrameAnimationDriver.h @@ -35,6 +35,7 @@ class FrameAnimationDriver : public AnimationDriver { std::vector frames_{}; double toValue_{0}; std::optional startValue_{}; + bool deferredStart_{false}; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/animated/tests/AnimationDriverTests.cpp b/packages/react-native/ReactCommon/react/renderer/animated/tests/AnimationDriverTests.cpp index e5de4cd7198e..6b85cf112ca7 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/tests/AnimationDriverTests.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/tests/AnimationDriverTests.cpp @@ -117,4 +117,52 @@ TEST_F(AnimationDriverTests, framesAnimationReconfigurationClearsFrames) { EXPECT_EQ(round(nodesManager_->getValue(valueNodeTag).value()), toValue2); } +TEST_F(AnimationDriverTests, framesAnimationDeferredStart) { + // Deferred start outputs frame 0 on the first update and re-anchors + // startFrameTimeMs_ so the second update also sees timeDelta=0. + // Without the defer the second frame would already be at value 25. + initNodesManager(); + + auto rootTag = getNextRootViewTag(); + + auto valueNodeTag = ++rootTag; + nodesManager_->createAnimatedNode( + valueNodeTag, + folly::dynamic::object("type", "value")("value", 0)("offset", 0)); + + const auto animationId = 1; + const auto frames = folly::dynamic::array(0.0f, 0.25f, 0.5f, 0.75f, 1.0f); + const auto toValue = 100; + nodesManager_->startAnimatingNode( + animationId, + valueNodeTag, + folly::dynamic::object("type", "frames")("frames", frames)( + "toValue", toValue)("deferredStart", true), + std::nullopt); + + const double t = 12345; + + // Frame 1: both with and without deferredStart, timeDelta=0 → value=0 + runAnimationFrame(t); + EXPECT_EQ(nodesManager_->getValue(valueNodeTag).value(), 0); + + // Frame 2: WITHOUT deferredStart timeDelta=SI → value≈25. + // WITH deferredStart the deferred start re-anchored startFrameTimeMs_, so + // timeDelta=0 → value=0. This assertion fails without deferredStart. + runAnimationFrame(t + SingleFrameIntervalMs); + EXPECT_EQ(nodesManager_->getValue(valueNodeTag).value(), 0); + + // Frame 3: now timeDelta=SI from the re-anchored start + runAnimationFrame(t + SingleFrameIntervalMs * 2); + EXPECT_NEAR(nodesManager_->getValue(valueNodeTag).value(), 25, 0.01); + + // Frame 4 + runAnimationFrame(t + SingleFrameIntervalMs * 3); + EXPECT_NEAR(nodesManager_->getValue(valueNodeTag).value(), 50, 0.01); + + // Complete + runAnimationFrame(t + SingleFrameIntervalMs * 5); + EXPECT_EQ(nodesManager_->getValue(valueNodeTag).value(), toValue); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp index 39983aaa7895..672802ed7aa6 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp @@ -7,6 +7,7 @@ #include "ViewTransitionModule.h" +#include #include #include #include @@ -72,6 +73,7 @@ void ViewTransitionModule::applyViewTransitionName( const ShadowNode& shadowNode, const std::string& name, const std::string& /*className*/) { + TraceSection s("ViewTransitionModule::applyViewTransitionName", "name", name); auto tag = shadowNode.getTag(); auto surfaceId = shadowNode.getSurfaceId(); @@ -86,14 +88,22 @@ void ViewTransitionModule::applyViewTransitionName( .size = layoutMetrics.frame.size, .pointScaleFactor = layoutMetrics.pointScaleFactor}; - nameRegistry_[tag].insert(name); - - // If applyViewTransitionName is called after transition started, this is the - // "new" state (end snapshot). Otherwise, this is the "old" state (start - // snapshot) - if (!transitionStarted_) { + // Calls outside mutationCallback are from the before-mutation phase (old + // state for an upcoming transition). Assign a provisional next transition ID + // so startViewTransitionEnd cleanup preserves these entries. + auto currentTransitionId = + insideMutationCallback_ ? activeTransitionId_ : activeTransitionId_ + 1; + nameRegistry_[tag].names.insert(name); + nameRegistry_[tag].transitionId = currentTransitionId; + + // Old state: called outside mutationCallback (before-mutation phase). + // New state: called inside mutationCallback (after-mutation phase). + if (!insideMutationCallback_) { AnimationKeyFrameView oldView{ - .layoutMetrics = keyframeMetrics, .tag = tag, .surfaceId = surfaceId}; + .layoutMetrics = keyframeMetrics, + .tag = tag, + .surfaceId = surfaceId, + .transitionId = currentTransitionId}; oldLayout_[name] = oldView; // Request the platform to capture a bitmap snapshot of the old view @@ -101,6 +111,10 @@ void ViewTransitionModule::applyViewTransitionName( if (uiManager_ != nullptr) { auto* delegate = uiManager_->getDelegate(); if (delegate != nullptr) { + TraceSection snapshotSection( + "ViewTransitionModule::applyViewTransitionName - uiManagerDidCaptureViewSnapshot", + "name", + name); delegate->uiManagerDidCaptureViewSnapshot(tag, surfaceId); } } @@ -111,6 +125,11 @@ void ViewTransitionModule::applyViewTransitionName( auto& pseudoElementsBySourceTag = it->second; auto innerIt = pseudoElementsBySourceTag.find(tag); + TraceSection clonePseudoElementSection( + "ViewTransitionModule::applyViewTransitionName - maybeClonePseudoElement", + "name", + name); + if (innerIt != pseudoElementsBySourceTag.end()) { // Only clone the pseudo-element if the layout metrics changed // since it was last created/refreshed (e.g. due to scrolling or @@ -147,7 +166,10 @@ void ViewTransitionModule::applyViewTransitionName( } else { AnimationKeyFrameView newView{ - .layoutMetrics = keyframeMetrics, .tag = tag, .surfaceId = surfaceId}; + .layoutMetrics = keyframeMetrics, + .tag = tag, + .surfaceId = surfaceId, + .transitionId = activeTransitionId_}; newLayout_[name] = newView; } } @@ -155,6 +177,8 @@ void ViewTransitionModule::applyViewTransitionName( void ViewTransitionModule::createViewTransitionInstance( const std::string& name, Tag pseudoElementTag) { + TraceSection s( + "ViewTransitionModule::createViewTransitionInstance", "name", name); if (uiManager_ == nullptr) { return; } @@ -209,6 +233,7 @@ RootShadowNode::Unshared ViewTransitionModule::shadowTreeWillCommit( if (oldPseudoElementNodes_.empty()) { return newRootShadowNode; } + TraceSection s("ViewTransitionModule::shadowTreeWillCommit"); auto surfaceId = shadowTree.getSurfaceId(); @@ -316,12 +341,14 @@ void ViewTransitionModule::cancelViewTransitionName( void ViewTransitionModule::restoreViewTransitionName( const ShadowNode& shadowNode) { - nameRegistry_[shadowNode.getTag()].merge( + nameRegistry_[shadowNode.getTag()].names.merge( cancelledNameRegistry_[shadowNode.getTag()]); cancelledNameRegistry_.erase(shadowNode.getTag()); } void ViewTransitionModule::applySnapshotsOnPseudoElementShadowNodes() { + TraceSection s( + "ViewTransitionModule::applySnapshotsOnPseudoElementShadowNodes"); if (oldPseudoElementNodes_.empty() || uiManager_ == nullptr) { return; } @@ -342,6 +369,7 @@ void ViewTransitionModule::applySnapshotsOnPseudoElementShadowNodes() { LayoutMetrics ViewTransitionModule::captureLayoutMetricsFromRoot( const ShadowNode& shadowNode) { + TraceSection s("ViewTransitionModule::captureLayoutMetricsFromRoot"); if (uiManager_ == nullptr) { return EmptyLayoutMetrics; } @@ -387,13 +415,22 @@ void ViewTransitionModule::startViewTransition( // Mark transition as started transitionStarted_ = true; + activeTransitionId_ = ++transitionIdCounter_; + + TraceSection s( + "ViewTransitionModule::startViewTransition", + "transitionId", + activeTransitionId_); + pendingAnimationIds_.clear(); onCompleteCallback_ = onCompleteCallback; // Call mutation callback (including commitRoot, measureInstance, // applyViewTransitionName, createViewTransitionInstance for old & new) if (mutationCallback) { + insideMutationCallback_ = true; mutationCallback(); + insideMutationCallback_ = false; } applySnapshotsOnPseudoElementShadowNodes(); @@ -442,13 +479,32 @@ void ViewTransitionModule::suspendOnActiveViewTransition() { } void ViewTransitionModule::startViewTransitionEnd() { - for (const auto& [tag, names] : nameRegistry_) { - for (const auto& name : names) { - oldLayout_.erase(name); - newLayout_.erase(name); + TraceSection s( + "ViewTransitionModule::startViewTransitionEnd", + "transitionId", + activeTransitionId_); + auto finishedId = activeTransitionId_; + + // Only clear layout and registry entries belonging to the finished + // transition. A suspended transition's before-mutation phase may have + // already written entries with a newer transitionId — preserve those. + for (auto it = nameRegistry_.begin(); it != nameRegistry_.end();) { + if (it->second.transitionId == finishedId) { + for (const auto& name : it->second.names) { + if (auto oit = oldLayout_.find(name); + oit != oldLayout_.end() && oit->second.transitionId == finishedId) { + oldLayout_.erase(oit); + } + if (auto nit = newLayout_.find(name); + nit != newLayout_.end() && nit->second.transitionId == finishedId) { + newLayout_.erase(nit); + } + } + it = nameRegistry_.erase(it); + } else { + ++it; } } - nameRegistry_.clear(); oldPseudoElementNodes_.clear(); // Clear any pending bitmap snapshots that were captured but never consumed. diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h index 28f7f2f86113..ea3583794881 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h @@ -98,17 +98,25 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate, AnimationKeyFrameViewLayoutMetrics layoutMetrics; Tag tag{0}; SurfaceId surfaceId{0}; + uint32_t transitionId{0}; }; private: + uint32_t transitionIdCounter_{0}; + uint32_t activeTransitionId_{0}; + // registry of layout of old/new views std::unordered_map oldLayout_{}; std::unordered_map newLayout_{}; - // tag -> names registry, populated during applyViewTransitionName + // tag -> (names, transitionId) registry, populated during applyViewTransitionName // Note that tag and name are not 1:1 mapping // - In some nested composition 2 names are mappped to the same tag // - tags of old and new views are mapped to the same name(s) - std::unordered_map> nameRegistry_{}; + struct NameRegistryEntry { + std::unordered_set names; + uint32_t transitionId{0}; + }; + std::unordered_map nameRegistry_{}; // used for cancel/restore viewTransitionName std::unordered_map> cancelledNameRegistry_{}; @@ -138,6 +146,8 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate, bool transitionStarted_{false}; + bool insideMutationCallback_{false}; + bool transitionReadyFinished_{false}; // When suspendNextTransition_ is true and a transition is active, the next diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 0bf9d70039b6..a07eb630de84 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 = { jsOnly: { ...testDefinitions.jsOnly, + animatedDeferStartOfTimingAnimations: { + defaultValue: false, + metadata: { + dateAdded: '2026-05-26', + description: + 'When enabled, native timing animations defer their first frame and re-anchor timing to prevent skipping initial frames when the UI thread is busy with layout work.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, animatedShouldDebounceQueueFlush: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/animated/__tests__/AnimatedNative-test.js b/packages/react-native/src/private/animated/__tests__/AnimatedNative-test.js index 88178f6fbd8e..7ba17f98b836 100644 --- a/packages/react-native/src/private/animated/__tests__/AnimatedNative-test.js +++ b/packages/react-native/src/private/animated/__tests__/AnimatedNative-test.js @@ -406,6 +406,7 @@ describe('Native Animated', () => { frames: expect.any(Array), toValue: expect.any(Number), iterations: 1, + deferredStart: false, }, expect.any(Function), ); @@ -1219,6 +1220,7 @@ describe('Native Animated', () => { frames: expect.any(Array), toValue: expect.any(Number), iterations: 1, + deferredStart: false, }, expect.any(Function), ); @@ -1360,6 +1362,7 @@ describe('Native Animated', () => { frames: expect.any(Array), toValue: expect.any(Number), iterations: 1, + deferredStart: false, }, expect.any(Function), ); diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 8b92e20f338b..a55d306356ee 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<<7f48f734cd7a098d04cb147980ef364a>> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -29,6 +29,7 @@ import { export type ReactNativeFeatureFlagsJsOnly = $ReadOnly<{ jsOnlyTestFlag: Getter, + animatedDeferStartOfTimingAnimations: Getter, animatedShouldDebounceQueueFlush: Getter, animatedShouldUseSingleOp: Getter, deferFlatListFocusChangeRenderUpdate: Getter, @@ -140,6 +141,11 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ */ export const jsOnlyTestFlag: Getter = createJavaScriptFlagGetter('jsOnlyTestFlag', false); +/** + * When enabled, native timing animations defer their first frame and re-anchor timing to prevent skipping initial frames when the UI thread is busy with layout work. + */ +export const animatedDeferStartOfTimingAnimations: Getter = createJavaScriptFlagGetter('animatedDeferStartOfTimingAnimations', false); + /** * Enables an experimental flush-queue debouncing in Animated.js. */ diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 2b4fbb3a4986..5bbaffe84b69 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -5453,6 +5453,7 @@ struct facebook::react::ViewTransitionModule::AnimationKeyFrameView { public facebook::react::SurfaceId surfaceId; public facebook::react::Tag tag; public facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics layoutMetrics; + public uint32_t transitionId; } struct facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics { diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 06d8694b4b82..f41430b9759f 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -5279,6 +5279,7 @@ struct facebook::react::ViewTransitionModule::AnimationKeyFrameView { public facebook::react::SurfaceId surfaceId; public facebook::react::Tag tag; public facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics layoutMetrics; + public uint32_t transitionId; } struct facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics { diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 1f7567f39773..b70b7f31f3e5 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -5444,6 +5444,7 @@ struct facebook::react::ViewTransitionModule::AnimationKeyFrameView { public facebook::react::SurfaceId surfaceId; public facebook::react::Tag tag; public facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics layoutMetrics; + public uint32_t transitionId; } struct facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics { diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index d4f7c124a3cb..008787a98b0c 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -8031,6 +8031,7 @@ struct facebook::react::ViewTransitionModule::AnimationKeyFrameView { public facebook::react::SurfaceId surfaceId; public facebook::react::Tag tag; public facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics layoutMetrics; + public uint32_t transitionId; } struct facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics { diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index cd89d347ccfa..c0dd7c1584c6 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -7506,6 +7506,7 @@ struct facebook::react::ViewTransitionModule::AnimationKeyFrameView { public facebook::react::SurfaceId surfaceId; public facebook::react::Tag tag; public facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics layoutMetrics; + public uint32_t transitionId; } struct facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics { diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 8219222a7c4d..6cc424cc9d43 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -8022,6 +8022,7 @@ struct facebook::react::ViewTransitionModule::AnimationKeyFrameView { public facebook::react::SurfaceId surfaceId; public facebook::react::Tag tag; public facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics layoutMetrics; + public uint32_t transitionId; } struct facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics { diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 5f68b22fb882..2e43b23d2fc1 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -3892,6 +3892,7 @@ struct facebook::react::ViewTransitionModule::AnimationKeyFrameView { public facebook::react::SurfaceId surfaceId; public facebook::react::Tag tag; public facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics layoutMetrics; + public uint32_t transitionId; } struct facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics { diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 81705abe51bd..e9700e2725df 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -3758,6 +3758,7 @@ struct facebook::react::ViewTransitionModule::AnimationKeyFrameView { public facebook::react::SurfaceId surfaceId; public facebook::react::Tag tag; public facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics layoutMetrics; + public uint32_t transitionId; } struct facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics { diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 29810fb16f09..bf1acdc6eb1a 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -3883,6 +3883,7 @@ struct facebook::react::ViewTransitionModule::AnimationKeyFrameView { public facebook::react::SurfaceId surfaceId; public facebook::react::Tag tag; public facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics layoutMetrics; + public uint32_t transitionId; } struct facebook::react::ViewTransitionModule::AnimationKeyFrameViewLayoutMetrics {