Skip to content

Commit d34f078

Browse files
zeyapfacebook-github-bot
authored andcommitted
Defer animation start time in FrameAnimationDriver (#56929)
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
1 parent f131c53 commit d34f078

3 files changed

Lines changed: 69 additions & 1 deletion

File tree

packages/react-native/ReactCommon/react/renderer/animated/drivers/FrameAnimationDriver.cpp

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,34 @@ void FrameAnimationDriver::onConfigChanged() {
4949
frames_.push_back(frameValue);
5050
}
5151
toValue_ = config_["toValue"].asDouble();
52+
auto deferIt = config_.find("deferredStart");
53+
deferredStart_ = deferIt != config_.items().end() && deferIt->second.asBool();
5254
}
5355

54-
bool FrameAnimationDriver::update(double timeDeltaMs, bool /*restarting*/) {
56+
bool FrameAnimationDriver::update(double timeDeltaMs, bool restarting) {
5557
if (auto node =
5658
manager_->getAnimatedNode<ValueAnimatedNode>(animatedValueTag_)) {
5759
if (!startValue_) {
5860
startValue_ = node->getRawValue();
5961
}
6062

63+
if (deferredStart_ && restarting) {
64+
// On the very first update after start: output the starting value
65+
// (frame 0) and defer the time anchor. The base class will re-anchor
66+
// startFrameTimeMs_ on the next call, so elapsed time is measured
67+
// from the first frame that has actually been rendered — not from
68+
// when startAnimatingNode was dispatched.
69+
//
70+
// This prevents skipping initial frames when the UI thread is busy
71+
// with layout/mount work between animation start and first composite.
72+
node->setRawValue(
73+
startValue_.value() + frames_[0] * (toValue_ - startValue_.value()));
74+
markNodeUpdated(node->tag());
75+
startFrameTimeMs_ = -1;
76+
deferredStart_ = false;
77+
return false;
78+
}
79+
6180
const auto startIndex =
6281
static_cast<size_t>(std::round(timeDeltaMs / SingleFrameIntervalMs));
6382
assert(startIndex >= 0);

packages/react-native/ReactCommon/react/renderer/animated/drivers/FrameAnimationDriver.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class FrameAnimationDriver : public AnimationDriver {
3535
std::vector<double> frames_{};
3636
double toValue_{0};
3737
std::optional<double> startValue_{};
38+
bool deferredStart_{false};
3839
};
3940

4041
} // namespace facebook::react

packages/react-native/ReactCommon/react/renderer/animated/tests/AnimationDriverTests.cpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,52 @@ TEST_F(AnimationDriverTests, framesAnimationReconfigurationClearsFrames) {
117117
EXPECT_EQ(round(nodesManager_->getValue(valueNodeTag).value()), toValue2);
118118
}
119119

120+
TEST_F(AnimationDriverTests, framesAnimationDeferredStart) {
121+
// Deferred start outputs frame 0 on the first update and re-anchors
122+
// startFrameTimeMs_ so the second update also sees timeDelta=0.
123+
// Without the defer the second frame would already be at value 25.
124+
initNodesManager();
125+
126+
auto rootTag = getNextRootViewTag();
127+
128+
auto valueNodeTag = ++rootTag;
129+
nodesManager_->createAnimatedNode(
130+
valueNodeTag,
131+
folly::dynamic::object("type", "value")("value", 0)("offset", 0));
132+
133+
const auto animationId = 1;
134+
const auto frames = folly::dynamic::array(0.0f, 0.25f, 0.5f, 0.75f, 1.0f);
135+
const auto toValue = 100;
136+
nodesManager_->startAnimatingNode(
137+
animationId,
138+
valueNodeTag,
139+
folly::dynamic::object("type", "frames")("frames", frames)(
140+
"toValue", toValue)("deferredStart", true),
141+
std::nullopt);
142+
143+
const double t = 12345;
144+
145+
// Frame 1: both with and without deferredStart, timeDelta=0 → value=0
146+
runAnimationFrame(t);
147+
EXPECT_EQ(nodesManager_->getValue(valueNodeTag).value(), 0);
148+
149+
// Frame 2: WITHOUT deferredStart timeDelta=SI → value≈25.
150+
// WITH deferredStart the deferred start re-anchored startFrameTimeMs_, so
151+
// timeDelta=0 → value=0. This assertion fails without deferredStart.
152+
runAnimationFrame(t + SingleFrameIntervalMs);
153+
EXPECT_EQ(nodesManager_->getValue(valueNodeTag).value(), 0);
154+
155+
// Frame 3: now timeDelta=SI from the re-anchored start
156+
runAnimationFrame(t + SingleFrameIntervalMs * 2);
157+
EXPECT_NEAR(nodesManager_->getValue(valueNodeTag).value(), 25, 0.01);
158+
159+
// Frame 4
160+
runAnimationFrame(t + SingleFrameIntervalMs * 3);
161+
EXPECT_NEAR(nodesManager_->getValue(valueNodeTag).value(), 50, 0.01);
162+
163+
// Complete
164+
runAnimationFrame(t + SingleFrameIntervalMs * 5);
165+
EXPECT_EQ(nodesManager_->getValue(valueNodeTag).value(), toValue);
166+
}
167+
120168
} // namespace facebook::react

0 commit comments

Comments
 (0)