From 798bb6e8e564033f3183886d79c932f5d39c1151 Mon Sep 17 00:00:00 2001 From: Bartlomiej Bloniarz Date: Mon, 11 May 2026 08:51:17 -0700 Subject: [PATCH 1/2] Add scaffolding for batched animated prop updates delegate chain Summary: Introduces the cross-platform delegate chain for batched animated property updates without any platform implementation: - Adds `schedulerShouldSynchronouslyUpdateAnimatedPropsOnUIThread` to `SchedulerDelegate` and forwards from `Scheduler`. - Adds `uiManagerShouldSynchronouslyUpdateAnimatedPropsOnUIThread` to `UIManagerDelegate` and `synchronouslyUpdateAnimatedPropsOnUIThread` to `UIManager`. - Adds no-op stubs in iOS (`RCTScheduler.mm`), macOS (`RCTScheduler.mm`), Windows (`FabricUIManagerModule`) and CxxPlatform (`SchedulerDelegateImpl`). No behavioural impact; Android implementation lands in a follow-up. Changelog: [Internal] Differential Revision: D104672455 --- packages/react-native/React/Fabric/RCTScheduler.mm | 7 +++++++ .../jni/react/fabric/FabricUIManagerBinding.cpp | 8 ++++++++ .../main/jni/react/fabric/FabricUIManagerBinding.h | 4 ++++ .../react/renderer/scheduler/Scheduler.cpp | 9 +++++++++ .../react/renderer/scheduler/Scheduler.h | 3 +++ .../react/renderer/scheduler/SchedulerDelegate.h | 14 ++++++++++++++ .../react/renderer/uimanager/UIManager.cpp | 9 +++++++++ .../react/renderer/uimanager/UIManager.h | 4 ++++ .../renderer/uimanager/UIManagerAnimationBackend.h | 1 + .../react/renderer/uimanager/UIManagerDelegate.h | 11 +++++++++++ .../renderer/scheduler/SchedulerDelegateImpl.cpp | 7 +++++++ .../renderer/scheduler/SchedulerDelegateImpl.h | 4 ++++ .../cxx-api/api-snapshots/ReactAndroidDebugCxx.api | 4 ++++ .../api-snapshots/ReactAndroidReleaseCxx.api | 4 ++++ .../cxx-api/api-snapshots/ReactAppleDebugCxx.api | 4 ++++ .../cxx-api/api-snapshots/ReactAppleReleaseCxx.api | 4 ++++ .../cxx-api/api-snapshots/ReactCommonDebugCxx.api | 4 ++++ .../api-snapshots/ReactCommonReleaseCxx.api | 4 ++++ 18 files changed, 105 insertions(+) diff --git a/packages/react-native/React/Fabric/RCTScheduler.mm b/packages/react-native/React/Fabric/RCTScheduler.mm index 9519f18428e4..61efe34743b2 100644 --- a/packages/react-native/React/Fabric/RCTScheduler.mm +++ b/packages/react-native/React/Fabric/RCTScheduler.mm @@ -79,6 +79,13 @@ void schedulerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, [scheduler.delegate schedulerDidSynchronouslyUpdateViewOnUIThread:tag props:props]; } + void schedulerShouldSynchronouslyUpdateAnimatedPropsOnUIThread( + SurfaceId surfaceId, + const std::unordered_map &updates) override + { + // Not implemented on iOS yet -- filled in by a later commit. + } + void schedulerDidUpdateShadowTree(const std::unordered_map &tagToProps) override { // Does nothing. diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp index dfaac96caefc..f74572b71b6a 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -792,6 +793,13 @@ void FabricUIManagerBinding::schedulerShouldSynchronouslyUpdateViewOnUIThread( } } +void FabricUIManagerBinding:: + schedulerShouldSynchronouslyUpdateAnimatedPropsOnUIThread( + SurfaceId /*surfaceId*/, + const std::unordered_map& /*updates*/) { + // Implemented in a follow-up. +} + void FabricUIManagerBinding::schedulerDidUpdateShadowTree( const std::unordered_map& /*tagToProps*/) { // no-op diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h index caf652d2352e..11478770b91a 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h @@ -115,6 +115,10 @@ class FabricUIManagerBinding : public jni::HybridClass, void schedulerShouldSynchronouslyUpdateViewOnUIThread(Tag tag, const folly::dynamic &props) override; + void schedulerShouldSynchronouslyUpdateAnimatedPropsOnUIThread( + SurfaceId surfaceId, + const std::unordered_map &updates) override; + void schedulerDidUpdateShadowTree(const std::unordered_map &tagToProps) override; void schedulerDidCaptureViewSnapshot(Tag tag, SurfaceId surfaceId) override; diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index 7ea5fc3c782f..6a2d66424215 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -401,6 +401,15 @@ void Scheduler::uiManagerShouldSynchronouslyUpdateViewOnUIThread( } } +void Scheduler::uiManagerShouldSynchronouslyUpdateAnimatedPropsOnUIThread( + SurfaceId surfaceId, + const std::unordered_map& updates) { + if (delegate_ != nullptr) { + delegate_->schedulerShouldSynchronouslyUpdateAnimatedPropsOnUIThread( + surfaceId, updates); + } +} + void Scheduler::uiManagerDidUpdateShadowTree( const std::unordered_map& tagToProps) { if (delegate_ != nullptr) { diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h index bd6de832e04d..591f5d7e7a69 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h @@ -94,6 +94,9 @@ class Scheduler final : public UIManagerDelegate { bool isJSResponder, bool blockNativeResponder) override; void uiManagerShouldSynchronouslyUpdateViewOnUIThread(Tag tag, const folly::dynamic &props) override; + void uiManagerShouldSynchronouslyUpdateAnimatedPropsOnUIThread( + SurfaceId surfaceId, + const std::unordered_map &updates) override; void uiManagerDidUpdateShadowTree(const std::unordered_map &tagToProps) override; void uiManagerDidCaptureViewSnapshot(Tag tag, SurfaceId surfaceId) override; void uiManagerDidSetViewSnapshot(Tag sourceTag, Tag targetTag, SurfaceId surfaceId) override; diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerDelegate.h b/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerDelegate.h index 06d9773e4ba5..e1b2c0196b9f 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerDelegate.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include @@ -15,6 +16,8 @@ namespace facebook::react { +struct AnimatedProps; + /* * Abstract class for Scheduler's delegate. */ @@ -64,6 +67,17 @@ class SchedulerDelegate { virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(Tag tag, const folly::dynamic &props) = 0; + /** + * Synchronously update animated properties for multiple views on the UI + * thread. Each entry maps a Tag to the AnimatedProps that should be applied + * to that view. Platform implementations translate AnimatedProps into the + * native update mechanism (e.g. buffer protocol on Android, typed props on + * iOS). + */ + virtual void schedulerShouldSynchronouslyUpdateAnimatedPropsOnUIThread( + SurfaceId surfaceId, + const std::unordered_map &updates) = 0; + virtual void schedulerDidUpdateShadowTree(const std::unordered_map &tagToProps) = 0; // View transition bitmap snapshot capture and application. diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index bce35dd6fa25..79799198539a 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -765,6 +765,15 @@ void UIManager::synchronouslyUpdateViewOnUIThread( } } +void UIManager::synchronouslyUpdateAnimatedPropsOnUIThread( + SurfaceId surfaceId, + const std::unordered_map& updates) { + if (delegate_ != nullptr) { + delegate_->uiManagerShouldSynchronouslyUpdateAnimatedPropsOnUIThread( + surfaceId, updates); + } +} + #pragma mark ContextContainer std::shared_ptr UIManager::getContextContainer() const { diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h index 28b8729e3e01..3d744e9ebb2c 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h @@ -85,6 +85,10 @@ class UIManager final : public ShadowTreeDelegate { void synchronouslyUpdateViewOnUIThread(Tag tag, const folly::dynamic &props); + void synchronouslyUpdateAnimatedPropsOnUIThread( + SurfaceId surfaceId, + const std::unordered_map &updates); + /* * Provides access to a UIManagerBinding. * The `callback` methods will not be called if the internal pointer to diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h index 27c39b18e5e0..1bbaabdb33c1 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h @@ -15,6 +15,7 @@ namespace facebook::react { +struct AnimatedProps; struct AnimationMutations; using AnimationTimestamp = std::chrono::duration; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerDelegate.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerDelegate.h index 235b1fe34eac..b4772d8a0dfc 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerDelegate.h @@ -7,6 +7,8 @@ #pragma once +#include + #include #include #include @@ -14,6 +16,8 @@ namespace facebook::react { +struct AnimatedProps; + /* * Abstract class for UIManager's delegate. */ @@ -64,6 +68,13 @@ class UIManagerDelegate { */ virtual void uiManagerShouldSynchronouslyUpdateViewOnUIThread(Tag tag, const folly::dynamic &props) = 0; + /* + * Synchronously update animated properties on the UI thread. + */ + virtual void uiManagerShouldSynchronouslyUpdateAnimatedPropsOnUIThread( + SurfaceId surfaceId, + const std::unordered_map &updates) = 0; + /* * Called after updateShadowTree is invoked. */ diff --git a/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.cpp b/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.cpp index 9ad1bb1a4f54..51e42552f23a 100644 --- a/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.cpp +++ b/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.cpp @@ -71,6 +71,13 @@ void SchedulerDelegateImpl::schedulerShouldSynchronouslyUpdateViewOnUIThread( mountingManager_->synchronouslyUpdateViewOnUIThread(tag, props); } +void SchedulerDelegateImpl:: + schedulerShouldSynchronouslyUpdateAnimatedPropsOnUIThread( + SurfaceId /*surfaceId*/, + const std::unordered_map& /*updates*/) { + // Not implemented on CxxPlatform. +} + void SchedulerDelegateImpl::schedulerDidUpdateShadowTree( const std::unordered_map& tagToProps) { mountingManager_->onUpdateShadowTree(tagToProps); diff --git a/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.h b/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.h index 089b64d5731c..ec84c938ea78 100644 --- a/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.h +++ b/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.h @@ -49,6 +49,10 @@ class SchedulerDelegateImpl : public SchedulerDelegate { void schedulerShouldSynchronouslyUpdateViewOnUIThread(Tag tag, const folly::dynamic &props) override; + void schedulerShouldSynchronouslyUpdateAnimatedPropsOnUIThread( + SurfaceId surfaceId, + const std::unordered_map &updates) override; + void schedulerDidUpdateShadowTree(const std::unordered_map &tagToProps) override; void schedulerDidCaptureViewSnapshot(Tag tag, SurfaceId surfaceId) override; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 54d994781de4..54bbd61c5b03 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -4479,6 +4479,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) final; public virtual void uiManagerShouldSetOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) override; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) override; public virtual void uiManagerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) override; public void addEventListener(std::shared_ptr listener); public void animationTick() const; @@ -4502,6 +4503,7 @@ class facebook::react::SchedulerDelegate { public virtual void schedulerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void schedulerShouldMergeReactRevision(facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerShouldRenderTransactions(const std::shared_ptr& mountingCoordinator) = 0; + public virtual void schedulerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) = 0; public virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~SchedulerDelegate() noexcept = default; } @@ -5216,6 +5218,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public void startEmptySurface(facebook::react::ShadowTree::Unique&& shadowTree) const noexcept; public void startSurface(facebook::react::ShadowTree::Unique&& shadowTree, const std::string& moduleName, const folly::dynamic& props, facebook::react::DisplayMode displayMode) const noexcept; public void stopSurfaceForAnimationDelegate(facebook::react::SurfaceId surfaceId) const; + public void synchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates); public void synchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props); public void unregisterCommitHook(facebook::react::UIManagerCommitHook& commitHook); public void unregisterMountHook(facebook::react::UIManagerMountHook& mountHook); @@ -5283,6 +5286,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) = 0; public virtual void uiManagerShouldSetOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) = 0; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) = 0; public virtual void uiManagerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~UIManagerDelegate() noexcept = default; } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 6a0a48343467..0fe8e277cabb 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -4476,6 +4476,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) final; public virtual void uiManagerShouldSetOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) override; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) override; public virtual void uiManagerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) override; public void addEventListener(std::shared_ptr listener); public void animationTick() const; @@ -4499,6 +4500,7 @@ class facebook::react::SchedulerDelegate { public virtual void schedulerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void schedulerShouldMergeReactRevision(facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerShouldRenderTransactions(const std::shared_ptr& mountingCoordinator) = 0; + public virtual void schedulerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) = 0; public virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~SchedulerDelegate() noexcept = default; } @@ -5207,6 +5209,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public void startEmptySurface(facebook::react::ShadowTree::Unique&& shadowTree) const noexcept; public void startSurface(facebook::react::ShadowTree::Unique&& shadowTree, const std::string& moduleName, const folly::dynamic& props, facebook::react::DisplayMode displayMode) const noexcept; public void stopSurfaceForAnimationDelegate(facebook::react::SurfaceId surfaceId) const; + public void synchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates); public void synchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props); public void unregisterCommitHook(facebook::react::UIManagerCommitHook& commitHook); public void unregisterMountHook(facebook::react::UIManagerMountHook& mountHook); @@ -5274,6 +5277,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) = 0; public virtual void uiManagerShouldSetOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) = 0; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) = 0; public virtual void uiManagerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~UIManagerDelegate() noexcept = default; } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index bb3a74b3522e..9a7d795ba8ce 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -7060,6 +7060,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) final; public virtual void uiManagerShouldSetOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) override; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) override; public virtual void uiManagerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) override; public void addEventListener(std::shared_ptr listener); public void animationTick() const; @@ -7083,6 +7084,7 @@ class facebook::react::SchedulerDelegate { public virtual void schedulerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void schedulerShouldMergeReactRevision(facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerShouldRenderTransactions(const std::shared_ptr& mountingCoordinator) = 0; + public virtual void schedulerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) = 0; public virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~SchedulerDelegate() noexcept = default; } @@ -7780,6 +7782,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public void startEmptySurface(facebook::react::ShadowTree::Unique&& shadowTree) const noexcept; public void startSurface(facebook::react::ShadowTree::Unique&& shadowTree, const std::string& moduleName, const folly::dynamic& props, facebook::react::DisplayMode displayMode) const noexcept; public void stopSurfaceForAnimationDelegate(facebook::react::SurfaceId surfaceId) const; + public void synchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates); public void synchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props); public void unregisterCommitHook(facebook::react::UIManagerCommitHook& commitHook); public void unregisterMountHook(facebook::react::UIManagerMountHook& mountHook); @@ -7847,6 +7850,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) = 0; public virtual void uiManagerShouldSetOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) = 0; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) = 0; public virtual void uiManagerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~UIManagerDelegate() noexcept = default; } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 53f03430fc4b..079956d60022 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -7057,6 +7057,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) final; public virtual void uiManagerShouldSetOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) override; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) override; public virtual void uiManagerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) override; public void addEventListener(std::shared_ptr listener); public void animationTick() const; @@ -7080,6 +7081,7 @@ class facebook::react::SchedulerDelegate { public virtual void schedulerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void schedulerShouldMergeReactRevision(facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerShouldRenderTransactions(const std::shared_ptr& mountingCoordinator) = 0; + public virtual void schedulerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) = 0; public virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~SchedulerDelegate() noexcept = default; } @@ -7771,6 +7773,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public void startEmptySurface(facebook::react::ShadowTree::Unique&& shadowTree) const noexcept; public void startSurface(facebook::react::ShadowTree::Unique&& shadowTree, const std::string& moduleName, const folly::dynamic& props, facebook::react::DisplayMode displayMode) const noexcept; public void stopSurfaceForAnimationDelegate(facebook::react::SurfaceId surfaceId) const; + public void synchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates); public void synchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props); public void unregisterCommitHook(facebook::react::UIManagerCommitHook& commitHook); public void unregisterMountHook(facebook::react::UIManagerMountHook& mountHook); @@ -7838,6 +7841,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) = 0; public virtual void uiManagerShouldSetOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) = 0; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) = 0; public virtual void uiManagerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~UIManagerDelegate() noexcept = default; } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index bcfb19940fae..821d63c10843 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -3041,6 +3041,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) final; public virtual void uiManagerShouldSetOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) override; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) override; public virtual void uiManagerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) override; public void addEventListener(std::shared_ptr listener); public void animationTick() const; @@ -3064,6 +3065,7 @@ class facebook::react::SchedulerDelegate { public virtual void schedulerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void schedulerShouldMergeReactRevision(facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerShouldRenderTransactions(const std::shared_ptr& mountingCoordinator) = 0; + public virtual void schedulerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) = 0; public virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~SchedulerDelegate() noexcept = default; } @@ -3677,6 +3679,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public void startEmptySurface(facebook::react::ShadowTree::Unique&& shadowTree) const noexcept; public void startSurface(facebook::react::ShadowTree::Unique&& shadowTree, const std::string& moduleName, const folly::dynamic& props, facebook::react::DisplayMode displayMode) const noexcept; public void stopSurfaceForAnimationDelegate(facebook::react::SurfaceId surfaceId) const; + public void synchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates); public void synchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props); public void unregisterCommitHook(facebook::react::UIManagerCommitHook& commitHook); public void unregisterMountHook(facebook::react::UIManagerMountHook& mountHook); @@ -3744,6 +3747,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) = 0; public virtual void uiManagerShouldSetOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) = 0; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) = 0; public virtual void uiManagerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~UIManagerDelegate() noexcept = default; } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 53c14ca18d22..1861d44d9cb9 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -3038,6 +3038,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) final; public virtual void uiManagerShouldSetOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) override; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) override; public virtual void uiManagerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) override; public void addEventListener(std::shared_ptr listener); public void animationTick() const; @@ -3061,6 +3062,7 @@ class facebook::react::SchedulerDelegate { public virtual void schedulerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void schedulerShouldMergeReactRevision(facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerShouldRenderTransactions(const std::shared_ptr& mountingCoordinator) = 0; + public virtual void schedulerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) = 0; public virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~SchedulerDelegate() noexcept = default; } @@ -3668,6 +3670,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public void startEmptySurface(facebook::react::ShadowTree::Unique&& shadowTree) const noexcept; public void startSurface(facebook::react::ShadowTree::Unique&& shadowTree, const std::string& moduleName, const folly::dynamic& props, facebook::react::DisplayMode displayMode) const noexcept; public void stopSurfaceForAnimationDelegate(facebook::react::SurfaceId surfaceId) const; + public void synchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates); public void synchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props); public void unregisterCommitHook(facebook::react::UIManagerCommitHook& commitHook); public void unregisterMountHook(facebook::react::UIManagerMountHook& mountHook); @@ -3735,6 +3738,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) = 0; public virtual void uiManagerShouldSetOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) = 0; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates) = 0; public virtual void uiManagerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~UIManagerDelegate() noexcept = default; } From b7548e833e7213f4133fec5043d94ecc57724748 Mon Sep 17 00:00:00 2001 From: Bartlomiej Bloniarz Date: Mon, 11 May 2026 09:15:16 -0700 Subject: [PATCH 2/2] Add batched animated prop update path on Android (#56468) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56468 Implements the Android batched animated prop update path on top of the cross-platform delegate chain introduced in the previous commit. Gated behind the `optimizedAnimatedPropUpdates` feature flag, so this change has no behavioural impact when the flag is off. - `BatchedAnimatedPropsMountItem.kt`: decodes the int/double buffer protocol and applies updates to views. - `AnimatedPropBufferEncoder` + `AnimatedPropCommands`: C++ side that encodes `AnimatedProps` into the buffer protocol. - `FabricMountingManager::synchronouslyUpdateAnimatedPropsOnUIThread`: builds the buffers and JNI-calls into Java. - `FabricUIManagerBinding`: wires the `SchedulerDelegate` override into `FabricMountingManager`. - `FabricUIManager.synchronouslyUpdateViewBatch`: Java entry point invoked from JNI that constructs and executes a `BatchedAnimatedPropsMountItem`. Changelog: [Android][Added] - Add a batched animated prop update path that applies all pending C++ animation backend updates via a single mount item (gated by `optimizedAnimatedPropUpdates`) Differential Revision: D101157453 --- .../ReactAndroid/api/ReactAndroid.api | 1 + .../react/fabric/FabricUIManager.java | 11 + .../BatchedAnimatedPropsMountItem.kt | 299 ++++++++++++ .../fabric/AnimatedPropBufferEncoder.cpp | 428 ++++++++++++++++++ .../react/fabric/AnimatedPropBufferEncoder.h | 26 ++ .../jni/react/fabric/AnimatedPropCommands.h | 78 ++++ .../react/fabric/FabricMountingManager.cpp | 39 ++ .../jni/react/fabric/FabricMountingManager.h | 4 + .../react/fabric/FabricUIManagerBinding.cpp | 9 +- .../api-snapshots/ReactAndroidDebugCxx.api | 53 +++ .../api-snapshots/ReactAndroidReleaseCxx.api | 53 +++ 11 files changed, 998 insertions(+), 3 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/BatchedAnimatedPropsMountItem.kt create mode 100644 packages/react-native/ReactAndroid/src/main/jni/react/fabric/AnimatedPropBufferEncoder.cpp create mode 100644 packages/react-native/ReactAndroid/src/main/jni/react/fabric/AnimatedPropBufferEncoder.h create mode 100644 packages/react-native/ReactAndroid/src/main/jni/react/fabric/AnimatedPropCommands.h diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index c2d6a186fa6a..128de9e93eac 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2267,6 +2267,7 @@ public class com/facebook/react/fabric/FabricUIManager : com/facebook/react/brid public fun stopSurface (I)V public fun stopSurface (Lcom/facebook/react/fabric/SurfaceHandlerBinding;)V public fun sweepActiveTouchForTag (II)V + public fun synchronouslyUpdateViewBatch ([I[D)V public fun synchronouslyUpdateViewOnUIThread (ILcom/facebook/react/bridge/ReadableMap;)V public fun updateRootLayoutSpecs (IIIII)V } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index 066dd9d8b29e..15c9731b38d6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -61,6 +61,7 @@ import com.facebook.react.fabric.mounting.MountingManager; import com.facebook.react.fabric.mounting.SurfaceMountingManager; import com.facebook.react.fabric.mounting.mountitems.BatchMountItem; +import com.facebook.react.fabric.mounting.mountitems.BatchedAnimatedPropsMountItem; import com.facebook.react.fabric.mounting.mountitems.DispatchCommandMountItem; import com.facebook.react.fabric.mounting.mountitems.MountItem; import com.facebook.react.fabric.mounting.mountitems.MountItemFactory; @@ -847,6 +848,16 @@ private synchronized ViewTransitionSnapshotManager getViewTransitionSnapshotMana return mViewTransitionSnapshotManager; } + @SuppressWarnings("unused") + @UiThread + @ThreadConfined(UI) + public void synchronouslyUpdateViewBatch(final int[] intBuffer, final double[] doubleBuffer) { + UiThreadUtil.assertOnUiThread(); + + MountItem mountItem = new BatchedAnimatedPropsMountItem(intBuffer, doubleBuffer); + mountItem.execute(mMountingManager); + } + @SuppressLint("NotInvokedPrivateMethod") @SuppressWarnings("unused") @AnyThread diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/BatchedAnimatedPropsMountItem.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/BatchedAnimatedPropsMountItem.kt new file mode 100644 index 000000000000..2c8c0132f61a --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/BatchedAnimatedPropsMountItem.kt @@ -0,0 +1,299 @@ +/* + * 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. + */ + +package com.facebook.react.fabric.mounting.mountitems + +import android.view.View +import com.facebook.react.bridge.JavaOnlyArray +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.fabric.mounting.MountingManager +import com.facebook.react.uimanager.ViewProps + +/** + * A [MountItem] that decodes a batched buffer of animated prop updates and applies them + * synchronously. The buffer protocol encodes multiple per-view prop updates into compact int/double + * arrays, which this mount item decodes into [JavaOnlyMap] props and applies via + * [MountingManager.updatePropsSynchronously]. + */ +internal class BatchedAnimatedPropsMountItem( + private val intBuffer: IntArray, + private val doubleBuffer: DoubleArray, +) : MountItem { + + override fun execute(mountingManager: MountingManager) { + var intIdx = 0 + var doubleIdx = 0 + while (intIdx < intBuffer.size) { + val command = intBuffer[intIdx++] + if (command != CMD_START_OF_VIEW) { + break + } + val viewTag = intBuffer[intIdx++] + val props = JavaOnlyMap() + + while (intIdx < intBuffer.size) { + val cmd = intBuffer[intIdx++] + if (cmd == CMD_END_OF_VIEW) { + break + } + + when (cmd) { + in CMD_OPACITY..CMD_SHADOW_RADIUS -> + props.putDouble(commandToString(cmd), doubleBuffer[doubleIdx++]) + in CMD_BACKGROUND_COLOR..CMD_TINT_COLOR, + in CMD_BORDER_COLOR..CMD_BORDER_END_COLOR -> + props.putInt(commandToString(cmd), intBuffer[intIdx++]) + in CMD_BORDER_RADIUS..CMD_BORDER_END_END_RADIUS -> { + // Border radius: value in doubleBuffer, unit in intBuffer + val value = doubleBuffer[doubleIdx++] + val unit = intBuffer[intIdx++] + if (unit == CMD_UNIT_PX) { + props.putDouble(commandToString(cmd), value) + } else if (unit == CMD_UNIT_PERCENT) { + props.putString(commandToString(cmd), "$value%") + } + } + CMD_START_OF_TRANSFORM -> { + val transform = JavaOnlyArray() + while (intIdx < intBuffer.size) { + val transformCmd = intBuffer[intIdx++] + if (transformCmd == CMD_END_OF_TRANSFORM) { + props.putArray(ViewProps.TRANSFORM, transform) + break + } + val name = transformCommandToString(transformCmd) + when (transformCmd) { + in CMD_SCALE..CMD_SCALE_Y, + CMD_PERSPECTIVE -> { + val entry = JavaOnlyMap() + entry.putDouble(name, doubleBuffer[doubleIdx++]) + transform.pushMap(entry) + } + in CMD_TRANSLATE_X..CMD_TRANSLATE_Y -> { + val value = doubleBuffer[doubleIdx++] + val unitCmd = intBuffer[intIdx++] + val entry = JavaOnlyMap() + if (unitCmd == CMD_UNIT_PX) { + entry.putDouble(name, value) + } else { + entry.putString(name, "$value%") + } + transform.pushMap(entry) + } + in CMD_ROTATE..CMD_SKEW_Y -> { + val angle = doubleBuffer[doubleIdx++] + val unitCmd = intBuffer[intIdx++] + val unitStr = if (unitCmd == CMD_UNIT_DEG) "deg" else "rad" + val entry = JavaOnlyMap() + entry.putString(name, "$angle$unitStr") + transform.pushMap(entry) + } + CMD_MATRIX -> { + // matrix + val size = intBuffer[intIdx++] + val matrix = JavaOnlyArray() + for (m in 0 until size) { + matrix.pushDouble(doubleBuffer[doubleIdx++]) + } + val entry = JavaOnlyMap() + entry.putArray(name, matrix) + transform.pushMap(entry) + } + } + } + } + } + } + + try { + mountingManager.updatePropsSynchronously(viewTag, props) + } catch (ex: Exception) { + // Same surface-teardown race as in SynchronousMountItem. + } + } + } + + override fun toString(): String { + val sb = StringBuilder("BATCHED UPDATE PROPS ") + var intIdx = 0 + var doubleIdx = 0 + try { + while (intIdx < intBuffer.size) { + if (intBuffer[intIdx++] != CMD_START_OF_VIEW) break + val viewTag = intBuffer[intIdx++] + sb.append('[').append(viewTag).append("]: {") + var firstProp = true + + view@ while (true) { + val cmd = intBuffer[intIdx++] + if (cmd == CMD_END_OF_VIEW) break@view + + if (!firstProp) sb.append(", ") + firstProp = false + + when (cmd) { + in CMD_OPACITY..CMD_SHADOW_RADIUS -> + sb.append(commandToString(cmd)).append('=').append(doubleBuffer[doubleIdx++]) + in CMD_BACKGROUND_COLOR..CMD_TINT_COLOR, + in CMD_BORDER_COLOR..CMD_BORDER_END_COLOR -> + sb.append(commandToString(cmd)).append('=').append(intBuffer[intIdx++]) + in CMD_BORDER_RADIUS..CMD_BORDER_END_END_RADIUS -> { + val value = doubleBuffer[doubleIdx++] + val unit = intBuffer[intIdx++] + sb.append(commandToString(cmd)).append('=').append(value) + if (unit == CMD_UNIT_PERCENT) sb.append('%') + } + CMD_START_OF_TRANSFORM -> { + sb.append(ViewProps.TRANSFORM).append("=[") + var firstEntry = true + while (true) { + val transformCmd = intBuffer[intIdx++] + if (transformCmd == CMD_END_OF_TRANSFORM) break + if (!firstEntry) sb.append(", ") + firstEntry = false + sb.append(transformCommandToString(transformCmd)).append('=') + when (transformCmd) { + in CMD_SCALE..CMD_SCALE_Y, + CMD_PERSPECTIVE -> sb.append(doubleBuffer[doubleIdx++]) + in CMD_TRANSLATE_X..CMD_TRANSLATE_Y -> { + sb.append(doubleBuffer[doubleIdx++]) + if (intBuffer[intIdx++] == CMD_UNIT_PERCENT) sb.append('%') + } + in CMD_ROTATE..CMD_SKEW_Y -> { + sb.append(doubleBuffer[doubleIdx++]) + sb.append(if (intBuffer[intIdx++] == CMD_UNIT_DEG) "deg" else "rad") + } + CMD_MATRIX -> { + val size = intBuffer[intIdx++] + sb.append('[') + for (i in 0 until size) { + if (i > 0) sb.append(", ") + sb.append(doubleBuffer[doubleIdx++]) + } + sb.append(']') + } + } + } + sb.append(']') + } + } + } + sb.append("}; ") + } + } catch (t: Throwable) { + sb.append("') + } + return sb.toString() + } + + override fun getSurfaceId(): Int = View.NO_ID + + companion object { + // Buffer protocol commands + private const val CMD_START_OF_VIEW = 1 + private const val CMD_START_OF_TRANSFORM = 2 + private const val CMD_END_OF_TRANSFORM = 3 + private const val CMD_END_OF_VIEW = 4 + private const val CMD_OPACITY = 10 + private const val CMD_ELEVATION = 11 + private const val CMD_Z_INDEX = 12 + private const val CMD_SHADOW_OPACITY = 13 + private const val CMD_SHADOW_RADIUS = 14 + private const val CMD_BACKGROUND_COLOR = 15 + private const val CMD_COLOR = 16 + private const val CMD_TINT_COLOR = 17 + private const val CMD_BORDER_RADIUS = 20 + private const val CMD_BORDER_TOP_LEFT_RADIUS = 21 + private const val CMD_BORDER_TOP_RIGHT_RADIUS = 22 + private const val CMD_BORDER_TOP_START_RADIUS = 23 + private const val CMD_BORDER_TOP_END_RADIUS = 24 + private const val CMD_BORDER_BOTTOM_LEFT_RADIUS = 25 + private const val CMD_BORDER_BOTTOM_RIGHT_RADIUS = 26 + private const val CMD_BORDER_BOTTOM_START_RADIUS = 27 + private const val CMD_BORDER_BOTTOM_END_RADIUS = 28 + private const val CMD_BORDER_START_START_RADIUS = 29 + private const val CMD_BORDER_START_END_RADIUS = 30 + private const val CMD_BORDER_END_START_RADIUS = 31 + private const val CMD_BORDER_END_END_RADIUS = 32 + private const val CMD_BORDER_COLOR = 40 + private const val CMD_BORDER_TOP_COLOR = 41 + private const val CMD_BORDER_BOTTOM_COLOR = 42 + private const val CMD_BORDER_LEFT_COLOR = 43 + private const val CMD_BORDER_RIGHT_COLOR = 44 + private const val CMD_BORDER_START_COLOR = 45 + private const val CMD_BORDER_END_COLOR = 46 + private const val CMD_TRANSLATE_X = 100 + private const val CMD_TRANSLATE_Y = 101 + private const val CMD_SCALE = 102 + private const val CMD_SCALE_X = 103 + private const val CMD_SCALE_Y = 104 + private const val CMD_ROTATE = 105 + private const val CMD_ROTATE_X = 106 + private const val CMD_ROTATE_Y = 107 + private const val CMD_ROTATE_Z = 108 + private const val CMD_SKEW_X = 109 + private const val CMD_SKEW_Y = 110 + private const val CMD_MATRIX = 111 + private const val CMD_PERSPECTIVE = 112 + private const val CMD_UNIT_DEG = 200 + private const val CMD_UNIT_PX = 202 + private const val CMD_UNIT_PERCENT = 203 + + @JvmStatic + fun commandToString(command: Int): String = + when (command) { + CMD_OPACITY -> ViewProps.OPACITY + CMD_ELEVATION -> ViewProps.ELEVATION + CMD_Z_INDEX -> ViewProps.Z_INDEX + CMD_SHADOW_OPACITY -> "shadowOpacity" + CMD_SHADOW_RADIUS -> "shadowRadius" + CMD_BACKGROUND_COLOR -> ViewProps.BACKGROUND_COLOR + CMD_COLOR -> ViewProps.COLOR + CMD_TINT_COLOR -> "tintColor" + CMD_BORDER_RADIUS -> ViewProps.BORDER_RADIUS + CMD_BORDER_TOP_LEFT_RADIUS -> ViewProps.BORDER_TOP_LEFT_RADIUS + CMD_BORDER_TOP_RIGHT_RADIUS -> ViewProps.BORDER_TOP_RIGHT_RADIUS + CMD_BORDER_TOP_START_RADIUS -> ViewProps.BORDER_TOP_START_RADIUS + CMD_BORDER_TOP_END_RADIUS -> ViewProps.BORDER_TOP_END_RADIUS + CMD_BORDER_BOTTOM_LEFT_RADIUS -> ViewProps.BORDER_BOTTOM_LEFT_RADIUS + CMD_BORDER_BOTTOM_RIGHT_RADIUS -> ViewProps.BORDER_BOTTOM_RIGHT_RADIUS + CMD_BORDER_BOTTOM_START_RADIUS -> ViewProps.BORDER_BOTTOM_START_RADIUS + CMD_BORDER_BOTTOM_END_RADIUS -> ViewProps.BORDER_BOTTOM_END_RADIUS + CMD_BORDER_START_START_RADIUS -> ViewProps.BORDER_START_START_RADIUS + CMD_BORDER_START_END_RADIUS -> ViewProps.BORDER_START_END_RADIUS + CMD_BORDER_END_START_RADIUS -> ViewProps.BORDER_END_START_RADIUS + CMD_BORDER_END_END_RADIUS -> ViewProps.BORDER_END_END_RADIUS + CMD_BORDER_COLOR -> ViewProps.BORDER_COLOR + CMD_BORDER_TOP_COLOR -> ViewProps.BORDER_TOP_COLOR + CMD_BORDER_BOTTOM_COLOR -> ViewProps.BORDER_BOTTOM_COLOR + CMD_BORDER_LEFT_COLOR -> ViewProps.BORDER_LEFT_COLOR + CMD_BORDER_RIGHT_COLOR -> ViewProps.BORDER_RIGHT_COLOR + CMD_BORDER_START_COLOR -> ViewProps.BORDER_START_COLOR + CMD_BORDER_END_COLOR -> ViewProps.BORDER_END_COLOR + else -> "unknown" + } + + @JvmStatic + fun transformCommandToString(command: Int): String = + when (command) { + CMD_TRANSLATE_X -> "translateX" + CMD_TRANSLATE_Y -> "translateY" + CMD_SCALE -> "scale" + CMD_SCALE_X -> ViewProps.SCALE_X + CMD_SCALE_Y -> ViewProps.SCALE_Y + CMD_ROTATE -> "rotate" + CMD_ROTATE_X -> "rotateX" + CMD_ROTATE_Y -> "rotateY" + CMD_ROTATE_Z -> "rotateZ" + CMD_SKEW_X -> "skewX" + CMD_SKEW_Y -> "skewY" + CMD_MATRIX -> "matrix" + CMD_PERSPECTIVE -> "perspective" + else -> "unknown" + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/AnimatedPropBufferEncoder.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/AnimatedPropBufferEncoder.cpp new file mode 100644 index 000000000000..f522c4b6fe61 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/AnimatedPropBufferEncoder.cpp @@ -0,0 +1,428 @@ +/* + * 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 "AnimatedPropBufferEncoder.h" +#include "AnimatedPropCommands.h" + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +namespace { + +// Maps a rawProps key (as it appears in folly::dynamic) to its buffer-protocol +// command constant. Returns std::nullopt for keys that are not buffer-eligible. +// NOTE: Keep in sync with BatchedAnimatedPropsMountItem.commandToString on the +// Java side. +std::optional propNameToCommand(const std::string& name) { + static const std::unordered_map kMap = { + {"opacity", animationbackend::CMD_OPACITY}, + {"elevation", animationbackend::CMD_ELEVATION}, + {"zIndex", animationbackend::CMD_Z_INDEX}, + {"shadowOpacity", animationbackend::CMD_SHADOW_OPACITY}, + {"shadowRadius", animationbackend::CMD_SHADOW_RADIUS}, + {"backgroundColor", animationbackend::CMD_BACKGROUND_COLOR}, + {"color", animationbackend::CMD_COLOR}, + {"tintColor", animationbackend::CMD_TINT_COLOR}, + {"borderRadius", animationbackend::CMD_BORDER_RADIUS}, + {"borderTopLeftRadius", animationbackend::CMD_BORDER_TOP_LEFT_RADIUS}, + {"borderTopRightRadius", animationbackend::CMD_BORDER_TOP_RIGHT_RADIUS}, + {"borderTopStartRadius", animationbackend::CMD_BORDER_TOP_START_RADIUS}, + {"borderTopEndRadius", animationbackend::CMD_BORDER_TOP_END_RADIUS}, + {"borderBottomLeftRadius", + animationbackend::CMD_BORDER_BOTTOM_LEFT_RADIUS}, + {"borderBottomRightRadius", + animationbackend::CMD_BORDER_BOTTOM_RIGHT_RADIUS}, + {"borderBottomStartRadius", + animationbackend::CMD_BORDER_BOTTOM_START_RADIUS}, + {"borderBottomEndRadius", animationbackend::CMD_BORDER_BOTTOM_END_RADIUS}, + {"borderStartStartRadius", + animationbackend::CMD_BORDER_START_START_RADIUS}, + {"borderStartEndRadius", animationbackend::CMD_BORDER_START_END_RADIUS}, + {"borderEndStartRadius", animationbackend::CMD_BORDER_END_START_RADIUS}, + {"borderEndEndRadius", animationbackend::CMD_BORDER_END_END_RADIUS}, + {"borderColor", animationbackend::CMD_BORDER_COLOR}, + {"borderTopColor", animationbackend::CMD_BORDER_TOP_COLOR}, + {"borderBottomColor", animationbackend::CMD_BORDER_BOTTOM_COLOR}, + {"borderLeftColor", animationbackend::CMD_BORDER_LEFT_COLOR}, + {"borderRightColor", animationbackend::CMD_BORDER_RIGHT_COLOR}, + {"borderStartColor", animationbackend::CMD_BORDER_START_COLOR}, + {"borderEndColor", animationbackend::CMD_BORDER_END_COLOR}, + // "transform" is dispatched as the START_OF_TRANSFORM marker; the + // packDynamicEntryToBuffers switch handles the nested operations. + {"transform", animationbackend::CMD_START_OF_TRANSFORM}, + }; + auto it = kMap.find(name); + if (it == kMap.end()) { + return std::nullopt; + } + return it->second; +} + +// Maps a transform operation name (e.g. "translateX", "rotate") to its +// transform command constant. Returns std::nullopt for unknown operations. +std::optional transformNameToCommand(const std::string& name) { + static const std::unordered_map kMap = { + {"translateX", animationbackend::CMD_TRANSFORM_TRANSLATE_X}, + {"translateY", animationbackend::CMD_TRANSFORM_TRANSLATE_Y}, + {"scale", animationbackend::CMD_TRANSFORM_SCALE}, + {"scaleX", animationbackend::CMD_TRANSFORM_SCALE_X}, + {"scaleY", animationbackend::CMD_TRANSFORM_SCALE_Y}, + {"rotate", animationbackend::CMD_TRANSFORM_ROTATE}, + {"rotateX", animationbackend::CMD_TRANSFORM_ROTATE_X}, + {"rotateY", animationbackend::CMD_TRANSFORM_ROTATE_Y}, + {"rotateZ", animationbackend::CMD_TRANSFORM_ROTATE_Z}, + {"skewX", animationbackend::CMD_TRANSFORM_SKEW_X}, + {"skewY", animationbackend::CMD_TRANSFORM_SKEW_Y}, + {"matrix", animationbackend::CMD_TRANSFORM_MATRIX}, + {"perspective", animationbackend::CMD_TRANSFORM_PERSPECTIVE}, + }; + auto it = kMap.find(name); + if (it == kMap.end()) { + return std::nullopt; + } + return it->second; +} + +} // namespace + +namespace animationbackend { + +// Packs a single TransformOperation directly into the buffer protocol. +// Mirrors the (TransformOperation -> serialized name -> buffer command) +// path used by updateTransformProps + the dynamic-based packer, but reads +// the typed values straight from the operation. +static void packTransformOperationToBuffers( + const Transform& transform, + const TransformOperation& op, + std::vector& intBuffer, + std::vector& doubleBuffer) { + auto pushUnit = [&](int cmd, const ValueUnit& vu) { + intBuffer.push_back(cmd); + intBuffer.push_back( + vu.unit == UnitType::Percent ? CMD_UNIT_PERCENT : CMD_UNIT_PX); + doubleBuffer.push_back(vu.value); + }; + // updateTransformProps preserves rotation values as floats (radians); + // ValueUnit carries no Deg/Rad distinction, so always emit RAD. + auto pushAngle = [&](int cmd, const ValueUnit& vu) { + intBuffer.push_back(cmd); + intBuffer.push_back(CMD_UNIT_RAD); + doubleBuffer.push_back(vu.value); + }; + + switch (op.type) { + case TransformOperationType::Scale: + if (op.x == op.y && op.x == op.z) { + intBuffer.push_back(CMD_TRANSFORM_SCALE); + doubleBuffer.push_back(op.x.value); + } else { + if (op.x.value != 1.0f) { + intBuffer.push_back(CMD_TRANSFORM_SCALE_X); + doubleBuffer.push_back(op.x.value); + } + if (op.y.value != 1.0f) { + intBuffer.push_back(CMD_TRANSFORM_SCALE_Y); + doubleBuffer.push_back(op.y.value); + } + // No CMD_TRANSFORM_SCALE_Z in the protocol. + } + return; + case TransformOperationType::Translate: + if (op.x.value != 0) { + pushUnit(CMD_TRANSFORM_TRANSLATE_X, op.x); + } + if (op.y.value != 0) { + pushUnit(CMD_TRANSFORM_TRANSLATE_Y, op.y); + } + // No CMD_TRANSFORM_TRANSLATE_Z in the protocol. + return; + case TransformOperationType::Rotate: + if (op.x.value != 0) { + pushAngle(CMD_TRANSFORM_ROTATE_X, op.x); + } + if (op.y.value != 0) { + pushAngle(CMD_TRANSFORM_ROTATE_Y, op.y); + } + if (op.z.value != 0) { + pushAngle(CMD_TRANSFORM_ROTATE_Z, op.z); + } + return; + case TransformOperationType::Skew: + if (op.x.value != 0) { + pushAngle(CMD_TRANSFORM_SKEW_X, op.x); + } + if (op.y.value != 0) { + pushAngle(CMD_TRANSFORM_SKEW_Y, op.y); + } + return; + case TransformOperationType::Perspective: + // updateTransformProps emits "perspectiveX/Y/Z" via + // serializeTransformAxis which has no entries in transformNameToCommand, + // so the dynamic path is a no-op. Match that behavior here. + return; + case TransformOperationType::Arbitrary: + intBuffer.push_back(CMD_TRANSFORM_MATRIX); + intBuffer.push_back(static_cast(transform.matrix.size())); + for (const auto& elem : transform.matrix) { + doubleBuffer.push_back(elem); + } + return; + case TransformOperationType::Identity: + return; + } +} + +// Packs a single AnimatedPropBase straight into the buffer protocol, +// without going through folly::dynamic. Asserts the propName is one of +// the buffer-eligible props supported by this path; the caller of +// synchronouslyUpdatePropsBuffered is contractually required to only +// supply such props. +static void packAnimatedPropToBuffers( + const AnimatedPropBase& animatedProp, + std::vector& intBuffer, + std::vector& doubleBuffer) { + switch (animatedProp.propName) { + case OPACITY: + intBuffer.push_back(CMD_OPACITY); + doubleBuffer.push_back(get(animatedProp)); + return; + case BACKGROUND_COLOR: + intBuffer.push_back(CMD_BACKGROUND_COLOR); + intBuffer.push_back( + static_cast(*get(animatedProp))); + return; + case SHADOW_OPACITY: + intBuffer.push_back(CMD_SHADOW_OPACITY); + doubleBuffer.push_back(get(animatedProp)); + return; + case SHADOW_RADIUS: + intBuffer.push_back(CMD_SHADOW_RADIUS); + doubleBuffer.push_back(get(animatedProp)); + return; + case BORDER_RADII: { + const auto borderRadii = get(animatedProp); + auto pushCorner = [&](int cmd, const std::optional& corner) { + if (corner.has_value()) { + intBuffer.push_back(cmd); + intBuffer.push_back(CMD_UNIT_PX); + doubleBuffer.push_back(corner.value().value); + } + }; + pushCorner(CMD_BORDER_TOP_LEFT_RADIUS, borderRadii.topLeft); + pushCorner(CMD_BORDER_TOP_RIGHT_RADIUS, borderRadii.topRight); + pushCorner(CMD_BORDER_BOTTOM_LEFT_RADIUS, borderRadii.bottomLeft); + pushCorner(CMD_BORDER_BOTTOM_RIGHT_RADIUS, borderRadii.bottomRight); + pushCorner(CMD_BORDER_TOP_START_RADIUS, borderRadii.topStart); + pushCorner(CMD_BORDER_TOP_END_RADIUS, borderRadii.topEnd); + pushCorner(CMD_BORDER_BOTTOM_START_RADIUS, borderRadii.bottomStart); + pushCorner(CMD_BORDER_BOTTOM_END_RADIUS, borderRadii.bottomEnd); + pushCorner(CMD_BORDER_START_START_RADIUS, borderRadii.startStart); + pushCorner(CMD_BORDER_START_END_RADIUS, borderRadii.startEnd); + pushCorner(CMD_BORDER_END_START_RADIUS, borderRadii.endStart); + pushCorner(CMD_BORDER_END_END_RADIUS, borderRadii.endEnd); + pushCorner(CMD_BORDER_RADIUS, borderRadii.all); + return; + } + case BORDER_COLOR: { + const auto borderColors = get(animatedProp); + auto pushEdge = [&](int cmd, const std::optional& color) { + if (color.has_value() && color.value()) { + intBuffer.push_back(cmd); + intBuffer.push_back(static_cast(*color.value())); + } + }; + pushEdge(CMD_BORDER_LEFT_COLOR, borderColors.left); + pushEdge(CMD_BORDER_TOP_COLOR, borderColors.top); + pushEdge(CMD_BORDER_RIGHT_COLOR, borderColors.right); + pushEdge(CMD_BORDER_BOTTOM_COLOR, borderColors.bottom); + pushEdge(CMD_BORDER_START_COLOR, borderColors.start); + pushEdge(CMD_BORDER_END_COLOR, borderColors.end); + pushEdge(CMD_BORDER_COLOR, borderColors.all); + return; + } + case TRANSFORM: { + const auto transform = get(animatedProp); + intBuffer.push_back(CMD_START_OF_TRANSFORM); + for (const auto& op : transform.operations) { + packTransformOperationToBuffers(transform, op, intBuffer, doubleBuffer); + } + intBuffer.push_back(CMD_END_OF_TRANSFORM); + return; + } + default: + // Contract: synchronouslyUpdatePropsBuffered must only be invoked + // with props that can flow through the buffer protocol. + react_native_assert(false); + return; + } +} + +// Packs a single rawProps (key, value) entry into the buffer protocol. +// rawProps still has to flow through folly::dynamic because RawProps +// exposes no public iteration API. +static void packDynamicEntryToBuffers( + const std::string& key, + const folly::dynamic& value, + std::vector& intBuffer, + std::vector& doubleBuffer) { + auto cmd = propNameToCommand(key); + react_native_assert(cmd.has_value()); + + switch (cmd.value()) { + case CMD_OPACITY: + case CMD_ELEVATION: + case CMD_Z_INDEX: + case CMD_SHADOW_OPACITY: + case CMD_SHADOW_RADIUS: + intBuffer.push_back(cmd.value()); + doubleBuffer.push_back(value.asDouble()); + break; + + case CMD_BACKGROUND_COLOR: + case CMD_COLOR: + case CMD_TINT_COLOR: + case CMD_BORDER_COLOR: + case CMD_BORDER_TOP_COLOR: + case CMD_BORDER_BOTTOM_COLOR: + case CMD_BORDER_LEFT_COLOR: + case CMD_BORDER_RIGHT_COLOR: + case CMD_BORDER_START_COLOR: + case CMD_BORDER_END_COLOR: + intBuffer.push_back(cmd.value()); + intBuffer.push_back(value.asInt()); + break; + + case CMD_BORDER_RADIUS: + case CMD_BORDER_TOP_LEFT_RADIUS: + case CMD_BORDER_TOP_RIGHT_RADIUS: + case CMD_BORDER_TOP_START_RADIUS: + case CMD_BORDER_TOP_END_RADIUS: + case CMD_BORDER_BOTTOM_LEFT_RADIUS: + case CMD_BORDER_BOTTOM_RIGHT_RADIUS: + case CMD_BORDER_BOTTOM_START_RADIUS: + case CMD_BORDER_BOTTOM_END_RADIUS: + case CMD_BORDER_START_START_RADIUS: + case CMD_BORDER_START_END_RADIUS: + case CMD_BORDER_END_START_RADIUS: + case CMD_BORDER_END_END_RADIUS: + intBuffer.push_back(cmd.value()); + if (value.isDouble()) { + intBuffer.push_back(CMD_UNIT_PX); + doubleBuffer.push_back(value.getDouble()); + } else if (value.isString()) { + intBuffer.push_back(CMD_UNIT_PERCENT); + auto str = value.getString(); + doubleBuffer.push_back(std::stof(str.substr(0, str.size() - 1))); + } + break; + + case CMD_START_OF_TRANSFORM: + intBuffer.push_back(CMD_START_OF_TRANSFORM); + if (value.isArray()) { + for (const auto& item : value) { + if (!item.isObject() || item.size() != 1) { + continue; + } + auto transformName = item.keys().begin()->getString(); + auto transformCmd = transformNameToCommand(transformName); + if (!transformCmd.has_value()) { + continue; + } + const auto& transformValue = *item.values().begin(); + + switch (transformCmd.value()) { + case CMD_TRANSFORM_SCALE: + case CMD_TRANSFORM_SCALE_X: + case CMD_TRANSFORM_SCALE_Y: + case CMD_TRANSFORM_PERSPECTIVE: + intBuffer.push_back(transformCmd.value()); + doubleBuffer.push_back(transformValue.asDouble()); + break; + case CMD_TRANSFORM_TRANSLATE_X: + case CMD_TRANSFORM_TRANSLATE_Y: + intBuffer.push_back(transformCmd.value()); + if (transformValue.isDouble()) { + intBuffer.push_back(CMD_UNIT_PX); + doubleBuffer.push_back(transformValue.getDouble()); + } else if (transformValue.isString()) { + auto str = transformValue.getString(); + intBuffer.push_back(CMD_UNIT_PERCENT); + doubleBuffer.push_back( + std::stof(str.substr(0, str.size() - 1))); + } + break; + case CMD_TRANSFORM_ROTATE: + case CMD_TRANSFORM_ROTATE_X: + case CMD_TRANSFORM_ROTATE_Y: + case CMD_TRANSFORM_ROTATE_Z: + case CMD_TRANSFORM_SKEW_X: + case CMD_TRANSFORM_SKEW_Y: { + intBuffer.push_back(transformCmd.value()); + if (transformValue.isDouble()) { + intBuffer.push_back(CMD_UNIT_RAD); + doubleBuffer.push_back(transformValue.getDouble()); + } else { + auto str = transformValue.getString(); + if (str.size() > 3 && str.substr(str.size() - 3) == "deg") { + intBuffer.push_back(CMD_UNIT_DEG); + } else { + intBuffer.push_back(CMD_UNIT_RAD); + } + doubleBuffer.push_back( + std::stof(str.substr(0, str.size() - 3))); + } + break; + } + case CMD_TRANSFORM_MATRIX: + intBuffer.push_back(transformCmd.value()); + if (transformValue.isArray()) { + intBuffer.push_back(static_cast(transformValue.size())); + for (const auto& elem : transformValue) { + doubleBuffer.push_back(elem.asDouble()); + } + } + break; + } + } + } + intBuffer.push_back(CMD_END_OF_TRANSFORM); + break; + } +} + +void packDynamicPropsToBuffers( + Tag tag, + const AnimatedProps& animatedProps, + std::vector& intBuffer, + std::vector& doubleBuffer) { + intBuffer.push_back(CMD_START_OF_VIEW); + intBuffer.push_back(tag); + + for (auto& animatedProp : animatedProps.props) { + packAnimatedPropToBuffers(*animatedProp, intBuffer, doubleBuffer); + } + + if (animatedProps.rawProps) { + auto rawDyn = animatedProps.rawProps->toDynamic(); + for (auto& [key, value] : rawDyn.items()) { + packDynamicEntryToBuffers( + key.getString(), value, intBuffer, doubleBuffer); + } + } + + intBuffer.push_back(CMD_END_OF_VIEW); +} + +} // namespace animationbackend + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/AnimatedPropBufferEncoder.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/AnimatedPropBufferEncoder.h new file mode 100644 index 000000000000..3bfbd9e482d9 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/AnimatedPropBufferEncoder.h @@ -0,0 +1,26 @@ +/* + * 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 +#include +#include + +namespace facebook::react::animationbackend { + +/** + * Packs an AnimatedProps object for a single view directly into the buffer + * protocol used by the synchronous batched update path on Android. + */ +void packDynamicPropsToBuffers( + Tag tag, + const AnimatedProps &animatedProps, + std::vector &intBuffer, + std::vector &doubleBuffer); + +} // namespace facebook::react::animationbackend diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/AnimatedPropCommands.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/AnimatedPropCommands.h new file mode 100644 index 000000000000..ed2ad63031f3 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/AnimatedPropCommands.h @@ -0,0 +1,78 @@ +/* + * 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 + +namespace facebook::react::animationbackend { + +// Buffer protocol command constants for batched animated prop updates. +// NOTE: Keep in sync with AnimatedPropCommandConstants.kt on the Java side. + +// View delimiters +static constexpr int CMD_START_OF_VIEW = 1; +static constexpr int CMD_START_OF_TRANSFORM = 2; +static constexpr int CMD_END_OF_TRANSFORM = 3; +static constexpr int CMD_END_OF_VIEW = 4; + +// Simple numeric props (value in doubleBuffer) +static constexpr int CMD_OPACITY = 10; +static constexpr int CMD_ELEVATION = 11; +static constexpr int CMD_Z_INDEX = 12; +static constexpr int CMD_SHADOW_OPACITY = 13; +static constexpr int CMD_SHADOW_RADIUS = 14; + +// Color props (value as int in intBuffer) +static constexpr int CMD_BACKGROUND_COLOR = 15; +static constexpr int CMD_COLOR = 16; +static constexpr int CMD_TINT_COLOR = 17; + +// Border radius props (value in doubleBuffer, unit in intBuffer) +static constexpr int CMD_BORDER_RADIUS = 20; +static constexpr int CMD_BORDER_TOP_LEFT_RADIUS = 21; +static constexpr int CMD_BORDER_TOP_RIGHT_RADIUS = 22; +static constexpr int CMD_BORDER_TOP_START_RADIUS = 23; +static constexpr int CMD_BORDER_TOP_END_RADIUS = 24; +static constexpr int CMD_BORDER_BOTTOM_LEFT_RADIUS = 25; +static constexpr int CMD_BORDER_BOTTOM_RIGHT_RADIUS = 26; +static constexpr int CMD_BORDER_BOTTOM_START_RADIUS = 27; +static constexpr int CMD_BORDER_BOTTOM_END_RADIUS = 28; +static constexpr int CMD_BORDER_START_START_RADIUS = 29; +static constexpr int CMD_BORDER_START_END_RADIUS = 30; +static constexpr int CMD_BORDER_END_START_RADIUS = 31; +static constexpr int CMD_BORDER_END_END_RADIUS = 32; + +// Border color props (value as int in intBuffer) +static constexpr int CMD_BORDER_COLOR = 40; +static constexpr int CMD_BORDER_TOP_COLOR = 41; +static constexpr int CMD_BORDER_BOTTOM_COLOR = 42; +static constexpr int CMD_BORDER_LEFT_COLOR = 43; +static constexpr int CMD_BORDER_RIGHT_COLOR = 44; +static constexpr int CMD_BORDER_START_COLOR = 45; +static constexpr int CMD_BORDER_END_COLOR = 46; + +// Transform commands +static constexpr int CMD_TRANSFORM_TRANSLATE_X = 100; +static constexpr int CMD_TRANSFORM_TRANSLATE_Y = 101; +static constexpr int CMD_TRANSFORM_SCALE = 102; +static constexpr int CMD_TRANSFORM_SCALE_X = 103; +static constexpr int CMD_TRANSFORM_SCALE_Y = 104; +static constexpr int CMD_TRANSFORM_ROTATE = 105; +static constexpr int CMD_TRANSFORM_ROTATE_X = 106; +static constexpr int CMD_TRANSFORM_ROTATE_Y = 107; +static constexpr int CMD_TRANSFORM_ROTATE_Z = 108; +static constexpr int CMD_TRANSFORM_SKEW_X = 109; +static constexpr int CMD_TRANSFORM_SKEW_Y = 110; +static constexpr int CMD_TRANSFORM_MATRIX = 111; +static constexpr int CMD_TRANSFORM_PERSPECTIVE = 112; + +// Unit commands +static constexpr int CMD_UNIT_DEG = 200; +static constexpr int CMD_UNIT_RAD = 201; +static constexpr int CMD_UNIT_PX = 202; +static constexpr int CMD_UNIT_PERCENT = 203; + +} // namespace facebook::react::animationbackend diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp index b82bdab278c2..154da216f2de 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp @@ -7,6 +7,7 @@ #include "FabricMountingManager.h" +#include "AnimatedPropBufferEncoder.h" #include "EventEmitterWrapper.h" #include "MountItem.h" #include "StateWrapperImpl.h" @@ -1253,6 +1254,44 @@ void FabricMountingManager::clearPendingSnapshots() { clearPendingSnapshotsJNI(javaUIManager_); } +void FabricMountingManager::synchronouslyUpdateAnimatedPropsOnUIThread( + SurfaceId /*surfaceId*/, + const std::unordered_map& updates) { + std::vector intBuffer; + std::vector doubleBuffer; + + for (const auto& [tag, animatedProps] : updates) { + animationbackend::packDynamicPropsToBuffers( + tag, animatedProps, intBuffer, doubleBuffer); + } + + if (intBuffer.empty()) { + return; + } + + auto env = jni::Environment::current(); + + auto jIntArray = env->NewIntArray(static_cast(intBuffer.size())); + env->SetIntArrayRegion( + jIntArray, 0, static_cast(intBuffer.size()), intBuffer.data()); + + auto jDoubleArray = + env->NewDoubleArray(static_cast(doubleBuffer.size())); + env->SetDoubleArrayRegion( + jDoubleArray, + 0, + static_cast(doubleBuffer.size()), + doubleBuffer.data()); + + static auto synchronouslyUpdateViewBatchJNI = + JFabricUIManager::javaClassStatic() + ->getMethod( + "synchronouslyUpdateViewBatch"); + synchronouslyUpdateViewBatchJNI(javaUIManager_, jIntArray, jDoubleArray); + + env->DeleteLocalRef(jIntArray); + env->DeleteLocalRef(jDoubleArray); +} void FabricMountingManager::scheduleReactRevisionMerge(SurfaceId surfaceId) { static const auto scheduleReactRevisionMerge = JFabricUIManager::javaClassStatic()->getMethod( diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h index 377880b658bb..da8d9531b63e 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h @@ -13,6 +13,7 @@ #include #include +#include #include namespace facebook::react { @@ -74,6 +75,9 @@ class FabricMountingManager final { void clearPendingSnapshots(); + void synchronouslyUpdateAnimatedPropsOnUIThread( + SurfaceId surfaceId, + const std::unordered_map &updates); void scheduleReactRevisionMerge(SurfaceId surfaceId); private: diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp index f74572b71b6a..b33593d19d22 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp @@ -795,9 +795,12 @@ void FabricUIManagerBinding::schedulerShouldSynchronouslyUpdateViewOnUIThread( void FabricUIManagerBinding:: schedulerShouldSynchronouslyUpdateAnimatedPropsOnUIThread( - SurfaceId /*surfaceId*/, - const std::unordered_map& /*updates*/) { - // Implemented in a follow-up. + SurfaceId surfaceId, + const std::unordered_map& updates) { + if (ReactNativeFeatureFlags::cxxNativeAnimatedEnabled() && mountingManager_) { + mountingManager_->synchronouslyUpdateAnimatedPropsOnUIThread( + surfaceId, updates); + } } void FabricUIManagerBinding::schedulerDidUpdateShadowTree( diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 54bbd61c5b03..969abe162bba 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -2332,6 +2332,7 @@ class facebook::react::FabricMountingManager { public void sendAccessibilityEvent(const facebook::react::ShadowView& shadowView, const std::string& eventType); public void setIsJSResponder(const facebook::react::ShadowView& shadowView, bool isJSResponder, bool blockNativeResponder); public void setViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId); + public void synchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates); public void synchronouslyUpdateViewOnUIThread(facebook::react::Tag viewTag, const folly::dynamic& props); public ~FabricMountingManager(); } @@ -11462,6 +11463,58 @@ struct facebook::react::dom::RNMeasureRect { static constexpr facebook::react::Tag facebook::react::animated::undefinedAnimatedNodeIdentifier; +static constexpr int facebook::react::animationbackend::CMD_BACKGROUND_COLOR; +static constexpr int facebook::react::animationbackend::CMD_BORDER_BOTTOM_COLOR; +static constexpr int facebook::react::animationbackend::CMD_BORDER_BOTTOM_END_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_BOTTOM_LEFT_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_BOTTOM_RIGHT_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_BOTTOM_START_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_COLOR; +static constexpr int facebook::react::animationbackend::CMD_BORDER_END_COLOR; +static constexpr int facebook::react::animationbackend::CMD_BORDER_END_END_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_END_START_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_LEFT_COLOR; +static constexpr int facebook::react::animationbackend::CMD_BORDER_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_RIGHT_COLOR; +static constexpr int facebook::react::animationbackend::CMD_BORDER_START_COLOR; +static constexpr int facebook::react::animationbackend::CMD_BORDER_START_END_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_START_START_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_TOP_COLOR; +static constexpr int facebook::react::animationbackend::CMD_BORDER_TOP_END_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_TOP_LEFT_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_TOP_RIGHT_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_TOP_START_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_COLOR; +static constexpr int facebook::react::animationbackend::CMD_ELEVATION; +static constexpr int facebook::react::animationbackend::CMD_END_OF_TRANSFORM; +static constexpr int facebook::react::animationbackend::CMD_END_OF_VIEW; +static constexpr int facebook::react::animationbackend::CMD_OPACITY; +static constexpr int facebook::react::animationbackend::CMD_SHADOW_OPACITY; +static constexpr int facebook::react::animationbackend::CMD_SHADOW_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_START_OF_TRANSFORM; +static constexpr int facebook::react::animationbackend::CMD_START_OF_VIEW; +static constexpr int facebook::react::animationbackend::CMD_TINT_COLOR; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_MATRIX; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_PERSPECTIVE; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_ROTATE; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_ROTATE_X; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_ROTATE_Y; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_ROTATE_Z; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_SCALE; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_SCALE_X; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_SCALE_Y; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_SKEW_X; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_SKEW_Y; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_TRANSLATE_X; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_TRANSLATE_Y; +static constexpr int facebook::react::animationbackend::CMD_UNIT_DEG; +static constexpr int facebook::react::animationbackend::CMD_UNIT_PERCENT; +static constexpr int facebook::react::animationbackend::CMD_UNIT_PX; +static constexpr int facebook::react::animationbackend::CMD_UNIT_RAD; +static constexpr int facebook::react::animationbackend::CMD_Z_INDEX; +void facebook::react::animationbackend::packDynamicPropsToBuffers(facebook::react::Tag tag, const facebook::react::AnimatedProps& animatedProps, std::vector& intBuffer, std::vector& doubleBuffer); + + std::shared_ptr facebook::react::CoreComponentsRegistry::sharedProviderRegistry(); void facebook::react::CoreComponentsRegistry::addCoreComponents(std::shared_ptr registry); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 0fe8e277cabb..328b66a8b920 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -2330,6 +2330,7 @@ class facebook::react::FabricMountingManager { public void sendAccessibilityEvent(const facebook::react::ShadowView& shadowView, const std::string& eventType); public void setIsJSResponder(const facebook::react::ShadowView& shadowView, bool isJSResponder, bool blockNativeResponder); public void setViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId); + public void synchronouslyUpdateAnimatedPropsOnUIThread(facebook::react::SurfaceId surfaceId, const std::unordered_map& updates); public void synchronouslyUpdateViewOnUIThread(facebook::react::Tag viewTag, const folly::dynamic& props); public ~FabricMountingManager(); } @@ -11318,6 +11319,58 @@ struct facebook::react::dom::RNMeasureRect { static constexpr facebook::react::Tag facebook::react::animated::undefinedAnimatedNodeIdentifier; +static constexpr int facebook::react::animationbackend::CMD_BACKGROUND_COLOR; +static constexpr int facebook::react::animationbackend::CMD_BORDER_BOTTOM_COLOR; +static constexpr int facebook::react::animationbackend::CMD_BORDER_BOTTOM_END_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_BOTTOM_LEFT_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_BOTTOM_RIGHT_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_BOTTOM_START_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_COLOR; +static constexpr int facebook::react::animationbackend::CMD_BORDER_END_COLOR; +static constexpr int facebook::react::animationbackend::CMD_BORDER_END_END_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_END_START_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_LEFT_COLOR; +static constexpr int facebook::react::animationbackend::CMD_BORDER_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_RIGHT_COLOR; +static constexpr int facebook::react::animationbackend::CMD_BORDER_START_COLOR; +static constexpr int facebook::react::animationbackend::CMD_BORDER_START_END_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_START_START_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_TOP_COLOR; +static constexpr int facebook::react::animationbackend::CMD_BORDER_TOP_END_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_TOP_LEFT_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_TOP_RIGHT_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_BORDER_TOP_START_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_COLOR; +static constexpr int facebook::react::animationbackend::CMD_ELEVATION; +static constexpr int facebook::react::animationbackend::CMD_END_OF_TRANSFORM; +static constexpr int facebook::react::animationbackend::CMD_END_OF_VIEW; +static constexpr int facebook::react::animationbackend::CMD_OPACITY; +static constexpr int facebook::react::animationbackend::CMD_SHADOW_OPACITY; +static constexpr int facebook::react::animationbackend::CMD_SHADOW_RADIUS; +static constexpr int facebook::react::animationbackend::CMD_START_OF_TRANSFORM; +static constexpr int facebook::react::animationbackend::CMD_START_OF_VIEW; +static constexpr int facebook::react::animationbackend::CMD_TINT_COLOR; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_MATRIX; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_PERSPECTIVE; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_ROTATE; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_ROTATE_X; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_ROTATE_Y; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_ROTATE_Z; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_SCALE; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_SCALE_X; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_SCALE_Y; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_SKEW_X; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_SKEW_Y; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_TRANSLATE_X; +static constexpr int facebook::react::animationbackend::CMD_TRANSFORM_TRANSLATE_Y; +static constexpr int facebook::react::animationbackend::CMD_UNIT_DEG; +static constexpr int facebook::react::animationbackend::CMD_UNIT_PERCENT; +static constexpr int facebook::react::animationbackend::CMD_UNIT_PX; +static constexpr int facebook::react::animationbackend::CMD_UNIT_RAD; +static constexpr int facebook::react::animationbackend::CMD_Z_INDEX; +void facebook::react::animationbackend::packDynamicPropsToBuffers(facebook::react::Tag tag, const facebook::react::AnimatedProps& animatedProps, std::vector& intBuffer, std::vector& doubleBuffer); + + std::shared_ptr facebook::react::CoreComponentsRegistry::sharedProviderRegistry(); void facebook::react::CoreComponentsRegistry::addCoreComponents(std::shared_ptr registry);