From af5641496ea17f47f55ea34514731dcff2f41a1c Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 21 May 2026 14:59:57 -0700 Subject: [PATCH] Defer animation start time in FrameAnimationDriver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: ## Changelog: [Internal] [Fixed] - Defer animation start time in FrameAnimationDriver **Problem**: In complex apps, if animation is started in commit phase (the case if animation starts in useLayoutEffect, or from ViewTransition event handlers), it'll skip initial frames — the user sees the animation snap to an intermediate position. This happens because `FrameAnimationDriver` anchors its start time on the first `runAnimationStep` call, but the UI thread may be busy with layout/mount work for several frames before the view actually composites. The elapsed wall-clock time advances, causing `frameIndex` to jump ahead. **Why**: `startFrameTimeMs_` is set to the Choreographer frame time on the first tick. If the UI thread is blocked processing a heavy tree (many views mounting), subsequent ticks arrive much later — `timeDeltaMs` jumps and the animation skips to a mid-point. - Every major framework solves this: Flutter uses lazy start (`_startTime ??= timeStamp` on first actual tick), Android native uses `CALLBACK_COMMIT` to adjust post-traversal, and CSS View Transitions spec defers start until post-composite. **Fix**: On the very first `update()` call, output the starting value (frame 0) and reset `startFrameTimeMs_ = -1`. This causes the base class to re-anchor on the next `runAnimationStep`, so elapsed time is measured from the first frame that has actually been rendered — not from when `startAnimatingNode` was dispatched. The flag disables itself after one use, so all subsequent frames use pure elapsed-time with no behavioral change. Differential Revision: D106007152 --- .../animated/drivers/FrameAnimationDriver.cpp | 20 ++++++++++++++++++- .../animated/drivers/FrameAnimationDriver.h | 1 + 2 files changed, 20 insertions(+), 1 deletion(-) 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..c66dcf80b071 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,33 @@ void FrameAnimationDriver::onConfigChanged() { frames_.push_back(frameValue); } toValue_ = config_["toValue"].asDouble(); + deferredStart_ = true; } -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..5c2933b44a2b 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_{true}; }; } // namespace facebook::react