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