Skip to content

Commit bfbece4

Browse files
chore: Prepare platform-routed CSS transitions for more properties (#9654)
## Summary Routes CSS transition properties to Core Animation behind `IOS_CSS_CORE_ANIMATION`, starting with `opacity`. - `CSSPlatformTransitionProxy` is a stateless shared router: per property it picks the platform or the C++ loop and forwards the raw JS value to the platform, never holding a platform value type. - The platform side is injected as hooks (canRoute / apply / remove), so shared C++ stays platform-agnostic and Android can implement the same contract later. - iOS implements them in `REACSSPlatformProps` (routing predicate, value parsing, CA conversions, timing) and `REACSSPlatformTransitions` (the CALayer applier). With the flag off there is no behavior change. ## Test plan - `example-ios-build-check` green - opacity transitions verified running on Core Animation in the simulator - `-fsyntax-only`, clang-tidy and clang-format clean --------- Co-authored-by: Jakub Wiśniewski <119816982+wisniewskij@users.noreply.github.com>
1 parent 6d6aa87 commit bfbece4

19 files changed

Lines changed: 603 additions & 458 deletions

packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSColor.cpp

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include <reanimated/CSS/common/values/CSSColor.h>
22
#include <reanimated/CSS/svg/values/SVGBrush.h>
3+
#include <reanimated/CSS/utils/props.h>
34

45
#include <utility>
56

@@ -17,24 +18,7 @@ CSSColorBase<TColorType, TDerived>::CSSColorBase(TColorType colorType) : channel
1718

1819
template <ColorTypeEnum TColorType, typename TDerived>
1920
CSSColorBase<TColorType, TDerived>::CSSColorBase(int64_t numberValue)
20-
: channels{0, 0, 0, 0}, colorType(TColorType::Rgba) {
21-
uint32_t color;
22-
// On Android, colors are represented as signed 32-bit integers. In JS, we use
23-
// a bitwise operation (normalizedColor = normalizedColor | 0x0) to ensure the
24-
// value is treated as a signed int, causing numbers above 2^31 to become
25-
// negative. To correctly interpret these in C++, we cast negative values to
26-
// int32_t to preserve their bit pattern, then assign to uint32_t. This wraps
27-
// the bits (modulo 2^32), effectively reversing the JS-side bit shift.
28-
if (numberValue < 0) {
29-
color = static_cast<int32_t>(numberValue);
30-
} else {
31-
color = static_cast<uint32_t>(numberValue);
32-
}
33-
channels[0] = (color >> 16) & 0xFF; // Red
34-
channels[1] = (color >> 8) & 0xFF; // Green
35-
channels[2] = color & 0xFF; // Blue
36-
channels[3] = (color >> 24) & 0xFF; // Alpha
37-
}
21+
: channels(extractColorChannels(numberValue)), colorType(TColorType::Rgba) {}
3822

3923
template <ColorTypeEnum TColorType, typename TDerived>
4024
CSSColorBase<TColorType, TDerived>::CSSColorBase(bool value)

packages/react-native-reanimated/Common/cpp/reanimated/CSS/core/transition/CSSPlatformTransition.cpp

Lines changed: 0 additions & 91 deletions
This file was deleted.

packages/react-native-reanimated/Common/cpp/reanimated/CSS/core/transition/CSSPlatformTransition.h

Lines changed: 0 additions & 58 deletions
This file was deleted.
Lines changed: 95 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#include <reanimated/CSS/core/transition/CSSPlatformTransitionProxy.h>
2-
#include <reanimated/Tools/FeatureFlags.h>
32

43
#include <react/debug/react_native_assert.h>
54

@@ -9,23 +8,36 @@ namespace reanimated::css {
98

109
CSSPlatformTransitionProxy::CSSPlatformTransitionProxy(
1110
CSSCanRoutePropertyFunction canRoute,
12-
CSSApplyTransitionFunction applyTransition,
11+
CSSApplyTransitionJSIFunction applyJSI,
12+
CSSApplyTransitionDynamicFunction applyDynamic,
1313
CSSRemoveTransitionFunction removeTransition)
1414
: canRoute_(std::move(canRoute)),
15-
applyTransition_(std::move(applyTransition)),
15+
applyJSI_(std::move(applyJSI)),
16+
applyDynamic_(std::move(applyDynamic)),
1617
removeTransition_(std::move(removeTransition)) {}
1718

1819
bool CSSPlatformTransitionProxy::canRoute(const std::string &propertyName, const EasingConfig &easing) const {
19-
if constexpr (!StaticFeatureFlags::getFlag("IOS_CSS_CORE_ANIMATION")) {
20-
return false;
21-
}
2220
return canRoute_ && canRoute_(propertyName, easing);
2321
}
2422

25-
void CSSPlatformTransitionProxy::run(const CSSPlatformTransitionPropertyConfig &config) const {
26-
if (applyTransition_) {
27-
applyTransition_(config);
28-
}
23+
bool CSSPlatformTransitionProxy::apply(
24+
jsi::Runtime &rt,
25+
const Tag viewTag,
26+
const std::string &propertyName,
27+
const jsi::Value &fromValue,
28+
const jsi::Value &toValue,
29+
const CSSTransitionPropertySettings &settings,
30+
const double timestamp) const {
31+
return applyJSI_ && applyJSI_(rt, viewTag, propertyName, fromValue, toValue, settings, timestamp);
32+
}
33+
34+
bool CSSPlatformTransitionProxy::apply(
35+
const Tag viewTag,
36+
const std::string &propertyName,
37+
const folly::dynamic &fromValue,
38+
const folly::dynamic &toValue,
39+
const double timestamp) const {
40+
return applyDynamic_ && applyDynamic_(viewTag, propertyName, fromValue, toValue, timestamp);
2941
}
3042

3143
void CSSPlatformTransitionProxy::remove(const Tag viewTag, const std::string &propertyName) const {
@@ -34,67 +46,92 @@ void CSSPlatformTransitionProxy::remove(const Tag viewTag, const std::string &pr
3446
}
3547
}
3648

37-
// Splits the new split-shape config into loop / platform buckets. result.routing
38-
// starts as a copy of the previous call's routing and is updated as we route
39-
// each prop; erasing from the *other* side's set returns nonzero exactly when
40-
// the prop is migrating sides - that's how we emit cancels.
41-
CSSPlatformTransitionProxy::ProcessedConfig CSSPlatformTransitionProxy::processConfig(
42-
CSSTransitionConfig &&config,
43-
const CSSTransitionRouting &previousRouting) const {
44-
ProcessedConfig result;
45-
result.routing = previousRouting;
46-
47-
// Drain changedPropertiesSettings; for each, decide routing and bucket.
48-
// extract() preserves move-only PropertyValueDiff (jsi::Value pair).
49-
while (!config.changedPropertiesSettings.empty()) {
50-
auto settingsNode = config.changedPropertiesSettings.extract(config.changedPropertiesSettings.begin());
51-
const auto &propertyName = settingsNode.key();
52-
const auto &settings = settingsNode.mapped();
49+
CSSTransitionConfig CSSPlatformTransitionProxy::processConfig(
50+
jsi::Runtime &rt,
51+
const Tag viewTag,
52+
const CSSTransitionConfig &config,
53+
CSSTransitionRouting &routing,
54+
const double timestamp) const {
55+
CSSTransitionConfig loopConfig;
56+
size_t matchedValues = 0;
57+
58+
for (const auto &[propertyName, settings] : config.changedPropertiesSettings) {
5359
const auto valueIt = config.changedProperties.find(propertyName);
60+
const bool hasValue = valueIt != config.changedProperties.end();
61+
if (hasValue) {
62+
++matchedValues;
63+
}
5464

55-
if (canRoute(propertyName, settings.easingConfig)) {
56-
// loop -> platform migration: cancel on loop.
57-
if (result.routing.loop.erase(propertyName) > 0) {
58-
result.loop.removedProperties.push_back(propertyName);
59-
}
60-
result.routing.platform.insert(propertyName);
65+
bool routable = canRoute(propertyName, settings.easingConfig);
66+
if (routable && hasValue) {
67+
routable = apply(rt, viewTag, propertyName, valueIt->second.first, valueIt->second.second, settings, timestamp);
68+
} else if (routable) {
69+
// Settings-only: stay on the platform only if already animating there.
70+
routable = routing.platform.contains(propertyName);
71+
}
6172

62-
if (valueIt != config.changedProperties.end()) {
63-
auto valueNode = config.changedProperties.extract(valueIt);
64-
result.platform.changedProperties.push_back(
65-
CSSPlatformTransitionRawEntry{propertyName, std::move(valueNode.mapped()), settings});
73+
if (routable) {
74+
// loop -> platform migration cancels on the loop side.
75+
if (routing.loop.erase(propertyName) > 0) {
76+
loopConfig.removedProperties.push_back(propertyName);
6677
}
67-
result.platform.changedPropertiesSettings.insert(std::move(settingsNode));
78+
routing.platform.insert(propertyName);
6879
} else {
69-
// platform -> loop migration: cancel on platform.
70-
if (result.routing.platform.erase(propertyName) > 0) {
71-
result.platform.removedProperties.push_back(propertyName);
80+
// platform -> loop migration cancels on the platform side.
81+
if (routing.platform.erase(propertyName) > 0) {
82+
remove(viewTag, propertyName);
7283
}
73-
result.routing.loop.insert(propertyName);
74-
75-
if (valueIt != config.changedProperties.end()) {
76-
auto valueNode = config.changedProperties.extract(valueIt);
77-
result.loop.changedProperties.insert(std::move(valueNode));
84+
routing.loop.insert(propertyName);
85+
if (hasValue) {
86+
loopConfig.changedProperties.emplace(
87+
propertyName,
88+
std::make_pair(jsi::Value(rt, valueIt->second.first), jsi::Value(rt, valueIt->second.second)));
7889
}
79-
result.loop.changedPropertiesSettings.insert(std::move(settingsNode));
90+
loopConfig.changedPropertiesSettings.emplace(propertyName, settings);
8091
}
8192
}
8293

83-
// The parser pairs every value diff with settings, so the drain above must
84-
// have consumed all of changedProperties.
85-
react_native_assert(config.changedProperties.empty() && "[Reanimated] CSS transition value diff without settings");
86-
87-
// Props JS asked to stop transitioning: look up the owning side in routing
88-
// and forward the cancel there.
89-
for (auto &propertyName : config.removedProperties) {
90-
if (result.routing.platform.erase(propertyName) > 0) {
91-
result.platform.removedProperties.push_back(std::move(propertyName));
92-
} else if (result.routing.loop.erase(propertyName) > 0) {
93-
result.loop.removedProperties.push_back(std::move(propertyName));
94+
// The parser pairs every value diff with settings, so all must have matched one.
95+
react_native_assert(
96+
matchedValues == config.changedProperties.size() && "[Reanimated] CSS transition value diff without settings");
97+
98+
for (const auto &propertyName : config.removedProperties) {
99+
if (routing.platform.erase(propertyName) > 0) {
100+
remove(viewTag, propertyName);
101+
} else if (routing.loop.erase(propertyName) > 0) {
102+
loopConfig.removedProperties.push_back(propertyName);
94103
}
95104
}
96105

97-
return result;
106+
return loopConfig;
107+
}
108+
109+
PropertyValueDynamicDiffsMap CSSPlatformTransitionProxy::processDynamicDiffs(
110+
const Tag viewTag,
111+
const PropertyValueDynamicDiffsMap &propertyDiffs,
112+
CSSTransitionRouting &routing,
113+
const double timestamp) const {
114+
PropertyValueDynamicDiffsMap loopDiffs;
115+
for (const auto &[propertyName, propertyDiff] : propertyDiffs) {
116+
// A platform-routed property keeps animating natively while the platform can
117+
// still express the toggled value; otherwise it migrates to the loop.
118+
if (routing.platform.contains(propertyName)) {
119+
if (apply(viewTag, propertyName, propertyDiff.first, propertyDiff.second, timestamp)) {
120+
continue;
121+
}
122+
routing.platform.erase(propertyName);
123+
remove(viewTag, propertyName);
124+
routing.loop.insert(propertyName);
125+
}
126+
loopDiffs.emplace(propertyName, propertyDiff);
127+
}
128+
return loopDiffs;
129+
}
130+
131+
void CSSPlatformTransitionProxy::cancelAll(const Tag viewTag, const TransitionProperties &properties) const {
132+
for (const auto &propertyName : properties) {
133+
remove(viewTag, propertyName);
134+
}
98135
}
99136

100137
} // namespace reanimated::css

0 commit comments

Comments
 (0)