Compare commits

...
3 Commits
Author SHA1 Message Date
Bartlomiej BloniarzandFacebook GitHub Bot 3a7ef613d5 Expose the backend via UIManager
Summary:
### Summary

Exposes the animation backend via UIManager by introducing two new methods: `unstable_setAnimationBackend` and `unstable_getAnimationBackend`. The `NativeAnimatedNodesManagerProvider` is updated to initialize the animation backend using the `useSharedAnimatedBackend` feature flag.

Reviewed By: zeyap

Differential Revision: D81137954
2025-09-30 07:18:13 -07:00
Bartlomiej BloniarzandFacebook GitHub Bot 41d856c40f Init the backend closer to the uiManager, Make the test pass again
Differential Revision: D81138133
2025-09-30 06:22:40 -07:00
Bartlomiej BloniarzandFacebook GitHub Bot c8e1f8ccba boostrap shared animation backend
Reviewed By: zeyap, sammy-SC

Differential Revision: D80809364
2025-09-30 06:22:40 -07:00
33 changed files with 509 additions and 30 deletions
@@ -0,0 +1,75 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @fantom_flags useSharedAnimatedBackend:true
* @flow strict-local
* @format
*/
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
import type {HostInstance} from 'react-native';
import ensureInstance from '../../../src/private/__tests__/utilities/ensureInstance';
import * as Fantom from '@react-native/fantom';
import {createRef} from 'react';
import {Animated, useAnimatedValue} from 'react-native';
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
test('animated opacity', () => {
let _opacity;
let _opacityAnimation;
const viewRef = createRef<HostInstance>();
function MyApp() {
const opacity = useAnimatedValue(1);
_opacity = opacity;
return (
<Animated.View
ref={viewRef}
style={[
{
width: 100,
height: 100,
opacity: opacity,
},
]}
/>
);
}
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<MyApp />);
});
const viewElement = ensureInstance(viewRef.current, ReactNativeElement);
expect(viewElement.getBoundingClientRect().x).toBe(0);
Fantom.runTask(() => {
_opacityAnimation = Animated.timing(_opacity, {
toValue: 0,
duration: 30,
useNativeDriver: true,
}).start();
});
Fantom.unstable_produceFramesForDuration(30);
expect(Fantom.unstable_getDirectManipulationProps(viewElement).opacity).toBe(
0,
);
// TODO: this shouldn't be neccessary since animation should be stopped after duration
Fantom.runTask(() => {
_opacityAnimation?.stop();
});
expect(root.getRenderedOutput({props: ['opacity']}).toJSX()).toEqual(
<rn-view opacity="0" />,
);
});
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<f707e26d09b6f7962ec97296a1a215b6>>
* @generated SignedSource<<8af0aea8bbf4cffaad7d312032bef0e4>>
*/
/**
@@ -456,6 +456,12 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun useShadowNodeStateOnClone(): Boolean = accessor.useShadowNodeStateOnClone()
/**
* Use shared animation backend in C++ Animated
*/
@JvmStatic
public fun useSharedAnimatedBackend(): Boolean = accessor.useSharedAnimatedBackend()
/**
* In Bridgeless mode, should legacy NativeModules use the TurboModule system?
*/
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<e7cfc5135c7b3c731324297e92e41e3f>>
* @generated SignedSource<<5c2aa1e15e7fbe129b8f774d2dc7be70>>
*/
/**
@@ -91,6 +91,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
private var useOptimizedEventBatchingOnAndroidCache: Boolean? = null
private var useRawPropsJsiValueCache: Boolean? = null
private var useShadowNodeStateOnCloneCache: Boolean? = null
private var useSharedAnimatedBackendCache: Boolean? = null
private var useTurboModuleInteropCache: Boolean? = null
private var useTurboModulesCache: Boolean? = null
private var viewCullingOutsetRatioCache: Double? = null
@@ -736,6 +737,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
return cached
}
override fun useSharedAnimatedBackend(): Boolean {
var cached = useSharedAnimatedBackendCache
if (cached == null) {
cached = ReactNativeFeatureFlagsCxxInterop.useSharedAnimatedBackend()
useSharedAnimatedBackendCache = cached
}
return cached
}
override fun useTurboModuleInterop(): Boolean {
var cached = useTurboModuleInteropCache
if (cached == null) {
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<3254fef626b10ef21d8d9ee1bdbb1880>>
* @generated SignedSource<<0bd8796f4580322a78690ec4469c07ce>>
*/
/**
@@ -170,6 +170,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
@DoNotStrip @JvmStatic public external fun useShadowNodeStateOnClone(): Boolean
@DoNotStrip @JvmStatic public external fun useSharedAnimatedBackend(): Boolean
@DoNotStrip @JvmStatic public external fun useTurboModuleInterop(): Boolean
@DoNotStrip @JvmStatic public external fun useTurboModules(): Boolean
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<9f7eb1bb5e24fd8ee87accf60209cce3>>
* @generated SignedSource<<7c7796cbe2722f0f6e5b2cc4ed43a0d5>>
*/
/**
@@ -165,6 +165,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
override fun useShadowNodeStateOnClone(): Boolean = false
override fun useSharedAnimatedBackend(): Boolean = false
override fun useTurboModuleInterop(): Boolean = false
override fun useTurboModules(): Boolean = false
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<9e6e04ca37edd1ad9265b74e251ff4de>>
* @generated SignedSource<<f3420de2307aab53b885fa491183d84b>>
*/
/**
@@ -95,6 +95,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
private var useOptimizedEventBatchingOnAndroidCache: Boolean? = null
private var useRawPropsJsiValueCache: Boolean? = null
private var useShadowNodeStateOnCloneCache: Boolean? = null
private var useSharedAnimatedBackendCache: Boolean? = null
private var useTurboModuleInteropCache: Boolean? = null
private var useTurboModulesCache: Boolean? = null
private var viewCullingOutsetRatioCache: Double? = null
@@ -811,6 +812,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
return cached
}
override fun useSharedAnimatedBackend(): Boolean {
var cached = useSharedAnimatedBackendCache
if (cached == null) {
cached = currentProvider.useSharedAnimatedBackend()
accessedFeatureFlags.add("useSharedAnimatedBackend")
useSharedAnimatedBackendCache = cached
}
return cached
}
override fun useTurboModuleInterop(): Boolean {
var cached = useTurboModuleInteropCache
if (cached == null) {
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<65e895433fc609bd7c2b5d7faa507f46>>
* @generated SignedSource<<d4370bcde97d3c2b0b8d2a77e42a6031>>
*/
/**
@@ -165,6 +165,8 @@ public interface ReactNativeFeatureFlagsProvider {
@DoNotStrip public fun useShadowNodeStateOnClone(): Boolean
@DoNotStrip public fun useSharedAnimatedBackend(): Boolean
@DoNotStrip public fun useTurboModuleInterop(): Boolean
@DoNotStrip public fun useTurboModules(): Boolean
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<7a0a26494846d6b4881bea01beabb9d6>>
* @generated SignedSource<<740fec2589997816f17778f57b1d4abf>>
*/
/**
@@ -465,6 +465,12 @@ class ReactNativeFeatureFlagsJavaProvider
return method(javaProvider_);
}
bool useSharedAnimatedBackend() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("useSharedAnimatedBackend");
return method(javaProvider_);
}
bool useTurboModuleInterop() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("useTurboModuleInterop");
@@ -854,6 +860,11 @@ bool JReactNativeFeatureFlagsCxxInterop::useShadowNodeStateOnClone(
return ReactNativeFeatureFlags::useShadowNodeStateOnClone();
}
bool JReactNativeFeatureFlagsCxxInterop::useSharedAnimatedBackend(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::useSharedAnimatedBackend();
}
bool JReactNativeFeatureFlagsCxxInterop::useTurboModuleInterop(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::useTurboModuleInterop();
@@ -1123,6 +1134,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
makeNativeMethod(
"useShadowNodeStateOnClone",
JReactNativeFeatureFlagsCxxInterop::useShadowNodeStateOnClone),
makeNativeMethod(
"useSharedAnimatedBackend",
JReactNativeFeatureFlagsCxxInterop::useSharedAnimatedBackend),
makeNativeMethod(
"useTurboModuleInterop",
JReactNativeFeatureFlagsCxxInterop::useTurboModuleInterop),
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<ad2e322ef177ced7eb07412552596a5b>>
* @generated SignedSource<<2d4ccbeed5ffafd40715357ff98d83b1>>
*/
/**
@@ -243,6 +243,9 @@ class JReactNativeFeatureFlagsCxxInterop
static bool useShadowNodeStateOnClone(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
static bool useSharedAnimatedBackend(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
static bool useTurboModuleInterop(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<f01f3d1a08880a7fa9d3490e6bd9c61a>>
* @generated SignedSource<<287689f62d05f2d27c146856b7857010>>
*/
/**
@@ -310,6 +310,10 @@ bool ReactNativeFeatureFlags::useShadowNodeStateOnClone() {
return getAccessor().useShadowNodeStateOnClone();
}
bool ReactNativeFeatureFlags::useSharedAnimatedBackend() {
return getAccessor().useSharedAnimatedBackend();
}
bool ReactNativeFeatureFlags::useTurboModuleInterop() {
return getAccessor().useTurboModuleInterop();
}
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<559a8be87c24238e70fceded8ac962a0>>
* @generated SignedSource<<ce76bbae9b797c6c1c9a1ceb1350f370>>
*/
/**
@@ -394,6 +394,11 @@ class ReactNativeFeatureFlags {
*/
RN_EXPORT static bool useShadowNodeStateOnClone();
/**
* Use shared animation backend in C++ Animated
*/
RN_EXPORT static bool useSharedAnimatedBackend();
/**
* In Bridgeless mode, should legacy NativeModules use the TurboModule system?
*/
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<379732a049a8539a1cde814996ff4791>>
* @generated SignedSource<<57620c5eb3ba6b9fa70f21908c4ea71c>>
*/
/**
@@ -1307,6 +1307,24 @@ bool ReactNativeFeatureFlagsAccessor::useShadowNodeStateOnClone() {
return flagValue.value();
}
bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() {
auto flagValue = useSharedAnimatedBackend_.load();
if (!flagValue.has_value()) {
// This block is not exclusive but it is not necessary.
// If multiple threads try to initialize the feature flag, we would only
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(71, "useSharedAnimatedBackend");
flagValue = currentProvider_->useSharedAnimatedBackend();
useSharedAnimatedBackend_ = flagValue;
}
return flagValue.value();
}
bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() {
auto flagValue = useTurboModuleInterop_.load();
@@ -1316,7 +1334,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(71, "useTurboModuleInterop");
markFlagAsAccessed(72, "useTurboModuleInterop");
flagValue = currentProvider_->useTurboModuleInterop();
useTurboModuleInterop_ = flagValue;
@@ -1334,7 +1352,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModules() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(72, "useTurboModules");
markFlagAsAccessed(73, "useTurboModules");
flagValue = currentProvider_->useTurboModules();
useTurboModules_ = flagValue;
@@ -1352,7 +1370,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(73, "viewCullingOutsetRatio");
markFlagAsAccessed(74, "viewCullingOutsetRatio");
flagValue = currentProvider_->viewCullingOutsetRatio();
viewCullingOutsetRatio_ = flagValue;
@@ -1370,7 +1388,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewHysteresisRatio() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(74, "virtualViewHysteresisRatio");
markFlagAsAccessed(75, "virtualViewHysteresisRatio");
flagValue = currentProvider_->virtualViewHysteresisRatio();
virtualViewHysteresisRatio_ = flagValue;
@@ -1388,7 +1406,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(75, "virtualViewPrerenderRatio");
markFlagAsAccessed(76, "virtualViewPrerenderRatio");
flagValue = currentProvider_->virtualViewPrerenderRatio();
virtualViewPrerenderRatio_ = flagValue;
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<ae0e9dbf8605126b358f776226e68130>>
* @generated SignedSource<<6c6458278652bc91570db82b2be6c7f1>>
*/
/**
@@ -103,6 +103,7 @@ class ReactNativeFeatureFlagsAccessor {
bool useOptimizedEventBatchingOnAndroid();
bool useRawPropsJsiValue();
bool useShadowNodeStateOnClone();
bool useSharedAnimatedBackend();
bool useTurboModuleInterop();
bool useTurboModules();
double viewCullingOutsetRatio();
@@ -119,7 +120,7 @@ class ReactNativeFeatureFlagsAccessor {
std::unique_ptr<ReactNativeFeatureFlagsProvider> currentProvider_;
bool wasOverridden_;
std::array<std::atomic<const char*>, 76> accessedFeatureFlags_;
std::array<std::atomic<const char*>, 77> accessedFeatureFlags_;
std::atomic<std::optional<bool>> commonTestFlag_;
std::atomic<std::optional<bool>> cdpInteractionMetricsEnabled_;
@@ -192,6 +193,7 @@ class ReactNativeFeatureFlagsAccessor {
std::atomic<std::optional<bool>> useOptimizedEventBatchingOnAndroid_;
std::atomic<std::optional<bool>> useRawPropsJsiValue_;
std::atomic<std::optional<bool>> useShadowNodeStateOnClone_;
std::atomic<std::optional<bool>> useSharedAnimatedBackend_;
std::atomic<std::optional<bool>> useTurboModuleInterop_;
std::atomic<std::optional<bool>> useTurboModules_;
std::atomic<std::optional<double>> viewCullingOutsetRatio_;
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<e8eb056e546c41e5863073e460362020>>
* @generated SignedSource<<7879aa079d5dc8603faac4dbf40f70c5>>
*/
/**
@@ -311,6 +311,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider {
return false;
}
bool useSharedAnimatedBackend() override {
return false;
}
bool useTurboModuleInterop() override {
return false;
}
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<00b4d80631374e0714c8aa9f65060220>>
* @generated SignedSource<<b61bcdcabe00ba0e97e42a7ffc2fa8ef>>
*/
/**
@@ -684,6 +684,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef
return ReactNativeFeatureFlagsDefaults::useShadowNodeStateOnClone();
}
bool useSharedAnimatedBackend() override {
auto value = values_["useSharedAnimatedBackend"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::useSharedAnimatedBackend();
}
bool useTurboModuleInterop() override {
auto value = values_["useTurboModuleInterop"];
if (!value.isNull()) {
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<fde4b531c2b0f19bef7bc69aaf3c5639>>
* @generated SignedSource<<b4b62078cf82f1444cb14af59bc7d2f5>>
*/
/**
@@ -96,6 +96,7 @@ class ReactNativeFeatureFlagsProvider {
virtual bool useOptimizedEventBatchingOnAndroid() = 0;
virtual bool useRawPropsJsiValue() = 0;
virtual bool useShadowNodeStateOnClone() = 0;
virtual bool useSharedAnimatedBackend() = 0;
virtual bool useTurboModuleInterop() = 0;
virtual bool useTurboModules() = 0;
virtual double viewCullingOutsetRatio() = 0;
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<27c7bc7a528da06ffe2bcea48211f6bd>>
* @generated SignedSource<<e799e5ce5aea7adbef02a7e7431f8a35>>
*/
/**
@@ -399,6 +399,11 @@ bool NativeReactNativeFeatureFlags::useShadowNodeStateOnClone(
return ReactNativeFeatureFlags::useShadowNodeStateOnClone();
}
bool NativeReactNativeFeatureFlags::useSharedAnimatedBackend(
jsi::Runtime& /*runtime*/) {
return ReactNativeFeatureFlags::useSharedAnimatedBackend();
}
bool NativeReactNativeFeatureFlags::useTurboModuleInterop(
jsi::Runtime& /*runtime*/) {
return ReactNativeFeatureFlags::useTurboModuleInterop();
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<39140c882a6faa0403d3a59806c6bea5>>
* @generated SignedSource<<990b6518e7b1d196deb0965e9ada6605>>
*/
/**
@@ -178,6 +178,8 @@ class NativeReactNativeFeatureFlags
bool useShadowNodeStateOnClone(jsi::Runtime& runtime);
bool useSharedAnimatedBackend(jsi::Runtime& runtime);
bool useTurboModuleInterop(jsi::Runtime& runtime);
bool useTurboModules(jsi::Runtime& runtime);
@@ -26,6 +26,7 @@ target_link_libraries(react_renderer_animated
react_renderer_mounting
react_renderer_uimanager
react_renderer_scheduler
react_renderer_animationbackend
glog
folly_runtime
)
@@ -91,6 +91,10 @@ NativeAnimatedNodesManager::NativeAnimatedNodesManager(
}
}
NativeAnimatedNodesManager::NativeAnimatedNodesManager(
std::shared_ptr<AnimationBackend> animationBackend) noexcept
: animationBackend_(std::move(animationBackend)) {}
NativeAnimatedNodesManager::~NativeAnimatedNodesManager() noexcept {
stopRenderCallbackIfNeeded();
}
@@ -509,6 +513,11 @@ NativeAnimatedNodesManager::ensureEventEmitterListener() noexcept {
}
void NativeAnimatedNodesManager::startRenderCallbackIfNeeded() {
if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) {
animationBackend_->start(
[this](float /*f*/) { return pullAnimationMutations(); });
return;
}
// This method can be called from either the UI thread or JavaScript thread.
// It ensures `startOnRenderCallback_` is called exactly once using atomic
// operations. We use std::atomic_bool rather than std::mutex to avoid
@@ -526,6 +535,10 @@ void NativeAnimatedNodesManager::startRenderCallbackIfNeeded() {
}
void NativeAnimatedNodesManager::stopRenderCallbackIfNeeded() noexcept {
if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) {
animationBackend_->stop();
return;
}
// When multiple threads reach this point, only one thread should call
// stopOnRenderCallback_. This synchronization is primarily needed during
// destruction of NativeAnimatedNodesManager. In normal operation,
@@ -838,6 +851,11 @@ void NativeAnimatedNodesManager::schedulePropsCommit(
const folly::dynamic& props,
bool layoutStyleUpdated,
bool forceFabricCommit) noexcept {
if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) {
mergeObjects(updateViewProps_[viewTag], props);
return;
}
// When fabricCommitCallback_ & directManipulationCallback_ are both
// available, we commit layout props via Fabric and the other using direct
// manipulation. If only fabricCommitCallback_ is available, we commit all
@@ -860,7 +878,122 @@ void NativeAnimatedNodesManager::schedulePropsCommit(
}
}
AnimationMutations NativeAnimatedNodesManager::pullAnimationMutations() {
if (!ReactNativeFeatureFlags::useSharedAnimatedBackend()) {
return {};
}
TraceSection s(
"NativeAnimatedNodesManager::pullAnimations",
"numActiveAnimations",
activeAnimations_.size());
isOnRenderThread_ = true;
// Run operations scheduled from AnimatedModule
std::vector<UiTask> operations;
{
std::lock_guard<std::mutex> lock(uiTasksMutex_);
std::swap(operations_, operations);
}
for (auto& task : operations) {
task();
}
AnimationMutations mutations;
// Step through the animation loop
if (isAnimationUpdateNeeded()) {
auto microseconds = std::chrono::duration_cast<std::chrono::microseconds>(
g_now().time_since_epoch())
.count();
auto timestamp = static_cast<double>(microseconds) / 1000.0;
bool containsChange = false;
{
// copied from onAnimationFrame
// Run all active animations
auto hasFinishedAnimations = false;
std::set<int> finishedAnimationValueNodes;
for (const auto& [_id, driver] : activeAnimations_) {
driver->runAnimationStep(timestamp);
if (driver->getIsComplete()) {
hasFinishedAnimations = true;
const auto shouldRemoveJsSync =
ReactNativeFeatureFlags::cxxNativeAnimatedRemoveJsSync() &&
!ReactNativeFeatureFlags::disableFabricCommitInCXXAnimated();
if (shouldRemoveJsSync) {
finishedAnimationValueNodes.insert(driver->getAnimatedValueTag());
}
}
}
// Update all animated nodes
updateNodes(finishedAnimationValueNodes);
// remove finished animations
if (hasFinishedAnimations) {
std::vector<int> finishedAnimations;
for (const auto& [animationId, driver] : activeAnimations_) {
if (driver->getIsComplete()) {
if (getAnimatedNode<ValueAnimatedNode>(
driver->getAnimatedValueTag()) != nullptr) {
driver->stopAnimation();
}
finishedAnimations.emplace_back(animationId);
}
}
for (const auto& id : finishedAnimations) {
activeAnimations_.erase(id);
}
}
for (auto& [tag, props] : updateViewProps_) {
mutations.emplace_back(
AnimationMutation{tag, props["opacity"].asDouble()});
containsChange = true;
}
}
if (!containsChange) {
// The last animation tick didn't result in any changes to the UI.
// It is safe to assume any event animation that was in progress has
// completed.
// Step 1: gather all animations driven by events.
std::set<int> finishedAnimationValueNodes;
for (auto& [key, drivers] : eventDrivers_) {
for (auto& driver : drivers) {
finishedAnimationValueNodes.insert(driver->getAnimatedNodeTag());
if (auto node = getAnimatedNode<ValueAnimatedNode>(
driver->getAnimatedNodeTag())) {
updatedNodeTags_.insert(node->tag());
}
}
}
// Step 2: update all nodes that are connected to the finished animations.
updateNodes(finishedAnimationValueNodes);
isEventAnimationInProgress_ = false;
for (auto& [tag, props] : updateViewProps_) {
mutations.emplace_back(
AnimationMutation{tag, props["opacity"].asDouble()});
}
}
} else {
// There is no active animation. Stop the render callback.
stopRenderCallbackIfNeeded();
}
return mutations;
}
void NativeAnimatedNodesManager::onRender() {
if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) {
return;
}
TraceSection s(
"NativeAnimatedNodesManager::onRender",
"numActiveAnimations",
@@ -13,6 +13,7 @@
#include <react/debug/flags.h>
#include <react/renderer/animated/EventEmitterListener.h>
#include <react/renderer/animated/event_drivers/EventAnimationDriver.h>
#include <react/renderer/animationbackend/AnimationBackend.h>
#include <react/renderer/core/ReactPrimitives.h>
#include <chrono>
#include <memory>
@@ -62,6 +63,9 @@ class NativeAnimatedNodesManager {
StartOnRenderCallback&& startOnRenderCallback = nullptr,
StopOnRenderCallback&& stopOnRenderCallback = nullptr) noexcept;
explicit NativeAnimatedNodesManager(
std::shared_ptr<AnimationBackend> animationBackend) noexcept;
~NativeAnimatedNodesManager() noexcept;
template <
@@ -105,6 +109,8 @@ class NativeAnimatedNodesManager {
void setAnimatedNodeOffset(Tag tag, double offset);
AnimationMutations pullAnimationMutations();
#pragma mark - Drivers
void startAnimatingNode(
@@ -202,6 +208,8 @@ class NativeAnimatedNodesManager {
const std::string& eventName,
const EventPayload& payload) noexcept;
std::shared_ptr<AnimationBackend> animationBackend_;
std::unique_ptr<AnimatedNode> animatedNode(
Tag tag,
const folly::dynamic& config) noexcept;
@@ -11,6 +11,7 @@
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/renderer/animated/MergedValueDispatcher.h>
#include <react/renderer/animated/internal/AnimatedMountingOverrideDelegate.h>
#include <react/renderer/animationbackend/AnimationBackend.h>
#include <react/renderer/uimanager/UIManagerBinding.h>
namespace facebook::react {
@@ -64,11 +65,26 @@ NativeAnimatedNodesManagerProvider::getOrCreate(
uiManager->synchronouslyUpdateViewOnUIThread(viewTag, props);
};
nativeAnimatedNodesManager_ = std::make_shared<NativeAnimatedNodesManager>(
std::move(directManipulationCallback),
std::move(fabricCommitCallback),
std::move(startOnRenderCallback_),
std::move(stopOnRenderCallback_));
if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) {
// TODO: this should be initialized outside of animated, but for now it
// was convenient to do it here
animationBackend_ = std::make_shared<AnimationBackend>(
std::move(startOnRenderCallback_),
std::move(stopOnRenderCallback_),
std::move(directManipulationCallback));
nativeAnimatedNodesManager_ =
std::make_shared<NativeAnimatedNodesManager>(animationBackend_);
uiManager->unstable_setAnimationBackend(animationBackend_);
} else {
nativeAnimatedNodesManager_ =
std::make_shared<NativeAnimatedNodesManager>(
std::move(directManipulationCallback),
std::move(fabricCommitCallback),
std::move(startOnRenderCallback_),
std::move(stopOnRenderCallback_));
}
addEventEmitterListener(
nativeAnimatedNodesManager_->getEventEmitterListener());
@@ -46,6 +46,7 @@ class NativeAnimatedNodesManagerProvider {
std::shared_ptr<EventEmitterListener> getEventEmitterListener();
private:
std::shared_ptr<AnimationBackend> animationBackend_;
std::shared_ptr<NativeAnimatedNodesManager> nativeAnimatedNodesManager_;
std::shared_ptr<EventEmitterListenerContainer> eventEmitterListenerContainer_;
@@ -0,0 +1,39 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "AnimationBackend.h"
namespace facebook::react {
AnimationBackend::AnimationBackend(
StartOnRenderCallback&& startOnRenderCallback,
StopOnRenderCallback&& stopOnRenderCallback,
DirectManipulationCallback&& directManipulationCallback)
: startOnRenderCallback_(std::move(startOnRenderCallback)),
stopOnRenderCallback_(std::move(stopOnRenderCallback)),
directManipulationCallback_(std::move(directManipulationCallback)) {}
void AnimationBackend::onAnimationFrame(double timestamp) {
for (auto& callback : callbacks) {
auto muatations = callback(static_cast<float>(timestamp));
for (auto& mutation : muatations) {
directManipulationCallback_(
mutation.tag, folly::dynamic::object("opacity", mutation.opacity));
}
}
}
void AnimationBackend::start(const Callback& callback) {
callbacks.push_back(callback);
// startOnRenderCallback_ should provide the timestamp from the platform
startOnRenderCallback_([this]() { onAnimationFrame(0); });
}
void AnimationBackend::stop() {
stopOnRenderCallback_();
callbacks.clear();
}
} // namespace facebook::react
@@ -0,0 +1,44 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <folly/dynamic.h>
#include <react/renderer/core/ReactPrimitives.h>
#include <functional>
#include <vector>
namespace facebook::react {
struct AnimationMutation {
Tag tag;
double opacity;
};
using AnimationMutations = std::vector<AnimationMutation>;
using Callback = std::function<AnimationMutations(float)>;
using StartOnRenderCallback = std::function<void(std::function<void()>&&)>;
using StopOnRenderCallback = std::function<void()>;
using DirectManipulationCallback =
std::function<void(Tag, const folly::dynamic&)>;
class AnimationBackend {
public:
std::vector<Callback> callbacks;
const StartOnRenderCallback startOnRenderCallback_;
const StopOnRenderCallback stopOnRenderCallback_;
const DirectManipulationCallback directManipulationCallback_;
AnimationBackend(
StartOnRenderCallback&& startOnRenderCallback,
StopOnRenderCallback&& stopOnRenderCallback,
DirectManipulationCallback&& directManipulationCallback);
void onAnimationFrame(double timestamp);
void start(const Callback& callback);
void stop();
};
} // namespace facebook::react
@@ -0,0 +1,28 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
file(GLOB react_renderer_animationbackend_SRC CONFIGURE_DEPENDS *.cpp)
add_library(react_renderer_animationbackend OBJECT ${react_renderer_animationbackend_SRC})
target_include_directories(react_renderer_animationbackend PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(react_renderer_animationbackend
react_codegen_rncore
react_debug
react_renderer_core
react_renderer_graphics
react_renderer_mounting
react_renderer_uimanager
react_renderer_scheduler
glog
folly_runtime
)
target_compile_reactnative_options(react_renderer_animationbackend PRIVATE)
target_compile_options(react_renderer_animationbackend PRIVATE -Wpedantic)
@@ -678,6 +678,15 @@ void UIManager::setNativeAnimatedDelegate(
nativeAnimatedDelegate_ = delegate;
}
void UIManager::unstable_setAnimationBackend(
std::weak_ptr<AnimationBackend> animationBackend) {
animationBackend_ = animationBackend;
}
std::weak_ptr<AnimationBackend> UIManager::unstable_getAnimationBackend() {
return animationBackend_;
}
void UIManager::animationTick() const {
if (animationDelegate_ != nullptr &&
animationDelegate_->shouldAnimateFrame()) {
@@ -36,6 +36,7 @@ namespace facebook::react {
class UIManagerBinding;
class UIManagerCommitHook;
class UIManagerMountHook;
class AnimationBackend;
class UIManager final : public ShadowTreeDelegate {
public:
@@ -62,6 +63,9 @@ class UIManager final : public ShadowTreeDelegate {
* the pointer before being destroyed.
*/
void setAnimationDelegate(UIManagerAnimationDelegate* delegate);
void unstable_setAnimationBackend(
std::weak_ptr<AnimationBackend> animationBackend);
std::weak_ptr<AnimationBackend> unstable_getAnimationBackend();
/**
* Execute stopSurface on any UIMAnagerAnimationDelegate.
@@ -258,6 +262,8 @@ class UIManager final : public ShadowTreeDelegate {
std::unique_ptr<LazyShadowTreeRevisionConsistencyManager>
lazyShadowTreeRevisionConsistencyManager_;
std::weak_ptr<AnimationBackend> animationBackend_;
};
} // namespace facebook::react
@@ -32,6 +32,7 @@ target_link_libraries(react_cxx_platform_react_runtime
react_cxx_platform_react_logging
react_cxx_platform_react_nativemodule
react_renderer_animated
react_renderer_animationbackend
react_cxx_platform_react_renderer_uimanager
react_cxx_platform_react_threading
react_cxx_platform_react_utils
@@ -804,6 +804,16 @@ const definitions: FeatureFlagDefinitions = {
},
ossReleaseStage: 'none',
},
useSharedAnimatedBackend: {
defaultValue: false,
metadata: {
dateAdded: '2025-08-2',
description: 'Use shared animation backend in C++ Animated',
expectedReleaseValue: true,
purpose: 'experimentation',
},
ossReleaseStage: 'none',
},
useTurboModuleInterop: {
defaultValue: false,
metadata: {
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<cddccaa2570aa27af142d90430a14044>>
* @generated SignedSource<<3507d3aa8ee32332308a94bf932e01c2>>
* @flow strict
* @noformat
*/
@@ -121,6 +121,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{
useOptimizedEventBatchingOnAndroid: Getter<boolean>,
useRawPropsJsiValue: Getter<boolean>,
useShadowNodeStateOnClone: Getter<boolean>,
useSharedAnimatedBackend: Getter<boolean>,
useTurboModuleInterop: Getter<boolean>,
useTurboModules: Getter<boolean>,
viewCullingOutsetRatio: Getter<number>,
@@ -491,6 +492,10 @@ export const useRawPropsJsiValue: Getter<boolean> = createNativeFlagGetter('useR
* Use the state stored on the source shadow node when cloning it instead of reading in the most recent state on the shadow node family.
*/
export const useShadowNodeStateOnClone: Getter<boolean> = createNativeFlagGetter('useShadowNodeStateOnClone', false);
/**
* Use shared animation backend in C++ Animated
*/
export const useSharedAnimatedBackend: Getter<boolean> = createNativeFlagGetter('useSharedAnimatedBackend', false);
/**
* In Bridgeless mode, should legacy NativeModules use the TurboModule system?
*/
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<765a2f99602c00c38047e25ce805af86>>
* @generated SignedSource<<da804704d063a7f5eacdf1f5b7a15a6f>>
* @flow strict
* @noformat
*/
@@ -96,6 +96,7 @@ export interface Spec extends TurboModule {
+useOptimizedEventBatchingOnAndroid?: () => boolean;
+useRawPropsJsiValue?: () => boolean;
+useShadowNodeStateOnClone?: () => boolean;
+useSharedAnimatedBackend?: () => boolean;
+useTurboModuleInterop?: () => boolean;
+useTurboModules?: () => boolean;
+viewCullingOutsetRatio?: () => number;
@@ -75,6 +75,7 @@ add_react_common_subdir(react/nativemodule/webperformance)
add_react_common_subdir(react/performance/cdpmetrics)
add_react_common_subdir(react/performance/timeline)
add_react_common_subdir(react/renderer/animated)
add_react_common_subdir(react/renderer/animationbackend)
add_react_common_subdir(react/renderer/attributedstring)
add_react_common_subdir(react/renderer/bridging)
add_react_common_subdir(react/renderer/componentregistry)
@@ -231,6 +232,7 @@ target_link_libraries(fantom_tester
react_cxx_platform_react_logging
react_cxx_platform_react_profiling
react_renderer_animated
react_renderer_animationbackend
react_cxx_platform_react_renderer_scheduler
react_cxx_platform_react_runtime
react_cxx_platform_react_threading