From dc358d48d42c2919660516167b60731138bc0c8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20B=C5=82oniarz?= Date: Fri, 30 Oct 2026 03:39:48 -0700 Subject: [PATCH 1/2] Notify surface-start listeners when an empty surface starts (#59584) Summary: `UIManager::startSurface` runs the callbacks registered with `UIManager::addOnSurfaceStartCallback` after adding the shadow tree. `UIManager::startEmptySurface`, which `SurfaceHandler` uses when a surface has no module name, only added the shadow tree, so those callbacks never ran for such surfaces. The shared animation backend, `ViewTransitionModule` and C++ Animated set up per-surface state in that callback. C++ Animated installs `AnimatedMountingOverrideDelegate` there. Fantom starts all its surfaces without a module name, so none of them saw a surface start in Fantom tests. `startEmptySurface` now notifies the delegate the same way `startSurface` does. ## Changelog: [GENERAL] [FIXED] - Surfaces started without a module name now run the callbacks registered with `UIManager::addOnSurfaceStartCallback` Pull Request resolved: https://github.com/react/react-native/pull/59584 Test Plan: - `SchedulerDelegateInvalidationTest.EmptySurfaceStartRunsSurfaceStartCallbacks`: starting a surface with no module name runs a registered surface-start callback with that surface's shadow tree. It fails without this change. - `yarn fantom packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js` passes. Differential Revision: D128154376 Pulled By: bartlomiejbloniarz --- .../SchedulerDelegateInvalidationTest.cpp | 18 ++++++++++++++++++ .../react/renderer/uimanager/UIManager.cpp | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/tests/SchedulerDelegateInvalidationTest.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/tests/SchedulerDelegateInvalidationTest.cpp index 9005250d6063..056b774559d0 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/tests/SchedulerDelegateInvalidationTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/tests/SchedulerDelegateInvalidationTest.cpp @@ -589,6 +589,24 @@ TEST_F( EXPECT_EQ(delegate_->shouldRenderTransactionsCount(), 1); } +TEST_F( + SchedulerDelegateInvalidationTest, + EmptySurfaceStartRunsSurfaceStartCallbacks) { + setUp(); + std::vector startedSurfaces; + scheduler_->getUIManager()->addOnSurfaceStartCallback( + [&](const ShadowTree& shadowTree) { + startedSurfaces.push_back(shadowTree.getSurfaceId()); + }); + surfaceHandler_->constraintLayout( + {.layoutDirection = LayoutDirection::LeftToRight}, {}); + + surfaceHandler_->start(); + + EXPECT_EQ(startedSurfaces, std::vector{11}); + surfaceHandler_->stop(); +} + TEST(SchedulerDelegateTest, animatedPropsUseExistingViewUpdatesByDefault) { class PropsDelegate : public RecordingDelegate { public: diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index 7eefe4606177..8bdad687b8a3 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -266,7 +266,15 @@ void UIManager::startSurface( void UIManager::startEmptySurface( ShadowTree::Unique&& shadowTree) const noexcept { TraceSection s("UIManager::startEmptySurface"); + auto surfaceId = shadowTree->getSurfaceId(); shadowTreeRegistry_.add(std::move(shadowTree)); + + shadowTreeRegistry_.visit( + surfaceId, [delegate = delegate_](const ShadowTree& shadowTree) { + if (delegate != nullptr) { + delegate->uiManagerDidStartSurface(shadowTree); + } + }); } void UIManager::setSurfaceProps( From 3eb25a13020f1d78d8b540f28662e9200613398b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20B=C5=82oniarz?= Date: Fri, 30 Oct 2026 05:05:10 -0700 Subject: [PATCH 2/2] Register animation frontends with the animation backend (#59585) Summary: Stacked on https://github.com/react/react-native/issues/59584. Review the last two commits: the change and its tests. Producers of animation mutations, such as C++ Animated and Reanimated, use the shared animation backend through anonymous frame callbacks. Settings that differ between producers have nowhere to live. A producer can now register once as an `AnimationFrontend` with its frame callback, then start and stop it by id with `startFrontend` and `stopFrontend`. Each frame runs the callbacks passed to `start(callback)` first, then the started frontends in registration order. `start(callback)`, `stop(callbackId)` and `pushAnimationMutations(callback)` are unchanged. C++ Animated now registers a frontend instead of starting a callback. Its frame callback therefore runs after other producers' `start(callback)` callbacks instead of in the order they were started. The next PR adds the first frontend setting. ## Changelog: [INTERNAL] [ADDED] - Let animation producers register with the animation backend as frontends Pull Request resolved: https://github.com/react/react-native/pull/59585 Test Plan: - `AnimationBackendFrontendTest`: a started frontend runs once per frame, starting and stopping are idempotent, frames are requested while any frontend or callback is started, a frontend without a frame callback is not started, and frames run in registration order after the callbacks. - `NativeAnimatedFrontendTests.registersAndStartsAFrontend`: C++ Animated registers and starts a frontend with the backend instead of starting a callback. - The other `animationbackend` and `animated` gtests pass. - `yarn fantom packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js` and `yarn fantom packages/react-native/Libraries/Animated/__tests__/Animated-itest.js` pass. Differential Revision: D128154522 Pulled By: bartlomiejbloniarz --- .../animated/NativeAnimatedNodesManager.cpp | 51 ++-- .../animated/NativeAnimatedNodesManager.h | 8 +- .../animated/tests/AnimationTestsBase.h | 5 + .../tests/EventAnimationDriverTests.cpp | 6 + .../tests/NativeAnimatedFrontendTests.cpp | 109 +++++++++ .../animationbackend/AnimationBackend.cpp | 65 +++++- .../animationbackend/AnimationBackend.h | 12 + .../tests/AnimationBackendFrontendTest.cpp | 219 ++++++++++++++++++ .../uimanager/UIManagerAnimationBackend.h | 12 + .../ReactAndroidFrameworksCxx.api | 16 ++ .../api-snapshots/ReactAppleFrameworksCxx.api | 16 ++ .../ReactCommonFrameworksCxx.api | 16 ++ 12 files changed, 494 insertions(+), 41 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/animated/tests/NativeAnimatedFrontendTests.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimationBackendFrontendTest.cpp diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp index de87a75b0c5a..0d6d09a2d016 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp @@ -609,23 +609,9 @@ void NativeAnimatedNodesManager::startRenderCallbackIfNeeded(bool isAsync) { if (!animationBackend) { return; } - // AnimationBackend::start registers the callback before returning the id. - // Keep registration and id publication in one critical section; otherwise a - // concurrent stop can observe no id and leave the registered callback - // orphaned in the backend. std::lock_guard lock(animationBackendCallbackMutex_); - if (animationBackendCallbackId_.has_value()) { - return; - } - auto weak = weak_from_this(); - auto callbackId = animationBackend->start( - [weak](AnimationTimestamp timestamp) -> AnimationMutations { - if (auto self = weak.lock()) { - return self->pullAnimationMutations(timestamp); - } - return {}; - }); - animationBackendCallbackId_ = callbackId; + animationBackend->startFrontend( + animationFrontendIdLocked(*animationBackend)); } } @@ -651,24 +637,29 @@ void NativeAnimatedNodesManager::stopRenderCallbackIfNeeded( { auto animationBackend = animationBackend_.lock(); - CallbackId callbackId = 0; - { - std::lock_guard lock(animationBackendCallbackMutex_); - if (!animationBackendCallbackId_.has_value()) { - return; - } - // Clear the published id while holding the same mutex that protects - // start registration. This makes a concurrent start either see an active - // callback or wait until this stop owns the callback id. - callbackId = *animationBackendCallbackId_; - animationBackendCallbackId_.reset(); - } - if (animationBackend) { - animationBackend->stop(callbackId); + std::lock_guard lock(animationBackendCallbackMutex_); + if (animationBackend && animationFrontendId_.has_value()) { + animationBackend->stopFrontend(*animationFrontendId_); } } } +AnimationFrontendId NativeAnimatedNodesManager::animationFrontendIdLocked( + UIManagerAnimationBackend& animationBackend) { + if (!animationFrontendId_.has_value()) { + animationFrontendId_ = animationBackend.registerFrontend( + {.onAnimationFrame = + [weak = weak_from_this()]( + AnimationTimestamp timestamp) -> AnimationMutations { + if (auto self = weak.lock()) { + return self->pullAnimationMutations(timestamp); + } + return {}; + }}); + } + return *animationFrontendId_; +} + bool NativeAnimatedNodesManager::isAnimationUpdateNeeded() const noexcept { return !activeAnimations_.empty() || !updatedNodeTags_.empty() || isEventAnimationInProgress_; diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h index a85cbf4e6d95..270383e90fe5 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h @@ -219,6 +219,8 @@ class NativeAnimatedNodesManager : public std::enable_shared_from_this animationBackendCallbackId_; + // Registered on the first start, and kept until the backend is destroyed. + std::optional animationFrontendId_; friend class ColorAnimatedNode; friend class AnimationDriver; diff --git a/packages/react-native/ReactCommon/react/renderer/animated/tests/AnimationTestsBase.h b/packages/react-native/ReactCommon/react/renderer/animated/tests/AnimationTestsBase.h index 47bcc9994956..501dec25ef17 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/tests/AnimationTestsBase.h +++ b/packages/react-native/ReactCommon/react/renderer/animated/tests/AnimationTestsBase.h @@ -40,6 +40,11 @@ class AnimationTestsBase : public testing::Test { NativeAnimatedNodesManager::isOnRenderThread_ = true; } + static void runOnRenderThread() noexcept + { + NativeAnimatedNodesManager::isOnRenderThread_ = true; + } + bool nodeNeedsUpdate(Tag nodeTag) const { return nodesManager_->updatedNodeTags_.contains(nodeTag); diff --git a/packages/react-native/ReactCommon/react/renderer/animated/tests/EventAnimationDriverTests.cpp b/packages/react-native/ReactCommon/react/renderer/animated/tests/EventAnimationDriverTests.cpp index d7ffcb9845df..932b471526be 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/tests/EventAnimationDriverTests.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/tests/EventAnimationDriverTests.cpp @@ -22,6 +22,12 @@ class FakeAnimationBackend : public UIManagerAnimationBackend { AnimationTimestamp pushTimestamp{0}; void onAnimationFrame(AnimationTimestamp /*timestamp*/) override {} + AnimationFrontendId registerFrontend( + AnimationFrontend /*frontend*/) override { + return 1; + } + void startFrontend(AnimationFrontendId /*frontendId*/) override {} + void stopFrontend(AnimationFrontendId /*frontendId*/) override {} CallbackId start(const Callback& /*callback*/) override { return 0; } diff --git a/packages/react-native/ReactCommon/react/renderer/animated/tests/NativeAnimatedFrontendTests.cpp b/packages/react-native/ReactCommon/react/renderer/animated/tests/NativeAnimatedFrontendTests.cpp new file mode 100644 index 000000000000..6c92a1247698 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/animated/tests/NativeAnimatedFrontendTests.cpp @@ -0,0 +1,109 @@ +/* + * 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 "AnimationTestsBase.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +namespace { + +class RecordingAnimationBackend : public UIManagerAnimationBackend { + public: + std::vector frontends; + std::vector startedFrontends; + int startedCallbacks{0}; + + void onAnimationFrame(AnimationTimestamp /*timestamp*/) override {} + AnimationFrontendId registerFrontend(AnimationFrontend frontend) override { + frontends.push_back(std::move(frontend)); + return static_cast(frontends.size()); + } + void startFrontend(AnimationFrontendId frontendId) override { + startedFrontends.push_back(frontendId); + } + void stopFrontend(AnimationFrontendId /*frontendId*/) override {} + CallbackId start(const Callback& /*callback*/) override { + startedCallbacks++; + return 0; + } + void stop(CallbackId /*callbackId*/) override {} + void clearRegistry(SurfaceId /*surfaceId*/) override {} + void clearRegistryOnSurfaceStop(SurfaceId /*surfaceId*/) override {} + void trigger() override {} + void pushAnimationMutations(const Callback& /*callback*/) override {} + void registerJSInvoker(std::shared_ptr /*jsInvoker*/) override {} +}; + +constexpr Tag kValueNode = 1; +constexpr Tag kStyleNode = 2; +constexpr Tag kPropsNode = 3; +constexpr Tag kView = 10; + +} // namespace + +class NativeAnimatedFrontendTests : public AnimationTestsBase { + protected: + void SetUp() override { + runOnRenderThread(); + } + + // Starts animating the view's opacity in four frames. + void startAnimation() { + manager_ = std::make_shared(backend_); + manager_->createAnimatedNode( + kValueNode, + folly::dynamic::object("type", "value")("value", 0)("offset", 0)); + manager_->createAnimatedNode( + kStyleNode, + folly::dynamic::object("type", "style")( + "style", folly::dynamic::object("opacity", kValueNode))); + manager_->createAnimatedNode( + kPropsNode, + folly::dynamic::object("type", "props")( + "props", folly::dynamic::object("style", kStyleNode))); + manager_->connectAnimatedNodes(kValueNode, kStyleNode); + manager_->connectAnimatedNodes(kStyleNode, kPropsNode); + manager_->connectAnimatedNodeToView(kPropsNode, kView); + manager_->connectAnimatedNodeToShadowNodeFamily( + kPropsNode, + std::const_pointer_cast(view_->getFamilyShared())); + manager_->startAnimatingNode( + 1, + kValueNode, + folly::dynamic::object("type", "frames")( + "frames", folly::dynamic::array(0.0, 0.5, 1.0))("toValue", 1), + std::nullopt); + manager_->startRenderCallbackIfNeeded(false); + } + + std::shared_ptr backend_ = + std::make_shared(); + std::shared_ptr manager_; + ComponentBuilder builder_ = + simpleComponentBuilder(std::make_shared()); + std::shared_ptr view_ = + builder_.build(Element().surfaceId(1).tag(kView)); +}; + +TEST_F(NativeAnimatedFrontendTests, registersAndStartsAFrontend) { + startAnimation(); + + ASSERT_EQ(backend_->frontends.size(), 1u); + EXPECT_EQ(backend_->startedFrontends, std::vector{1}); + EXPECT_EQ(backend_->startedCallbacks, 0); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp index 0e3db03a1024..d6d680c44015 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp @@ -163,15 +163,22 @@ void AnimationBackend::applyMutations(std::vector batches) { void AnimationBackend::onAnimationFrame(AnimationTimestamp timestamp) { std::vector callbacksCopy; + std::vector startedFrontends; { std::lock_guard lock(mutex_); callbacksCopy = callbacks; + for (const auto& registered : frontends_) { + if (registered.started) { + startedFrontends.push_back(registered.frontend); + } + } } // Sized up front rather than grown: MSVC's std::set move isn't noexcept, so // growing a vector of AnimationMutations would try to copy move-only props. - std::vector batches(callbacksCopy.size()); + std::vector batches( + callbacksCopy.size() + startedFrontends.size()); std::transform( callbacksCopy.begin(), callbacksCopy.end(), @@ -179,18 +186,36 @@ void AnimationBackend::onAnimationFrame(AnimationTimestamp timestamp) { [timestamp](const CallbackWithId& callbackWithId) { return callbackWithId.callback(timestamp); }); + for (size_t i = 0; i < startedFrontends.size(); i++) { + batches[callbacksCopy.size() + i] = + startedFrontends[i].onAnimationFrame(timestamp); + } applyMutations(std::move(batches)); } +void AnimationBackend::resumeFramesLocked() { + if (!isRenderCallbackStarted_) { + animationChoreographer_->resume(); + isRenderCallbackStarted_ = true; + } +} + +void AnimationBackend::pauseFramesIfIdleLocked() { + if (isRenderCallbackStarted_ && callbacks.empty() && + std::none_of(frontends_.begin(), frontends_.end(), [](auto& frontend) { + return frontend.started; + })) { + animationChoreographer_->pause(); + isRenderCallbackStarted_ = false; + } +} + CallbackId AnimationBackend::start(const Callback& callback) { std::lock_guard lock(mutex_); auto callbackId = nextCallbackId_++; callbacks.push_back({.callbackId = callbackId, .callback = callback}); - if (!isRenderCallbackStarted_) { - animationChoreographer_->resume(); - isRenderCallbackStarted_ = true; - } + resumeFramesLocked(); return callbackId; } @@ -206,10 +231,34 @@ void AnimationBackend::stop(CallbackId callbackId) { } callbacks.erase(it); - if (isRenderCallbackStarted_ && callbacks.empty()) { - animationChoreographer_->pause(); - isRenderCallbackStarted_ = false; + pauseFramesIfIdleLocked(); +} + +AnimationFrontendId AnimationBackend::registerFrontend( + AnimationFrontend frontend) { + std::lock_guard lock(mutex_); + frontends_.push_back({.frontend = std::move(frontend)}); + return static_cast(frontends_.size()); +} + +void AnimationBackend::startFrontend(AnimationFrontendId frontendId) { + std::lock_guard lock(mutex_); + auto& registered = frontends_.at(frontendId - 1); + if (registered.started || !registered.frontend.onAnimationFrame) { + return; + } + registered.started = true; + resumeFramesLocked(); +} + +void AnimationBackend::stopFrontend(AnimationFrontendId frontendId) { + std::lock_guard lock(mutex_); + auto& registered = frontends_.at(frontendId - 1); + if (!registered.started) { + return; } + registered.started = false; + pauseFramesIfIdleLocked(); } void AnimationBackend::trigger() { diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h index 3c328ff2adb1..59d18c4c64f0 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h @@ -36,6 +36,11 @@ struct CallbackWithId { Callback callback; }; +struct RegisteredFrontend { + AnimationFrontend frontend; + bool started{false}; +}; + class AnimationBackend : public UIManagerAnimationBackend { public: using ResumeCallback = std::function; @@ -56,6 +61,9 @@ class AnimationBackend : public UIManagerAnimationBackend { void pushAnimationMutations(const Callback &callback) override; CallbackId start(const Callback &callback) override; void stop(CallbackId callbackId) override; + AnimationFrontendId registerFrontend(AnimationFrontend frontend) override; + void startFrontend(AnimationFrontendId frontendId) override; + void stopFrontend(AnimationFrontendId frontendId) override; private: using SurfaceUpdates = std::unordered_map; @@ -69,7 +77,11 @@ class AnimationBackend : public UIManagerAnimationBackend { std::unordered_map &surfaceUpdates, const std::set &asyncFlushSurfaces); void applyMutations(std::vector batches); + void resumeFramesLocked(); + void pauseFramesIfIdleLocked(); std::vector callbacks; + // Indexed by frontend id - 1. + std::vector frontends_; std::shared_ptr animatedPropsRegistry_; std::shared_ptr animationChoreographer_; std::unique_ptr commitHook_; diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimationBackendFrontendTest.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimationBackendFrontendTest.cpp new file mode 100644 index 000000000000..d315a4c546a0 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimationBackendFrontendTest.cpp @@ -0,0 +1,219 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +namespace { + +constexpr SurfaceId kSurface = 1; +constexpr Tag kView = 10; + +class TestAnimationChoreographer : public AnimationChoreographer { + public: + bool isResumed{false}; + + void resume() override { + isResumed = true; + } + void pause() override { + isResumed = false; + } +}; + +class RecordingUIManagerDelegate : public UIManagerDelegate { + public: + std::unordered_map synchronousUpdates; + + void uiManagerShouldSynchronouslyUpdateViewOnUIThread( + Tag tag, + const folly::dynamic& props) override { + synchronousUpdates.insert_or_assign(tag, props); + } + + void uiManagerShouldSynchronouslyUpdateAnimatedProps( + const std::unordered_map& /*updates*/) override {} + void uiManagerDidFinishTransaction( + std::shared_ptr /*mountingCoordinator*/, + bool /*mountSynchronously*/) override {} + void uiManagerDidCreateShadowNode(const ShadowNode& /*shadowNode*/) override { + } + void uiManagerDidDispatchCommand( + const std::shared_ptr& /*shadowNode*/, + const std::string& /*commandName*/, + const folly::dynamic& /*args*/) override {} + void uiManagerDidSendAccessibilityEvent( + const std::shared_ptr& /*shadowNode*/, + const std::string& /*eventType*/) override {} + void uiManagerDidSetIsJSResponder( + const std::shared_ptr& /*shadowNode*/, + bool /*isJSResponder*/, + bool /*blockNativeResponder*/) override {} + void uiManagerDidUpdateShadowTree( + const std::unordered_map& /*tagToProps*/) override {} + void uiManagerShouldAddEventListener( + std::shared_ptr /*listener*/) override {} + void uiManagerShouldRemoveEventListener( + const std::shared_ptr& /*listener*/) override {} + void uiManagerDidStartSurface(const ShadowTree& /*shadowTree*/) override {} + void uiManagerDidFinishReactCommit( + const ShadowTree& /*shadowTree*/) override {} + void uiManagerShouldAddOnSurfaceStartCallback( + OnSurfaceStartCallback&& /*callback*/) override {} + void uiManagerDidCaptureViewSnapshot(Tag /*tag*/, SurfaceId /*surfaceId*/) + override {} + void uiManagerDidSetViewSnapshot( + Tag /*sourceTag*/, + Tag /*targetTag*/, + SurfaceId /*surfaceId*/) override {} + void uiManagerDidClearPendingSnapshots() override {} +}; + +} // namespace + +// A view on a running surface. +class AnimationBackendFrontendTest : public ::testing::Test { + protected: + std::shared_ptr contextContainer = + std::make_shared(); + ComponentBuilder builder = simpleComponentBuilder(contextContainer); + RecordingUIManagerDelegate delegate; + std::shared_ptr uiManager = std::make_shared( + [](std::function&& /*callback*/) {}, + contextContainer); + std::shared_ptr view; + std::shared_ptr choreographer = + std::make_shared(); + std::shared_ptr backend; + double frameTime{1000}; + + void SetUp() override { + uiManager->setDelegate(&delegate); + view = + builder.build(Element().tag(kView).surfaceId(kSurface)); + auto tree = std::make_unique( + kSurface, + LayoutConstraints{}, + LayoutContext{}, + *uiManager, + *contextContainer); + tree->commit( + [&](const RootShadowNode& oldRootShadowNode) { + return std::static_pointer_cast( + oldRootShadowNode.ShadowNode::clone( + {.children = std::make_shared< + std::vector>>( + std::vector>{view})})); + }, + {}); + uiManager->startSurface( + std::move(tree), "test", folly::dynamic::object, DisplayMode::Visible); + backend = std::make_shared(choreographer, uiManager); + } + + void TearDown() override { + backend.reset(); + uiManager->stopSurface(kSurface); + uiManager->setDelegate(nullptr); + } + + AnimationMutations opacity(double value) const { + AnimationMutations mutations; + mutations.batch.push_back( + AnimationMutation{ + .tag = kView, + .family = view->getFamilyShared(), + .props = { + .props = {}, + .rawProps = std::make_unique( + folly::dynamic::object("opacity", value))}}); + return mutations; + } + + void frame() { + backend->onAnimationFrame(AnimationTimestamp{frameTime += 17}); + } +}; + +TEST_F(AnimationBackendFrontendTest, aStartedFrontendRunsOncePerFrame) { + int frames = 0; + auto frontendId = + backend->registerFrontend({.onAnimationFrame = [&](AnimationTimestamp) { + frames++; + return AnimationMutations{}; + }}); + backend->startFrontend(frontendId); + backend->startFrontend(frontendId); + frame(); + EXPECT_EQ(frames, 1); + + backend->stopFrontend(frontendId); + backend->stopFrontend(frontendId); + frame(); + EXPECT_EQ(frames, 1); + + backend->startFrontend(frontendId); + frame(); + EXPECT_EQ(frames, 2); +} + +TEST_F(AnimationBackendFrontendTest, framesComeWhileAFrontendIsStarted) { + auto idle = [](AnimationTimestamp) { return AnimationMutations{}; }; + auto first = backend->registerFrontend({.onAnimationFrame = idle}); + auto second = backend->registerFrontend({.onAnimationFrame = idle}); + backend->startFrontend(first); + backend->startFrontend(second); + EXPECT_TRUE(choreographer->isResumed); + backend->stopFrontend(first); + EXPECT_TRUE(choreographer->isResumed); + backend->stopFrontend(second); + EXPECT_FALSE(choreographer->isResumed); + + auto callbackId = backend->start(idle); + backend->startFrontend(first); + backend->stopFrontend(first); + EXPECT_TRUE(choreographer->isResumed); + backend->stop(callbackId); + EXPECT_FALSE(choreographer->isResumed); +} + +TEST_F( + AnimationBackendFrontendTest, + aFrontendWithoutAFrameCallbackIsNotStarted) { + auto frontendId = backend->registerFrontend({}); + backend->startFrontend(frontendId); + EXPECT_FALSE(choreographer->isResumed); + frame(); +} + +TEST_F(AnimationBackendFrontendTest, framesRunInRegistrationOrder) { + auto first = backend->registerFrontend( + {.onAnimationFrame = [&](AnimationTimestamp) { return opacity(0.3); }}); + auto second = backend->registerFrontend( + {.onAnimationFrame = [&](AnimationTimestamp) { return opacity(0.5); }}); + backend->startFrontend(second); + backend->startFrontend(first); + auto legacy = + backend->start([&](AnimationTimestamp) { return opacity(0.7); }); + frame(); + backend->stop(legacy); + + EXPECT_EQ(delegate.synchronousUpdates.at(kView)["opacity"], 0.5); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h index d57f1e772ca9..b6a6a7dd6388 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h @@ -23,6 +23,14 @@ struct AnimationMutations; using AnimationTimestamp = std::chrono::duration; using CallbackId = uint64_t; +using AnimationFrontendId = uint32_t; + +// A producer of animation mutations, such as C++ Animated or Reanimated. +struct AnimationFrontend { + // Runs every frame while the frontend is started, after the callbacks passed + // to `start(callback)` and those of frontends registered earlier. + std::function onAnimationFrame; +}; class UIManagerAnimationBackend { public: @@ -31,6 +39,10 @@ class UIManagerAnimationBackend { virtual ~UIManagerAnimationBackend() = default; virtual void onAnimationFrame(AnimationTimestamp timestamp) = 0; + virtual AnimationFrontendId registerFrontend(AnimationFrontend frontend) = 0; + // Starting a started frontend or stopping a stopped one does nothing. + virtual void startFrontend(AnimationFrontendId frontendId) = 0; + virtual void stopFrontend(AnimationFrontendId frontendId) = 0; virtual CallbackId start(const Callback &callback) = 0; virtual void stop(CallbackId callbackId) = 0; virtual void clearRegistry(SurfaceId surfaceId) = 0; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api index e793d6f7de2c..b08014b61273 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api @@ -480,6 +480,7 @@ using facebook::react::AndroidSwipeRefreshLayoutComponentDescriptor = facebook:: using facebook::react::AndroidSwipeRefreshLayoutShadowNode = facebook::react::ConcreteViewShadowNode; using facebook::react::AndroidSwipeRefreshLayoutState = facebook::react::StateData; using facebook::react::AnimationEndCallback = facebook::react::AsyncCallback; +using facebook::react::AnimationFrontendId = uint32_t; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; using facebook::react::BackgroundImage = std::variant; @@ -1302,13 +1303,16 @@ class facebook::react::AnimationBackend : public facebook::react::UIManagerAnima public AnimationBackend(std::shared_ptr animationChoreographer, std::shared_ptr uiManager); public using PauseCallback = std::function; public using ResumeCallback = std::function; + public virtual facebook::react::AnimationFrontendId registerFrontend(facebook::react::AnimationFrontend frontend) override; public virtual facebook::react::CallbackId start(const facebook::react::UIManagerAnimationBackend::Callback& callback) override; public virtual void clearRegistry(facebook::react::SurfaceId surfaceId) override; public virtual void clearRegistryOnSurfaceStop(facebook::react::SurfaceId surfaceId) override; public virtual void onAnimationFrame(facebook::react::AnimationTimestamp timestamp) override; public virtual void pushAnimationMutations(const facebook::react::UIManagerAnimationBackend::Callback& callback) override; public virtual void registerJSInvoker(std::shared_ptr jsInvoker) override; + public virtual void startFrontend(facebook::react::AnimationFrontendId frontendId) override; public virtual void stop(facebook::react::CallbackId callbackId) override; + public virtual void stopFrontend(facebook::react::AnimationFrontendId frontendId) override; public virtual void trigger() override; public void requestAsyncFlushForSurfaces(const std::set& surfaces); public void synchronouslyUpdateProps(const std::unordered_map& updates); @@ -4240,13 +4244,16 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { class facebook::react::UIManagerAnimationBackend { public using Callback = std::function; + public virtual facebook::react::AnimationFrontendId registerFrontend(facebook::react::AnimationFrontend frontend) = 0; public virtual facebook::react::CallbackId start(const facebook::react::UIManagerAnimationBackend::Callback& callback) = 0; public virtual void clearRegistry(facebook::react::SurfaceId surfaceId) = 0; public virtual void clearRegistryOnSurfaceStop(facebook::react::SurfaceId surfaceId) = 0; public virtual void onAnimationFrame(facebook::react::AnimationTimestamp timestamp) = 0; public virtual void pushAnimationMutations(const facebook::react::UIManagerAnimationBackend::Callback& callback) = 0; public virtual void registerJSInvoker(std::shared_ptr jsInvoker) = 0; + public virtual void startFrontend(facebook::react::AnimationFrontendId frontendId) = 0; public virtual void stop(facebook::react::CallbackId callbackId) = 0; + public virtual void stopFrontend(facebook::react::AnimationFrontendId frontendId) = 0; public virtual void trigger() = 0; public virtual ~UIManagerAnimationBackend() = default; } @@ -5623,6 +5630,10 @@ struct facebook::react::AnimationConfig { public facebook::react::Float springDamping; } +struct facebook::react::AnimationFrontend { + public std::function onAnimationFrame; +} + struct facebook::react::AnimationKeyFrame { public bool generateFinalSyntheticMutations; public bool invalidated; @@ -6507,6 +6518,11 @@ struct facebook::react::Rect { public void unionInPlace(const facebook::react::Rect& rect) noexcept; } +struct facebook::react::RegisteredFrontend { + public bool started; + public facebook::react::AnimationFrontend frontend; +} + struct facebook::react::SchedulerToolbox { public facebook::react::ComponentRegistryFactory componentRegistryFactory; public facebook::react::EventBeat::Factory eventBeatFactory; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api index b05f7665f7da..1aa7a114a27b 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -3264,6 +3264,7 @@ using facebook::react::ActivityIndicatorViewComponentDescriptor = facebook::reac using facebook::react::ActivityIndicatorViewShadowNode = facebook::react::ConcreteViewShadowNode; using facebook::react::ActivityIndicatorViewState = facebook::react::StateData; using facebook::react::AnimationEndCallback = facebook::react::AsyncCallback; +using facebook::react::AnimationFrontendId = uint32_t; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; using facebook::react::BackgroundImage = std::variant; @@ -3857,13 +3858,16 @@ class facebook::react::AnimationBackend : public facebook::react::UIManagerAnima public AnimationBackend(std::shared_ptr animationChoreographer, std::shared_ptr uiManager); public using PauseCallback = std::function; public using ResumeCallback = std::function; + public virtual facebook::react::AnimationFrontendId registerFrontend(facebook::react::AnimationFrontend frontend) override; public virtual facebook::react::CallbackId start(const facebook::react::UIManagerAnimationBackend::Callback& callback) override; public virtual void clearRegistry(facebook::react::SurfaceId surfaceId) override; public virtual void clearRegistryOnSurfaceStop(facebook::react::SurfaceId surfaceId) override; public virtual void onAnimationFrame(facebook::react::AnimationTimestamp timestamp) override; public virtual void pushAnimationMutations(const facebook::react::UIManagerAnimationBackend::Callback& callback) override; public virtual void registerJSInvoker(std::shared_ptr jsInvoker) override; + public virtual void startFrontend(facebook::react::AnimationFrontendId frontendId) override; public virtual void stop(facebook::react::CallbackId callbackId) override; + public virtual void stopFrontend(facebook::react::AnimationFrontendId frontendId) override; public virtual void trigger() override; public void requestAsyncFlushForSurfaces(const std::set& surfaces); public void synchronouslyUpdateProps(const std::unordered_map& updates); @@ -6379,13 +6383,16 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { class facebook::react::UIManagerAnimationBackend { public using Callback = std::function; + public virtual facebook::react::AnimationFrontendId registerFrontend(facebook::react::AnimationFrontend frontend) = 0; public virtual facebook::react::CallbackId start(const facebook::react::UIManagerAnimationBackend::Callback& callback) = 0; public virtual void clearRegistry(facebook::react::SurfaceId surfaceId) = 0; public virtual void clearRegistryOnSurfaceStop(facebook::react::SurfaceId surfaceId) = 0; public virtual void onAnimationFrame(facebook::react::AnimationTimestamp timestamp) = 0; public virtual void pushAnimationMutations(const facebook::react::UIManagerAnimationBackend::Callback& callback) = 0; public virtual void registerJSInvoker(std::shared_ptr jsInvoker) = 0; + public virtual void startFrontend(facebook::react::AnimationFrontendId frontendId) = 0; public virtual void stop(facebook::react::CallbackId callbackId) = 0; + public virtual void stopFrontend(facebook::react::AnimationFrontendId frontendId) = 0; public virtual void trigger() = 0; public virtual ~UIManagerAnimationBackend() = default; } @@ -7763,6 +7770,10 @@ struct facebook::react::AnimationConfig { public facebook::react::Float springDamping; } +struct facebook::react::AnimationFrontend { + public std::function onAnimationFrame; +} + struct facebook::react::AnimationKeyFrame { public bool generateFinalSyntheticMutations; public bool invalidated; @@ -8493,6 +8504,11 @@ struct facebook::react::Rect { public void unionInPlace(const facebook::react::Rect& rect) noexcept; } +struct facebook::react::RegisteredFrontend { + public bool started; + public facebook::react::AnimationFrontend frontend; +} + struct facebook::react::SchedulerToolbox { public facebook::react::ComponentRegistryFactory componentRegistryFactory; public facebook::react::EventBeat::Factory eventBeatFactory; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api index 2772d8e63b9e..595ddc5f9a7d 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api @@ -108,6 +108,7 @@ using facebook::react::EventListenerT = std::function; typedef std::function facebook::react::ValueListenerCallback; using facebook::react::ActivePointerRegistry = std::unordered_map; using facebook::react::AnimationEndCallback = facebook::react::AsyncCallback; +using facebook::react::AnimationFrontendId = uint32_t; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; using facebook::react::BackgroundImage = std::variant; @@ -643,13 +644,16 @@ class facebook::react::AnimationBackend : public facebook::react::UIManagerAnima public AnimationBackend(std::shared_ptr animationChoreographer, std::shared_ptr uiManager); public using PauseCallback = std::function; public using ResumeCallback = std::function; + public virtual facebook::react::AnimationFrontendId registerFrontend(facebook::react::AnimationFrontend frontend) override; public virtual facebook::react::CallbackId start(const facebook::react::UIManagerAnimationBackend::Callback& callback) override; public virtual void clearRegistry(facebook::react::SurfaceId surfaceId) override; public virtual void clearRegistryOnSurfaceStop(facebook::react::SurfaceId surfaceId) override; public virtual void onAnimationFrame(facebook::react::AnimationTimestamp timestamp) override; public virtual void pushAnimationMutations(const facebook::react::UIManagerAnimationBackend::Callback& callback) override; public virtual void registerJSInvoker(std::shared_ptr jsInvoker) override; + public virtual void startFrontend(facebook::react::AnimationFrontendId frontendId) override; public virtual void stop(facebook::react::CallbackId callbackId) override; + public virtual void stopFrontend(facebook::react::AnimationFrontendId frontendId) override; public virtual void trigger() override; public void requestAsyncFlushForSurfaces(const std::set& surfaces); public void synchronouslyUpdateProps(const std::unordered_map& updates); @@ -2711,13 +2715,16 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { class facebook::react::UIManagerAnimationBackend { public using Callback = std::function; + public virtual facebook::react::AnimationFrontendId registerFrontend(facebook::react::AnimationFrontend frontend) = 0; public virtual facebook::react::CallbackId start(const facebook::react::UIManagerAnimationBackend::Callback& callback) = 0; public virtual void clearRegistry(facebook::react::SurfaceId surfaceId) = 0; public virtual void clearRegistryOnSurfaceStop(facebook::react::SurfaceId surfaceId) = 0; public virtual void onAnimationFrame(facebook::react::AnimationTimestamp timestamp) = 0; public virtual void pushAnimationMutations(const facebook::react::UIManagerAnimationBackend::Callback& callback) = 0; public virtual void registerJSInvoker(std::shared_ptr jsInvoker) = 0; + public virtual void startFrontend(facebook::react::AnimationFrontendId frontendId) = 0; public virtual void stop(facebook::react::CallbackId callbackId) = 0; + public virtual void stopFrontend(facebook::react::AnimationFrontendId frontendId) = 0; public virtual void trigger() = 0; public virtual ~UIManagerAnimationBackend() = default; } @@ -3974,6 +3981,10 @@ struct facebook::react::AnimationConfig { public facebook::react::AnimationType animationType; } +struct facebook::react::AnimationFrontend { + public std::function onAnimationFrame; +} + struct facebook::react::AnimationKeyFrame { public bool generateFinalSyntheticMutations; public bool invalidated; @@ -4666,6 +4677,11 @@ struct facebook::react::Rect { public void unionInPlace(const facebook::react::Rect& rect) noexcept; } +struct facebook::react::RegisteredFrontend { + public bool started; + public facebook::react::AnimationFrontend frontend; +} + struct facebook::react::SchedulerToolbox { public facebook::react::ComponentRegistryFactory componentRegistryFactory; public facebook::react::EventBeat::Factory eventBeatFactory;