From 93d2a9f402ea07fef3676a2a3aed2f2470f0b242 Mon Sep 17 00:00:00 2001 From: Bartlomiej Bloniarz Date: Mon, 5 Oct 2026 09:22:23 -0700 Subject: [PATCH 1/4] Keep non-layout animations on the synchronous path while another view animates layout (#58772) Summary: Pull Request resolved: https://github.com/react/react-native/pull/58772 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. `applySurfaceUpdates` splits a surface's mutations, keyed by view tag, into views with layout updates, which go through `commitUpdates`, and the rest, which are applied synchronously. Views with layout updates are also written synchronously, as the non-shared Animated path already does, so a view that starts animating a layout prop while animating `opacity` doesn't freeze its opacity behind the cache either. `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. When the same view receives several mutations in one frame, they are merged, so the mounted view and the registry stay in sync. Raw animated props are now merged per key, like the animation drivers merge them, instead of with merge-patch, which dropped `null` values meant to reset a prop. `RawProps::getDynamic` exposes a dynamic source without copying it. ## Changelog: [General] [Fixed] - Non-layout props animated on the shared animation backend no longer freeze while the same view or another view on the surface animates a layout prop Differential Revision: D122570616 --- .../__tests__/AnimatedBackend-itest.js | 192 +++++++++++++++++- .../AnimatedPropsRegistry.cpp | 51 +++-- .../animationbackend/AnimatedPropsRegistry.h | 15 +- .../animationbackend/AnimationBackend.cpp | 110 +++++++--- .../animationbackend/AnimationBackend.h | 19 +- .../animationbackend/AnimationMutation.h | 32 +++ .../__docs__/AnimationBackend.md | 17 +- .../react/renderer/core/RawProps.h | 6 + .../renderer/core/tests/RawPropsTest.cpp | 23 +++ .../api-snapshots/ReactAndroidDebugCxx.api | 11 +- .../api-snapshots/ReactAndroidNewarchCxx.api | 11 +- .../api-snapshots/ReactAndroidReleaseCxx.api | 11 +- .../api-snapshots/ReactAppleDebugCxx.api | 11 +- .../api-snapshots/ReactAppleNewarchCxx.api | 11 +- .../api-snapshots/ReactAppleReleaseCxx.api | 11 +- .../api-snapshots/ReactCommonDebugCxx.api | 11 +- .../api-snapshots/ReactCommonNewarchCxx.api | 11 +- .../api-snapshots/ReactCommonReleaseCxx.api | 11 +- 18 files changed, 410 insertions(+), 154 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..33c228543e2e 100644 --- a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js @@ -17,7 +17,7 @@ import * as Fantom from '@react-native/fantom'; import nullthrows from 'nullthrows'; import * as React from 'react'; import {Component, createRef, memo, useEffect, useMemo, useState} from 'react'; -import {Animated, View, useAnimatedValue} from 'react-native'; +import {Animated, Easing, View, useAnimatedValue} from 'react-native'; // marginLeft (and the other margin props) are only on the native animated // allowlist when the shared backend is enabled. This verifies the prop is @@ -77,6 +77,196 @@ test('animate marginLeft layout prop', () => { ); }); +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('direct-path props survive a re-render while another view animates layout', () => { + let _opacity; + let _opacityAnimation; + let _siblingHeight; + let _siblingHeightAnimation; + let _setWidth; + + function MyApp() { + const opacity = useAnimatedValue(0); + const siblingHeight = useAnimatedValue(10); + const [width, setWidth] = useState(100); + _opacity = opacity; + _siblingHeight = siblingHeight; + _setWidth = setWidth; + return ( + + + + + ); + } + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + Fantom.runTask(() => { + _opacityAnimation = Animated.timing(_opacity, { + toValue: 0.5, + duration: 1000, + useNativeDriver: true, + }).start(); + _siblingHeightAnimation = Animated.timing(_siblingHeight, { + toValue: 110, + duration: 1000, + useNativeDriver: true, + }).start(); + }); + + Fantom.unstable_produceFramesForDuration(500); + + Fantom.runTask(() => { + _setWidth(150); + }); + + expect( + root.getRenderedOutput({props: ['opacity', 'width', 'height']}).toJSX(), + ).toEqual( + + + + , + ); + + Fantom.runTask(() => { + _opacityAnimation?.stop(); + _siblingHeightAnimation?.stop(); + }); +}); + +test('a view keeps writing its props directly after it starts animating layout', () => { + const viewRef = createRef(); + + let _opacity; + let _opacityAnimation; + let _height; + let _heightAnimation; + + function MyApp() { + const opacity = useAnimatedValue(0); + const height = useAnimatedValue(10); + _opacity = opacity; + _height = height; + return ( + + ); + } + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const viewElement = nullthrows(viewRef.current); + + Fantom.runTask(() => { + _opacityAnimation = Animated.timing(_opacity, { + toValue: 0.5, + duration: 1000, + easing: Easing.linear, + useNativeDriver: true, + }).start(); + }); + + Fantom.unstable_produceFramesForDuration(500); + + expect( + Fantom.unstable_getDirectManipulationProps(viewElement).opacity, + ).toBeCloseTo(0.25, 0.001); + + Fantom.runTask(() => { + _heightAnimation = Animated.timing(_height, { + toValue: 110, + duration: 1000, + easing: Easing.linear, + useNativeDriver: true, + }).start(); + }); + + Fantom.unstable_produceFramesForDuration(250); + + expect(root.getRenderedOutput({props: ['height']}).toJSX()).toEqual( + , + ); + expect( + Fantom.unstable_getDirectManipulationProps(viewElement).opacity, + ).toBeCloseTo(0.375, 0.001); + + Fantom.runTask(() => { + _opacityAnimation?.stop(); + _heightAnimation?.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..e8df4f4ca0e1 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp @@ -6,31 +6,40 @@ */ #include "AnimatedPropsRegistry.h" +#include #include #include "AnimatedProps.h" namespace facebook::react { +void mergeAnimatedRawProps( + folly::dynamic& target, + const folly::dynamic& source) { + if (!target.isObject() || !source.isObject()) { + target = source; + return; + } + for (const auto& [key, value] : source.items()) { + target[key] = value; + } +} + 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; + react_native_assert(family != nullptr); + 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()) @@ -44,8 +53,7 @@ void AnimatedPropsRegistry::update( auto& currentRawProps = snapshot->rawProps; if (currentRawProps) { - auto newRawPropsDynamic = newRawProps.toDynamic(); - currentRawProps->merge_patch(newRawPropsDynamic); + mergeAnimatedRawProps(*currentRawProps, newRawProps.toDynamic()); } else { currentRawProps = std::make_unique(newRawProps.toDynamic()); @@ -83,7 +91,8 @@ AnimatedPropsRegistry::getMap(SurfaceId surfaceId) { auto& currentSnapshot = currentIt->second; if (propsSnapshot->rawProps) { if (currentSnapshot->rawProps) { - currentSnapshot->rawProps->merge_patch(*propsSnapshot->rawProps); + mergeAnimatedRawProps( + *currentSnapshot->rawProps, *propsSnapshot->rawProps); } else { currentSnapshot->rawProps = std::move(propsSnapshot->rawProps); } diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h index 3c004ec9502f..da0e6e884248 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,17 @@ 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>; +/* + * Merges raw animated props per key, like the props merged by the animation + * drivers. A null value is kept, so it still resets the prop on commit. + */ +void mergeAnimatedRawProps(folly::dynamic &target, const folly::dynamic &source); + 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..d8fb510dd41d 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -48,6 +49,26 @@ static inline Props::Shared cloneProps( return newProps; } +// Combines two mutations of the same view from one frame. +static void mergeMutation( + AnimationMutation& existing, + AnimationMutation&& incoming) { + auto& props = existing.props; + for (auto& animatedProp : incoming.props.props) { + props.props.push_back(std::move(animatedProp)); + } + if (incoming.props.rawProps) { + if (props.rawProps) { + auto merged = props.rawProps->toDynamic(); + mergeAnimatedRawProps(merged, incoming.props.rawProps->toDynamic()); + props.rawProps = std::make_unique(std::move(merged)); + } else { + props.rawProps = std::move(incoming.props.rawProps); + } + } + existing.hasLayoutUpdates |= incoming.hasLayoutUpdates; +} + AnimationBackend::AnimationBackend( std::shared_ptr animationChoreographer, std::shared_ptr uiManager) @@ -80,14 +101,13 @@ 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); + auto& updates = surfaceUpdates[mutation.family->getSurfaceId()]; + const auto tag = mutation.tag; + if (auto it = updates.find(tag); it != updates.end()) { + mergeMutation(it->second, std::move(mutation)); + } else { + updates.emplace(tag, std::move(mutation)); + } } asyncFlushSurfaces.merge(mutations.asyncFlushSurfaces); @@ -96,23 +116,42 @@ 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()) { + // A platform may re-apply a view's earlier direct writes when mounting + // it, so committed views are written directly too. + if (auto uiManager = uiManager_.lock()) { + for (const auto& [tag, mutation] : layoutUpdates) { + uiManager->synchronouslyUpdateViewOnUIThread( + tag, animationbackend::packAnimatedProps(mutation.props)); + } + } + 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 +163,17 @@ void AnimationBackend::onAnimationFrame(AnimationTimestamp timestamp) { callbacksCopy = callbacks; } - std::unordered_map surfaceUpdates; - std::set asyncFlushSurfaces; - for (auto& callbackWithId : callbacksCopy) { - auto mutations = callbackWithId.callback(timestamp); - unpackMutations(mutations, surfaceUpdates, asyncFlushSurfaces); - } - applySurfaceUpdates(surfaceUpdates, asyncFlushSurfaces); + // Sized up front rather than grown: MSVC's std::set move isn't noexcept, so + // growing a vector of AnimationMutations would try to copy move-only props. + std::vector batches(callbacksCopy.size()); + std::transform( + callbacksCopy.begin(), + callbacksCopy.end(), + batches.begin(), + [timestamp](const CallbackWithId& callbackWithId) { + return callbackWithId.callback(timestamp); + }); + applyMutations(std::move(batches)); } CallbackId AnimationBackend::start(const Callback& callback) { @@ -169,8 +212,9 @@ void AnimationBackend::trigger() { void AnimationBackend::pushAnimationMutations(const Callback& callback) { auto timestamp = animationChoreographer_->now(); - auto mutations = callback(timestamp); - applyMutations(std::move(mutations)); + std::vector batches(1); + batches[0] = callback(timestamp); + applyMutations(std::move(batches)); } void AnimationBackend::commitUpdates( @@ -181,24 +225,28 @@ void AnimationBackend::commitUpdates( return; } - auto& surfaceFamilies = surfaceUpdates.families; - auto& updates = surfaceUpdates.propsMap; + std::unordered_set> surfaceFamilies; + for (const auto& [tag, mutation] : surfaceUpdates) { + surfaceFamilies.insert(mutation.family); + } uiManager->getShadowTreeRegistry().visit( - surfaceId, [&surfaceFamilies, &updates](const ShadowTree& shadowTree) { + surfaceId, + [&surfaceFamilies, &surfaceUpdates](const ShadowTree& shadowTree) { shadowTree.commit( [&surfaceFamilies, - &updates](const RootShadowNode& oldRootShadowNode) { + &surfaceUpdates](const RootShadowNode& oldRootShadowNode) { return std::static_pointer_cast( oldRootShadowNode.cloneMultiple( surfaceFamilies, - [&surfaceFamilies, &updates]( + [&surfaceFamilies, &surfaceUpdates]( const ShadowNode& shadowNode, const ShadowNodeFragment& fragment) { auto newProps = ShadowNodeFragment::propsPlaceholder(); if (surfaceFamilies.contains( shadowNode.getFamilyShared())) { - auto& animatedProps = updates.at(shadowNode.getTag()); + auto& animatedProps = + surfaceUpdates.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 af4a37a9c260..33de5c50a0ef 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h @@ -21,23 +21,12 @@ #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; -}; - using Callback = std::function; struct CallbackWithId { @@ -53,7 +42,6 @@ class AnimationBackend : public UIManagerAnimationBackend { AnimationBackend( std::shared_ptr animationChoreographer, std::shared_ptr uiManager); - void commitUpdates(SurfaceId surfaceId, SurfaceUpdates &surfaceUpdates); void synchronouslyUpdateProps(const std::unordered_map &updates); void requestAsyncFlushForSurfaces(const std::set &surfaces); void clearRegistry(SurfaceId surfaceId) override; @@ -67,6 +55,9 @@ class AnimationBackend : public UIManagerAnimationBackend { void stop(CallbackId callbackId) override; private: + using SurfaceUpdates = std::unordered_map; + + void commitUpdates(SurfaceId surfaceId, SurfaceUpdates &surfaceUpdates); void unpackMutations( AnimationMutations &mutations, std::unordered_map &surfaceUpdates, @@ -74,7 +65,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..fb491d290959 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationMutation.h @@ -0,0 +1,32 @@ +/* + * 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 "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/packages/react-native/ReactCommon/react/renderer/animationbackend/__docs__/AnimationBackend.md b/packages/react-native/ReactCommon/react/renderer/animationbackend/__docs__/AnimationBackend.md index 65414a372252..ddb1bc77cd63 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/__docs__/AnimationBackend.md +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/__docs__/AnimationBackend.md @@ -6,10 +6,11 @@ Shared Animation Backend is a part of the React Native renderer that enables animation frameworks to update props of React components without going through React's JavaScript rendering pipeline. -Animation Backend allows for updates of both layout and non-layout props. If -there are no layout updates, the animations will go through the -`synchronouslyUpdateProps` path, otherwise a Fabric commit will be performed. To -synchronize the changes with React, we use the `AnimationBackendCommitHook`. +Animation Backend allows for updates of both layout and non-layout props. The +decision is made per view: views whose updates include layout props go through a +Fabric commit, and the remaining views go through the `synchronouslyUpdateProps` +path. To synchronize the changes with React, we use the +`AnimationBackendCommitHook`. ## 🚀 Usage @@ -42,10 +43,10 @@ components that make up the Shared Animation Backend. ### AnimationBackend This is the heart of the backend. This component is responsible for managing -user-provided callbacks, segregating props, and applying them either through the -`synchronouslyUpdateProps` path (when there are no layout updates), or through a -Fabric commit performed on the main thread. This component is exposed through -`UIManager` with the `UIManagerAnimationBackend` interface. +user-provided callbacks, segregating props, and applying them per view: through +a Fabric commit performed on the main thread for views with layout updates, and +through the `synchronouslyUpdateProps` path for the rest. This component is +exposed through `UIManager` with the `UIManagerAnimationBackend` interface. ### AnimatedProps 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 777213ff609a..499c34243263 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -1139,6 +1139,7 @@ void facebook::react::fromRawValueShared(const facebook::react::ContextContainer void facebook::react::fromString(const std::string& string, facebook::react::AccessibilityTraits& result); void facebook::react::g_setNativeAnimatedNowTimestampFunction(facebook::react::TimePointFunction nowFunction); void facebook::react::handleJSError(facebook::jsi::Runtime& runtime, const facebook::jsi::JSError& error, bool isFatal); +void facebook::react::mergeAnimatedRawProps(folly::dynamic& target, const folly::dynamic& source); void facebook::react::parseProcessedBackgroundImage(const facebook::react::PropsParserContext& context, const facebook::react::RawValue& value, std::vector& result); void facebook::react::parseProcessedFontVariant(const facebook::react::PropsParserContext&, const facebook::react::RawValue& value, facebook::react::FontVariant& result); void facebook::react::parseUnprocessedBackgroundImageList(const facebook::react::PropsParserContext& context, const std::vector& value, std::vector& result); @@ -1553,7 +1554,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 { @@ -1568,7 +1569,6 @@ class facebook::react::AnimationBackend : public facebook::react::UIManagerAnima public virtual void registerJSInvoker(std::shared_ptr jsInvoker) override; public virtual void stop(facebook::react::CallbackId callbackId) override; public virtual void trigger() override; - public void commitUpdates(facebook::react::SurfaceId surfaceId, facebook::react::SurfaceUpdates& surfaceUpdates); public void requestAsyncFlushForSurfaces(const std::set& surfaces); public void synchronouslyUpdateProps(const std::unordered_map& updates); } @@ -4232,6 +4232,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; @@ -8085,12 +8086,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 030f65f3918b..2d9df2450727 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -1135,6 +1135,7 @@ void facebook::react::fromRawValueShared(const facebook::react::ContextContainer void facebook::react::fromString(const std::string& string, facebook::react::AccessibilityTraits& result); void facebook::react::g_setNativeAnimatedNowTimestampFunction(facebook::react::TimePointFunction nowFunction); void facebook::react::handleJSError(facebook::jsi::Runtime& runtime, const facebook::jsi::JSError& error, bool isFatal); +void facebook::react::mergeAnimatedRawProps(folly::dynamic& target, const folly::dynamic& source); void facebook::react::parseProcessedBackgroundImage(const facebook::react::PropsParserContext& context, const facebook::react::RawValue& value, std::vector& result); void facebook::react::parseProcessedFontVariant(const facebook::react::PropsParserContext&, const facebook::react::RawValue& value, facebook::react::FontVariant& result); void facebook::react::parseUnprocessedBackgroundImageList(const facebook::react::PropsParserContext& context, const std::vector& value, std::vector& result); @@ -1548,7 +1549,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 { @@ -1563,7 +1564,6 @@ class facebook::react::AnimationBackend : public facebook::react::UIManagerAnima public virtual void registerJSInvoker(std::shared_ptr jsInvoker) override; public virtual void stop(facebook::react::CallbackId callbackId) override; public virtual void trigger() override; - public void commitUpdates(facebook::react::SurfaceId surfaceId, facebook::react::SurfaceUpdates& surfaceUpdates); public void requestAsyncFlushForSurfaces(const std::set& surfaces); public void synchronouslyUpdateProps(const std::unordered_map& updates); } @@ -4076,6 +4076,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; @@ -7845,12 +7846,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 0717d9252c85..2c7ee302a255 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -1139,6 +1139,7 @@ void facebook::react::fromRawValueShared(const facebook::react::ContextContainer void facebook::react::fromString(const std::string& string, facebook::react::AccessibilityTraits& result); void facebook::react::g_setNativeAnimatedNowTimestampFunction(facebook::react::TimePointFunction nowFunction); void facebook::react::handleJSError(facebook::jsi::Runtime& runtime, const facebook::jsi::JSError& error, bool isFatal); +void facebook::react::mergeAnimatedRawProps(folly::dynamic& target, const folly::dynamic& source); void facebook::react::parseProcessedBackgroundImage(const facebook::react::PropsParserContext& context, const facebook::react::RawValue& value, std::vector& result); void facebook::react::parseProcessedFontVariant(const facebook::react::PropsParserContext&, const facebook::react::RawValue& value, facebook::react::FontVariant& result); void facebook::react::parseUnprocessedBackgroundImageList(const facebook::react::PropsParserContext& context, const std::vector& value, std::vector& result); @@ -1552,7 +1553,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 { @@ -1567,7 +1568,6 @@ class facebook::react::AnimationBackend : public facebook::react::UIManagerAnima public virtual void registerJSInvoker(std::shared_ptr jsInvoker) override; public virtual void stop(facebook::react::CallbackId callbackId) override; public virtual void trigger() override; - public void commitUpdates(facebook::react::SurfaceId surfaceId, facebook::react::SurfaceUpdates& surfaceUpdates); public void requestAsyncFlushForSurfaces(const std::set& surfaces); public void synchronouslyUpdateProps(const std::unordered_map& updates); } @@ -4229,6 +4229,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; @@ -8076,12 +8077,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 dd76eb05a126..3d21853a7450 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -3940,6 +3940,7 @@ void facebook::react::fromString(const std::string& string, facebook::react::Acc void facebook::react::g_setNativeAnimatedNowTimestampFunction(facebook::react::TimePointFunction nowFunction); void facebook::react::handleJSError(facebook::jsi::Runtime& runtime, const facebook::jsi::JSError& error, bool isFatal); void facebook::react::installLegacyUIManagerConstantsProviderBinding(facebook::jsi::Runtime& runtime); +void facebook::react::mergeAnimatedRawProps(folly::dynamic& target, const folly::dynamic& source); void facebook::react::parseProcessedBackgroundImage(const facebook::react::PropsParserContext& context, const facebook::react::RawValue& value, std::vector& result); void facebook::react::parseProcessedFontVariant(const facebook::react::PropsParserContext&, const facebook::react::RawValue& value, facebook::react::FontVariant& result); void facebook::react::parseUnprocessedBackgroundImageList(const facebook::react::PropsParserContext& context, const std::vector& value, std::vector& result); @@ -4103,7 +4104,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 { @@ -4118,7 +4119,6 @@ class facebook::react::AnimationBackend : public facebook::react::UIManagerAnima public virtual void registerJSInvoker(std::shared_ptr jsInvoker) override; public virtual void stop(facebook::react::CallbackId callbackId) override; public virtual void trigger() override; - public void commitUpdates(facebook::react::SurfaceId surfaceId, facebook::react::SurfaceUpdates& surfaceUpdates); public void requestAsyncFlushForSurfaces(const std::set& surfaces); public void synchronouslyUpdateProps(const std::unordered_map& updates); } @@ -6404,6 +6404,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; @@ -10053,12 +10054,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 c85b1a3fed92..20bb8848f0ec 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -3929,6 +3929,7 @@ void facebook::react::fromString(const std::string& string, facebook::react::Acc void facebook::react::g_setNativeAnimatedNowTimestampFunction(facebook::react::TimePointFunction nowFunction); void facebook::react::handleJSError(facebook::jsi::Runtime& runtime, const facebook::jsi::JSError& error, bool isFatal); void facebook::react::installLegacyUIManagerConstantsProviderBinding(facebook::jsi::Runtime& runtime); +void facebook::react::mergeAnimatedRawProps(folly::dynamic& target, const folly::dynamic& source); void facebook::react::parseProcessedBackgroundImage(const facebook::react::PropsParserContext& context, const facebook::react::RawValue& value, std::vector& result); void facebook::react::parseProcessedFontVariant(const facebook::react::PropsParserContext&, const facebook::react::RawValue& value, facebook::react::FontVariant& result); void facebook::react::parseUnprocessedBackgroundImageList(const facebook::react::PropsParserContext& context, const std::vector& value, std::vector& result); @@ -4091,7 +4092,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 { @@ -4106,7 +4107,6 @@ class facebook::react::AnimationBackend : public facebook::react::UIManagerAnima public virtual void registerJSInvoker(std::shared_ptr jsInvoker) override; public virtual void stop(facebook::react::CallbackId callbackId) override; public virtual void trigger() override; - public void commitUpdates(facebook::react::SurfaceId surfaceId, facebook::react::SurfaceUpdates& surfaceUpdates); public void requestAsyncFlushForSurfaces(const std::set& surfaces); public void synchronouslyUpdateProps(const std::unordered_map& updates); } @@ -6280,6 +6280,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; @@ -9869,12 +9870,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 ae19725e5501..040250966842 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -3940,6 +3940,7 @@ void facebook::react::fromString(const std::string& string, facebook::react::Acc void facebook::react::g_setNativeAnimatedNowTimestampFunction(facebook::react::TimePointFunction nowFunction); void facebook::react::handleJSError(facebook::jsi::Runtime& runtime, const facebook::jsi::JSError& error, bool isFatal); void facebook::react::installLegacyUIManagerConstantsProviderBinding(facebook::jsi::Runtime& runtime); +void facebook::react::mergeAnimatedRawProps(folly::dynamic& target, const folly::dynamic& source); void facebook::react::parseProcessedBackgroundImage(const facebook::react::PropsParserContext& context, const facebook::react::RawValue& value, std::vector& result); void facebook::react::parseProcessedFontVariant(const facebook::react::PropsParserContext&, const facebook::react::RawValue& value, facebook::react::FontVariant& result); void facebook::react::parseUnprocessedBackgroundImageList(const facebook::react::PropsParserContext& context, const std::vector& value, std::vector& result); @@ -4102,7 +4103,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 { @@ -4117,7 +4118,6 @@ class facebook::react::AnimationBackend : public facebook::react::UIManagerAnima public virtual void registerJSInvoker(std::shared_ptr jsInvoker) override; public virtual void stop(facebook::react::CallbackId callbackId) override; public virtual void trigger() override; - public void commitUpdates(facebook::react::SurfaceId surfaceId, facebook::react::SurfaceUpdates& surfaceUpdates); public void requestAsyncFlushForSurfaces(const std::set& surfaces); public void synchronouslyUpdateProps(const std::unordered_map& updates); } @@ -6401,6 +6401,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; @@ -10044,12 +10045,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 25492550cc4f..346e09d1b082 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -699,6 +699,7 @@ void facebook::react::fromRawValueShared(const facebook::react::ContextContainer void facebook::react::fromString(const std::string& string, facebook::react::AccessibilityTraits& result); void facebook::react::g_setNativeAnimatedNowTimestampFunction(facebook::react::TimePointFunction nowFunction); void facebook::react::handleJSError(facebook::jsi::Runtime& runtime, const facebook::jsi::JSError& error, bool isFatal); +void facebook::react::mergeAnimatedRawProps(folly::dynamic& target, const folly::dynamic& source); void facebook::react::parseProcessedBackgroundImage(const facebook::react::PropsParserContext& context, const facebook::react::RawValue& value, std::vector& result); void facebook::react::parseProcessedFontVariant(const facebook::react::PropsParserContext&, const facebook::react::RawValue& value, facebook::react::FontVariant& result); void facebook::react::parseUnprocessedBackgroundImageList(const facebook::react::PropsParserContext& context, const std::vector& value, std::vector& result); @@ -878,7 +879,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 { @@ -893,7 +894,6 @@ class facebook::react::AnimationBackend : public facebook::react::UIManagerAnima public virtual void registerJSInvoker(std::shared_ptr jsInvoker) override; public virtual void stop(facebook::react::CallbackId callbackId) override; public virtual void trigger() override; - public void commitUpdates(facebook::react::SurfaceId surfaceId, facebook::react::SurfaceUpdates& surfaceUpdates); public void requestAsyncFlushForSurfaces(const std::set& surfaces); public void synchronouslyUpdateProps(const std::unordered_map& updates); } @@ -2804,6 +2804,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; @@ -6202,12 +6203,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 3c2a34f00aad..be70bc4948ba 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -696,6 +696,7 @@ void facebook::react::fromRawValueShared(const facebook::react::ContextContainer void facebook::react::fromString(const std::string& string, facebook::react::AccessibilityTraits& result); void facebook::react::g_setNativeAnimatedNowTimestampFunction(facebook::react::TimePointFunction nowFunction); void facebook::react::handleJSError(facebook::jsi::Runtime& runtime, const facebook::jsi::JSError& error, bool isFatal); +void facebook::react::mergeAnimatedRawProps(folly::dynamic& target, const folly::dynamic& source); void facebook::react::parseProcessedBackgroundImage(const facebook::react::PropsParserContext& context, const facebook::react::RawValue& value, std::vector& result); void facebook::react::parseProcessedFontVariant(const facebook::react::PropsParserContext&, const facebook::react::RawValue& value, facebook::react::FontVariant& result); void facebook::react::parseUnprocessedBackgroundImageList(const facebook::react::PropsParserContext& context, const std::vector& value, std::vector& result); @@ -874,7 +875,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 { @@ -889,7 +890,6 @@ class facebook::react::AnimationBackend : public facebook::react::UIManagerAnima public virtual void registerJSInvoker(std::shared_ptr jsInvoker) override; public virtual void stop(facebook::react::CallbackId callbackId) override; public virtual void trigger() override; - public void commitUpdates(facebook::react::SurfaceId surfaceId, facebook::react::SurfaceUpdates& surfaceUpdates); public void requestAsyncFlushForSurfaces(const std::set& surfaces); public void synchronouslyUpdateProps(const std::unordered_map& updates); } @@ -2688,6 +2688,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; @@ -6026,12 +6027,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 f2736afc7b4f..858e8722f7f5 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -699,6 +699,7 @@ void facebook::react::fromRawValueShared(const facebook::react::ContextContainer void facebook::react::fromString(const std::string& string, facebook::react::AccessibilityTraits& result); void facebook::react::g_setNativeAnimatedNowTimestampFunction(facebook::react::TimePointFunction nowFunction); void facebook::react::handleJSError(facebook::jsi::Runtime& runtime, const facebook::jsi::JSError& error, bool isFatal); +void facebook::react::mergeAnimatedRawProps(folly::dynamic& target, const folly::dynamic& source); void facebook::react::parseProcessedBackgroundImage(const facebook::react::PropsParserContext& context, const facebook::react::RawValue& value, std::vector& result); void facebook::react::parseProcessedFontVariant(const facebook::react::PropsParserContext&, const facebook::react::RawValue& value, facebook::react::FontVariant& result); void facebook::react::parseUnprocessedBackgroundImageList(const facebook::react::PropsParserContext& context, const std::vector& value, std::vector& result); @@ -877,7 +878,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 { @@ -892,7 +893,6 @@ class facebook::react::AnimationBackend : public facebook::react::UIManagerAnima public virtual void registerJSInvoker(std::shared_ptr jsInvoker) override; public virtual void stop(facebook::react::CallbackId callbackId) override; public virtual void trigger() override; - public void commitUpdates(facebook::react::SurfaceId surfaceId, facebook::react::SurfaceUpdates& surfaceUpdates); public void requestAsyncFlushForSurfaces(const std::set& surfaces); public void synchronouslyUpdateProps(const std::unordered_map& updates); } @@ -2801,6 +2801,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; @@ -6193,12 +6194,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 6352fefa5b0f6c89bfad40803e529f5d5ffc0fc1 Mon Sep 17 00:00:00 2001 From: Bartlomiej Bloniarz Date: Mon, 5 Oct 2026 09:22:23 -0700 Subject: [PATCH 2/4] Add a batched scheduler delegate call for animated prop updates (#58773) Summary: Pull Request resolved: https://github.com/react/react-native/pull/58773 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, and that flag's description now only promises what is implemented. The default `SchedulerDelegate` implementation forwards to the existing per-view update, so platforms that don't override it are unaffected. `UIManagerDelegate` gains a required method, implemented by `Scheduler`. ## Changelog: [Internal] Differential Revision: D122570617 --- .../__tests__/AnimatedBackend-itest.js | 2 +- .../featureflags/ReactNativeFeatureFlags.kt | 4 +-- .../featureflags/ReactNativeFeatureFlags.h | 4 +-- .../animationbackend/AnimationBackend.cpp | 6 ++++ .../react/renderer/scheduler/Scheduler.cpp | 7 ++++ .../react/renderer/scheduler/Scheduler.h | 1 + .../renderer/scheduler/SchedulerDelegate.cpp | 23 +++++++++++++ .../renderer/scheduler/SchedulerDelegate.h | 9 +++++ .../SchedulerDelegateInvalidationTest.cpp | 33 +++++++++++++++++++ .../react/renderer/uimanager/UIManager.cpp | 7 ++++ .../react/renderer/uimanager/UIManager.h | 2 ++ .../renderer/uimanager/UIManagerDelegate.h | 8 +++++ .../ReactNativeFeatureFlags.config.js | 2 +- .../featureflags/ReactNativeFeatureFlags.js | 4 +-- .../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 +++ 23 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerDelegate.cpp diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js index 33c228543e2e..df8472d8fe90 100644 --- a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @fantom_flags useSharedAnimatedBackend:true updateRuntimeShadowNodeReferencesOnCommitThread:* + * @fantom_flags useSharedAnimatedBackend:true updateRuntimeShadowNodeReferencesOnCommitThread:* optimizedAnimatedPropUpdates:* * @flow strict-local * @format */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 79092ab4a78b..fe85f503ae4c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<86390dbd6b2c57cb4f8cdd7cb73cc1f7>> + * @generated SignedSource<<81b1343c027b377be3bdea9167e16a5d>> */ /** @@ -415,7 +415,7 @@ public object ReactNativeFeatureFlags { public fun fuseboxWebSocketEventsEnabled(): Boolean = accessor.fuseboxWebSocketEventsEnabled() /** - * When enabled, uses optimized platform-specific paths to apply animated props synchronously. On Android, this uses a batched int/double buffer protocol with a single JNI call. On iOS, this passes AnimatedProps directly through the delegate chain and applies them via cloneProps, avoiding the folly::dynamic round-trip. + * When enabled, uses optimized platform-specific paths to apply animated props synchronously. On Android, this uses a batched int/double buffer protocol with a single JNI call. Other platforms apply the props of each view through the existing synchronous update. */ @JvmStatic public fun optimizedAnimatedPropUpdates(): Boolean = accessor.optimizedAnimatedPropUpdates() diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 1437e361ec6e..6cfd3491e98c 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -362,7 +362,7 @@ class ReactNativeFeatureFlags { RN_EXPORT static bool fuseboxWebSocketEventsEnabled(); /** - * When enabled, uses optimized platform-specific paths to apply animated props synchronously. On Android, this uses a batched int/double buffer protocol with a single JNI call. On iOS, this passes AnimatedProps directly through the delegate chain and applies them via cloneProps, avoiding the folly::dynamic round-trip. + * When enabled, uses optimized platform-specific paths to apply animated props synchronously. On Android, this uses a batched int/double buffer protocol with a single JNI call. Other platforms apply the props of each view through the existing synchronous update. */ RN_EXPORT static bool optimizedAnimatedPropUpdates(); diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp index d8fb510dd41d..5b10873950e9 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp @@ -261,6 +261,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 28c2d86462bf..929421f9ed8a 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h @@ -97,6 +97,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.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerDelegate.cpp new file mode 100644 index 000000000000..52a9f38d50c1 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerDelegate.cpp @@ -0,0 +1,23 @@ +/* + * 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 "SchedulerDelegate.h" + +#include +#include + +namespace facebook::react { + +void SchedulerDelegate::schedulerShouldSynchronouslyUpdateAnimatedProps( + const std::unordered_map& updates) { + for (const auto& [tag, props] : updates) { + schedulerShouldSynchronouslyUpdateViewOnUIThread( + tag, animationbackend::packAnimatedProps(props)); + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerDelegate.h b/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerDelegate.h index e6ca676fdbae..27c28495fe23 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerDelegate.h @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -17,6 +18,8 @@ namespace facebook::react { +struct AnimatedProps; + /* * Abstract class for Scheduler's delegate. */ @@ -66,6 +69,12 @@ class SchedulerDelegate { virtual void schedulerShouldSynchronouslyUpdateViewOnUIThread(Tag tag, const folly::dynamic &props) = 0; + /* + * Synchronous update of animated props for many views at once. By default, + * forwards each view to `schedulerShouldSynchronouslyUpdateViewOnUIThread`. + */ + virtual void schedulerShouldSynchronouslyUpdateAnimatedProps(const std::unordered_map &updates); + 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/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 9f8be49d36ac..0d65492be708 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -741,7 +741,7 @@ const definitions: FeatureFlagDefinitions = { metadata: { dateAdded: '2026-04-07', description: - 'When enabled, uses optimized platform-specific paths to apply animated props synchronously. On Android, this uses a batched int/double buffer protocol with a single JNI call. On iOS, this passes AnimatedProps directly through the delegate chain and applies them via cloneProps, avoiding the folly::dynamic round-trip.', + 'When enabled, uses optimized platform-specific paths to apply animated props synchronously. On Android, this uses a batched int/double buffer protocol with a single JNI call. Other platforms apply the props of each view through the existing synchronous update.', expectedReleaseValue: true, purpose: 'experimentation', }, diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index f94c96edd29f..816d971c6b79 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<8075ab58916ce356d5acac15888aff7a>> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -472,7 +472,7 @@ export const fuseboxScreenshotCaptureEnabled: Getter = createNativeFlag */ export const fuseboxWebSocketEventsEnabled: Getter = createNativeFlagGetter('fuseboxWebSocketEventsEnabled', false); /** - * When enabled, uses optimized platform-specific paths to apply animated props synchronously. On Android, this uses a batched int/double buffer protocol with a single JNI call. On iOS, this passes AnimatedProps directly through the delegate chain and applies them via cloneProps, avoiding the folly::dynamic round-trip. + * When enabled, uses optimized platform-specific paths to apply animated props synchronously. On Android, this uses a batched int/double buffer protocol with a single JNI call. Other platforms apply the props of each view through the existing synchronous update. */ export const optimizedAnimatedPropUpdates: Getter = createNativeFlagGetter('optimizedAnimatedPropUpdates', false); /** diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 499c34243263..b9ea38b1d674 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -4591,6 +4591,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; @@ -4614,6 +4615,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; } @@ -5345,6 +5347,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); @@ -5412,6 +5415,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 2d9df2450727..421319844691 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -4407,6 +4407,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; @@ -4430,6 +4431,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; } @@ -5155,6 +5157,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); @@ -5222,6 +5225,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 2c7ee302a255..b8f96d32e514 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -4588,6 +4588,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; @@ -4611,6 +4612,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; } @@ -5336,6 +5338,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); @@ -5403,6 +5406,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 3d21853a7450..e94665b89643 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -6744,6 +6744,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; @@ -6767,6 +6768,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; } @@ -7479,6 +7481,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); @@ -7546,6 +7549,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 20bb8848f0ec..685ee6ca2867 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -6592,6 +6592,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; @@ -6615,6 +6616,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; } @@ -7321,6 +7323,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); @@ -7388,6 +7391,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 040250966842..12d35b592ac4 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -6741,6 +6741,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; @@ -6764,6 +6765,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; } @@ -7470,6 +7472,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); @@ -7537,6 +7540,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 346e09d1b082..8941b020c4fd 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -3136,6 +3136,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; @@ -3159,6 +3160,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; } @@ -3787,6 +3789,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); @@ -3854,6 +3857,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 be70bc4948ba..1fad1ff1700c 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -2992,6 +2992,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; @@ -3015,6 +3016,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; } @@ -3637,6 +3639,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); @@ -3704,6 +3707,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 858e8722f7f5..491276d164ad 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -3133,6 +3133,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; @@ -3156,6 +3157,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; } @@ -3778,6 +3780,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); @@ -3845,6 +3848,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 efad0c47efdf792343c051f63fab74dbffd82aab Mon Sep 17 00:00:00 2001 From: Bartlomiej Bloniarz Date: Mon, 5 Oct 2026 09:22:23 -0700 Subject: [PATCH 3/4] Batch animated prop updates into one JNI call per surface (#58774) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/react/react-native/pull/58774 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. This conversion is also used by native-driven Animated transforms built as Java-only arrays, so those now render scale combined with 3D rotation the same way as JS-driven transforms, whether or not `optimizedAnimatedPropUpdates` is enabled. 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] [Fixed] - Apply uniform `scale` to the Z axis in Java-only transforms, so native-driven Animated transforms combining `scale` with 3D rotation match JS-driven ones Differential Revision: D122570618 --- .../react/fabric/FabricUIManager.java | 30 ++ .../react/fabric/mounting/MountingManager.kt | 10 + .../BatchedAnimatedPropsMountItem.kt | 274 ++++++++++++ .../react/uimanager/TransformHelper.kt | 1 + .../fabric/AnimatedPropBufferEncoder.cpp | 395 +++++++++++++++++ .../react/fabric/AnimatedPropBufferEncoder.h | 25 ++ .../src/main/jni/react/fabric/CMakeLists.txt | 1 + .../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 | 418 ++++++++++++++++++ .../BatchedAnimatedPropsMountItemTest.kt | 374 ++++++++++++++++ .../MountingManagerAnimatedPropsTest.kt | 99 +++++ .../api-snapshots/ReactAndroidDebugCxx.api | 8 + .../api-snapshots/ReactAndroidNewarchCxx.api | 8 + .../api-snapshots/ReactAndroidReleaseCxx.api | 8 + 17 files changed, 1693 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..a3cca69f4bfd 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,35 @@ 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); + + if (ReactNativeFeatureFlags.enableFabricLogs()) { + FLog.d( + TAG, + "SynchronouslyUpdateViewBatch: %d ints, %d doubles, %d raw props", + intBuffer.length, + doubleBuffer.length, + rawPropsBuffer.length); + } + + 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..6331bfe710f1 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/AnimatedPropBufferEncoder.cpp @@ -0,0 +1,395 @@ +/* + * 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() || + color.value() < std::numeric_limits::min() || + color.value() > std::numeric_limits::max()) { + 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..d7377cd1674c --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/AnimatedPropBufferEncoder.h @@ -0,0 +1,25 @@ +/* + * 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 + +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/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CMakeLists.txt index bdef71c0d4a6..4db218564823 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CMakeLists.txt @@ -30,6 +30,7 @@ target_link_libraries( react_codegen_rncore react_debug react_featureflags + react_renderer_animationbackend react_renderer_animations react_renderer_attributedstring react_renderer_componentregistry 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 bcedfa1e440c..c5820ca24877 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" @@ -1268,6 +1269,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 7ed9262f660b..0398ca8c33c6 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..56282385afbb --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/test/AnimatedPropBufferEncoderTest.cpp @@ -0,0 +1,418 @@ +/* + * 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, fallsBackForOutOfRangeIntegerColors) { + for (const auto& color : std::vector{ + int64_t{std::numeric_limits::max()} + 1, + int64_t{std::numeric_limits::min()} - 1, + double{0x100000000}}) { + SCOPED_TRACE(color.asString()); + auto buffer = + encodeRawProps(folly::dynamic::object("backgroundColor", color)); + 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]["backgroundColor"], 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..bc348b36c5a0 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/BatchedAnimatedPropsMountItemTest.kt @@ -0,0 +1,374 @@ +/* + * 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/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index b9ea38b1d674..e3958a20bdb2 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); @@ -2373,6 +2374,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(); } @@ -6757,6 +6759,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 421319844691..53b3bdefc282 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); @@ -2356,6 +2357,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(); } @@ -6567,6 +6569,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 b8f96d32e514..6f423ce86ca9 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); @@ -2371,6 +2372,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(); } @@ -6748,6 +6750,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; From 6c32ed407086cb8fd39a7d7992c8b160ce155c9e Mon Sep 17 00:00:00 2001 From: Bartlomiej Bloniarz Date: Mon, 5 Oct 2026 09:40:33 -0700 Subject: [PATCH 4/4] Reduce animation registry copying and allocation (#58775) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/react/react-native/pull/58775 For every raw update, `AnimatedPropsRegistry::update` copied the incoming props before merging them, and it built a full typed view-props snapshot even when no typed prop was animated. It now merges from the incoming `folly::dynamic` without copying it and allocates typed props only when a typed update arrives. The merge semantics and the caller's input are unchanged. On a low-end Android phone (4× Cortex-A53) with 12 views animating opacity, the registry update per frame dropped from about 0.22 ms to 0.13 ms. The new registry tests are excluded from React-Fabric's CocoaPods and Swift Package Manager production sources. ## Changelog: [Internal] Differential Revision: D122570619 --- packages/react-native/Package.swift | 1 + .../ReactCommon/React-Fabric.podspec | 1 + .../AnimatedPropsRegistry.cpp | 24 +- .../animationbackend/AnimatedPropsRegistry.h | 167 +++++++------- .../tests/AnimatedPropsRegistryTest.cpp | 214 ++++++++++++++++++ .../api-snapshots/ReactAndroidDebugCxx.api | 2 +- .../api-snapshots/ReactAndroidNewarchCxx.api | 2 +- .../api-snapshots/ReactAndroidReleaseCxx.api | 2 +- .../api-snapshots/ReactAppleDebugCxx.api | 2 +- .../api-snapshots/ReactAppleNewarchCxx.api | 2 +- .../api-snapshots/ReactAppleReleaseCxx.api | 2 +- .../api-snapshots/ReactCommonDebugCxx.api | 2 +- .../api-snapshots/ReactCommonNewarchCxx.api | 2 +- .../api-snapshots/ReactCommonReleaseCxx.api | 2 +- 14 files changed, 328 insertions(+), 97 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimatedPropsRegistryTest.cpp diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index 44e0c4707665..da76a10ac812 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -467,6 +467,7 @@ let reactFabric = RNTarget( path: "ReactCommon/react/renderer", excludedPaths: [ "animated/tests", + "animationbackend/tests", "animations/tests", "attributedstring/tests", "core/tests", diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index b2850cf6df46..ce02f4138872 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -80,6 +80,7 @@ Pod::Spec.new do |s| ss.dependency "React-callinvoker" ss.dependency "React-timing" ss.source_files = podspec_sources("react/renderer/animationbackend/**/*.{m,mm,cpp,h}", "react/renderer/animationbackend/**/*.{h}") + ss.exclude_files = "react/renderer/animationbackend/tests" ss.header_dir = "react/renderer/animationbackend" end diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp index e8df4f4ca0e1..bca9b7a82d01 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp @@ -46,22 +46,27 @@ void AnimatedPropsRegistry::update( .first; } auto& snapshot = it->second; - auto& viewProps = snapshot->props; - if (animatedProps.rawProps) { const auto& newRawProps = *animatedProps.rawProps; auto& currentRawProps = snapshot->rawProps; if (currentRawProps) { - mergeAnimatedRawProps(*currentRawProps, newRawProps.toDynamic()); + if (const auto* dynamic = newRawProps.getDynamic()) { + mergeAnimatedRawProps(*currentRawProps, *dynamic); + } else { + mergeAnimatedRawProps(*currentRawProps, newRawProps.toDynamic()); + } } else { currentRawProps = std::make_unique(newRawProps.toDynamic()); } } + if (!animatedProps.props.empty() && !snapshot->props) { + snapshot->props = std::make_unique(); + } for (const auto& animatedProp : animatedProps.props) { snapshot->propNames.insert(animatedProp->propName); - cloneProp(viewProps, *animatedProp); + cloneProp(*snapshot->props, *animatedProp); } } } @@ -97,9 +102,14 @@ AnimatedPropsRegistry::getMap(SurfaceId surfaceId) { currentSnapshot->rawProps = std::move(propsSnapshot->rawProps); } } - for (auto& propName : propsSnapshot->propNames) { - currentSnapshot->propNames.insert(propName); - updateProp(propName, currentSnapshot->props, *propsSnapshot); + if (!currentSnapshot->props) { + currentSnapshot->props = std::move(propsSnapshot->props); + currentSnapshot->propNames = std::move(propsSnapshot->propNames); + } else { + for (auto& propName : propsSnapshot->propNames) { + currentSnapshot->propNames.insert(propName); + updateProp(propName, *currentSnapshot->props, *propsSnapshot); + } } } } diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h index da0e6e884248..452b0b5c8dfd 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h @@ -9,6 +9,7 @@ #include +#include #include #include #include @@ -20,7 +21,7 @@ namespace facebook::react { struct PropsSnapshot { - BaseViewProps props; + std::unique_ptr props; std::unordered_set propNames; std::unique_ptr rawProps; }; @@ -53,249 +54,253 @@ class AnimatedPropsRegistry { inline void updateProp(const PropName propName, BaseViewProps &viewProps, const PropsSnapshot &snapshot) { + react_native_assert(snapshot.propNames.contains(propName)); + if (!snapshot.props) { + return; + } switch (propName) { case OPACITY: - viewProps.opacity = snapshot.props.opacity; + viewProps.opacity = snapshot.props->opacity; break; case WIDTH: viewProps.yogaStyle.setDimension( - yoga::Dimension::Width, snapshot.props.yogaStyle.dimension(yoga::Dimension::Width)); + yoga::Dimension::Width, snapshot.props->yogaStyle.dimension(yoga::Dimension::Width)); break; case HEIGHT: { - auto d = snapshot.props.yogaStyle.dimension(yoga::Dimension::Height); + auto d = snapshot.props->yogaStyle.dimension(yoga::Dimension::Height); viewProps.yogaStyle.setDimension(yoga::Dimension::Height, d); break; } case TRANSFORM: - viewProps.transform = snapshot.props.transform; + viewProps.transform = snapshot.props->transform; break; case TRANSFORM_ORIGIN: - viewProps.transformOrigin = snapshot.props.transformOrigin; + viewProps.transformOrigin = snapshot.props->transformOrigin; break; case BORDER_RADII: - viewProps.borderRadii = snapshot.props.borderRadii; + viewProps.borderRadii = snapshot.props->borderRadii; break; case FLEX: - viewProps.yogaStyle.setFlex(snapshot.props.yogaStyle.flex()); + viewProps.yogaStyle.setFlex(snapshot.props->yogaStyle.flex()); break; case BACKGROUND_COLOR: - viewProps.backgroundColor = snapshot.props.backgroundColor; + viewProps.backgroundColor = snapshot.props->backgroundColor; break; case SHADOW_COLOR: - viewProps.shadowColor = snapshot.props.shadowColor; + viewProps.shadowColor = snapshot.props->shadowColor; break; case SHADOW_OFFSET: - viewProps.shadowOffset = snapshot.props.shadowOffset; + viewProps.shadowOffset = snapshot.props->shadowOffset; break; case SHADOW_OPACITY: - viewProps.shadowOpacity = snapshot.props.shadowOpacity; + viewProps.shadowOpacity = snapshot.props->shadowOpacity; break; case SHADOW_RADIUS: - viewProps.shadowRadius = snapshot.props.shadowRadius; + viewProps.shadowRadius = snapshot.props->shadowRadius; break; case MARGIN: - viewProps.yogaStyle.setMargin(yoga::Edge::Left, snapshot.props.yogaStyle.margin(yoga::Edge::Left)); - viewProps.yogaStyle.setMargin(yoga::Edge::Right, snapshot.props.yogaStyle.margin(yoga::Edge::Right)); - viewProps.yogaStyle.setMargin(yoga::Edge::Top, snapshot.props.yogaStyle.margin(yoga::Edge::Top)); - viewProps.yogaStyle.setMargin(yoga::Edge::Bottom, snapshot.props.yogaStyle.margin(yoga::Edge::Bottom)); - viewProps.yogaStyle.setMargin(yoga::Edge::Start, snapshot.props.yogaStyle.margin(yoga::Edge::Start)); - viewProps.yogaStyle.setMargin(yoga::Edge::End, snapshot.props.yogaStyle.margin(yoga::Edge::End)); - viewProps.yogaStyle.setMargin(yoga::Edge::Horizontal, snapshot.props.yogaStyle.margin(yoga::Edge::Horizontal)); - viewProps.yogaStyle.setMargin(yoga::Edge::Vertical, snapshot.props.yogaStyle.margin(yoga::Edge::Vertical)); + viewProps.yogaStyle.setMargin(yoga::Edge::Left, snapshot.props->yogaStyle.margin(yoga::Edge::Left)); + viewProps.yogaStyle.setMargin(yoga::Edge::Right, snapshot.props->yogaStyle.margin(yoga::Edge::Right)); + viewProps.yogaStyle.setMargin(yoga::Edge::Top, snapshot.props->yogaStyle.margin(yoga::Edge::Top)); + viewProps.yogaStyle.setMargin(yoga::Edge::Bottom, snapshot.props->yogaStyle.margin(yoga::Edge::Bottom)); + viewProps.yogaStyle.setMargin(yoga::Edge::Start, snapshot.props->yogaStyle.margin(yoga::Edge::Start)); + viewProps.yogaStyle.setMargin(yoga::Edge::End, snapshot.props->yogaStyle.margin(yoga::Edge::End)); + viewProps.yogaStyle.setMargin(yoga::Edge::Horizontal, snapshot.props->yogaStyle.margin(yoga::Edge::Horizontal)); + viewProps.yogaStyle.setMargin(yoga::Edge::Vertical, snapshot.props->yogaStyle.margin(yoga::Edge::Vertical)); break; case PADDING: - viewProps.yogaStyle.setPadding(yoga::Edge::Left, snapshot.props.yogaStyle.padding(yoga::Edge::Left)); - viewProps.yogaStyle.setPadding(yoga::Edge::Right, snapshot.props.yogaStyle.padding(yoga::Edge::Right)); - viewProps.yogaStyle.setPadding(yoga::Edge::Top, snapshot.props.yogaStyle.padding(yoga::Edge::Top)); - viewProps.yogaStyle.setPadding(yoga::Edge::Bottom, snapshot.props.yogaStyle.padding(yoga::Edge::Bottom)); - viewProps.yogaStyle.setPadding(yoga::Edge::Start, snapshot.props.yogaStyle.padding(yoga::Edge::Start)); - viewProps.yogaStyle.setPadding(yoga::Edge::End, snapshot.props.yogaStyle.padding(yoga::Edge::End)); - viewProps.yogaStyle.setPadding(yoga::Edge::Horizontal, snapshot.props.yogaStyle.padding(yoga::Edge::Horizontal)); - viewProps.yogaStyle.setPadding(yoga::Edge::Vertical, snapshot.props.yogaStyle.padding(yoga::Edge::Vertical)); + viewProps.yogaStyle.setPadding(yoga::Edge::Left, snapshot.props->yogaStyle.padding(yoga::Edge::Left)); + viewProps.yogaStyle.setPadding(yoga::Edge::Right, snapshot.props->yogaStyle.padding(yoga::Edge::Right)); + viewProps.yogaStyle.setPadding(yoga::Edge::Top, snapshot.props->yogaStyle.padding(yoga::Edge::Top)); + viewProps.yogaStyle.setPadding(yoga::Edge::Bottom, snapshot.props->yogaStyle.padding(yoga::Edge::Bottom)); + viewProps.yogaStyle.setPadding(yoga::Edge::Start, snapshot.props->yogaStyle.padding(yoga::Edge::Start)); + viewProps.yogaStyle.setPadding(yoga::Edge::End, snapshot.props->yogaStyle.padding(yoga::Edge::End)); + viewProps.yogaStyle.setPadding(yoga::Edge::Horizontal, snapshot.props->yogaStyle.padding(yoga::Edge::Horizontal)); + viewProps.yogaStyle.setPadding(yoga::Edge::Vertical, snapshot.props->yogaStyle.padding(yoga::Edge::Vertical)); break; case POSITION: - viewProps.yogaStyle.setPosition(yoga::Edge::Left, snapshot.props.yogaStyle.position(yoga::Edge::Left)); - viewProps.yogaStyle.setPosition(yoga::Edge::Right, snapshot.props.yogaStyle.position(yoga::Edge::Right)); - viewProps.yogaStyle.setPosition(yoga::Edge::Top, snapshot.props.yogaStyle.position(yoga::Edge::Top)); - viewProps.yogaStyle.setPosition(yoga::Edge::Bottom, snapshot.props.yogaStyle.position(yoga::Edge::Bottom)); - viewProps.yogaStyle.setPosition(yoga::Edge::Start, snapshot.props.yogaStyle.position(yoga::Edge::Start)); - viewProps.yogaStyle.setPosition(yoga::Edge::End, snapshot.props.yogaStyle.position(yoga::Edge::End)); + viewProps.yogaStyle.setPosition(yoga::Edge::Left, snapshot.props->yogaStyle.position(yoga::Edge::Left)); + viewProps.yogaStyle.setPosition(yoga::Edge::Right, snapshot.props->yogaStyle.position(yoga::Edge::Right)); + viewProps.yogaStyle.setPosition(yoga::Edge::Top, snapshot.props->yogaStyle.position(yoga::Edge::Top)); + viewProps.yogaStyle.setPosition(yoga::Edge::Bottom, snapshot.props->yogaStyle.position(yoga::Edge::Bottom)); + viewProps.yogaStyle.setPosition(yoga::Edge::Start, snapshot.props->yogaStyle.position(yoga::Edge::Start)); + viewProps.yogaStyle.setPosition(yoga::Edge::End, snapshot.props->yogaStyle.position(yoga::Edge::End)); viewProps.yogaStyle.setPosition( - yoga::Edge::Horizontal, snapshot.props.yogaStyle.position(yoga::Edge::Horizontal)); - viewProps.yogaStyle.setPosition(yoga::Edge::Vertical, snapshot.props.yogaStyle.position(yoga::Edge::Vertical)); + yoga::Edge::Horizontal, snapshot.props->yogaStyle.position(yoga::Edge::Horizontal)); + viewProps.yogaStyle.setPosition(yoga::Edge::Vertical, snapshot.props->yogaStyle.position(yoga::Edge::Vertical)); break; case BORDER_WIDTH: - viewProps.yogaStyle.setBorder(yoga::Edge::Left, snapshot.props.yogaStyle.border(yoga::Edge::Left)); - viewProps.yogaStyle.setBorder(yoga::Edge::Right, snapshot.props.yogaStyle.border(yoga::Edge::Right)); - viewProps.yogaStyle.setBorder(yoga::Edge::Top, snapshot.props.yogaStyle.border(yoga::Edge::Top)); - viewProps.yogaStyle.setBorder(yoga::Edge::Bottom, snapshot.props.yogaStyle.border(yoga::Edge::Bottom)); - viewProps.yogaStyle.setBorder(yoga::Edge::Start, snapshot.props.yogaStyle.border(yoga::Edge::Start)); - viewProps.yogaStyle.setBorder(yoga::Edge::End, snapshot.props.yogaStyle.border(yoga::Edge::End)); - viewProps.yogaStyle.setBorder(yoga::Edge::Horizontal, snapshot.props.yogaStyle.border(yoga::Edge::Horizontal)); - viewProps.yogaStyle.setBorder(yoga::Edge::Vertical, snapshot.props.yogaStyle.border(yoga::Edge::Vertical)); + viewProps.yogaStyle.setBorder(yoga::Edge::Left, snapshot.props->yogaStyle.border(yoga::Edge::Left)); + viewProps.yogaStyle.setBorder(yoga::Edge::Right, snapshot.props->yogaStyle.border(yoga::Edge::Right)); + viewProps.yogaStyle.setBorder(yoga::Edge::Top, snapshot.props->yogaStyle.border(yoga::Edge::Top)); + viewProps.yogaStyle.setBorder(yoga::Edge::Bottom, snapshot.props->yogaStyle.border(yoga::Edge::Bottom)); + viewProps.yogaStyle.setBorder(yoga::Edge::Start, snapshot.props->yogaStyle.border(yoga::Edge::Start)); + viewProps.yogaStyle.setBorder(yoga::Edge::End, snapshot.props->yogaStyle.border(yoga::Edge::End)); + viewProps.yogaStyle.setBorder(yoga::Edge::Horizontal, snapshot.props->yogaStyle.border(yoga::Edge::Horizontal)); + viewProps.yogaStyle.setBorder(yoga::Edge::Vertical, snapshot.props->yogaStyle.border(yoga::Edge::Vertical)); break; case BORDER_COLOR: - viewProps.borderColors = snapshot.props.borderColors; + viewProps.borderColors = snapshot.props->borderColors; break; case FILTER: - viewProps.filter = snapshot.props.filter; + viewProps.filter = snapshot.props->filter; break; case OUTLINE_COLOR: - viewProps.outlineColor = snapshot.props.outlineColor; + viewProps.outlineColor = snapshot.props->outlineColor; break; case OUTLINE_OFFSET: - viewProps.outlineOffset = snapshot.props.outlineOffset; + viewProps.outlineOffset = snapshot.props->outlineOffset; break; case OUTLINE_STYLE: - viewProps.outlineStyle = snapshot.props.outlineStyle; + viewProps.outlineStyle = snapshot.props->outlineStyle; break; case OUTLINE_WIDTH: - viewProps.outlineWidth = snapshot.props.outlineWidth; + viewProps.outlineWidth = snapshot.props->outlineWidth; break; case ALIGN_CONTENT: - viewProps.yogaStyle.setAlignContent(snapshot.props.yogaStyle.alignContent()); + viewProps.yogaStyle.setAlignContent(snapshot.props->yogaStyle.alignContent()); break; case ALIGN_ITEMS: - viewProps.yogaStyle.setAlignItems(snapshot.props.yogaStyle.alignItems()); + viewProps.yogaStyle.setAlignItems(snapshot.props->yogaStyle.alignItems()); break; case ALIGN_SELF: - viewProps.yogaStyle.setAlignSelf(snapshot.props.yogaStyle.alignSelf()); + viewProps.yogaStyle.setAlignSelf(snapshot.props->yogaStyle.alignSelf()); break; case ASPECT_RATIO: - viewProps.yogaStyle.setAspectRatio(snapshot.props.yogaStyle.aspectRatio()); + viewProps.yogaStyle.setAspectRatio(snapshot.props->yogaStyle.aspectRatio()); break; case BOX_SIZING: - viewProps.yogaStyle.setBoxSizing(snapshot.props.yogaStyle.boxSizing()); + viewProps.yogaStyle.setBoxSizing(snapshot.props->yogaStyle.boxSizing()); break; case DISPLAY: - viewProps.yogaStyle.setDisplay(snapshot.props.yogaStyle.display()); + viewProps.yogaStyle.setDisplay(snapshot.props->yogaStyle.display()); break; case FLEX_BASIS: - viewProps.yogaStyle.setFlexBasis(snapshot.props.yogaStyle.flexBasis()); + viewProps.yogaStyle.setFlexBasis(snapshot.props->yogaStyle.flexBasis()); break; case FLEX_DIRECTION: - viewProps.yogaStyle.setFlexDirection(snapshot.props.yogaStyle.flexDirection()); + viewProps.yogaStyle.setFlexDirection(snapshot.props->yogaStyle.flexDirection()); break; case ROW_GAP: - viewProps.yogaStyle.setGap(yoga::Gutter::Row, snapshot.props.yogaStyle.gap(yoga::Gutter::Row)); + viewProps.yogaStyle.setGap(yoga::Gutter::Row, snapshot.props->yogaStyle.gap(yoga::Gutter::Row)); break; case COLUMN_GAP: - viewProps.yogaStyle.setGap(yoga::Gutter::Column, snapshot.props.yogaStyle.gap(yoga::Gutter::Column)); + viewProps.yogaStyle.setGap(yoga::Gutter::Column, snapshot.props->yogaStyle.gap(yoga::Gutter::Column)); break; case FLEX_GROW: - viewProps.yogaStyle.setFlexGrow(snapshot.props.yogaStyle.flexGrow()); + viewProps.yogaStyle.setFlexGrow(snapshot.props->yogaStyle.flexGrow()); break; case FLEX_SHRINK: - viewProps.yogaStyle.setFlexShrink(snapshot.props.yogaStyle.flexShrink()); + viewProps.yogaStyle.setFlexShrink(snapshot.props->yogaStyle.flexShrink()); break; case FLEX_WRAP: - viewProps.yogaStyle.setFlexWrap(snapshot.props.yogaStyle.flexWrap()); + viewProps.yogaStyle.setFlexWrap(snapshot.props->yogaStyle.flexWrap()); break; case JUSTIFY_CONTENT: - viewProps.yogaStyle.setJustifyContent(snapshot.props.yogaStyle.justifyContent()); + viewProps.yogaStyle.setJustifyContent(snapshot.props->yogaStyle.justifyContent()); break; case MAX_HEIGHT: viewProps.yogaStyle.setMaxDimension( - yoga::Dimension::Height, snapshot.props.yogaStyle.maxDimension(yoga::Dimension::Height)); + yoga::Dimension::Height, snapshot.props->yogaStyle.maxDimension(yoga::Dimension::Height)); break; case MAX_WIDTH: viewProps.yogaStyle.setMaxDimension( - yoga::Dimension::Width, snapshot.props.yogaStyle.maxDimension(yoga::Dimension::Width)); + yoga::Dimension::Width, snapshot.props->yogaStyle.maxDimension(yoga::Dimension::Width)); break; case MIN_HEIGHT: viewProps.yogaStyle.setMinDimension( - yoga::Dimension::Height, snapshot.props.yogaStyle.minDimension(yoga::Dimension::Height)); + yoga::Dimension::Height, snapshot.props->yogaStyle.minDimension(yoga::Dimension::Height)); break; case MIN_WIDTH: viewProps.yogaStyle.setMinDimension( - yoga::Dimension::Width, snapshot.props.yogaStyle.minDimension(yoga::Dimension::Width)); + yoga::Dimension::Width, snapshot.props->yogaStyle.minDimension(yoga::Dimension::Width)); break; case STYLE_OVERFLOW: - viewProps.yogaStyle.setOverflow(snapshot.props.yogaStyle.overflow()); + viewProps.yogaStyle.setOverflow(snapshot.props->yogaStyle.overflow()); break; case POSITION_TYPE: - viewProps.yogaStyle.setPositionType(snapshot.props.yogaStyle.positionType()); + viewProps.yogaStyle.setPositionType(snapshot.props->yogaStyle.positionType()); break; case Z_INDEX: - viewProps.zIndex = snapshot.props.zIndex; + viewProps.zIndex = snapshot.props->zIndex; break; case DIRECTION: - viewProps.yogaStyle.setDirection(snapshot.props.yogaStyle.direction()); + viewProps.yogaStyle.setDirection(snapshot.props->yogaStyle.direction()); break; case BORDER_CURVES: - viewProps.borderCurves = snapshot.props.borderCurves; + viewProps.borderCurves = snapshot.props->borderCurves; break; case BORDER_STYLES: - viewProps.borderStyles = snapshot.props.borderStyles; + viewProps.borderStyles = snapshot.props->borderStyles; break; case POINTER_EVENTS: - viewProps.pointerEvents = snapshot.props.pointerEvents; + viewProps.pointerEvents = snapshot.props->pointerEvents; break; case ISOLATION: - viewProps.isolation = snapshot.props.isolation; + viewProps.isolation = snapshot.props->isolation; break; case CURSOR: - viewProps.cursor = snapshot.props.cursor; + viewProps.cursor = snapshot.props->cursor; break; case BOX_SHADOW: - viewProps.boxShadow = snapshot.props.boxShadow; + viewProps.boxShadow = snapshot.props->boxShadow; break; case MIX_BLEND_MODE: - viewProps.mixBlendMode = snapshot.props.mixBlendMode; + viewProps.mixBlendMode = snapshot.props->mixBlendMode; break; case BACKFACE_VISIBILITY: - viewProps.backfaceVisibility = snapshot.props.backfaceVisibility; + viewProps.backfaceVisibility = snapshot.props->backfaceVisibility; break; default: diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimatedPropsRegistryTest.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimatedPropsRegistryTest.cpp new file mode 100644 index 000000000000..90ff00a3b6d2 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimatedPropsRegistryTest.cpp @@ -0,0 +1,214 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +class AnimatedPropsRegistryTest : public ::testing::Test { + protected: + ComponentBuilder builder = + simpleComponentBuilder(std::make_shared()); + std::shared_ptr node = + builder.build(Element().surfaceId(1).tag(10)); + AnimatedPropsRegistry registry; + + void SetUp() override { + registry.initializeSurface(1); + } + + void update(AnimatedProps props) { + std::vector batches(1); + batches[0].batch.push_back( + AnimationMutation{ + .tag = 10, + .family = node->getFamilyShared(), + .props = std::move(props)}); + registry.update(batches); + } + + void updateRaw(folly::dynamic props) { + update({{}, std::make_unique(std::move(props))}); + } + + void expectRawProps(folly::dynamic expected) { + EXPECT_EQ(*snapshot().rawProps, expected); + } + + PropsSnapshot& snapshot() { + return *registry.getMap(1).second.at(10); + } +}; + +TEST_F(AnimatedPropsRegistryTest, rawUpdatesDoNotAllocateTypedProps) { + for (int frame = 0; frame < 10; frame++) { + updateRaw(folly::dynamic::object("opacity", frame / 10.0)); + auto& props = snapshot(); + EXPECT_EQ(props.props, nullptr); + EXPECT_TRUE(props.propNames.empty()); + EXPECT_EQ((*props.rawProps)["opacity"], frame / 10.0); + } +} + +TEST_F(AnimatedPropsRegistryTest, updatePreservesInputForSynchronousMount) { + folly::dynamic rawProps = folly::dynamic::object("opacity", 0.25)( + "transform", + folly::dynamic::array(folly::dynamic::object("translateX", 10))); + std::vector batches(1); + batches[0].batch.push_back( + AnimationMutation{ + .tag = 10, + .family = node->getFamilyShared(), + .props = AnimatedProps{{}, std::make_unique(rawProps)}}); + registry.update(batches); + registry.update(batches); + EXPECT_EQ(batches[0].batch[0].props.rawProps->toDynamic(), rawProps); + EXPECT_EQ(*snapshot().rawProps, rawProps); +} + +TEST_F(AnimatedPropsRegistryTest, mergesPendingUpdatesPerKey) { + updateRaw( + folly::dynamic::object("opacity", 0.25)( + "shadowOffset", folly::dynamic::object("width", 2)("height", 3))); + updateRaw( + folly::dynamic::object( + "shadowOffset", folly::dynamic::object("width", 7))); + expectRawProps( + folly::dynamic::object("opacity", 0.25)( + "shadowOffset", folly::dynamic::object("width", 7))); +} + +TEST_F(AnimatedPropsRegistryTest, mergesCommittedUpdatesPerKey) { + updateRaw( + folly::dynamic::object("opacity", 0.25)( + "shadowOffset", folly::dynamic::object("width", 2)("height", 3))); + snapshot(); + updateRaw( + folly::dynamic::object( + "shadowOffset", folly::dynamic::object("width", 7))); + expectRawProps( + folly::dynamic::object("opacity", 0.25)( + "shadowOffset", folly::dynamic::object("width", 7))); +} + +TEST_F(AnimatedPropsRegistryTest, nullResetsArePreservedInPendingProps) { + updateRaw(folly::dynamic::object("opacity", 0.25)("borderRadius", 4)); + updateRaw(folly::dynamic::object("opacity", nullptr)); + expectRawProps(folly::dynamic::object("opacity", nullptr)("borderRadius", 4)); +} + +TEST_F(AnimatedPropsRegistryTest, nullResetsArePreservedInCommittedProps) { + updateRaw(folly::dynamic::object("opacity", 0.25)("borderRadius", 4)); + snapshot(); + updateRaw(folly::dynamic::object("opacity", nullptr)); + expectRawProps(folly::dynamic::object("opacity", nullptr)("borderRadius", 4)); + updateRaw(folly::dynamic::object("opacity", 0.5)); + expectRawProps(folly::dynamic::object("opacity", 0.5)("borderRadius", 4)); +} + +TEST_F(AnimatedPropsRegistryTest, arraysReplaceInsteadOfMerging) { + updateRaw( + folly::dynamic::object( + "transform", + folly::dynamic::array( + folly::dynamic::object("translateX", 2), + folly::dynamic::object("scale", 3)))); + snapshot(); + auto transform = + folly::dynamic::array(folly::dynamic::object("rotate", "1rad")); + updateRaw(folly::dynamic::object("transform", transform)); + EXPECT_EQ((*snapshot().rawProps)["transform"], transform); +} + +TEST_F(AnimatedPropsRegistryTest, valuesReplaceRegardlessOfType) { + updateRaw(folly::dynamic::object("value", 5)); + snapshot(); + folly::dynamic object = folly::dynamic::object("width", 2)("height", nullptr); + updateRaw(folly::dynamic::object("value", object)); + expectRawProps(folly::dynamic::object("value", object)); + updateRaw(folly::dynamic::object("value", 7)); + expectRawProps(folly::dynamic::object("value", 7)); +} + +TEST_F(AnimatedPropsRegistryTest, typedPropsCanFollowRawProps) { + updateRaw(folly::dynamic::object("opacity", 0.25)); + snapshot(); + AnimatedPropsBuilder builder; + builder.setOpacity(0.75); + update(builder.get()); + auto& props = snapshot(); + ASSERT_NE(props.props, nullptr); + EXPECT_FLOAT_EQ(props.props->opacity, 0.75); + EXPECT_TRUE(props.propNames.contains(OPACITY)); + EXPECT_EQ((*props.rawProps)["opacity"], 0.25); + BaseViewProps viewProps; + updateProp(OPACITY, viewProps, props); + EXPECT_FLOAT_EQ(viewProps.opacity, 0.75); +} + +TEST_F(AnimatedPropsRegistryTest, rawPropsCanFollowTypedProps) { + AnimatedPropsBuilder builder; + builder.setOpacity(0.75); + update(builder.get()); + snapshot(); + updateRaw(folly::dynamic::object("opacity", 0.25)); + auto& props = snapshot(); + ASSERT_NE(props.props, nullptr); + EXPECT_FLOAT_EQ(props.props->opacity, 0.75); + EXPECT_TRUE(props.propNames.contains(OPACITY)); + EXPECT_EQ((*props.rawProps)["opacity"], 0.25); +} + +TEST_F(AnimatedPropsRegistryTest, mergesTypedUpdatesAcrossCommits) { + AnimatedPropsBuilder builder; + builder.setOpacity(0.75); + builder.setShadowRadius(2); + update(builder.get()); + snapshot(); + builder.setOpacity(0.5); + update(builder.get()); + auto& props = snapshot(); + ASSERT_NE(props.props, nullptr); + EXPECT_FLOAT_EQ(props.props->opacity, 0.5); + EXPECT_FLOAT_EQ(props.props->shadowRadius, 2); + EXPECT_EQ(props.propNames.size(), 2u); +} + +TEST_F(AnimatedPropsRegistryTest, jsiRawPropsAreConverted) { + auto runtime = hermes::makeHermesRuntime(); + { + auto object = jsi::Object(*runtime); + object.setProperty(*runtime, "opacity", 0.25); + update( + {{}, + std::make_unique(*runtime, jsi::Value(*runtime, object))}); + object.setProperty(*runtime, "opacity", 0.5); + update( + {{}, + std::make_unique(*runtime, jsi::Value(*runtime, object))}); + } + runtime.reset(); + expectRawProps(folly::dynamic::object("opacity", 0.5)); + EXPECT_EQ(snapshot().props, nullptr); +} + +TEST_F(AnimatedPropsRegistryTest, stoppedSurfacesIgnoreUpdates) { + registry.clearOnSurfaceStop(1); + updateRaw(folly::dynamic::object("opacity", 0.5)); + registry.initializeSurface(1); + EXPECT_TRUE(registry.getMap(1).second.empty()); +} + +} // namespace facebook::react diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index e3958a20bdb2..335123f0b592 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -7790,7 +7790,7 @@ struct facebook::react::PropsParserContext { } struct facebook::react::PropsSnapshot { - public facebook::react::BaseViewProps props; + public std::unique_ptr props; public std::unique_ptr rawProps; public std::unordered_set propNames; } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 53b3bdefc282..3a0ac7e8e3e6 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -7550,7 +7550,7 @@ struct facebook::react::PropsParserContext { } struct facebook::react::PropsSnapshot { - public facebook::react::BaseViewProps props; + public std::unique_ptr props; public std::unique_ptr rawProps; public std::unordered_set propNames; } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 6f423ce86ca9..7ce3f0027ce8 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -7781,7 +7781,7 @@ struct facebook::react::PropsParserContext { } struct facebook::react::PropsSnapshot { - public facebook::react::BaseViewProps props; + public std::unique_ptr props; public std::unique_ptr rawProps; public std::unordered_set propNames; } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index e94665b89643..3ae686723246 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -9781,7 +9781,7 @@ struct facebook::react::PropsParserContext { } struct facebook::react::PropsSnapshot { - public facebook::react::BaseViewProps props; + public std::unique_ptr props; public std::unique_ptr rawProps; public std::unordered_set propNames; } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 685ee6ca2867..c69683d4969d 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -9597,7 +9597,7 @@ struct facebook::react::PropsParserContext { } struct facebook::react::PropsSnapshot { - public facebook::react::BaseViewProps props; + public std::unique_ptr props; public std::unique_ptr rawProps; public std::unordered_set propNames; } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 12d35b592ac4..a028ac755a8b 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -9772,7 +9772,7 @@ struct facebook::react::PropsParserContext { } struct facebook::react::PropsSnapshot { - public facebook::react::BaseViewProps props; + public std::unique_ptr props; public std::unique_ptr rawProps; public std::unordered_set propNames; } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 8941b020c4fd..3b2a0d8dcc7c 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -5930,7 +5930,7 @@ struct facebook::react::PropsParserContext { } struct facebook::react::PropsSnapshot { - public facebook::react::BaseViewProps props; + public std::unique_ptr props; public std::unique_ptr rawProps; public std::unordered_set propNames; } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 1fad1ff1700c..565d65172da6 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -5754,7 +5754,7 @@ struct facebook::react::PropsParserContext { } struct facebook::react::PropsSnapshot { - public facebook::react::BaseViewProps props; + public std::unique_ptr props; public std::unique_ptr rawProps; public std::unordered_set propNames; } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 491276d164ad..a71fd3030287 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -5921,7 +5921,7 @@ struct facebook::react::PropsParserContext { } struct facebook::react::PropsSnapshot { - public facebook::react::BaseViewProps props; + public std::unique_ptr props; public std::unique_ptr rawProps; public std::unordered_set propNames; }