Animation Backend init (#53526)

Summary:
This diff adds AnimatedProps class that allows prop diffs to be passed to the backend. Currently it is implemented through subclassing, but the implementation details are subject to change. To simplify integration with animation frameworks AnimatedPropsBuilder class was created - frameworks can use it to prepare their diffs.

## Changelog:

<!-- Help reviewers and the release process by writing your own changelog entry.

Pick one each for the category and type tags:

[GENERAL] [ADDED] - AnimatedProps.h AnimatedPropsBuilder.h
[GENERAL] [CHANGED] - AnimationBackend initialisation and style updates

For more details, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests

Pull Request resolved: https://github.com/facebook/react-native/pull/53526

Test Plan:
js1 fantom AnimatedBackend-itest.js
js1 fantom Animated-itest.js

Differential Revision: D81309558

Pulled By: bartlomiejbloniarz
This commit is contained in:
Bartłomiej Błoniarz
2025-09-22 07:07:59 -07:00
committed by Facebook GitHub Bot
parent 53a872fb4f
commit 635f89aaa7
9 changed files with 287 additions and 16 deletions
@@ -54,12 +54,23 @@ Pod::Spec.new do |s|
depend_on_js_engine(s)
add_rn_third_party_dependencies(s)
add_rncore_dependency(s)
s.subspec "animated" do |ss|
ss.source_files = podspec_sources("react/renderer/animated/**/*.{m,mm,cpp,h}", "react/renderer/animated/**/*.{h}")
ss.exclude_files = "react/renderer/animated/tests"
ss.header_dir = "react/renderer/animated"
end
s.subspec "animations" do |ss|
ss.source_files = podspec_sources("react/renderer/animations/**/*.{m,mm,cpp,h}", "react/renderer/animations/**/*.{h}")
ss.exclude_files = "react/renderer/animations/tests"
ss.header_dir = "react/renderer/animations"
end
s.subspec "animationbackend" do |ss|
ss.source_files = podspec_sources("react/renderer/animationbackend/**/*.{m,mm,cpp,h}", "react/renderer/animationbackend/**/*.{h}")
ss.header_dir = "react/renderer/animationbackend"
end
s.subspec "attributedstring" do |ss|
ss.source_files = podspec_sources("react/renderer/attributedstring/**/*.{m,mm,cpp,h}", "react/renderer/attributedstring/**/*.{h}")
@@ -7,7 +7,11 @@
#pragma once
#if __has_include("FBReactNativeSpecJSI.h") // CocoaPod headers on Apple
#include "FBReactNativeSpecJSI.h"
#else
#include <FBReactNativeSpec/FBReactNativeSpecJSI.h>
#endif
#include <ReactCommon/TurboModuleWithJSIBindings.h>
#include <folly/dynamic.h>
#include <react/renderer/animated/NativeAnimatedNodesManager.h>
@@ -852,7 +852,11 @@ void NativeAnimatedNodesManager::schedulePropsCommit(
bool layoutStyleUpdated,
bool forceFabricCommit) noexcept {
if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) {
mergeObjects(updateViewProps_[viewTag], props);
if (layoutStyleUpdated) {
mergeObjects(updateViewProps_[viewTag], props);
} else {
mergeObjects(updateViewPropsDirect_[viewTag], props);
}
return;
}
@@ -910,6 +914,7 @@ AnimationMutations NativeAnimatedNodesManager::pullAnimationMutations() {
auto timestamp = static_cast<double>(microseconds) / 1000.0;
bool containsChange = false;
AnimatedPropsBuilder propsBuilder;
{
// copied from onAnimationFrame
// Run all active animations
@@ -949,9 +954,13 @@ AnimationMutations NativeAnimatedNodesManager::pullAnimationMutations() {
}
}
for (auto& [tag, props] : updateViewProps_) {
mutations.emplace_back(
AnimationMutation{tag, props["opacity"].asDouble()});
for (auto& [tag, props] : updateViewPropsDirect_) {
// TODO: also handle layout props (updateViewProps_). It is skipped for
// now, because the backend requires shadowNodeFamilies to be able to
// commit to the ShadowTree
propsBuilder.storeDynamic(props);
mutations.push_back(
AnimationMutation{tag, nullptr, propsBuilder.get()});
containsChange = true;
}
}
@@ -978,9 +987,11 @@ AnimationMutations NativeAnimatedNodesManager::pullAnimationMutations() {
isEventAnimationInProgress_ = false;
for (auto& [tag, props] : updateViewProps_) {
mutations.emplace_back(
AnimationMutation{tag, props["opacity"].asDouble()});
for (auto& [tag, props] : updateViewPropsDirect_) {
// TODO: handle layout props
propsBuilder.storeDynamic(props);
mutations.push_back(
AnimationMutation{tag, nullptr, propsBuilder.get()});
}
}
} else {
@@ -7,7 +7,11 @@
#pragma once
#if __has_include("FBReactNativeSpecJSI.h") // CocoaPod headers on Apple
#include "FBReactNativeSpecJSI.h"
#else
#include <FBReactNativeSpec/FBReactNativeSpecJSI.h>
#endif
#include <folly/dynamic.h>
#include <react/bridging/Function.h>
#include <react/debug/flags.h>
@@ -71,7 +71,9 @@ NativeAnimatedNodesManagerProvider::getOrCreate(
animationBackend_ = std::make_shared<AnimationBackend>(
std::move(startOnRenderCallback_),
std::move(stopOnRenderCallback_),
std::move(directManipulationCallback));
std::move(directManipulationCallback),
std::move(fabricCommitCallback),
uiManager);
nativeAnimatedNodesManager_ =
std::make_shared<NativeAnimatedNodesManager>(animationBackend_);
@@ -0,0 +1,40 @@
/*
* 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 <react/renderer/components/view/BaseViewProps.h>
#include <utility>
namespace facebook::react {
enum PropName { OPACITY, WIDTH, HEIGHT, BORDER_RADII, FLEX, TRANSFORM };
struct AnimatedPropBase {
PropName propName;
explicit AnimatedPropBase(PropName propName) : propName(propName) {}
virtual ~AnimatedPropBase() = default;
};
template <typename T>
struct AnimatedProp : AnimatedPropBase {
T value;
AnimatedProp() = default;
AnimatedProp(PropName propName, const T& value)
: AnimatedPropBase{propName}, value(std::move(value)) {}
};
template <typename T>
T get(const std::unique_ptr<AnimatedPropBase>& animatedProp) {
return static_cast<AnimatedProp<T>*>(animatedProp.get())->value;
}
struct AnimatedProps {
std::vector<std::unique_ptr<AnimatedPropBase>> props;
std::unique_ptr<RawProps> rawProps;
};
} // namespace facebook::react
@@ -0,0 +1,48 @@
/*
* 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 <react/renderer/components/view/BaseViewProps.h>
#include "AnimatedProps.h"
namespace facebook::react {
struct AnimatedPropsBuilder {
std::vector<std::unique_ptr<AnimatedPropBase>> props;
std::unique_ptr<RawProps> rawProps;
void setOpacity(Float value) {
props.push_back(std::make_unique<AnimatedProp<Float>>(OPACITY, value));
}
void setWidth(yoga::Style::SizeLength value) {
props.push_back(
std::make_unique<AnimatedProp<yoga::Style::SizeLength>>(WIDTH, value));
}
void setHeight(yoga::Style::SizeLength value) {
props.push_back(
std::make_unique<AnimatedProp<yoga::Style::SizeLength>>(HEIGHT, value));
}
void setBorderRadii(CascadedBorderRadii& value) {
props.push_back(std::make_unique<AnimatedProp<CascadedBorderRadii>>(
BORDER_RADII, value));
}
void setTransform(Transform& t) {
props.push_back(
std::make_unique<AnimatedProp<Transform>>(TRANSFORM, std::move(t)));
}
void storeDynamic(folly::dynamic& d) {
rawProps = std::make_unique<RawProps>(std::move(d));
}
void storeJSI(jsi::Runtime& runtime, jsi::Value& value) {
rawProps = std::make_unique<RawProps>(runtime, value);
}
AnimatedProps get() {
return AnimatedProps{std::move(props), std::move(rawProps)};
}
};
} // namespace facebook::react
@@ -6,34 +6,169 @@
*/
#include "AnimationBackend.h"
#include <chrono>
namespace facebook::react {
static inline Props::Shared cloneProps(
AnimatedProps& animatedProps,
const ShadowNode& shadowNode) {
PropsParserContext propsParserContext{
shadowNode.getSurfaceId(), *shadowNode.getContextContainer()};
Props::Shared newProps;
if (animatedProps.rawProps) {
newProps = shadowNode.getComponentDescriptor().cloneProps(
propsParserContext,
shadowNode.getProps(),
std::move(*animatedProps.rawProps));
} else {
newProps = shadowNode.getComponentDescriptor().cloneProps(
propsParserContext, shadowNode.getProps(), {});
}
auto viewProps = std::const_pointer_cast<BaseViewProps>(
std::static_pointer_cast<const BaseViewProps>(newProps));
for (auto& animatedProp : animatedProps.props) {
switch (animatedProp->propName) {
case OPACITY:
viewProps->opacity = get<Float>(animatedProp);
break;
case WIDTH:
viewProps->yogaStyle.setDimension(
yoga::Dimension::Width, get<yoga::Style::SizeLength>(animatedProp));
break;
case HEIGHT:
viewProps->yogaStyle.setDimension(
yoga::Dimension::Height,
get<yoga::Style::SizeLength>(animatedProp));
break;
case BORDER_RADII:
viewProps->borderRadii = get<CascadedBorderRadii>(animatedProp);
break;
case FLEX:
viewProps->yogaStyle.setFlex(get<yoga::FloatOptional>(animatedProp));
break;
case TRANSFORM:
viewProps->transform = get<Transform>(animatedProp);
break;
}
}
return newProps;
}
static inline bool mutationHasLayoutUpdates(
facebook::react::AnimationMutation& mutation) {
for (auto& animatedProp : mutation.props.props) {
// TODO: there should also be a check for the dynamic part
if (animatedProp->propName == WIDTH || animatedProp->propName == HEIGHT ||
animatedProp->propName == FLEX) {
return true;
}
}
return false;
}
AnimationBackend::AnimationBackend(
StartOnRenderCallback&& startOnRenderCallback,
StopOnRenderCallback&& stopOnRenderCallback,
DirectManipulationCallback&& directManipulationCallback)
DirectManipulationCallback&& directManipulationCallback,
FabricCommitCallback&& fabricCommitCallback,
UIManager* uiManager)
: startOnRenderCallback_(startOnRenderCallback),
stopOnRenderCallback_(stopOnRenderCallback),
directManipulationCallback_(directManipulationCallback) {}
directManipulationCallback_(directManipulationCallback),
fabricCommitCallback_(fabricCommitCallback),
uiManager_(uiManager) {}
void AnimationBackend::onAnimationFrame(double timestamp) {
std::unordered_map<Tag, AnimatedProps> updates;
std::unordered_set<const ShadowNodeFamily*> families;
bool hasAnyLayoutUpdates = false;
for (auto& callback : callbacks) {
auto muatations = callback(timestamp);
for (auto& mutation : muatations) {
directManipulationCallback_(
mutation.tag, folly::dynamic::object("opacity", mutation.opacity));
hasAnyLayoutUpdates |= mutationHasLayoutUpdates(mutation);
families.insert(mutation.family);
updates[mutation.tag] = std::move(mutation.props);
}
}
if (hasAnyLayoutUpdates) {
commitUpdatesWithFamilies(families, updates);
} else {
synchronouslyUpdateProps(updates);
}
}
void AnimationBackend::start(const Callback& callback) {
callbacks.push_back(callback);
// startOnRenderCallback_ should provide the timestamp from the platform
startOnRenderCallback_([this]() { onAnimationFrame(0); });
// TODO: startOnRenderCallback_ should provide the timestamp from the platform
startOnRenderCallback_([this]() {
onAnimationFrame(
std::chrono::steady_clock::now().time_since_epoch().count() / 1000);
});
}
void AnimationBackend::stop() {
stopOnRenderCallback_();
callbacks.clear();
}
void AnimationBackend::commitUpdatesWithFamilies(
const std::unordered_set<const ShadowNodeFamily*>& families,
std::unordered_map<Tag, AnimatedProps>& updates) {
uiManager_->getShadowTreeRegistry().enumerate(
[families, &updates](const ShadowTree& shadowTree, bool& /*stop*/) {
shadowTree.commit(
[families, &updates](const RootShadowNode& oldRootShadowNode) {
return std::static_pointer_cast<RootShadowNode>(
oldRootShadowNode.cloneMultiple(
families,
[families, &updates](
const ShadowNode& shadowNode,
const ShadowNodeFragment& fragment) {
auto& animatedProps = updates.at(shadowNode.getTag());
auto newProps = cloneProps(animatedProps, shadowNode);
return shadowNode.clone(
{newProps,
fragment.children,
shadowNode.getState()});
}));
},
{.mountSynchronously = true});
});
}
void AnimationBackend::synchronouslyUpdateProps(
const std::unordered_map<Tag, AnimatedProps>& updates) {
for (auto& [tag, animatedProps] : updates) {
auto dyn = animatedProps.rawProps ? animatedProps.rawProps->toDynamic()
: folly::dynamic::object();
for (auto& animatedProp : animatedProps.props) {
// TODO: We shouldn't repack it into dynamic, but for that a rewrite of
// directManipulationCallback_ is needed
switch (animatedProp->propName) {
case OPACITY:
dyn.insert("opacity", get<Float>(animatedProp));
break;
case BORDER_RADII:
case TRANSFORM:
// TODO: handle other things than opacity
break;
case WIDTH:
case HEIGHT:
case FLEX:
throw "Tried to synchronously update layout props";
}
}
directManipulationCallback_(tag, dyn);
}
}
} // namespace facebook::react
@@ -9,14 +9,18 @@
#include <folly/dynamic.h>
#include <react/renderer/core/ReactPrimitives.h>
#include <react/renderer/uimanager/UIManager.h>
#include <functional>
#include <vector>
#include "AnimatedProps.h"
#include "AnimatedPropsBuilder.h"
namespace facebook::react {
struct AnimationMutation {
Tag tag;
double opacity;
const ShadowNodeFamily* family;
AnimatedProps props;
};
using AnimationMutations = std::vector<AnimationMutation>;
@@ -25,6 +29,8 @@ using StartOnRenderCallback = std::function<void(std::function<void()>&&)>;
using StopOnRenderCallback = std::function<void()>;
using DirectManipulationCallback =
std::function<void(Tag, const folly::dynamic&)>;
using FabricCommitCallback =
std::function<void(std::unordered_map<Tag, folly::dynamic>&)>;
class AnimationBackend {
public:
@@ -32,11 +38,21 @@ class AnimationBackend {
const StartOnRenderCallback startOnRenderCallback_;
const StopOnRenderCallback stopOnRenderCallback_;
const DirectManipulationCallback directManipulationCallback_;
const FabricCommitCallback fabricCommitCallback_;
UIManager* uiManager_;
AnimationBackend(
StartOnRenderCallback&& startOnRenderCallback,
StopOnRenderCallback&& stopOnRenderCallback,
DirectManipulationCallback&& directManipulationCallback);
DirectManipulationCallback&& directManipulationCallback,
FabricCommitCallback&& fabricCommitCallback,
UIManager* uiManager);
void commitUpdatesWithFamilies(
const std::unordered_set<const ShadowNodeFamily*>& families,
std::unordered_map<Tag, AnimatedProps>& updates);
void synchronouslyUpdateProps(
const std::unordered_map<Tag, AnimatedProps>& updates);
void onAnimationFrame(double timestamp);
void start(const Callback& callback);
void stop();