Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<ValueAnimatedNode>(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<size_t>(std::round(timeDeltaMs / SingleFrameIntervalMs));
assert(startIndex >= 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class FrameAnimationDriver : public AnimationDriver {
std::vector<double> frames_{};
double toValue_{0};
std::optional<double> startValue_{};
bool deferredStart_{false};
};

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading