From fcaf99d7183be225273083fb0fccd4e6fdd1fb8a Mon Sep 17 00:00:00 2001 From: Bartlomiej Bloniarz Date: Wed, 30 Sep 2026 07:34:49 -0700 Subject: [PATCH 1/3] Keep non-layout animations on the synchronous path while another view animates layout Summary: The shared animation backend decided per surface whether a frame's animated props go through a shadow tree commit or straight to the mounted views. As soon as one view animated a layout prop, every animated view on that surface went through the commit path for the whole animation, including views that only animate `transform` or `opacity`. Besides the extra commit work, on Android this moved those views from `updatePropsSynchronously` to regular mounts. There the synchronous mount props cache (`overrideBySynchronousMountPropsAtMountingAndroid`) replaced the incoming transform with its last synchronously written value, so the view froze while the other view's layout animation ran and jumped when it ended. The decision is now made per view. `SurfaceUpdates` holds a surface's mutations keyed by view tag, and `applySurfaceUpdates` splits them into views with layout updates, which go through `commitUpdates`, and the rest, which are applied synchronously. `AnimationMutation` and `AnimationMutations` move to `AnimationMutation.h`, still included by `AnimationBackend.h`. `AnimatedPropsRegistry::update` reads the frame's batches instead of the merged per-surface map. ## Changelog: [General] [Fixed] - A view animating only non-layout props on the shared animation backend no longer freezes while another view on the surface animates a layout prop Differential Revision: D122570616 --- .../__tests__/AnimatedBackend-itest.js | 66 +++++++++++++++++++ .../AnimatedPropsRegistry.cpp | 31 ++++----- .../animationbackend/AnimatedPropsRegistry.h | 9 +-- .../animationbackend/AnimationBackend.cpp | 63 ++++++++++-------- .../animationbackend/AnimationBackend.h | 18 ++--- .../animationbackend/AnimationMutation.h | 33 ++++++++++ .../api-snapshots/ReactAndroidDebugCxx.api | 9 +-- .../api-snapshots/ReactAndroidNewarchCxx.api | 9 +-- .../api-snapshots/ReactAndroidReleaseCxx.api | 9 +-- .../api-snapshots/ReactAppleDebugCxx.api | 9 +-- .../api-snapshots/ReactAppleNewarchCxx.api | 9 +-- .../api-snapshots/ReactAppleReleaseCxx.api | 9 +-- .../api-snapshots/ReactCommonDebugCxx.api | 9 +-- .../api-snapshots/ReactCommonNewarchCxx.api | 9 +-- .../api-snapshots/ReactCommonReleaseCxx.api | 9 +-- 15 files changed, 174 insertions(+), 127 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationMutation.h diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js index 15df90071365..8c4a37c81100 100644 --- a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js @@ -77,6 +77,72 @@ test('animate marginLeft layout prop', () => { ); }); +// A layout animation on one view must not push the other views of the +// surface through a shadow tree commit: their non-layout props keep taking +// the direct path to the mounted views. +test('non-layout props stay on the direct path while another view animates layout', () => { + const movingRef = createRef(); + + let _translateX; + let _translateXAnimation; + let _siblingHeight; + let _siblingHeightAnimation; + + function MyApp() { + const translateX = useAnimatedValue(0); + const siblingHeight = useAnimatedValue(10); + _translateX = translateX; + _siblingHeight = siblingHeight; + return ( + + + + + ); + } + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + Fantom.runTask(() => { + _translateXAnimation = Animated.timing(_translateX, { + toValue: 100, + duration: 200, + useNativeDriver: true, + }).start(); + _siblingHeightAnimation = Animated.timing(_siblingHeight, { + toValue: 110, + duration: 200, + useNativeDriver: true, + }).start(); + }); + + Fantom.unstable_produceFramesForDuration(100); + + // The sibling's height went through a commit; the transform did not. + expect(root.getRenderedOutput({props: ['height']}).toJSX()).toEqual( + + + + , + ); + expect( + Fantom.unstable_getDirectManipulationProps(nullthrows(movingRef.current)) + .transform, + ).toEqual([{translateX: 50}]); + + Fantom.runTask(() => { + _translateXAnimation?.stop(); + _siblingHeightAnimation?.stop(); + }); +}); + test('animated opacity', () => { let _opacity; let _opacityAnimation; diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp index 02fef88041cd..8ffa63811469 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp @@ -12,25 +12,20 @@ namespace facebook::react { void AnimatedPropsRegistry::update( - const std::unordered_map& surfaceUpdates) { + const std::vector& batches) { auto lock = std::lock_guard(mutex_); - for (const auto& [surfaceId, updates] : surfaceUpdates) { - auto contextIt = surfaceContexts_.find(surfaceId); - if (contextIt == surfaceContexts_.end()) { - continue; - } - auto& surfaceContext = contextIt->second; - auto& pendingMap = surfaceContext.pendingMap; - auto& pendingFamilies = surfaceContext.pendingFamilies; - - auto& updatesMap = updates.propsMap; - auto& updatesFamilies = updates.families; - - for (auto& family : updatesFamilies) { - pendingFamilies.insert(family); - } - - for (auto& [tag, animatedProps] : updatesMap) { + for (const auto& mutations : batches) { + for (const auto& mutation : mutations.batch) { + const auto& family = mutation.family; + auto contextIt = surfaceContexts_.find(family->getSurfaceId()); + if (contextIt == surfaceContexts_.end()) { + continue; + } + auto& surfaceContext = contextIt->second; + auto& pendingMap = surfaceContext.pendingMap; + surfaceContext.pendingFamilies.insert(family); + const auto tag = mutation.tag; + const auto& animatedProps = mutation.props; auto it = pendingMap.find(tag); if (it == pendingMap.end()) { it = pendingMap.insert_or_assign(tag, std::make_unique()) diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h index 64eaa12b863a..394e8c446a29 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h @@ -15,6 +15,7 @@ #include #include #include "AnimatedProps.h" +#include "AnimationMutation.h" namespace facebook::react { @@ -29,17 +30,11 @@ struct SurfaceContext { std::unordered_set> pendingFamilies, families; }; -struct SurfaceUpdates { - std::unordered_set> families; - std::unordered_map propsMap; - bool hasLayoutUpdates{false}; -}; - using SnapshotMap = std::unordered_map>; class AnimatedPropsRegistry { public: - void update(const std::unordered_map &surfaceUpdates); + void update(const std::vector &batches); void initializeSurface(SurfaceId surfaceId); void clear(SurfaceId surfaceId); void clearOnSurfaceStop(SurfaceId surfaceId); diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp index e38386a9e44a..963f65663e16 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp @@ -80,14 +80,10 @@ void AnimationBackend::unpackMutations( std::unordered_map& surfaceUpdates, std::set& asyncFlushSurfaces) { for (auto& mutation : mutations.batch) { - const auto family = mutation.family; - react_native_assert(family != nullptr); - - auto& [families, updates, hasLayoutUpdates] = - surfaceUpdates[family->getSurfaceId()]; - hasLayoutUpdates |= mutation.hasLayoutUpdates; - families.insert(family); - updates[mutation.tag] = std::move(mutation.props); + react_native_assert(mutation.family != nullptr); + auto& updates = surfaceUpdates[mutation.family->getSurfaceId()]; + const auto tag = mutation.tag; + updates.insert_or_assign(tag, std::move(mutation)); } asyncFlushSurfaces.merge(mutations.asyncFlushSurfaces); @@ -96,23 +92,34 @@ void AnimationBackend::unpackMutations( void AnimationBackend::applySurfaceUpdates( std::unordered_map& surfaceUpdates, const std::set& asyncFlushSurfaces) { - animatedPropsRegistry_->update(surfaceUpdates); - for (auto& [surfaceId, updates] : surfaceUpdates) { - if (updates.hasLayoutUpdates) { - commitUpdates(surfaceId, updates); - } else { - synchronouslyUpdateProps(updates.propsMap); + SurfaceUpdates layoutUpdates; + std::unordered_map directProps; + for (auto& [tag, mutation] : updates) { + if (mutation.hasLayoutUpdates) { + layoutUpdates.emplace(tag, std::move(mutation)); + } else { + directProps.emplace(tag, std::move(mutation.props)); + } + } + if (!layoutUpdates.empty()) { + commitUpdates(surfaceId, layoutUpdates); + } + if (!directProps.empty()) { + synchronouslyUpdateProps(directProps); } } requestAsyncFlushForSurfaces(asyncFlushSurfaces); } -void AnimationBackend::applyMutations(AnimationMutations mutations) { +void AnimationBackend::applyMutations(std::vector batches) { + animatedPropsRegistry_->update(batches); std::unordered_map surfaceUpdates; std::set asyncFlushSurfaces; - unpackMutations(mutations, surfaceUpdates, asyncFlushSurfaces); + for (auto& mutations : batches) { + unpackMutations(mutations, surfaceUpdates, asyncFlushSurfaces); + } applySurfaceUpdates(surfaceUpdates, asyncFlushSurfaces); } @@ -124,13 +131,11 @@ void AnimationBackend::onAnimationFrame(AnimationTimestamp timestamp) { callbacksCopy = callbacks; } - std::unordered_map surfaceUpdates; - std::set asyncFlushSurfaces; + std::vector batches; for (auto& callbackWithId : callbacksCopy) { - auto mutations = callbackWithId.callback(timestamp); - unpackMutations(mutations, surfaceUpdates, asyncFlushSurfaces); + batches.push_back(callbackWithId.callback(timestamp)); } - applySurfaceUpdates(surfaceUpdates, asyncFlushSurfaces); + applyMutations(std::move(batches)); } CallbackId AnimationBackend::start(const Callback& callback) { @@ -169,20 +174,23 @@ void AnimationBackend::trigger() { void AnimationBackend::pushAnimationMutations(const Callback& callback) { auto timestamp = animationChoreographer_->now(); - auto mutations = callback(timestamp); - applyMutations(std::move(mutations)); + std::vector batches; + batches.push_back(callback(timestamp)); + applyMutations(std::move(batches)); } void AnimationBackend::commitUpdates( SurfaceId surfaceId, - SurfaceUpdates& surfaceUpdates) { + SurfaceUpdates& updates) { auto uiManager = uiManager_.lock(); if (!uiManager) { return; } - auto& surfaceFamilies = surfaceUpdates.families; - auto& updates = surfaceUpdates.propsMap; + std::unordered_set> surfaceFamilies; + for (const auto& [tag, mutation] : updates) { + surfaceFamilies.insert(mutation.family); + } uiManager->getShadowTreeRegistry().visit( surfaceId, [&surfaceFamilies, &updates](const ShadowTree& shadowTree) { @@ -198,7 +206,8 @@ void AnimationBackend::commitUpdates( auto newProps = ShadowNodeFragment::propsPlaceholder(); if (surfaceFamilies.contains( shadowNode.getFamilyShared())) { - auto& animatedProps = updates.at(shadowNode.getTag()); + auto& animatedProps = + updates.at(shadowNode.getTag()).props; newProps = cloneProps(animatedProps, shadowNode); } return shadowNode.clone( diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h index d1a7d02df118..6abae3a73b27 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h @@ -21,22 +21,16 @@ #include "AnimatedPropsRegistry.h" #include "AnimationBackendCommitHook.h" #include "AnimationChoreographer.h" +#include "AnimationMutation.h" namespace facebook::react { class AnimationBackend; -struct AnimationMutation { - Tag tag; - std::shared_ptr family; - AnimatedProps props; - bool hasLayoutUpdates{false}; -}; - -struct AnimationMutations { - std::vector batch; - std::set asyncFlushSurfaces; -}; +// A frame's mutations on one surface, by view. Views with layout updates go +// through a shadow tree commit, the rest is applied directly to the mounted +// views. +using SurfaceUpdates = std::unordered_map; using Callback = std::function; @@ -74,7 +68,7 @@ class AnimationBackend : public UIManagerAnimationBackend { void applySurfaceUpdates( std::unordered_map &surfaceUpdates, const std::set &asyncFlushSurfaces); - void applyMutations(AnimationMutations mutations); + void applyMutations(std::vector batches); std::vector callbacks; std::shared_ptr animatedPropsRegistry_; std::shared_ptr animationChoreographer_; diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationMutation.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationMutation.h new file mode 100644 index 000000000000..a17749160aae --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationMutation.h @@ -0,0 +1,33 @@ +/* + * 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 +#include +#include +#include +#include "AnimatedProps.h" + +namespace facebook::react { + +struct AnimationMutation { + Tag tag; + std::shared_ptr family; + AnimatedProps props; + bool hasLayoutUpdates{false}; +}; + +struct AnimationMutations { + std::vector batch; + std::set asyncFlushSurfaces; +}; + +} // namespace facebook::react diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index c61ae05c5935..7329262535fb 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -710,6 +710,7 @@ using facebook::react::SizeAsTuple = std::tuple>; using facebook::react::StatePipe = std::function; using facebook::react::SurfaceId = int32_t; +using facebook::react::SurfaceUpdates = std::unordered_map; using facebook::react::Tag = int32_t; using facebook::react::TelemetryClock = std::chrono::steady_clock; using facebook::react::TelemetryDuration = std::chrono::nanoseconds; @@ -1580,7 +1581,7 @@ class facebook::react::AnimatedPropsRegistry { public void clear(facebook::react::SurfaceId surfaceId); public void clearOnSurfaceStop(facebook::react::SurfaceId surfaceId); public void initializeSurface(facebook::react::SurfaceId surfaceId); - public void update(const std::unordered_map& surfaceUpdates); + public void update(const std::vector& batches); } class facebook::react::AnimationBackend : public facebook::react::UIManagerAnimationBackend { @@ -8112,12 +8113,6 @@ struct facebook::react::SurfaceContext { public std::unordered_set> pendingFamilies; } -struct facebook::react::SurfaceUpdates { - public bool hasLayoutUpdates; - public std::unordered_map propsMap; - public std::unordered_set> families; -} - struct facebook::react::SystraceSection : public facebook::react::DummyTraceSection { template public SystraceSection(const char* name, ConvertsToStringPiece &&... args); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 9d5008343459..3fbdbaa1036d 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -709,6 +709,7 @@ using facebook::react::SizeAsTuple = std::tuple>; using facebook::react::StatePipe = std::function; using facebook::react::SurfaceId = int32_t; +using facebook::react::SurfaceUpdates = std::unordered_map; using facebook::react::Tag = int32_t; using facebook::react::TelemetryClock = std::chrono::steady_clock; using facebook::react::TelemetryDuration = std::chrono::nanoseconds; @@ -1575,7 +1576,7 @@ class facebook::react::AnimatedPropsRegistry { public void clear(facebook::react::SurfaceId surfaceId); public void clearOnSurfaceStop(facebook::react::SurfaceId surfaceId); public void initializeSurface(facebook::react::SurfaceId surfaceId); - public void update(const std::unordered_map& surfaceUpdates); + public void update(const std::vector& batches); } class facebook::react::AnimationBackend : public facebook::react::UIManagerAnimationBackend { @@ -7872,12 +7873,6 @@ struct facebook::react::SurfaceContext { public std::unordered_set> pendingFamilies; } -struct facebook::react::SurfaceUpdates { - public bool hasLayoutUpdates; - public std::unordered_map propsMap; - public std::unordered_set> families; -} - struct facebook::react::SystraceSection : public facebook::react::DummyTraceSection { template public SystraceSection(const char* name, ConvertsToStringPiece &&... args); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 2470a7083fb9..5feacf3d73f8 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -710,6 +710,7 @@ using facebook::react::SizeAsTuple = std::tuple>; using facebook::react::StatePipe = std::function; using facebook::react::SurfaceId = int32_t; +using facebook::react::SurfaceUpdates = std::unordered_map; using facebook::react::Tag = int32_t; using facebook::react::TelemetryClock = std::chrono::steady_clock; using facebook::react::TelemetryDuration = std::chrono::nanoseconds; @@ -1579,7 +1580,7 @@ class facebook::react::AnimatedPropsRegistry { public void clear(facebook::react::SurfaceId surfaceId); public void clearOnSurfaceStop(facebook::react::SurfaceId surfaceId); public void initializeSurface(facebook::react::SurfaceId surfaceId); - public void update(const std::unordered_map& surfaceUpdates); + public void update(const std::vector& batches); } class facebook::react::AnimationBackend : public facebook::react::UIManagerAnimationBackend { @@ -8103,12 +8104,6 @@ struct facebook::react::SurfaceContext { public std::unordered_set> pendingFamilies; } -struct facebook::react::SurfaceUpdates { - public bool hasLayoutUpdates; - public std::unordered_map propsMap; - public std::unordered_set> families; -} - struct facebook::react::SystraceSection : public facebook::react::DummyTraceSection { template public SystraceSection(const char* name, ConvertsToStringPiece &&... args); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index a8f040724fa6..83e348be0cce 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -3538,6 +3538,7 @@ using facebook::react::SizeAsTuple = std::tuple>; using facebook::react::StatePipe = std::function; using facebook::react::SurfaceId = int32_t; +using facebook::react::SurfaceUpdates = std::unordered_map; using facebook::react::Tag = int32_t; using facebook::react::TelemetryClock = std::chrono::steady_clock; using facebook::react::TelemetryDuration = std::chrono::nanoseconds; @@ -4126,7 +4127,7 @@ class facebook::react::AnimatedPropsRegistry { public void clear(facebook::react::SurfaceId surfaceId); public void clearOnSurfaceStop(facebook::react::SurfaceId surfaceId); public void initializeSurface(facebook::react::SurfaceId surfaceId); - public void update(const std::unordered_map& surfaceUpdates); + public void update(const std::vector& batches); } class facebook::react::AnimationBackend : public facebook::react::UIManagerAnimationBackend { @@ -10076,12 +10077,6 @@ struct facebook::react::SurfaceContext { public std::unordered_set> pendingFamilies; } -struct facebook::react::SurfaceUpdates { - public bool hasLayoutUpdates; - public std::unordered_map propsMap; - public std::unordered_set> families; -} - struct facebook::react::SystraceSection : public facebook::react::DummyTraceSection { template public SystraceSection(const char* name, ConvertsToStringPiece &&... args); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 3c0a535fb10e..0c9852094866 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -3529,6 +3529,7 @@ using facebook::react::SizeAsTuple = std::tuple>; using facebook::react::StatePipe = std::function; using facebook::react::SurfaceId = int32_t; +using facebook::react::SurfaceUpdates = std::unordered_map; using facebook::react::Tag = int32_t; using facebook::react::TelemetryClock = std::chrono::steady_clock; using facebook::react::TelemetryDuration = std::chrono::nanoseconds; @@ -4114,7 +4115,7 @@ class facebook::react::AnimatedPropsRegistry { public void clear(facebook::react::SurfaceId surfaceId); public void clearOnSurfaceStop(facebook::react::SurfaceId surfaceId); public void initializeSurface(facebook::react::SurfaceId surfaceId); - public void update(const std::unordered_map& surfaceUpdates); + public void update(const std::vector& batches); } class facebook::react::AnimationBackend : public facebook::react::UIManagerAnimationBackend { @@ -9892,12 +9893,6 @@ struct facebook::react::SurfaceContext { public std::unordered_set> pendingFamilies; } -struct facebook::react::SurfaceUpdates { - public bool hasLayoutUpdates; - public std::unordered_map propsMap; - public std::unordered_set> families; -} - struct facebook::react::SystraceSection : public facebook::react::DummyTraceSection { template public SystraceSection(const char* name, ConvertsToStringPiece &&... args); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 958586fc805e..d58e752a6458 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -3538,6 +3538,7 @@ using facebook::react::SizeAsTuple = std::tuple>; using facebook::react::StatePipe = std::function; using facebook::react::SurfaceId = int32_t; +using facebook::react::SurfaceUpdates = std::unordered_map; using facebook::react::Tag = int32_t; using facebook::react::TelemetryClock = std::chrono::steady_clock; using facebook::react::TelemetryDuration = std::chrono::nanoseconds; @@ -4125,7 +4126,7 @@ class facebook::react::AnimatedPropsRegistry { public void clear(facebook::react::SurfaceId surfaceId); public void clearOnSurfaceStop(facebook::react::SurfaceId surfaceId); public void initializeSurface(facebook::react::SurfaceId surfaceId); - public void update(const std::unordered_map& surfaceUpdates); + public void update(const std::vector& batches); } class facebook::react::AnimationBackend : public facebook::react::UIManagerAnimationBackend { @@ -10067,12 +10068,6 @@ struct facebook::react::SurfaceContext { public std::unordered_set> pendingFamilies; } -struct facebook::react::SurfaceUpdates { - public bool hasLayoutUpdates; - public std::unordered_map propsMap; - public std::unordered_set> families; -} - struct facebook::react::SystraceSection : public facebook::react::DummyTraceSection { template public SystraceSection(const char* name, ConvertsToStringPiece &&... args); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 8b46097a89f9..8b25c06f560f 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -331,6 +331,7 @@ using facebook::react::SizeAsTuple = std::tuple; using facebook::react::SnapshotMap = std::unordered_map>; using facebook::react::StatePipe = std::function; using facebook::react::SurfaceId = int32_t; +using facebook::react::SurfaceUpdates = std::unordered_map; using facebook::react::Tag = int32_t; using facebook::react::TelemetryClock = std::chrono::steady_clock; using facebook::react::TelemetryDuration = std::chrono::nanoseconds; @@ -904,7 +905,7 @@ class facebook::react::AnimatedPropsRegistry { public void clear(facebook::react::SurfaceId surfaceId); public void clearOnSurfaceStop(facebook::react::SurfaceId surfaceId); public void initializeSurface(facebook::react::SurfaceId surfaceId); - public void update(const std::unordered_map& surfaceUpdates); + public void update(const std::vector& batches); } class facebook::react::AnimationBackend : public facebook::react::UIManagerAnimationBackend { @@ -6228,12 +6229,6 @@ struct facebook::react::SurfaceContext { public std::unordered_set> pendingFamilies; } -struct facebook::react::SurfaceUpdates { - public bool hasLayoutUpdates; - public std::unordered_map propsMap; - public std::unordered_set> families; -} - struct facebook::react::SystraceSection : public facebook::react::DummyTraceSection { template public SystraceSection(const char* name, ConvertsToStringPiece &&... args); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 692b5064e583..17f0e43e2814 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -330,6 +330,7 @@ using facebook::react::SizeAsTuple = std::tuple; using facebook::react::SnapshotMap = std::unordered_map>; using facebook::react::StatePipe = std::function; using facebook::react::SurfaceId = int32_t; +using facebook::react::SurfaceUpdates = std::unordered_map; using facebook::react::Tag = int32_t; using facebook::react::TelemetryClock = std::chrono::steady_clock; using facebook::react::TelemetryDuration = std::chrono::nanoseconds; @@ -900,7 +901,7 @@ class facebook::react::AnimatedPropsRegistry { public void clear(facebook::react::SurfaceId surfaceId); public void clearOnSurfaceStop(facebook::react::SurfaceId surfaceId); public void initializeSurface(facebook::react::SurfaceId surfaceId); - public void update(const std::unordered_map& surfaceUpdates); + public void update(const std::vector& batches); } class facebook::react::AnimationBackend : public facebook::react::UIManagerAnimationBackend { @@ -6052,12 +6053,6 @@ struct facebook::react::SurfaceContext { public std::unordered_set> pendingFamilies; } -struct facebook::react::SurfaceUpdates { - public bool hasLayoutUpdates; - public std::unordered_map propsMap; - public std::unordered_set> families; -} - struct facebook::react::SystraceSection : public facebook::react::DummyTraceSection { template public SystraceSection(const char* name, ConvertsToStringPiece &&... args); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index ff0ef95bbfdb..5264f1786a53 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -331,6 +331,7 @@ using facebook::react::SizeAsTuple = std::tuple; using facebook::react::SnapshotMap = std::unordered_map>; using facebook::react::StatePipe = std::function; using facebook::react::SurfaceId = int32_t; +using facebook::react::SurfaceUpdates = std::unordered_map; using facebook::react::Tag = int32_t; using facebook::react::TelemetryClock = std::chrono::steady_clock; using facebook::react::TelemetryDuration = std::chrono::nanoseconds; @@ -903,7 +904,7 @@ class facebook::react::AnimatedPropsRegistry { public void clear(facebook::react::SurfaceId surfaceId); public void clearOnSurfaceStop(facebook::react::SurfaceId surfaceId); public void initializeSurface(facebook::react::SurfaceId surfaceId); - public void update(const std::unordered_map& surfaceUpdates); + public void update(const std::vector& batches); } class facebook::react::AnimationBackend : public facebook::react::UIManagerAnimationBackend { @@ -6219,12 +6220,6 @@ struct facebook::react::SurfaceContext { public std::unordered_set> pendingFamilies; } -struct facebook::react::SurfaceUpdates { - public bool hasLayoutUpdates; - public std::unordered_map propsMap; - public std::unordered_set> families; -} - struct facebook::react::SystraceSection : public facebook::react::DummyTraceSection { template public SystraceSection(const char* name, ConvertsToStringPiece &&... args); From 207eb92818477fc473b068d83bcfae42cf4a3a0b Mon Sep 17 00:00:00 2001 From: Bartlomiej Bloniarz Date: Wed, 30 Sep 2026 07:34:49 -0700 Subject: [PATCH 2/3] Add a batched scheduler delegate call for animated prop updates Summary: The animation backend hands its synchronous updates to the platform one view at a time. This adds `UIManager::synchronouslyUpdateAnimatedProps` and a matching `SchedulerDelegate` call that receive a whole frame's updates at once, continuing #56771. The backend uses it when `optimizedAnimatedPropUpdates` is on. The default delegate implementation forwards to the existing per-view update, so platforms that don't implement it are unaffected. ## Changelog: [General] [Changed] - Add a batched scheduler delegate call for animated prop updates behind `optimizedAnimatedPropUpdates` Differential Revision: D122570617 --- .../animationbackend/AnimationBackend.cpp | 6 ++++ .../react/renderer/scheduler/Scheduler.cpp | 7 ++++ .../react/renderer/scheduler/Scheduler.h | 1 + .../renderer/scheduler/SchedulerDelegate.h | 8 +++++ .../SchedulerDelegateInvalidationTest.cpp | 33 +++++++++++++++++++ .../react/renderer/uimanager/UIManager.cpp | 7 ++++ .../react/renderer/uimanager/UIManager.h | 2 ++ .../renderer/uimanager/UIManagerDelegate.h | 8 +++++ .../api-snapshots/ReactAndroidDebugCxx.api | 4 +++ .../api-snapshots/ReactAndroidNewarchCxx.api | 4 +++ .../api-snapshots/ReactAndroidReleaseCxx.api | 4 +++ .../api-snapshots/ReactAppleDebugCxx.api | 4 +++ .../api-snapshots/ReactAppleNewarchCxx.api | 4 +++ .../api-snapshots/ReactAppleReleaseCxx.api | 4 +++ .../api-snapshots/ReactCommonDebugCxx.api | 4 +++ .../api-snapshots/ReactCommonNewarchCxx.api | 4 +++ .../api-snapshots/ReactCommonReleaseCxx.api | 4 +++ 17 files changed, 108 insertions(+) diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp index 963f65663e16..a16fc96b78d0 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp @@ -222,6 +222,12 @@ void AnimationBackend::commitUpdates( void AnimationBackend::synchronouslyUpdateProps( const std::unordered_map& updates) { + if (ReactNativeFeatureFlags::optimizedAnimatedPropUpdates()) { + if (auto uiManager = uiManager_.lock()) { + uiManager->synchronouslyUpdateAnimatedProps(updates); + } + return; + } for (auto& [tag, animatedProps] : updates) { // TODO: We shouldn't repack it into dynamic, but for that a rewrite // of synchronouslyUpdateViewOnUIThread is needed diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index 81d9de17660c..a278fb2b3400 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -378,6 +378,13 @@ void Scheduler::uiManagerShouldSynchronouslyUpdateViewOnUIThread( } } +void Scheduler::uiManagerShouldSynchronouslyUpdateAnimatedProps( + const std::unordered_map& updates) { + if (delegate_ != nullptr) { + delegate_->schedulerShouldSynchronouslyUpdateAnimatedProps(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 179d6701727e..b29e9591c1c1 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h @@ -100,6 +100,7 @@ class Scheduler final : public UIManagerDelegate { bool isJSResponder, bool blockNativeResponder) override; void uiManagerShouldSynchronouslyUpdateViewOnUIThread(Tag tag, const folly::dynamic &props) override; + void uiManagerShouldSynchronouslyUpdateAnimatedProps(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 10f45018d94a..f68b830dffee 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerDelegate.h @@ -11,6 +11,7 @@ #include +#include #include #include #include @@ -66,6 +67,13 @@ class SchedulerDelegate { virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(Tag tag, const folly::dynamic &props) = 0; + virtual void schedulerShouldSynchronouslyUpdateAnimatedProps(const std::unordered_map &updates) + { + for (const auto &[tag, props] : updates) { + schedulerShouldSynchronouslyUpdateViewOnUIThread(tag, animationbackend::packAnimatedProps(props)); + } + } + 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/scheduler/tests/SchedulerDelegateInvalidationTest.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/tests/SchedulerDelegateInvalidationTest.cpp index 76c1646058f9..2f7957473128 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/tests/SchedulerDelegateInvalidationTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/tests/SchedulerDelegateInvalidationTest.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -588,4 +589,36 @@ TEST_F( EXPECT_EQ(delegate_->shouldRenderTransactionsCount(), 1); } +TEST(SchedulerDelegateTest, animatedPropsUseExistingViewUpdatesByDefault) { + class PropsDelegate : public RecordingDelegate { + public: + std::unordered_map received; + + void schedulerShouldSynchronouslyUpdateViewOnUIThread( + Tag tag, + const folly::dynamic& props) override { + received.emplace(tag, props); + } + } delegate; + + std::unordered_map updates; + updates.emplace( + 10, + AnimatedProps{ + {}, + std::make_unique(folly::dynamic::object("opacity", 0.25))}); + AnimatedPropsBuilder builder; + builder.setOpacity(0.75); + updates.emplace(20, builder.get()); + + delegate.schedulerShouldSynchronouslyUpdateAnimatedProps(updates); + + ASSERT_EQ(delegate.received.size(), 2); + folly::dynamic expectedRaw = folly::dynamic::object("opacity", 0.25); + folly::dynamic expectedTyped = folly::dynamic::object("opacity", 0.75); + EXPECT_EQ(delegate.received.at(10), expectedRaw); + EXPECT_EQ(delegate.received.at(20), expectedTyped); + EXPECT_EQ(updates.at(10).rawProps->toDynamic(), delegate.received.at(10)); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index 0cb62936c8b5..7eefe4606177 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -769,6 +769,13 @@ void UIManager::synchronouslyUpdateViewOnUIThread( } } +void UIManager::synchronouslyUpdateAnimatedProps( + const std::unordered_map& updates) { + if (delegate_ != nullptr) { + delegate_->uiManagerShouldSynchronouslyUpdateAnimatedProps(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 aaf758bcdf6b..541d57a0b258 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h @@ -89,6 +89,8 @@ class UIManager final : public ShadowTreeDelegate { void synchronouslyUpdateViewOnUIThread(Tag tag, const folly::dynamic &props); + void synchronouslyUpdateAnimatedProps(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/UIManagerDelegate.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerDelegate.h index 579989d8e01f..e431d6ee22b1 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerDelegate.h @@ -21,6 +21,8 @@ namespace facebook::react { +struct AnimatedProps; + /* * Abstract class for UIManager's delegate. */ @@ -71,6 +73,12 @@ class UIManagerDelegate { */ virtual void uiManagerShouldSynchronouslyUpdateViewOnUIThread(Tag tag, const folly::dynamic &props) = 0; + /* + * Synchronous update of animated props for many views at once. + */ + virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps( + const std::unordered_map &updates) = 0; + /* * Called after updateShadowTree is invoked. */ diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 7329262535fb..d24683a5d5b5 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -4618,6 +4618,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) override; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) final; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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; @@ -4641,6 +4642,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 schedulerShouldSynchronouslyUpdateAnimatedProps(const std::unordered_map& updates); public virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~SchedulerDelegate() noexcept = default; } @@ -5372,6 +5374,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, std::string moduleName, folly::dynamic props, facebook::react::DisplayMode displayMode) const noexcept; public void stopSurfaceForAnimationDelegate(facebook::react::SurfaceId surfaceId) const; + public void synchronouslyUpdateAnimatedProps(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); @@ -5439,6 +5442,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) = 0; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) = 0; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 3fbdbaa1036d..96f11569470d 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -4434,6 +4434,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) override; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) final; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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; @@ -4457,6 +4458,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 schedulerShouldSynchronouslyUpdateAnimatedProps(const std::unordered_map& updates); public virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~SchedulerDelegate() noexcept = default; } @@ -5182,6 +5184,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, std::string moduleName, folly::dynamic props, facebook::react::DisplayMode displayMode) const noexcept; public void stopSurfaceForAnimationDelegate(facebook::react::SurfaceId surfaceId) const; + public void synchronouslyUpdateAnimatedProps(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); @@ -5249,6 +5252,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) = 0; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) = 0; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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 5feacf3d73f8..bcc9d69640dd 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -4615,6 +4615,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) override; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) final; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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; @@ -4638,6 +4639,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 schedulerShouldSynchronouslyUpdateAnimatedProps(const std::unordered_map& updates); public virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~SchedulerDelegate() noexcept = default; } @@ -5363,6 +5365,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, std::string moduleName, folly::dynamic props, facebook::react::DisplayMode displayMode) const noexcept; public void stopSurfaceForAnimationDelegate(facebook::react::SurfaceId surfaceId) const; + public void synchronouslyUpdateAnimatedProps(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); @@ -5430,6 +5433,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) = 0; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) = 0; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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 83e348be0cce..53991b89c0c2 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -6767,6 +6767,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) override; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) final; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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; @@ -6790,6 +6791,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 schedulerShouldSynchronouslyUpdateAnimatedProps(const std::unordered_map& updates); public virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~SchedulerDelegate() noexcept = default; } @@ -7502,6 +7504,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, std::string moduleName, folly::dynamic props, facebook::react::DisplayMode displayMode) const noexcept; public void stopSurfaceForAnimationDelegate(facebook::react::SurfaceId surfaceId) const; + public void synchronouslyUpdateAnimatedProps(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); @@ -7569,6 +7572,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) = 0; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) = 0; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 0c9852094866..b1e8f49542ad 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -6615,6 +6615,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) override; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) final; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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; @@ -6638,6 +6639,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 schedulerShouldSynchronouslyUpdateAnimatedProps(const std::unordered_map& updates); public virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~SchedulerDelegate() noexcept = default; } @@ -7344,6 +7346,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, std::string moduleName, folly::dynamic props, facebook::react::DisplayMode displayMode) const noexcept; public void stopSurfaceForAnimationDelegate(facebook::react::SurfaceId surfaceId) const; + public void synchronouslyUpdateAnimatedProps(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); @@ -7411,6 +7414,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) = 0; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) = 0; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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 d58e752a6458..4c6f5a4a4685 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -6764,6 +6764,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) override; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) final; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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; @@ -6787,6 +6788,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 schedulerShouldSynchronouslyUpdateAnimatedProps(const std::unordered_map& updates); public virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~SchedulerDelegate() noexcept = default; } @@ -7493,6 +7495,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, std::string moduleName, folly::dynamic props, facebook::react::DisplayMode displayMode) const noexcept; public void stopSurfaceForAnimationDelegate(facebook::react::SurfaceId surfaceId) const; + public void synchronouslyUpdateAnimatedProps(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); @@ -7560,6 +7563,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) = 0; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) = 0; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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 8b25c06f560f..87a4c9ac4bb1 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -3162,6 +3162,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) override; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) final; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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; @@ -3185,6 +3186,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 schedulerShouldSynchronouslyUpdateAnimatedProps(const std::unordered_map& updates); public virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~SchedulerDelegate() noexcept = default; } @@ -3813,6 +3815,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, std::string moduleName, folly::dynamic props, facebook::react::DisplayMode displayMode) const noexcept; public void stopSurfaceForAnimationDelegate(facebook::react::SurfaceId surfaceId) const; + public void synchronouslyUpdateAnimatedProps(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); @@ -3880,6 +3883,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) = 0; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) = 0; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 17f0e43e2814..87569996e1ad 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -3018,6 +3018,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) override; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) final; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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; @@ -3041,6 +3042,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 schedulerShouldSynchronouslyUpdateAnimatedProps(const std::unordered_map& updates); public virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~SchedulerDelegate() noexcept = default; } @@ -3663,6 +3665,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, std::string moduleName, folly::dynamic props, facebook::react::DisplayMode displayMode) const noexcept; public void stopSurfaceForAnimationDelegate(facebook::react::SurfaceId surfaceId) const; + public void synchronouslyUpdateAnimatedProps(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); @@ -3730,6 +3733,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) = 0; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) = 0; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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 5264f1786a53..cfb7cfadb968 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -3159,6 +3159,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) override; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) final; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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; @@ -3182,6 +3183,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 schedulerShouldSynchronouslyUpdateAnimatedProps(const std::unordered_map& updates); public virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(facebook::react::Tag tag, const folly::dynamic& props) = 0; public virtual ~SchedulerDelegate() noexcept = default; } @@ -3804,6 +3806,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, std::string moduleName, folly::dynamic props, facebook::react::DisplayMode displayMode) const noexcept; public void stopSurfaceForAnimationDelegate(facebook::react::SurfaceId surfaceId) const; + public void synchronouslyUpdateAnimatedProps(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); @@ -3871,6 +3874,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; public virtual void uiManagerShouldAddOnSurfaceStartCallback(facebook::react::UIManagerDelegate::OnSurfaceStartCallback&& callback) = 0; public virtual void uiManagerShouldRemoveEventListener(const std::shared_ptr& listener) = 0; + public virtual void uiManagerShouldSynchronouslyUpdateAnimatedProps(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 31de566c2085138cc4f1f7bc60031c6514e7e84d Mon Sep 17 00:00:00 2001 From: Bartlomiej Bloniarz Date: Wed, 30 Sep 2026 07:46:03 -0700 Subject: [PATCH 3/3] Batch Android animated prop updates into one JNI call per surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: With the batched delegate from the previous diff, Android applied the backend's synchronous updates with one JNI call and one `ReadableNativeMap` per view per frame. When `optimizedAnimatedPropUpdates` is on, a surface's updates now go to the UI thread in a single call: `FabricMountingManager::synchronouslyUpdateAnimatedProps` encodes them into int and double buffers, and `BatchedAnimatedPropsMountItem` decodes and applies them. This continues the encoder and mount item from #56468. Views that are not mounted yet are still queued. A view is packed only when every prop it carries has a command. Unsupported props, null resets, platform colors, malformed values and typed props send the whole view through the existing `packAnimatedProps` serializer as a map instead. The commands cover the props Reanimated applies synchronously on Android, following the parity review in https://github.com/react/react-native/pull/56468#discussion_r3225982756. Angles are converted to radians in C++, and radii and translations keep their px or percent units. Because the decoder produces `JavaOnlyArray` transforms, `TransformHelper`'s Kotlin conversion now applies a uniform scale to Z as well as X and Y, matching the native conversion when scale is combined with 3D rotation. On a low-end Android phone (4× Cortex-A53) with 12 views animating opacity, UI-thread time per frame's synchronous batch dropped from about 6.5 ms to 3.4 ms. ## Changelog: [Android] [Performance] - Batch synchronous animated prop updates when `optimizedAnimatedPropUpdates` is enabled Differential Revision: D122570618 --- .../react/fabric/FabricUIManager.java | 22 + .../react/fabric/mounting/MountingManager.kt | 10 + .../BatchedAnimatedPropsMountItem.kt | 274 ++++++++++++ .../react/uimanager/TransformHelper.kt | 1 + .../fabric/AnimatedPropBufferEncoder.cpp | 393 +++++++++++++++++ .../react/fabric/AnimatedPropBufferEncoder.h | 24 ++ .../react/fabric/FabricMountingManager.cpp | 29 ++ .../jni/react/fabric/FabricMountingManager.h | 4 + .../react/fabric/FabricUIManagerBinding.cpp | 7 + .../jni/react/fabric/FabricUIManagerBinding.h | 2 + .../test/AnimatedPropBufferEncoderTest.cpp | 401 ++++++++++++++++++ .../BatchedAnimatedPropsMountItemTest.kt | 358 ++++++++++++++++ .../MountingManagerAnimatedPropsTest.kt | 99 +++++ .../react/renderer/core/RawProps.h | 6 + .../renderer/core/tests/RawPropsTest.cpp | 23 + .../api-snapshots/ReactAndroidDebugCxx.api | 9 + .../api-snapshots/ReactAndroidNewarchCxx.api | 9 + .../api-snapshots/ReactAndroidReleaseCxx.api | 9 + .../api-snapshots/ReactAppleDebugCxx.api | 1 + .../api-snapshots/ReactAppleNewarchCxx.api | 1 + .../api-snapshots/ReactAppleReleaseCxx.api | 1 + .../api-snapshots/ReactCommonDebugCxx.api | 1 + .../api-snapshots/ReactCommonNewarchCxx.api | 1 + .../api-snapshots/ReactCommonReleaseCxx.api | 1 + 24 files changed, 1686 insertions(+) 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/test/AnimatedPropBufferEncoderTest.cpp create mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/BatchedAnimatedPropsMountItemTest.kt create mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/MountingManagerAnimatedPropsTest.kt 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 3fe95a4595db..04c8e7a1a09b 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 @@ -62,6 +62,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; @@ -837,6 +838,27 @@ public void synchronouslyUpdateViewOnUIThread(final int reactTag, final Readable ReactMarkerConstants.FABRIC_UPDATE_UI_MAIN_THREAD_END, null, commitNumber); } + /** Called from C++ via JNI with a batch encoded by AnimatedPropBufferEncoder.cpp. */ + @SuppressLint("NotInvokedPrivateMethod") + @SuppressWarnings("unused") + @UiThread + @ThreadConfined(UI) + private void synchronouslyUpdateViewBatch( + int[] intBuffer, double[] doubleBuffer, Object[] rawPropsBuffer) { + UiThreadUtil.assertOnUiThread(); + + int commitNumber = mCurrentSynchronousCommitNumber++; + ReactMarker.logFabricMarker( + ReactMarkerConstants.FABRIC_UPDATE_UI_MAIN_THREAD_START, null, commitNumber); + + new BatchedAnimatedPropsMountItem( + intBuffer, doubleBuffer, rawPropsBuffer, mMountItemDispatcher) + .execute(mMountingManager); + + ReactMarker.logFabricMarker( + ReactMarkerConstants.FABRIC_UPDATE_UI_MAIN_THREAD_END, null, commitNumber); + } + /** Called from C++ via JNI. */ @SuppressLint("NotInvokedPrivateMethod") @SuppressWarnings("unused") diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt index fc1f576da33d..72b717d243da 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt @@ -268,6 +268,16 @@ internal class MountingManager( getSurfaceManagerForView(reactTag)?.storeSynchronousMountPropsOverride(reactTag, props) } + /** Returns false when no surface owns [reactTag] yet, so the caller can queue the update. */ + @UiThread + fun updateAnimatedPropsSynchronously(reactTag: Int, props: ReadableMap): Boolean { + assertOnUiThread() + val surfaceMountingManager = getSurfaceManagerForView(reactTag) ?: return false + surfaceMountingManager.storeSynchronousMountPropsOverride(reactTag, props) + surfaceMountingManager.updatePropsSynchronously(reactTag, props) + return true + } + @UiThread fun updatePropsSynchronously(reactTag: Int, props: ReadableMap?) { assertOnUiThread() 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..21807a3ade44 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/BatchedAnimatedPropsMountItem.kt @@ -0,0 +1,274 @@ +/* + * 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.bridge.ReadableMap +import com.facebook.react.fabric.FabricUIManager.IS_DEVELOPMENT_ENVIRONMENT +import com.facebook.react.fabric.mounting.MountItemDispatcher +import com.facebook.react.fabric.mounting.MountingManager +import com.facebook.react.uimanager.ViewProps + +internal class BatchedAnimatedPropsMountItem( + private val intBuffer: IntArray, + private val doubleBuffer: DoubleArray, + private val rawPropsBuffer: Array, + private val mountItemDispatcher: MountItemDispatcher, +) : MountItem { + + override fun execute(mountingManager: MountingManager) { + decode { viewTag, props -> + try { + if (!mountingManager.updateAnimatedPropsSynchronously(viewTag, props)) { + mountItemDispatcher.addMountItem(SynchronousMountItem(viewTag, props)) + } + } catch (ex: Exception) { + // Match SynchronousMountItem's handling of view-manager failures. + } + } + } + + internal fun decode(apply: (Int, ReadableMap) -> Unit) { + var intIdx = 0 + var doubleIdx = 0 + var rawIdx = 0 + while (intIdx < intBuffer.size) { + val command = intBuffer[intIdx++] + require(command == CMD_START_OF_VIEW) { "Expected start of animated view, got $command" } + val viewTag = intBuffer[intIdx++] + + if (intBuffer[intIdx] == CMD_RAW_PROPS) { + require(intBuffer[intIdx + 1] == CMD_END_OF_VIEW) { "Raw props must be the whole view" } + intIdx += 2 + apply(viewTag, rawPropsBuffer[rawIdx++] as ReadableMap) + continue + } + + val props = JavaOnlyMap() + while (true) { + val cmd = intBuffer[intIdx++] + if (cmd == CMD_END_OF_VIEW) { + break + } + + when (cmd) { + CMD_OPACITY, + CMD_ELEVATION, + CMD_Z_INDEX, + CMD_SHADOW_OPACITY, + CMD_SHADOW_RADIUS, + CMD_OUTLINE_OFFSET, + CMD_OUTLINE_WIDTH -> props.putDouble(commandToString(cmd), doubleBuffer[doubleIdx++]) + CMD_BACKGROUND_COLOR, + CMD_COLOR, + CMD_TINT_COLOR, + CMD_PLACEHOLDER_TEXT_COLOR, + CMD_SHADOW_COLOR, + CMD_BORDER_COLOR, + CMD_BORDER_TOP_COLOR, + CMD_BORDER_BOTTOM_COLOR, + CMD_BORDER_LEFT_COLOR, + CMD_BORDER_RIGHT_COLOR, + CMD_BORDER_START_COLOR, + CMD_BORDER_END_COLOR, + CMD_BORDER_BLOCK_COLOR, + CMD_BORDER_BLOCK_START_COLOR, + CMD_BORDER_BLOCK_END_COLOR, + CMD_OUTLINE_COLOR -> props.putInt(commandToString(cmd), intBuffer[intIdx++]) + CMD_BORDER_RADIUS, + CMD_BORDER_TOP_LEFT_RADIUS, + CMD_BORDER_TOP_RIGHT_RADIUS, + CMD_BORDER_TOP_START_RADIUS, + CMD_BORDER_TOP_END_RADIUS, + CMD_BORDER_BOTTOM_LEFT_RADIUS, + CMD_BORDER_BOTTOM_RIGHT_RADIUS, + CMD_BORDER_BOTTOM_START_RADIUS, + CMD_BORDER_BOTTOM_END_RADIUS, + CMD_BORDER_START_START_RADIUS, + CMD_BORDER_START_END_RADIUS, + CMD_BORDER_END_START_RADIUS, + CMD_BORDER_END_END_RADIUS -> + putLength( + props, + commandToString(cmd), + intBuffer[intIdx++], + doubleBuffer[doubleIdx++], + ) + CMD_START_OF_TRANSFORM -> { + val transform = JavaOnlyArray() + while (true) { + val transformCmd = intBuffer[intIdx++] + if (transformCmd == CMD_END_OF_TRANSFORM) { + break + } + val name = transformCommandToString(transformCmd) + val entry = JavaOnlyMap() + when (transformCmd) { + CMD_TRANSLATE_X, + CMD_TRANSLATE_Y -> + putLength(entry, name, intBuffer[intIdx++], doubleBuffer[doubleIdx++]) + CMD_MATRIX -> { + val matrix = JavaOnlyArray() + repeat(16) { matrix.pushDouble(doubleBuffer[doubleIdx++]) } + entry.putArray(name, matrix) + } + // Angles are in radians. + else -> entry.putDouble(name, doubleBuffer[doubleIdx++]) + } + transform.pushMap(entry) + } + props.putArray(ViewProps.TRANSFORM, transform) + } + else -> error("Unknown animated prop command: $cmd") + } + } + + apply(viewTag, props) + } + require(doubleIdx == doubleBuffer.size && rawIdx == rawPropsBuffer.size) { + "Unused values in animated props buffer" + } + } + + override fun toString(): String { + if (!IS_DEVELOPMENT_ENVIRONMENT) return "BATCHED UPDATE PROPS " + val updates = mutableListOf() + decode { tag, props -> updates.add("[$tag]: ${props.toHashMap()}") } + return "BATCHED UPDATE PROPS ${updates.joinToString()}" + } + + override fun getSurfaceId(): Int = View.NO_ID + + companion object { + // Keep command values in sync with AnimatedPropBufferEncoder.cpp. + 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_RAW_PROPS = 5 + 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_PLACEHOLDER_TEXT_COLOR = 18 + private const val CMD_SHADOW_COLOR = 19 + 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_BORDER_BLOCK_COLOR = 47 + private const val CMD_BORDER_BLOCK_START_COLOR = 48 + private const val CMD_BORDER_BLOCK_END_COLOR = 49 + private const val CMD_OUTLINE_COLOR = 50 + private const val CMD_OUTLINE_OFFSET = 51 + private const val CMD_OUTLINE_WIDTH = 52 + 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_PX = 202 + private const val CMD_UNIT_PERCENT = 203 + + private fun putLength(map: JavaOnlyMap, name: String, unit: Int, value: Double) = + when (unit) { + CMD_UNIT_PX -> map.putDouble(name, value) + CMD_UNIT_PERCENT -> map.putString(name, "$value%") + else -> error("Unknown length unit: $unit") + } + + private 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_PLACEHOLDER_TEXT_COLOR -> "placeholderTextColor" + CMD_SHADOW_COLOR -> ViewProps.SHADOW_COLOR + 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 + CMD_BORDER_BLOCK_COLOR -> ViewProps.BORDER_BLOCK_COLOR + CMD_BORDER_BLOCK_START_COLOR -> ViewProps.BORDER_BLOCK_START_COLOR + CMD_BORDER_BLOCK_END_COLOR -> ViewProps.BORDER_BLOCK_END_COLOR + CMD_OUTLINE_COLOR -> ViewProps.OUTLINE_COLOR + CMD_OUTLINE_OFFSET -> ViewProps.OUTLINE_OFFSET + CMD_OUTLINE_WIDTH -> ViewProps.OUTLINE_WIDTH + else -> error("Unknown animated prop command: $command") + } + + private fun transformCommandToString(command: Int): String = + when (command) { + CMD_TRANSLATE_X -> ViewProps.TRANSLATE_X + CMD_TRANSLATE_Y -> ViewProps.TRANSLATE_Y + 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 -> error("Unknown animated prop command: $command") + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TransformHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TransformHelper.kt index 8c713b98fbec..189c3ae53111 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TransformHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TransformHelper.kt @@ -141,6 +141,7 @@ public object TransformHelper { val scale = transform.getDouble(transformType) MatrixMathHelper.applyScaleX(helperMatrix, scale) MatrixMathHelper.applyScaleY(helperMatrix, scale) + MatrixMathHelper.applyScaleZ(helperMatrix, scale) } "scaleX" -> MatrixMathHelper.applyScaleX(helperMatrix, transform.getDouble(transformType)) "scaleY" -> MatrixMathHelper.applyScaleY(helperMatrix, transform.getDouble(transformType)) 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..a619b78b31d7 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/AnimatedPropBufferEncoder.cpp @@ -0,0 +1,393 @@ +/* + * 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 +#include +#include +#include +#include +#include + +namespace facebook::react { + +namespace { + +// A view is START_OF_VIEW, tag, its props, END_OF_VIEW. Props take their values +// from the int and double buffers in order. A view with a prop that has no +// command is RAW_PROPS instead, taking the next map of the raw props buffer. +// Keep in sync with BatchedAnimatedPropsMountItem.kt on the Java side. + +constexpr int CMD_START_OF_VIEW = 1; +constexpr int CMD_START_OF_TRANSFORM = 2; +constexpr int CMD_END_OF_TRANSFORM = 3; +constexpr int CMD_END_OF_VIEW = 4; +constexpr int CMD_RAW_PROPS = 5; + +constexpr int CMD_OPACITY = 10; +constexpr int CMD_ELEVATION = 11; +constexpr int CMD_Z_INDEX = 12; +constexpr int CMD_SHADOW_OPACITY = 13; +constexpr int CMD_SHADOW_RADIUS = 14; + +constexpr int CMD_BACKGROUND_COLOR = 15; +constexpr int CMD_COLOR = 16; +constexpr int CMD_TINT_COLOR = 17; +constexpr int CMD_PLACEHOLDER_TEXT_COLOR = 18; +constexpr int CMD_SHADOW_COLOR = 19; + +constexpr int CMD_BORDER_RADIUS = 20; +constexpr int CMD_BORDER_TOP_LEFT_RADIUS = 21; +constexpr int CMD_BORDER_TOP_RIGHT_RADIUS = 22; +constexpr int CMD_BORDER_TOP_START_RADIUS = 23; +constexpr int CMD_BORDER_TOP_END_RADIUS = 24; +constexpr int CMD_BORDER_BOTTOM_LEFT_RADIUS = 25; +constexpr int CMD_BORDER_BOTTOM_RIGHT_RADIUS = 26; +constexpr int CMD_BORDER_BOTTOM_START_RADIUS = 27; +constexpr int CMD_BORDER_BOTTOM_END_RADIUS = 28; +constexpr int CMD_BORDER_START_START_RADIUS = 29; +constexpr int CMD_BORDER_START_END_RADIUS = 30; +constexpr int CMD_BORDER_END_START_RADIUS = 31; +constexpr int CMD_BORDER_END_END_RADIUS = 32; + +constexpr int CMD_BORDER_COLOR = 40; +constexpr int CMD_BORDER_TOP_COLOR = 41; +constexpr int CMD_BORDER_BOTTOM_COLOR = 42; +constexpr int CMD_BORDER_LEFT_COLOR = 43; +constexpr int CMD_BORDER_RIGHT_COLOR = 44; +constexpr int CMD_BORDER_START_COLOR = 45; +constexpr int CMD_BORDER_END_COLOR = 46; +constexpr int CMD_BORDER_BLOCK_COLOR = 47; +constexpr int CMD_BORDER_BLOCK_START_COLOR = 48; +constexpr int CMD_BORDER_BLOCK_END_COLOR = 49; + +constexpr int CMD_OUTLINE_COLOR = 50; +constexpr int CMD_OUTLINE_OFFSET = 51; +constexpr int CMD_OUTLINE_WIDTH = 52; + +constexpr int CMD_TRANSFORM_TRANSLATE_X = 100; +constexpr int CMD_TRANSFORM_TRANSLATE_Y = 101; +constexpr int CMD_TRANSFORM_SCALE = 102; +constexpr int CMD_TRANSFORM_SCALE_X = 103; +constexpr int CMD_TRANSFORM_SCALE_Y = 104; +constexpr int CMD_TRANSFORM_ROTATE = 105; +constexpr int CMD_TRANSFORM_ROTATE_X = 106; +constexpr int CMD_TRANSFORM_ROTATE_Y = 107; +constexpr int CMD_TRANSFORM_ROTATE_Z = 108; +constexpr int CMD_TRANSFORM_SKEW_X = 109; +constexpr int CMD_TRANSFORM_SKEW_Y = 110; +constexpr int CMD_TRANSFORM_MATRIX = 111; +constexpr int CMD_TRANSFORM_PERSPECTIVE = 112; + +constexpr int CMD_UNIT_PX = 202; +constexpr int CMD_UNIT_PERCENT = 203; + +std::optional propNameToCommand(const std::string& name) { + static const std::unordered_map kMap = { + {"opacity", CMD_OPACITY}, + {"elevation", CMD_ELEVATION}, + {"zIndex", CMD_Z_INDEX}, + {"shadowOpacity", CMD_SHADOW_OPACITY}, + {"shadowRadius", CMD_SHADOW_RADIUS}, + {"backgroundColor", CMD_BACKGROUND_COLOR}, + {"color", CMD_COLOR}, + {"tintColor", CMD_TINT_COLOR}, + {"placeholderTextColor", CMD_PLACEHOLDER_TEXT_COLOR}, + {"shadowColor", CMD_SHADOW_COLOR}, + {"borderRadius", CMD_BORDER_RADIUS}, + {"borderTopLeftRadius", CMD_BORDER_TOP_LEFT_RADIUS}, + {"borderTopRightRadius", CMD_BORDER_TOP_RIGHT_RADIUS}, + {"borderTopStartRadius", CMD_BORDER_TOP_START_RADIUS}, + {"borderTopEndRadius", CMD_BORDER_TOP_END_RADIUS}, + {"borderBottomLeftRadius", CMD_BORDER_BOTTOM_LEFT_RADIUS}, + {"borderBottomRightRadius", CMD_BORDER_BOTTOM_RIGHT_RADIUS}, + {"borderBottomStartRadius", CMD_BORDER_BOTTOM_START_RADIUS}, + {"borderBottomEndRadius", CMD_BORDER_BOTTOM_END_RADIUS}, + {"borderStartStartRadius", CMD_BORDER_START_START_RADIUS}, + {"borderStartEndRadius", CMD_BORDER_START_END_RADIUS}, + {"borderEndStartRadius", CMD_BORDER_END_START_RADIUS}, + {"borderEndEndRadius", CMD_BORDER_END_END_RADIUS}, + {"borderColor", CMD_BORDER_COLOR}, + {"borderTopColor", CMD_BORDER_TOP_COLOR}, + {"borderBottomColor", CMD_BORDER_BOTTOM_COLOR}, + {"borderLeftColor", CMD_BORDER_LEFT_COLOR}, + {"borderRightColor", CMD_BORDER_RIGHT_COLOR}, + {"borderStartColor", CMD_BORDER_START_COLOR}, + {"borderEndColor", CMD_BORDER_END_COLOR}, + {"borderBlockColor", CMD_BORDER_BLOCK_COLOR}, + {"borderBlockStartColor", CMD_BORDER_BLOCK_START_COLOR}, + {"borderBlockEndColor", CMD_BORDER_BLOCK_END_COLOR}, + {"outlineColor", CMD_OUTLINE_COLOR}, + {"outlineOffset", CMD_OUTLINE_OFFSET}, + {"outlineWidth", CMD_OUTLINE_WIDTH}, + {"transform", CMD_START_OF_TRANSFORM}, + }; + auto it = kMap.find(name); + if (it == kMap.end()) { + return std::nullopt; + } + return it->second; +} + +std::optional transformNameToCommand(const std::string& name) { + static const std::unordered_map kMap = { + {"translateX", CMD_TRANSFORM_TRANSLATE_X}, + {"translateY", CMD_TRANSFORM_TRANSLATE_Y}, + {"scale", CMD_TRANSFORM_SCALE}, + {"scaleX", CMD_TRANSFORM_SCALE_X}, + {"scaleY", CMD_TRANSFORM_SCALE_Y}, + {"rotate", CMD_TRANSFORM_ROTATE}, + {"rotateX", CMD_TRANSFORM_ROTATE_X}, + {"rotateY", CMD_TRANSFORM_ROTATE_Y}, + {"rotateZ", CMD_TRANSFORM_ROTATE_Z}, + {"skewX", CMD_TRANSFORM_SKEW_X}, + {"skewY", CMD_TRANSFORM_SKEW_Y}, + {"matrix", CMD_TRANSFORM_MATRIX}, + {"perspective", CMD_TRANSFORM_PERSPECTIVE}, + }; + auto it = kMap.find(name); + if (it == kMap.end()) { + return std::nullopt; + } + return it->second; +} + +bool packLength( + const folly::dynamic& value, + std::vector& intBuffer, + std::vector& doubleBuffer) { + if (value.isNumber()) { + intBuffer.push_back(CMD_UNIT_PX); + doubleBuffer.push_back(value.asDouble()); + return true; + } + if (!value.isString() || !value.getString().ends_with("%")) { + return false; + } + const auto& text = value.getString(); + auto percent = folly::tryTo(text.substr(0, text.size() - 1)); + if (!percent.hasValue()) { + return false; + } + intBuffer.push_back(CMD_UNIT_PERCENT); + doubleBuffer.push_back(percent.value()); + return true; +} + +std::optional toRadians(const folly::dynamic& value) { + if (value.isNumber()) { + return value.asDouble(); + } + if (!value.isString()) { + return std::nullopt; + } + const auto& text = value.getString(); + bool isDegrees = text.ends_with("deg"); + if (!isDegrees && !text.ends_with("rad")) { + return std::nullopt; + } + auto angle = folly::tryTo(text.substr(0, text.size() - 3)); + if (!angle.hasValue()) { + return std::nullopt; + } + return isDegrees ? angle.value() * std::numbers::pi / 180 : angle.value(); +} + +bool packTransformToBuffers( + const folly::dynamic& transform, + std::vector& intBuffer, + std::vector& doubleBuffer) { + if (!transform.isArray()) { + return false; + } + intBuffer.push_back(CMD_START_OF_TRANSFORM); + for (const auto& item : transform) { + if (!item.isObject() || item.size() != 1) { + return false; + } + const auto& [name, value] = *item.items().begin(); + auto cmd = transformNameToCommand(name.getString()); + if (!cmd.has_value()) { + return false; + } + intBuffer.push_back(cmd.value()); + switch (cmd.value()) { + case CMD_TRANSFORM_SCALE: + case CMD_TRANSFORM_SCALE_X: + case CMD_TRANSFORM_SCALE_Y: + case CMD_TRANSFORM_PERSPECTIVE: + if (!value.isNumber()) { + return false; + } + doubleBuffer.push_back(value.asDouble()); + break; + case CMD_TRANSFORM_TRANSLATE_X: + case CMD_TRANSFORM_TRANSLATE_Y: + if (!packLength(value, intBuffer, doubleBuffer)) { + return false; + } + 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: { + auto radians = toRadians(value); + if (!radians.has_value()) { + return false; + } + doubleBuffer.push_back(radians.value()); + break; + } + case CMD_TRANSFORM_MATRIX: + if (!value.isArray() || value.size() != 16) { + return false; + } + for (const auto& element : value) { + if (!element.isNumber()) { + return false; + } + doubleBuffer.push_back(element.asDouble()); + } + break; + default: + return false; + } + } + intBuffer.push_back(CMD_END_OF_TRANSFORM); + return true; +} + +bool packDynamicEntryToBuffers( + const std::string& key, + const folly::dynamic& value, + std::vector& intBuffer, + std::vector& doubleBuffer) { + auto cmd = propNameToCommand(key); + if (!cmd.has_value()) { + return false; + } + + switch (cmd.value()) { + case CMD_OPACITY: + case CMD_ELEVATION: + case CMD_Z_INDEX: + case CMD_SHADOW_OPACITY: + case CMD_SHADOW_RADIUS: + case CMD_OUTLINE_OFFSET: + case CMD_OUTLINE_WIDTH: + if (!value.isNumber()) { + return false; + } + intBuffer.push_back(cmd.value()); + doubleBuffer.push_back(value.asDouble()); + return true; + + case CMD_BACKGROUND_COLOR: + case CMD_COLOR: + case CMD_TINT_COLOR: + case CMD_PLACEHOLDER_TEXT_COLOR: + case CMD_SHADOW_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: + case CMD_BORDER_BLOCK_COLOR: + case CMD_BORDER_BLOCK_START_COLOR: + case CMD_BORDER_BLOCK_END_COLOR: + case CMD_OUTLINE_COLOR: { + if (!value.isNumber()) { + return false; + } + auto color = value.isInt() ? folly::tryTo(value.getInt()) + : folly::tryTo(value.getDouble()); + if (!color.hasValue()) { + return false; + } + intBuffer.push_back(cmd.value()); + intBuffer.push_back(static_cast(color.value())); + return true; + } + + 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()); + return packLength(value, intBuffer, doubleBuffer); + + case CMD_START_OF_TRANSFORM: + return packTransformToBuffers(value, intBuffer, doubleBuffer); + + default: + return false; + } +} + +bool packAnimatedPropsToBuffers( + const AnimatedProps& animatedProps, + std::vector& intBuffer, + std::vector& doubleBuffer) { + if (!animatedProps.props.empty()) { + return false; + } + if (animatedProps.rawProps) { + std::optional converted; + const auto* rawProps = animatedProps.rawProps->getDynamic(); + if (rawProps == nullptr) { + rawProps = &converted.emplace(animatedProps.rawProps->toDynamic()); + } + for (const auto& [key, value] : rawProps->items()) { + if (!packDynamicEntryToBuffers( + key.getString(), value, intBuffer, doubleBuffer)) { + return false; + } + } + } + return true; +} + +} // namespace + +AnimatedPropsBuffer encodeAnimatedProps( + const std::unordered_map& updates) { + AnimatedPropsBuffer buffer; + buffer.ints.reserve(updates.size() * 4); + buffer.doubles.reserve(updates.size()); + for (const auto& [tag, animatedProps] : updates) { + buffer.ints.push_back(CMD_START_OF_VIEW); + buffer.ints.push_back(tag); + auto intSize = buffer.ints.size(); + auto doubleSize = buffer.doubles.size(); + if (!packAnimatedPropsToBuffers( + animatedProps, buffer.ints, buffer.doubles)) { + buffer.ints.resize(intSize); + buffer.doubles.resize(doubleSize); + buffer.ints.push_back(CMD_RAW_PROPS); + buffer.rawProps.push_back( + animationbackend::packAnimatedProps(animatedProps)); + } + buffer.ints.push_back(CMD_END_OF_VIEW); + } + return buffer; +} + +} // 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..316c910aa358 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/AnimatedPropBufferEncoder.h @@ -0,0 +1,24 @@ +/* + * 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 { + +struct AnimatedPropsBuffer { + std::vector ints; + std::vector doubles; + std::vector rawProps; +}; + +AnimatedPropsBuffer encodeAnimatedProps(const std::unordered_map &updates); + +} // namespace facebook::react 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 f1e15694da63..4be462f7c934 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" @@ -1246,6 +1247,34 @@ void FabricMountingManager::synchronouslyUpdateViewOnUIThread( synchronouslyUpdateViewOnUIThreadJNI(javaUIManager_, viewTag, propsMap); } +void FabricMountingManager::synchronouslyUpdateAnimatedProps( + const std::unordered_map& updates) { + TraceSection s( + "FabricMountingManager::synchronouslyUpdateAnimatedProps", + "viewCount", + updates.size()); + static auto synchronouslyUpdateAnimatedPropsJNI = + JFabricUIManager::javaClassStatic() + ->getMethod)>( + "synchronouslyUpdateViewBatch"); + auto buffer = encodeAnimatedProps(updates); + auto ints = jni::JArrayInt::newArray(buffer.ints.size()); + ints->setRegion( + 0, static_cast(buffer.ints.size()), buffer.ints.data()); + auto doubles = jni::JArrayDouble::newArray(buffer.doubles.size()); + doubles->setRegion( + 0, static_cast(buffer.doubles.size()), buffer.doubles.data()); + auto rawProps = jni::JArrayClass::newArray(buffer.rawProps.size()); + for (size_t i = 0; i < buffer.rawProps.size(); i++) { + rawProps->setElement( + i, + ReadableNativeMap::newObjectCxxArgs(std::move(buffer.rawProps[i])) + .get()); + } + synchronouslyUpdateAnimatedPropsJNI( + javaUIManager_, ints.get(), doubles.get(), rawProps.get()); +} + void FabricMountingManager::captureViewSnapshot(Tag tag, SurfaceId surfaceId) { static auto captureViewSnapshotJNI = 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 3d31c58db7eb..4067098e5c09 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 @@ -17,6 +17,8 @@ namespace facebook::react { +struct AnimatedProps; + class MountingTransaction; struct ShadowView; @@ -84,6 +86,8 @@ class FabricMountingManager final { void synchronouslyUpdateViewOnUIThread(Tag viewTag, const folly::dynamic &props); + void synchronouslyUpdateAnimatedProps(const std::unordered_map &updates); + void captureViewSnapshot(Tag tag, SurfaceId surfaceId); void setViewSnapshot(Tag sourceTag, Tag targetTag, SurfaceId surfaceId); 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 2d2a2f3d37f4..d5c154f1e6f1 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 @@ -863,6 +863,13 @@ void FabricUIManagerBinding::schedulerShouldSynchronouslyUpdateViewOnUIThread( } } +void FabricUIManagerBinding::schedulerShouldSynchronouslyUpdateAnimatedProps( + const std::unordered_map& updates) { + if (ReactNativeFeatureFlags::cxxNativeAnimatedEnabled() && mountingManager_) { + mountingManager_->synchronouslyUpdateAnimatedProps(updates); + } +} + 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 ea4b928357ec..97229f08fdcf 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,8 @@ class FabricUIManagerBinding : public jni::HybridClass, void schedulerShouldSynchronouslyUpdateViewOnUIThread(Tag tag, const folly::dynamic &props) override; + void schedulerShouldSynchronouslyUpdateAnimatedProps(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/ReactAndroid/src/main/jni/react/fabric/test/AnimatedPropBufferEncoderTest.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/test/AnimatedPropBufferEncoderTest.cpp new file mode 100644 index 000000000000..4e18b43b7c3d --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/test/AnimatedPropBufferEncoderTest.cpp @@ -0,0 +1,401 @@ +/* + * 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 + +namespace facebook::react { + +namespace { + +// Protocol codes read by BatchedAnimatedPropsMountItem. +enum Command : int { + CMD_START_OF_VIEW = 1, + CMD_START_OF_TRANSFORM = 2, + CMD_END_OF_TRANSFORM = 3, + CMD_END_OF_VIEW = 4, + CMD_RAW_PROPS = 5, + CMD_OPACITY = 10, + CMD_BACKGROUND_COLOR = 15, + CMD_PLACEHOLDER_TEXT_COLOR = 18, + CMD_SHADOW_COLOR = 19, + CMD_BORDER_RADIUS = 20, + CMD_BORDER_BLOCK_COLOR = 47, + CMD_BORDER_BLOCK_START_COLOR = 48, + CMD_BORDER_BLOCK_END_COLOR = 49, + CMD_OUTLINE_COLOR = 50, + CMD_OUTLINE_OFFSET = 51, + CMD_OUTLINE_WIDTH = 52, + CMD_TRANSFORM_TRANSLATE_X = 100, + CMD_TRANSFORM_ROTATE = 105, + CMD_TRANSFORM_SKEW_Y = 110, + CMD_UNIT_PX = 202, + CMD_UNIT_PERCENT = 203, +}; + +} // namespace + +TEST( + AnimatedPropBufferEncoderTest, + encodesDynamicOpacityAndTransformWithoutConsumingInput) { + folly::dynamic rawProps = folly::dynamic::object("opacity", 0.25)( + "transform", + folly::dynamic::array( + folly::dynamic::object("translateX", 10), + folly::dynamic::object("scale", 2))); + std::unordered_map updates; + updates.emplace(10, AnimatedProps{{}, std::make_unique(rawProps)}); + auto buffer = encodeAnimatedProps(updates); + EXPECT_EQ(updates.at(10).rawProps->toDynamic(), rawProps); + EXPECT_EQ(buffer.ints[0], CMD_START_OF_VIEW); + EXPECT_EQ(buffer.ints[1], 10); + EXPECT_EQ(buffer.ints.back(), CMD_END_OF_VIEW); + EXPECT_TRUE(buffer.rawProps.empty()); + auto again = encodeAnimatedProps(updates); + EXPECT_EQ(buffer.ints, again.ints); + EXPECT_EQ(buffer.doubles, again.doubles); +} + +TEST(AnimatedPropBufferEncoderTest, copiesFallbackPropsWithoutConsumingInput) { + folly::dynamic rawProps = folly::dynamic::object("opacity", nullptr)( + "transform", + folly::dynamic::array(folly::dynamic::object("futureTransform", 1)))( + "shadowOffset", folly::dynamic::object("width", 2)("height", 3)); + std::unordered_map updates; + updates.emplace(10, AnimatedProps{{}, std::make_unique(rawProps)}); + auto buffer = encodeAnimatedProps(updates); + EXPECT_EQ(updates.at(10).rawProps->toDynamic(), rawProps); + ASSERT_EQ(buffer.rawProps.size(), 1); + EXPECT_EQ(buffer.rawProps[0], rawProps); + EXPECT_EQ( + buffer.ints, + (std::vector{ + CMD_START_OF_VIEW, 10, CMD_RAW_PROPS, CMD_END_OF_VIEW})); + EXPECT_TRUE(buffer.doubles.empty()); +} + +TEST(AnimatedPropBufferEncoderTest, convertsJSIProps) { + auto runtime = hermes::makeHermesRuntime(); + auto object = jsi::Object(*runtime); + object.setProperty(*runtime, "opacity", 0.5); + std::unordered_map updates; + updates.emplace( + 10, + AnimatedProps{ + {}, + std::make_unique(*runtime, jsi::Value(*runtime, object))}); + auto buffer = encodeAnimatedProps(updates); + EXPECT_EQ( + buffer.ints, + (std::vector{CMD_START_OF_VIEW, 10, CMD_OPACITY, CMD_END_OF_VIEW})); + EXPECT_EQ(buffer.doubles, (std::vector{0.5})); + EXPECT_TRUE(buffer.rawProps.empty()); +} + +TEST(AnimatedPropBufferEncoderTest, convertsJSIFallbackWithoutConsumingInput) { + auto runtime = hermes::makeHermesRuntime(); + auto object = jsi::Object(*runtime); + object.setProperty(*runtime, "testID", "animated-view"); + std::unordered_map updates; + updates.emplace( + 10, + AnimatedProps{ + {}, + std::make_unique(*runtime, jsi::Value(*runtime, object))}); + auto buffer = encodeAnimatedProps(updates); + folly::dynamic expected = folly::dynamic::object("testID", "animated-view"); + ASSERT_EQ(buffer.rawProps.size(), 1); + EXPECT_EQ(buffer.rawProps[0], expected); + EXPECT_EQ(updates.at(10).rawProps->toDynamic(), expected); +} + +namespace { +AnimatedPropsBuffer encodeRawProps(const folly::dynamic& props) { + std::unordered_map updates; + updates.emplace(10, AnimatedProps{{}, std::make_unique(props)}); + return encodeAnimatedProps(updates); +} +} // namespace + +TEST(AnimatedPropBufferEncoderTest, encodesIntegerRadiusAndTranslation) { + auto radius = encodeRawProps(folly::dynamic::object("borderRadius", 4)); + EXPECT_EQ( + radius.ints, + (std::vector{ + CMD_START_OF_VIEW, + 10, + CMD_BORDER_RADIUS, + CMD_UNIT_PX, + CMD_END_OF_VIEW})); + EXPECT_EQ(radius.doubles, (std::vector{4})); + EXPECT_TRUE(radius.rawProps.empty()); + auto transform = encodeRawProps( + folly::dynamic::object( + "transform", + folly::dynamic::array(folly::dynamic::object("translateX", 10)))); + EXPECT_EQ( + transform.ints, + (std::vector{ + CMD_START_OF_VIEW, + 10, + CMD_START_OF_TRANSFORM, + CMD_TRANSFORM_TRANSLATE_X, + CMD_UNIT_PX, + CMD_END_OF_TRANSFORM, + CMD_END_OF_VIEW})); + EXPECT_EQ(transform.doubles, (std::vector{10})); + EXPECT_TRUE(transform.rawProps.empty()); +} + +TEST(AnimatedPropBufferEncoderTest, encodesPercentAndConvertsAnglesToRadians) { + folly::dynamic props = folly::dynamic::object( + "transform", + folly::dynamic::array( + folly::dynamic::object("translateX", "12.345678901%"), + folly::dynamic::object("rotate", "45.123456789deg"), + folly::dynamic::object("skewY", "0.123456789rad"))); + auto buffer = encodeRawProps(props); + EXPECT_EQ( + buffer.ints, + (std::vector{ + CMD_START_OF_VIEW, + 10, + CMD_START_OF_TRANSFORM, + CMD_TRANSFORM_TRANSLATE_X, + CMD_UNIT_PERCENT, + CMD_TRANSFORM_ROTATE, + CMD_TRANSFORM_SKEW_Y, + CMD_END_OF_TRANSFORM, + CMD_END_OF_VIEW})); + EXPECT_EQ( + buffer.doubles, + (std::vector{ + 12.345678901, 45.123456789 * std::numbers::pi / 180, 0.123456789})); + EXPECT_TRUE(buffer.rawProps.empty()); +} + +TEST(AnimatedPropBufferEncoderTest, sendsWholeViewAsRawPropsOnUnsupportedProp) { + folly::dynamic props = folly::dynamic::object("opacity", 0.5)( + "transform", + folly::dynamic::array( + folly::dynamic::object("scale", 2), + folly::dynamic::object("translateX", "auto"))); + auto buffer = encodeRawProps(props); + EXPECT_EQ( + buffer.ints, + (std::vector{ + CMD_START_OF_VIEW, 10, CMD_RAW_PROPS, CMD_END_OF_VIEW})); + EXPECT_TRUE(buffer.doubles.empty()); + ASSERT_EQ(buffer.rawProps.size(), 1); + EXPECT_EQ(buffer.rawProps[0], props); +} + +TEST(AnimatedPropBufferEncoderTest, preservesUnsupportedPropsAndResets) { + folly::dynamic props = + folly::dynamic::object("opacity", nullptr)("backgroundColor", nullptr)( + "transformOrigin", folly::dynamic::array("50%", 10, 0))( + "filter", + folly::dynamic::array(folly::dynamic::object("brightness", 0.5))); + auto buffer = encodeRawProps(props); + EXPECT_EQ( + buffer.ints, + (std::vector{ + CMD_START_OF_VIEW, 10, CMD_RAW_PROPS, CMD_END_OF_VIEW})); + ASSERT_EQ(buffer.rawProps.size(), 1); + EXPECT_EQ(buffer.rawProps[0], props); +} + +TEST(AnimatedPropBufferEncoderTest, rollsBackInvalidRadiusAndMatrix) { + for (const auto& props : std::vector{ + folly::dynamic::object("borderRadius", "wrong"), + folly::dynamic::object( + "transform", + folly::dynamic::array( + folly::dynamic::object( + "matrix", folly::dynamic::array(1, 2, 3))))}) { + auto buffer = encodeRawProps(props); + EXPECT_EQ( + buffer.ints, + (std::vector{ + CMD_START_OF_VIEW, 10, CMD_RAW_PROPS, CMD_END_OF_VIEW})); + EXPECT_TRUE(buffer.doubles.empty()); + ASSERT_EQ(buffer.rawProps.size(), 1); + EXPECT_EQ(buffer.rawProps[0], props); + } +} + +TEST(AnimatedPropBufferEncoderTest, encodesColorWithoutSignLoss) { + auto buffer = encodeRawProps( + folly::dynamic::object("backgroundColor", int64_t{0xff112233})); + EXPECT_EQ( + buffer.ints, + (std::vector{ + CMD_START_OF_VIEW, + 10, + CMD_BACKGROUND_COLOR, + static_cast(0xff112233), + CMD_END_OF_VIEW})); + EXPECT_TRUE(buffer.rawProps.empty()); +} + +TEST(AnimatedPropBufferEncoderTest, encodesAdditionalSynchronousColors) { + for (const auto& [name, command] : std::vector>{ + {"placeholderTextColor", CMD_PLACEHOLDER_TEXT_COLOR}, + {"shadowColor", CMD_SHADOW_COLOR}, + {"borderBlockColor", CMD_BORDER_BLOCK_COLOR}, + {"borderBlockStartColor", CMD_BORDER_BLOCK_START_COLOR}, + {"borderBlockEndColor", CMD_BORDER_BLOCK_END_COLOR}, + {"outlineColor", CMD_OUTLINE_COLOR}}) { + SCOPED_TRACE(name); + for (const auto& color : std::vector{ + int64_t{0xff112233}, double{0xff112233}, int64_t{-15654349}}) { + auto buffer = encodeRawProps(folly::dynamic::object(name, color)); + EXPECT_EQ( + buffer.ints, + (std::vector{ + CMD_START_OF_VIEW, + 10, + command, + static_cast(0xff112233), + CMD_END_OF_VIEW})); + EXPECT_TRUE(buffer.doubles.empty()); + EXPECT_TRUE(buffer.rawProps.empty()); + } + for (const auto& color : std::vector{ + nullptr, + folly::dynamic::object( + "resource_paths", + folly::dynamic::array("?attr/colorAccent"))}) { + folly::dynamic props = folly::dynamic::object(name, color); + auto buffer = encodeRawProps(props); + EXPECT_EQ( + buffer.ints, + (std::vector{ + CMD_START_OF_VIEW, 10, CMD_RAW_PROPS, CMD_END_OF_VIEW})); + EXPECT_TRUE(buffer.doubles.empty()); + ASSERT_EQ(buffer.rawProps.size(), 1); + EXPECT_EQ(buffer.rawProps[0], props); + } + } +} + +TEST(AnimatedPropBufferEncoderTest, encodesDoubleColor) { + auto buffer = encodeRawProps( + folly::dynamic::object("backgroundColor", double{0xff112233})); + EXPECT_EQ( + buffer.ints, + (std::vector{ + CMD_START_OF_VIEW, + 10, + CMD_BACKGROUND_COLOR, + static_cast(0xff112233), + CMD_END_OF_VIEW})); + EXPECT_TRUE(buffer.rawProps.empty()); +} + +TEST( + AnimatedPropBufferEncoderTest, + preservesUnsupportedNumericColorsInFallback) { + for (double color : + {-16777215.5, + 1.5, + std::numeric_limits::lowest(), + std::numeric_limits::max(), + -std::numeric_limits::infinity(), + std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN()}) { + SCOPED_TRACE(color); + auto buffer = encodeRawProps( + folly::dynamic::object("opacity", 0.5)("backgroundColor", color)); + EXPECT_EQ( + buffer.ints, + (std::vector{ + CMD_START_OF_VIEW, 10, CMD_RAW_PROPS, CMD_END_OF_VIEW})); + EXPECT_TRUE(buffer.doubles.empty()); + ASSERT_EQ(buffer.rawProps.size(), 1); + EXPECT_EQ(buffer.rawProps[0]["opacity"], 0.5); + auto decodedColor = buffer.rawProps[0]["backgroundColor"].asDouble(); + if (std::isnan(color)) { + EXPECT_TRUE(std::isnan(decodedColor)); + } else { + EXPECT_EQ(decodedColor, color); + } + } +} + +TEST(AnimatedPropBufferEncoderTest, encodesOutlineDimensions) { + for (const auto& [name, command] : std::vector>{ + {"outlineOffset", CMD_OUTLINE_OFFSET}, + {"outlineWidth", CMD_OUTLINE_WIDTH}}) { + SCOPED_TRACE(name); + for (const auto& value : std::vector{-2, 1.25}) { + auto buffer = encodeRawProps(folly::dynamic::object(name, value)); + EXPECT_EQ( + buffer.ints, + (std::vector{CMD_START_OF_VIEW, 10, command, CMD_END_OF_VIEW})); + EXPECT_EQ(buffer.doubles, (std::vector{value.asDouble()})); + EXPECT_TRUE(buffer.rawProps.empty()); + } + for (const auto& value : std::vector{nullptr, "10%"}) { + folly::dynamic props = folly::dynamic::object(name, value); + auto buffer = encodeRawProps(props); + EXPECT_EQ( + buffer.ints, + (std::vector{ + CMD_START_OF_VIEW, 10, CMD_RAW_PROPS, CMD_END_OF_VIEW})); + EXPECT_TRUE(buffer.doubles.empty()); + ASSERT_EQ(buffer.rawProps.size(), 1); + EXPECT_EQ(buffer.rawProps[0], props); + } + } +} + +TEST(AnimatedPropBufferEncoderTest, typedPropsOverrideRawPropsInFallback) { + AnimatedPropsBuilder builder; + builder.setShadowOffset(Size{2, 3}); + auto props = builder.get(); + props.rawProps = std::make_unique( + folly::dynamic::object("shadowOffset", nullptr)("opacity", 0.5)); + std::unordered_map updates; + updates.emplace(10, std::move(props)); + auto buffer = encodeAnimatedProps(updates); + EXPECT_EQ( + buffer.ints, + (std::vector{ + CMD_START_OF_VIEW, 10, CMD_RAW_PROPS, CMD_END_OF_VIEW})); + EXPECT_TRUE(buffer.doubles.empty()); + ASSERT_EQ(buffer.rawProps.size(), 1); + folly::dynamic expected = folly::dynamic::object("opacity", 0.5)( + "shadowOffset", folly::dynamic::object("width", 2)("height", 3)); + EXPECT_EQ(buffer.rawProps[0], expected); +} + +TEST(AnimatedPropBufferEncoderTest, sendsTypedPropsAsRawProps) { + AnimatedPropsBuilder builder; + builder.setOpacity(0.75); + auto props = builder.get(); + props.rawProps = + std::make_unique(folly::dynamic::object("opacity", 0.25)); + std::unordered_map updates; + updates.emplace(10, std::move(props)); + auto buffer = encodeAnimatedProps(updates); + EXPECT_EQ( + buffer.ints, + (std::vector{ + CMD_START_OF_VIEW, 10, CMD_RAW_PROPS, CMD_END_OF_VIEW})); + EXPECT_TRUE(buffer.doubles.empty()); + ASSERT_EQ(buffer.rawProps.size(), 1); + folly::dynamic expected = folly::dynamic::object("opacity", 0.75); + EXPECT_EQ(buffer.rawProps[0], expected); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/BatchedAnimatedPropsMountItemTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/BatchedAnimatedPropsMountItemTest.kt new file mode 100644 index 000000000000..f6c1d767da62 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/BatchedAnimatedPropsMountItemTest.kt @@ -0,0 +1,358 @@ +/* + * 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 + +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.ReactNativeJNISoLoader +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.fabric.mounting.MountItemDispatcher +import com.facebook.react.fabric.mounting.MountingManager +import com.facebook.react.fabric.mounting.mountitems.BatchedAnimatedPropsMountItem +import com.facebook.react.uimanager.MatrixMathHelper +import com.facebook.react.uimanager.TransformHelper +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.within +import org.junit.Test +import org.mockito.Mockito.mockStatic +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class BatchedAnimatedPropsMountItemTest { + private fun decode( + ints: IntArray, + doubles: DoubleArray, + rawProps: Array = emptyArray(), + ): List> { + val updates = mutableListOf>() + BatchedAnimatedPropsMountItem(ints, doubles, rawProps, mock()).decode { tag, props -> + updates.add(tag to props) + } + return updates + } + + @Test + fun decodesOpacityPerView() { + val updates = + decode( + intArrayOf( + CMD_START_OF_VIEW, + 11, + CMD_OPACITY, + CMD_END_OF_VIEW, + CMD_START_OF_VIEW, + 12, + CMD_OPACITY, + CMD_END_OF_VIEW), + doubleArrayOf(0.25, 0.75)) + + assertThat(updates.map { it.first }).containsExactly(11, 12) + assertThat(updates[0].second.getDouble("opacity")).isEqualTo(0.25) + assertThat(updates[1].second.getDouble("opacity")).isEqualTo(0.75) + } + + @Test + fun decodesAdditionalColorProps() { + val color = 0xff112233.toInt() + for ((command, name) in + listOf( + CMD_PLACEHOLDER_TEXT_COLOR to "placeholderTextColor", + CMD_SHADOW_COLOR to "shadowColor", + CMD_BORDER_BLOCK_COLOR to "borderBlockColor", + CMD_BORDER_BLOCK_START_COLOR to "borderBlockStartColor", + CMD_BORDER_BLOCK_END_COLOR to "borderBlockEndColor", + CMD_OUTLINE_COLOR to "outlineColor", + )) { + val props = + decode(intArrayOf(CMD_START_OF_VIEW, 7, command, color, CMD_END_OF_VIEW), doubleArrayOf()) + .single() + .second + assertThat(props.getInt(name)).describedAs(name).isEqualTo(color) + } + } + + @Test + fun decodesOutlinePropsAcrossViews() { + val updates = + decode( + intArrayOf( + CMD_START_OF_VIEW, + 7, + CMD_OUTLINE_OFFSET, + CMD_OUTLINE_WIDTH, + CMD_END_OF_VIEW, + CMD_START_OF_VIEW, + 8, + CMD_OPACITY, + CMD_END_OF_VIEW), + doubleArrayOf(-2.0, 1.25, 0.5)) + + assertThat(updates.map { it.first }).containsExactly(7, 8) + val props = updates[0].second + assertThat(props.getDouble("outlineOffset")).isEqualTo(-2.0) + assertThat(props.getDouble("outlineWidth")).isEqualTo(1.25) + assertThat(updates[1].second.getDouble("opacity")).isEqualTo(0.5) + } + + @Test + fun decodesTransformOpsInOrder() { + val updates = + decode( + intArrayOf( + CMD_START_OF_VIEW, + 7, + CMD_START_OF_TRANSFORM, + CMD_TRANSLATE_X, + CMD_UNIT_PX, + CMD_ROTATE, + CMD_SCALE, + CMD_END_OF_TRANSFORM, + CMD_END_OF_VIEW), + doubleArrayOf(10.0, 1.5, 2.0)) + + val transforms = checkNotNull(updates.single().second.getArray("transform")) + assertThat(transforms.size()).isEqualTo(3) + assertThat(transforms.getMap(0)?.getDouble("translateX")).isEqualTo(10.0) + assertThat(transforms.getMap(1)?.getDouble("rotate")).isEqualTo(1.5) + assertThat(transforms.getMap(2)?.getDouble("scale")).isEqualTo(2.0) + } + + @Test + fun decodesMatrixOpWithSixteenValues() { + val matrix = DoubleArray(16) { it.toDouble() } + val updates = + decode( + intArrayOf( + CMD_START_OF_VIEW, + 7, + CMD_START_OF_TRANSFORM, + CMD_MATRIX, + CMD_END_OF_TRANSFORM, + CMD_END_OF_VIEW), + matrix) + + val transforms = checkNotNull(updates.single().second.getArray("transform")) + val decoded = checkNotNull(transforms.getMap(0)?.getArray("matrix")) + assertThat(decoded.size()).isEqualTo(16) + assertThat(decoded.getDouble(15)).isEqualTo(15.0) + } + + @Test + fun preservesScaleWithThreeDimensionalRotations() { + mockStatic(ReactNativeJNISoLoader::class.java).use { + for ((rotation, axis) in listOf(CMD_ROTATE_X to 0, CMD_ROTATE_Y to 1)) { + val props = + decode( + intArrayOf( + CMD_START_OF_VIEW, + 7, + CMD_START_OF_TRANSFORM, + CMD_PERSPECTIVE, + CMD_SCALE, + rotation, + CMD_END_OF_TRANSFORM, + CMD_END_OF_VIEW, + ), + doubleArrayOf(200.0, 2.0, Math.PI / 4), + ) + .single() + .second + val matrix = DoubleArray(16) + TransformHelper.processTransform( + checkNotNull(props.getArray("transform")), + matrix, + 0f, + 0f, + null, + ) + val decomposition = MatrixMathHelper.MatrixDecompositionContext() + MatrixMathHelper.decomposeMatrix(matrix, decomposition) + + for (scale in decomposition.scale) { + assertThat(scale).isCloseTo(2.0, within(0.000001)) + } + assertThat(decomposition.rotationDegrees[axis]).isEqualTo(45.0) + } + } + } + + @Test + fun passesRawPropsThroughAsTheWholeView() { + val raw = object : ReadableMap by JavaOnlyMap.of("translateX", 4.0, "opacity", null) {} + val updates = + decode( + intArrayOf( + CMD_START_OF_VIEW, + 7, + CMD_RAW_PROPS, + CMD_END_OF_VIEW, + CMD_START_OF_VIEW, + 8, + CMD_OPACITY, + CMD_END_OF_VIEW), + doubleArrayOf(0.5), + arrayOf(raw)) + + assertThat(updates[0]).isEqualTo(7 to raw) + assertThat(updates[1].second.getDouble("opacity")).isEqualTo(0.5) + } + + @Test + fun rejectsUnknownEntries() { + org.junit.Assert.assertThrows(IllegalStateException::class.java) { + decode(intArrayOf(CMD_START_OF_VIEW, 7, UNKNOWN_COMMAND, CMD_END_OF_VIEW), doubleArrayOf()) + } + } + + @Test + fun decodesRadiusAndTranslationUnits() { + val props = + decode( + intArrayOf( + CMD_START_OF_VIEW, + 7, + CMD_BORDER_RADIUS, + CMD_UNIT_PX, + CMD_BORDER_TOP_LEFT_RADIUS, + CMD_UNIT_PERCENT, + CMD_START_OF_TRANSFORM, + CMD_TRANSLATE_X, + CMD_UNIT_PX, + CMD_TRANSLATE_Y, + CMD_UNIT_PERCENT, + CMD_END_OF_TRANSFORM, + CMD_END_OF_VIEW), + doubleArrayOf(4.0, 12.5, 10.0, 25.0), + ) + .single() + .second + assertThat(props.getDouble("borderRadius")).isEqualTo(4.0) + assertThat(props.getString("borderTopLeftRadius")).isEqualTo("12.5%") + val transform = checkNotNull(props.getArray("transform")) + assertThat(transform.getMap(0)?.getDouble("translateX")).isEqualTo(10.0) + assertThat(transform.getMap(1)?.getString("translateY")).isEqualTo("25.0%") + } + + @Test + fun rejectsMalformedCommandsAndUnits() { + for (ints in + listOf( + intArrayOf(UNKNOWN_COMMAND), + intArrayOf(CMD_START_OF_VIEW, 7, CMD_BORDER_RADIUS, UNKNOWN_UNIT, CMD_END_OF_VIEW), + intArrayOf( + CMD_START_OF_VIEW, + 7, + CMD_START_OF_TRANSFORM, + CMD_TRANSLATE_X, + UNKNOWN_UNIT, + CMD_END_OF_TRANSFORM, + CMD_END_OF_VIEW), + intArrayOf( + CMD_START_OF_VIEW, + 7, + CMD_START_OF_TRANSFORM, + UNKNOWN_TRANSFORM_COMMAND, + CMD_END_OF_TRANSFORM, + CMD_END_OF_VIEW), + intArrayOf( + CMD_START_OF_VIEW, + 7, + CMD_START_OF_TRANSFORM, + CMD_MATRIX, + CMD_END_OF_TRANSFORM, + CMD_END_OF_VIEW), + intArrayOf(CMD_START_OF_VIEW, 7, CMD_RAW_PROPS, CMD_OPACITY, CMD_END_OF_VIEW), + )) { + org.junit.Assert.assertThrows(RuntimeException::class.java) { + decode(ints, doubleArrayOf(1.0)) + } + } + } + + @Test + fun queuesMissingViewsAndContinuesTheBatch() { + val manager = mock() + val dispatcher = mock() + val second = JavaOnlyMap.of("opacity", 0.75) + whenever(manager.updateAnimatedPropsSynchronously(12, second)).thenReturn(true) + BatchedAnimatedPropsMountItem( + intArrayOf( + CMD_START_OF_VIEW, + 11, + CMD_OPACITY, + CMD_END_OF_VIEW, + CMD_START_OF_VIEW, + 12, + CMD_OPACITY, + CMD_END_OF_VIEW), + doubleArrayOf(0.25, 0.75), + emptyArray(), + dispatcher, + ) + .execute(manager) + verify(dispatcher).addMountItem(org.mockito.kotlin.any()) + verify(manager).updateAnimatedPropsSynchronously(12, second) + } + + @Test + fun continuesAfterOneViewManagerFails() { + val manager = mock() + val dispatcher = mock() + whenever(manager.updateAnimatedPropsSynchronously(11, JavaOnlyMap.of("opacity", 0.25))) + .thenThrow(IllegalStateException("view failure")) + BatchedAnimatedPropsMountItem( + intArrayOf( + CMD_START_OF_VIEW, + 11, + CMD_OPACITY, + CMD_END_OF_VIEW, + CMD_START_OF_VIEW, + 12, + CMD_OPACITY, + CMD_END_OF_VIEW), + doubleArrayOf(0.25, 0.75), + emptyArray(), + dispatcher, + ) + .execute(manager) + verify(manager).updateAnimatedPropsSynchronously(12, JavaOnlyMap.of("opacity", 0.75)) + } + + private companion object { + const val CMD_START_OF_VIEW = 1 + const val CMD_START_OF_TRANSFORM = 2 + const val CMD_END_OF_TRANSFORM = 3 + const val CMD_END_OF_VIEW = 4 + const val CMD_RAW_PROPS = 5 + const val CMD_OPACITY = 10 + const val CMD_PLACEHOLDER_TEXT_COLOR = 18 + const val CMD_SHADOW_COLOR = 19 + const val CMD_BORDER_RADIUS = 20 + const val CMD_BORDER_TOP_LEFT_RADIUS = 21 + const val CMD_BORDER_BLOCK_COLOR = 47 + const val CMD_BORDER_BLOCK_START_COLOR = 48 + const val CMD_BORDER_BLOCK_END_COLOR = 49 + const val CMD_OUTLINE_COLOR = 50 + const val CMD_OUTLINE_OFFSET = 51 + const val CMD_OUTLINE_WIDTH = 52 + const val CMD_TRANSLATE_X = 100 + const val CMD_TRANSLATE_Y = 101 + const val CMD_SCALE = 102 + const val CMD_ROTATE = 105 + const val CMD_ROTATE_X = 106 + const val CMD_ROTATE_Y = 107 + const val CMD_MATRIX = 111 + const val CMD_PERSPECTIVE = 112 + const val CMD_UNIT_PX = 202 + const val CMD_UNIT_PERCENT = 203 + + const val UNKNOWN_COMMAND = 9 + const val UNKNOWN_TRANSFORM_COMMAND = 999 + const val UNKNOWN_UNIT = 999 + } +} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/MountingManagerAnimatedPropsTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/MountingManagerAnimatedPropsTest.kt new file mode 100644 index 000000000000..f9f4f59e5795 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/MountingManagerAnimatedPropsTest.kt @@ -0,0 +1,99 @@ +/* + * 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. + */ + +@file:Suppress("DEPRECATION") + +package com.facebook.react.fabric + +import com.facebook.react.ReactRootView +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.ReactTestHelper +import com.facebook.react.fabric.mounting.MountingManager +import com.facebook.react.fabric.mounting.SurfaceMountingManager +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewManager +import com.facebook.react.uimanager.ViewManagerRegistry +import com.facebook.react.views.view.ReactViewManager +import com.facebook.testutils.shadows.ShadowNativeLoader +import com.facebook.testutils.shadows.ShadowNativeMap +import com.facebook.testutils.shadows.ShadowReadableNativeArray +import com.facebook.testutils.shadows.ShadowReadableNativeMap +import com.facebook.testutils.shadows.ShadowSoLoader +import com.facebook.testutils.shadows.ShadowWritableNativeArray +import com.facebook.testutils.shadows.ShadowWritableNativeMap +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config( + shadows = + [ + ShadowSoLoader::class, + ShadowNativeLoader::class, + ShadowNativeMap::class, + ShadowWritableNativeMap::class, + ShadowReadableNativeMap::class, + ShadowWritableNativeArray::class, + ShadowReadableNativeArray::class, + ], +) +class MountingManagerAnimatedPropsTest { + private lateinit var mountingManager: MountingManager + private lateinit var themedReactContext: ThemedReactContext + private val surfaceId = 1 + + @Before + fun setUp() { + ReactNativeFeatureFlagsForTests.setUp() + val reactContext = ReactTestHelper.createCatalystContextForTest() + themedReactContext = ThemedReactContext(reactContext, reactContext, null, -1) + mountingManager = + MountingManager(ViewManagerRegistry(listOf>(ReactViewManager())), {}) + } + + private fun startSurfaceWithView(tag: Int): SurfaceMountingManager { + mountingManager.startSurface(surfaceId, themedReactContext, ReactRootView(themedReactContext)) + val smm = mountingManager.getSurfaceManagerEnforced(surfaceId, "test") + smm.preallocateView("RCTView", tag, JavaOnlyMap.of(), null, true) + smm.addViewAt(surfaceId, tag, 0) + return smm + } + + @Test + fun appliesPropsToExistingView() { + val smm = startSurfaceWithView(42) + + val applied = + mountingManager.updateAnimatedPropsSynchronously(42, JavaOnlyMap.of("opacity", 0.3)) + + assertThat(applied).isTrue() + assertThat(smm.getView(42).alpha).isEqualTo(0.3f) + } + + @Test + fun reportsUnknownTag() { + startSurfaceWithView(42) + + assertThat(mountingManager.updateAnimatedPropsSynchronously(7, JavaOnlyMap.of("opacity", 0.3))) + .isFalse() + } + + @Test + fun storedValueOverridesStaleMountUpdate() { + val smm = startSurfaceWithView(42) + mountingManager.updateAnimatedPropsSynchronously(42, JavaOnlyMap.of("opacity", 0.3)) + + smm.updateProps(42, JavaOnlyMap.of("opacity", 1.0)) + + assertThat(smm.getView(42).alpha).isEqualTo(0.3f) + } +} diff --git a/packages/react-native/ReactCommon/react/renderer/core/RawProps.h b/packages/react-native/ReactCommon/react/renderer/core/RawProps.h index 8afc6f7ec890..8685ec6f575d 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/RawProps.h +++ b/packages/react-native/ReactCommon/react/renderer/core/RawProps.h @@ -83,6 +83,12 @@ class RawProps final { */ folly::dynamic toDynamic(const std::function &filterObjectKeys = nullptr) const; + // The pointer is valid while this RawProps owns its dynamic source. + const folly::dynamic *getDynamic() const noexcept + { + return mode_ == Mode::Dynamic ? &dynamic_ : nullptr; + } + /* * Returns `true` if the object is empty. * Empty `RawProps` does not have any stored data. diff --git a/packages/react-native/ReactCommon/react/renderer/core/tests/RawPropsTest.cpp b/packages/react-native/ReactCommon/react/renderer/core/tests/RawPropsTest.cpp index caff5c160c32..b23760236f21 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/tests/RawPropsTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/tests/RawPropsTest.cpp @@ -20,6 +20,29 @@ using namespace facebook; using namespace facebook::react; +TEST(RawPropsTest, dynamicSourceIsBorrowed) { + const auto rawProps = RawProps(folly::dynamic::object("opacity", 0.5)); + const auto* dynamic = rawProps.getDynamic(); + ASSERT_NE(dynamic, nullptr); + EXPECT_EQ(dynamic, rawProps.getDynamic()); + EXPECT_EQ(*dynamic, rawProps.toDynamic()); +} + +TEST(RawPropsTest, emptyPropsHaveNoDynamicSource) { + EXPECT_EQ(RawProps().getDynamic(), nullptr); + EXPECT_EQ(RawProps(nullptr).getDynamic(), nullptr); +} + +TEST(RawPropsTest, jsiPropsHaveNoDynamicSource) { + auto runtime = facebook::hermes::makeHermesRuntime(); + auto object = jsi::Object(*runtime); + object.setProperty(*runtime, "opacity", 0.5); + const auto rawProps = RawProps(*runtime, jsi::Value(*runtime, object)); + EXPECT_EQ(rawProps.getDynamic(), nullptr); + folly::dynamic expected = folly::dynamic::object("opacity", 0.5); + EXPECT_EQ(rawProps.toDynamic(), expected); +} + class PropsSingleFloat : public Props { public: PropsSingleFloat() = default; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index d24683a5d5b5..a233a78d244f 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -865,6 +865,7 @@ double facebook::react::getCPUTimeNanos(); double facebook::react::interpolate(double inputValue, double inputMin, double inputMax, double outputMin, double outputMax, std::string_view extrapolateLeft, std::string_view extrapolateRight); double facebook::react::round(double value) noexcept; facebook::jsi::Value facebook::react::createPromiseAsJSIValue(facebook::jsi::Runtime& rt, facebook::react::PromiseSetupFunctionType&& func); +facebook::react::AnimatedPropsBuffer facebook::react::encodeAnimatedProps(const std::unordered_map& updates); facebook::react::Color facebook::react::hostPlatformColorFromComponents(facebook::react::ColorComponents components); facebook::react::Color facebook::react::hostPlatformColorFromRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a); facebook::react::ColorComponents facebook::react::colorComponentsFromColor(facebook::react::SharedColor color); @@ -2401,6 +2402,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 synchronouslyUpdateAnimatedProps(const std::unordered_map& updates); public void synchronouslyUpdateViewOnUIThread(facebook::react::Tag viewTag, const folly::dynamic& props); public ~FabricMountingManager(); } @@ -4260,6 +4262,7 @@ class facebook::react::RawProps { public bool isEmpty() const noexcept; public const facebook::react::RawValue* at(const char* name) const noexcept; public const facebook::react::RawValue* at(const char* name, const char* prefix, const char* suffix) const noexcept; + public const folly::dynamic* getDynamic() const noexcept; public facebook::react::RawProps& operator=(const facebook::react::RawProps& other) noexcept = delete; public facebook::react::RawProps& operator=(facebook::react::RawProps&& other) noexcept = delete; public folly::dynamic toDynamic(const std::function& filterObjectKeys = nullptr) const; @@ -6784,6 +6787,12 @@ struct facebook::react::AnimatedProps { public std::vector> props; } +struct facebook::react::AnimatedPropsBuffer { + public std::vector doubles; + public std::vector rawProps; + public std::vector ints; +} + struct facebook::react::AnimatedPropsBuilder { public facebook::react::AnimatedProps get(); public std::unique_ptr rawProps; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 96f11569470d..1138238b6f7b 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -864,6 +864,7 @@ double facebook::react::getCPUTimeNanos(); double facebook::react::interpolate(double inputValue, double inputMin, double inputMax, double outputMin, double outputMax, std::string_view extrapolateLeft, std::string_view extrapolateRight); double facebook::react::round(double value) noexcept; facebook::jsi::Value facebook::react::createPromiseAsJSIValue(facebook::jsi::Runtime& rt, facebook::react::PromiseSetupFunctionType&& func); +facebook::react::AnimatedPropsBuffer facebook::react::encodeAnimatedProps(const std::unordered_map& updates); facebook::react::Color facebook::react::hostPlatformColorFromComponents(facebook::react::ColorComponents components); facebook::react::Color facebook::react::hostPlatformColorFromRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a); facebook::react::ColorComponents facebook::react::colorComponentsFromColor(facebook::react::SharedColor color); @@ -2384,6 +2385,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 synchronouslyUpdateAnimatedProps(const std::unordered_map& updates); public void synchronouslyUpdateViewOnUIThread(facebook::react::Tag viewTag, const folly::dynamic& props); public ~FabricMountingManager(); } @@ -4104,6 +4106,7 @@ class facebook::react::RawProps { public bool isEmpty() const noexcept; public const facebook::react::RawValue* at(const char* name) const noexcept; public const facebook::react::RawValue* at(const char* name, const char* prefix, const char* suffix) const noexcept; + public const folly::dynamic* getDynamic() const noexcept; public facebook::react::RawProps& operator=(const facebook::react::RawProps& other) noexcept = delete; public facebook::react::RawProps& operator=(facebook::react::RawProps&& other) noexcept = delete; public folly::dynamic toDynamic(const std::function& filterObjectKeys = nullptr) const; @@ -6594,6 +6597,12 @@ struct facebook::react::AnimatedProps { public std::vector> props; } +struct facebook::react::AnimatedPropsBuffer { + public std::vector doubles; + public std::vector rawProps; + public std::vector ints; +} + struct facebook::react::AnimatedPropsBuilder { public facebook::react::AnimatedProps get(); public std::unique_ptr rawProps; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index bcc9d69640dd..d633789807bb 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -865,6 +865,7 @@ double facebook::react::getCPUTimeNanos(); double facebook::react::interpolate(double inputValue, double inputMin, double inputMax, double outputMin, double outputMax, std::string_view extrapolateLeft, std::string_view extrapolateRight); double facebook::react::round(double value) noexcept; facebook::jsi::Value facebook::react::createPromiseAsJSIValue(facebook::jsi::Runtime& rt, facebook::react::PromiseSetupFunctionType&& func); +facebook::react::AnimatedPropsBuffer facebook::react::encodeAnimatedProps(const std::unordered_map& updates); facebook::react::Color facebook::react::hostPlatformColorFromComponents(facebook::react::ColorComponents components); facebook::react::Color facebook::react::hostPlatformColorFromRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a); facebook::react::ColorComponents facebook::react::colorComponentsFromColor(facebook::react::SharedColor color); @@ -2399,6 +2400,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 synchronouslyUpdateAnimatedProps(const std::unordered_map& updates); public void synchronouslyUpdateViewOnUIThread(facebook::react::Tag viewTag, const folly::dynamic& props); public ~FabricMountingManager(); } @@ -4257,6 +4259,7 @@ class facebook::react::RawProps { public bool isEmpty() const noexcept; public const facebook::react::RawValue* at(const char* name) const noexcept; public const facebook::react::RawValue* at(const char* name, const char* prefix, const char* suffix) const noexcept; + public const folly::dynamic* getDynamic() const noexcept; public facebook::react::RawProps& operator=(const facebook::react::RawProps& other) noexcept = delete; public facebook::react::RawProps& operator=(facebook::react::RawProps&& other) noexcept = delete; public folly::dynamic toDynamic(const std::function& filterObjectKeys = nullptr) const; @@ -6775,6 +6778,12 @@ struct facebook::react::AnimatedProps { public std::vector> props; } +struct facebook::react::AnimatedPropsBuffer { + public std::vector doubles; + public std::vector rawProps; + public std::vector ints; +} + struct facebook::react::AnimatedPropsBuilder { public facebook::react::AnimatedProps get(); public std::unique_ptr rawProps; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 53991b89c0c2..8dd5edcda290 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -6428,6 +6428,7 @@ class facebook::react::RawProps { public bool isEmpty() const noexcept; public const facebook::react::RawValue* at(const char* name) const noexcept; public const facebook::react::RawValue* at(const char* name, const char* prefix, const char* suffix) const noexcept; + public const folly::dynamic* getDynamic() const noexcept; public facebook::react::RawProps& operator=(const facebook::react::RawProps& other) noexcept = delete; public facebook::react::RawProps& operator=(facebook::react::RawProps&& other) noexcept = delete; public folly::dynamic toDynamic(const std::function& filterObjectKeys = nullptr) const; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index b1e8f49542ad..1819dfe3b52c 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -6304,6 +6304,7 @@ class facebook::react::RawProps { public bool isEmpty() const noexcept; public const facebook::react::RawValue* at(const char* name) const noexcept; public const facebook::react::RawValue* at(const char* name, const char* prefix, const char* suffix) const noexcept; + public const folly::dynamic* getDynamic() const noexcept; public facebook::react::RawProps& operator=(const facebook::react::RawProps& other) noexcept = delete; public facebook::react::RawProps& operator=(facebook::react::RawProps&& other) noexcept = delete; public folly::dynamic toDynamic(const std::function& filterObjectKeys = nullptr) const; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 4c6f5a4a4685..ab171200ceb8 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -6425,6 +6425,7 @@ class facebook::react::RawProps { public bool isEmpty() const noexcept; public const facebook::react::RawValue* at(const char* name) const noexcept; public const facebook::react::RawValue* at(const char* name, const char* prefix, const char* suffix) const noexcept; + public const folly::dynamic* getDynamic() const noexcept; public facebook::react::RawProps& operator=(const facebook::react::RawProps& other) noexcept = delete; public facebook::react::RawProps& operator=(facebook::react::RawProps&& other) noexcept = delete; public folly::dynamic toDynamic(const std::function& filterObjectKeys = nullptr) const; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 87a4c9ac4bb1..c878ab945208 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -2831,6 +2831,7 @@ class facebook::react::RawProps { public bool isEmpty() const noexcept; public const facebook::react::RawValue* at(const char* name) const noexcept; public const facebook::react::RawValue* at(const char* name, const char* prefix, const char* suffix) const noexcept; + public const folly::dynamic* getDynamic() const noexcept; public facebook::react::RawProps& operator=(const facebook::react::RawProps& other) noexcept = delete; public facebook::react::RawProps& operator=(facebook::react::RawProps&& other) noexcept = delete; public folly::dynamic toDynamic(const std::function& filterObjectKeys = nullptr) const; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 87569996e1ad..75c1654006e3 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -2715,6 +2715,7 @@ class facebook::react::RawProps { public bool isEmpty() const noexcept; public const facebook::react::RawValue* at(const char* name) const noexcept; public const facebook::react::RawValue* at(const char* name, const char* prefix, const char* suffix) const noexcept; + public const folly::dynamic* getDynamic() const noexcept; public facebook::react::RawProps& operator=(const facebook::react::RawProps& other) noexcept = delete; public facebook::react::RawProps& operator=(facebook::react::RawProps&& other) noexcept = delete; public folly::dynamic toDynamic(const std::function& filterObjectKeys = nullptr) const; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index cfb7cfadb968..6c3f59647833 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -2828,6 +2828,7 @@ class facebook::react::RawProps { public bool isEmpty() const noexcept; public const facebook::react::RawValue* at(const char* name) const noexcept; public const facebook::react::RawValue* at(const char* name, const char* prefix, const char* suffix) const noexcept; + public const folly::dynamic* getDynamic() const noexcept; public facebook::react::RawProps& operator=(const facebook::react::RawProps& other) noexcept = delete; public facebook::react::RawProps& operator=(facebook::react::RawProps&& other) noexcept = delete; public folly::dynamic toDynamic(const std::function& filterObjectKeys = nullptr) const;