Skip to content

Commit ade19e8

Browse files
sammy-SCmeta-codesync[bot]
authored andcommitted
small bugs fixes in C++ Animated that were not observed in production (facebook#54795)
Summary: changelog: [internal] Pull Request resolved: facebook#54795 Fix five bugs in C++ Animated that could cause incorrect animation behavior, memory issues, and graph traversal failures: - Fixed duplicate condition check in BFS graph traversal that prevented proper node visitation - Fixed RoundAnimatedNode reading wrong config key for the rounding factor - Fixed setOffset returning true even when value unchanged, causing unnecessary updates - Fixed FrameAnimationDriver accumulating frames on reconfiguration instead of clearing - Removed unreachable dead code in ColorAnimatedNode Reviewed By: zeyap Differential Revision: D88362078 fbshipit-source-id: 50b5cbf7ecb83f1a098498a752e2cb2297657dd8
1 parent d04e152 commit ade19e8

File tree

7 files changed

+131
-4
lines changed

7 files changed

+131
-4
lines changed

packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,8 @@ void NativeAnimatedNodesManager::updateNodes(
723723
for (auto childTag : nextNode.node->getChildren()) {
724724
auto child = getAnimatedNode<AnimatedNode>(childTag);
725725
child->activeIncomingNodes--;
726-
if (child->activeIncomingNodes == 0 && child->activeIncomingNodes == 0) {
726+
if (child->activeIncomingNodes == 0 &&
727+
child->bfsColor != animatedGraphBFSColor_) {
727728
child->bfsColor = animatedGraphBFSColor_;
728729
#ifdef REACT_NATIVE_DEBUG
729730
updatedNodesCount++;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ void FrameAnimationDriver::updateConfig(folly::dynamic config) {
4343
void FrameAnimationDriver::onConfigChanged() {
4444
auto frames = config_["frames"];
4545
react_native_assert(frames.type() == folly::dynamic::ARRAY);
46+
frames_.clear();
4647
for (const auto& frame : frames) {
4748
auto frameValue = frame.asDouble();
4849
frames_.push_back(frameValue);

packages/react-native/ReactCommon/react/renderer/animated/nodes/ColorAnimatedNode.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ Color ColorAnimatedNode::getColor() {
6262
manager_->updatedNodeTags_.erase(tag_);
6363
}
6464
return color_;
65-
return 0;
6665
}
6766

6867
} // namespace facebook::react

packages/react-native/ReactCommon/react/renderer/animated/nodes/RoundAnimatedNode.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ RoundAnimatedNode::RoundAnimatedNode(
2222
NativeAnimatedNodesManager& manager)
2323
: ValueAnimatedNode(tag, config, manager),
2424
inputNodeTag_(static_cast<Tag>(getConfig()["input"].asInt())),
25-
nearest_(getConfig()["input"].asDouble()) {
25+
nearest_(getConfig()["nearest"].asDouble()) {
2626
react_native_assert(
2727
nearest_ != 0 &&
2828
"'nearest' cannot be 0 (can't round to the nearest multiple of 0)");

packages/react-native/ReactCommon/react/renderer/animated/nodes/ValueAnimatedNode.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ bool ValueAnimatedNode::setOffset(double offset) noexcept {
5555
offset_ = offset;
5656
return true;
5757
}
58-
return true;
58+
return false;
5959
}
6060

6161
double ValueAnimatedNode::getValue() const noexcept {

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,73 @@ TEST_F(AnimatedNodeTests, DiffClampAnimatedNode) {
215215
EXPECT_EQ(nodesManager_->getValue(diffClampTag), 1);
216216
}
217217

218+
TEST_F(AnimatedNodeTests, RoundAnimatedNodeUsesNearestConfigKey) {
219+
// This test verifies that RoundAnimatedNode reads the "nearest" config key
220+
// for the rounding factor, not the "input" key.
221+
initNodesManager();
222+
223+
auto rootTag = getNextRootViewTag();
224+
225+
auto valueTag = ++rootTag;
226+
auto roundTag = ++rootTag;
227+
228+
nodesManager_->createAnimatedNode(
229+
valueTag,
230+
folly::dynamic::object("type", "value")("value", 7.3)("offset", 0));
231+
232+
// The round node should read "nearest" for the rounding factor (5.0),
233+
// not "input" (which is the valueTag integer).
234+
nodesManager_->createAnimatedNode(
235+
roundTag,
236+
folly::dynamic::object("type", "round")("input", valueTag)(
237+
"nearest", 5.0));
238+
nodesManager_->connectAnimatedNodes(valueTag, roundTag);
239+
240+
runAnimationFrame(0);
241+
242+
// 7.3 rounded to nearest 5.0 should be 5.0 (since round(7.3/5) * 5 = 1 * 5)
243+
// If the bug existed (reading "input" instead of "nearest"), it would use
244+
// the valueTag as the rounding factor, giving incorrect results.
245+
EXPECT_DOUBLE_EQ(nodesManager_->getValue(roundTag).value(), 5.0);
246+
247+
// Test another value to ensure rounding works correctly
248+
nodesManager_->setAnimatedNodeValue(valueTag, 12.6);
249+
runAnimationFrame(0);
250+
251+
// 12.6 rounded to nearest 5.0 should be 15.0 (since round(12.6/5) * 5 = 3 *
252+
// 5)
253+
EXPECT_DOUBLE_EQ(nodesManager_->getValue(roundTag).value(), 15.0);
254+
}
255+
256+
TEST_F(AnimatedNodeTests, SetOffsetReturnsFalseWhenUnchanged) {
257+
// This test verifies that setAnimatedNodeOffset doesn't trigger unnecessary
258+
// updates when the offset value hasn't changed.
259+
initNodesManager();
260+
261+
auto rootTag = getNextRootViewTag();
262+
auto valueTag = ++rootTag;
263+
264+
nodesManager_->createAnimatedNode(
265+
valueTag,
266+
folly::dynamic::object("type", "value")("value", 10)("offset", 0));
267+
268+
runAnimationFrame(0);
269+
EXPECT_EQ(nodeNeedsUpdate(valueTag), false);
270+
271+
// First setOffset should mark the node as needing update
272+
nodesManager_->setAnimatedNodeOffset(valueTag, 5);
273+
EXPECT_EQ(nodeNeedsUpdate(valueTag), true);
274+
EXPECT_EQ(nodesManager_->getValue(valueTag), 15); // 10 + 5
275+
276+
runAnimationFrame(0);
277+
EXPECT_EQ(nodeNeedsUpdate(valueTag), false);
278+
279+
// Setting the same offset again should NOT mark the node as needing update
280+
nodesManager_->setAnimatedNodeOffset(valueTag, 5);
281+
EXPECT_EQ(nodeNeedsUpdate(valueTag), false); // No change, no update needed
282+
EXPECT_EQ(nodesManager_->getValue(valueTag), 15); // Still 10 + 5
283+
}
284+
218285
TEST_F(AnimatedNodeTests, ObjectAnimatedNode) {
219286
initNodesManager();
220287

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,63 @@ TEST_F(AnimationDriverTests, framesAnimation) {
5858
EXPECT_EQ(round(nodesManager_->getValue(valueNodeTag).value()), toValue);
5959
}
6060

61+
TEST_F(AnimationDriverTests, framesAnimationReconfigurationClearsFrames) {
62+
// This test verifies that when an animation is reconfigured via updateConfig,
63+
// the frames array is cleared before adding new frames. Without clearing,
64+
// frames would accumulate and cause incorrect animation behavior.
65+
initNodesManager();
66+
67+
auto rootTag = getNextRootViewTag();
68+
69+
auto valueNodeTag = ++rootTag;
70+
nodesManager_->createAnimatedNode(
71+
valueNodeTag,
72+
folly::dynamic::object("type", "value")("value", 0)("offset", 0));
73+
74+
const auto animationId = 1;
75+
// First animation: 5 frames from 0 to 100
76+
const auto frames1 = folly::dynamic::array(0.0f, 0.25f, 0.5f, 0.75f, 1.0f);
77+
const auto toValue1 = 100;
78+
nodesManager_->startAnimatingNode(
79+
animationId,
80+
valueNodeTag,
81+
folly::dynamic::object("type", "frames")("frames", frames1)(
82+
"toValue", toValue1),
83+
std::nullopt);
84+
85+
const double startTimeInTick = 12345;
86+
87+
// Run first frame
88+
runAnimationFrame(startTimeInTick);
89+
EXPECT_EQ(round(nodesManager_->getValue(valueNodeTag).value()), 0);
90+
91+
// Reconfigure the same animation (same animationId) with new frames
92+
// This triggers updateConfig on the existing FrameAnimationDriver
93+
const auto frames2 = folly::dynamic::array(0.0f, 0.5f, 1.0f);
94+
const auto toValue2 = 200;
95+
nodesManager_->startAnimatingNode(
96+
animationId,
97+
valueNodeTag,
98+
folly::dynamic::object("type", "frames")("frames", frames2)(
99+
"toValue", toValue2),
100+
std::nullopt);
101+
102+
// Reset animation timing
103+
const double newStartTimeInTick = 20000;
104+
105+
// Run animation at halfway point (1 frame into 3-frame animation)
106+
runAnimationFrame(newStartTimeInTick);
107+
runAnimationFrame(newStartTimeInTick + SingleFrameIntervalMs * 1);
108+
109+
// At frame 1 of 3 frames (50% progress), value should be approximately:
110+
// startValue (0) + 0.5 * (toValue2 - startValue) = 0 + 0.5 * 200 = 100
111+
// If frames accumulated (5 + 3 = 8 frames), we'd be at wrong position
112+
// Use ceil rounding so 100.00x becomes 100.01
113+
EXPECT_EQ(round(nodesManager_->getValue(valueNodeTag).value()), 100.01);
114+
115+
// Complete the animation
116+
runAnimationFrame(newStartTimeInTick + SingleFrameIntervalMs * 2);
117+
EXPECT_EQ(round(nodesManager_->getValue(valueNodeTag).value()), toValue2);
118+
}
119+
61120
} // namespace facebook::react

0 commit comments

Comments
 (0)