first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
file(GLOB react_renderer_animations_SRC CONFIGURE_DEPENDS *.cpp)
add_library(react_renderer_animations STATIC ${react_renderer_animations_SRC})
target_include_directories(react_renderer_animations PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(react_renderer_animations
folly_runtime
glog
glog_init
jsi
react_debug
react_renderer_componentregistry
react_renderer_core
react_renderer_debug
react_renderer_graphics
react_renderer_mounting
react_renderer_uimanager
rrc_view
runtimeexecutor
yoga
)
target_compile_reactnative_options(react_renderer_animations PRIVATE)
target_compile_options(react_renderer_animations PRIVATE -Wpedantic)

View File

@@ -0,0 +1,35 @@
/*
* 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 <jsi/jsi.h>
#include <memory>
namespace facebook::react {
class LayoutAnimationCallbackWrapper {
public:
LayoutAnimationCallbackWrapper(jsi::Function &&callback)
: callback_(std::make_shared<jsi::Function>(std::move(callback)))
{
}
LayoutAnimationCallbackWrapper() : callback_(nullptr) {}
void call(jsi::Runtime &runtime) const
{
if (callback_) {
callback_->call(runtime);
callback_.reset();
}
}
private:
mutable std::shared_ptr<jsi::Function> callback_;
};
} // namespace facebook::react

View File

@@ -0,0 +1,110 @@
/*
* 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 "LayoutAnimationDriver.h"
#include <glog/logging.h>
#include <react/debug/react_native_assert.h>
#include <react/renderer/animations/utils.h>
#include <algorithm>
namespace facebook::react {
void LayoutAnimationDriver::animationMutationsForFrame(
SurfaceId surfaceId,
ShadowViewMutation::List& mutationsList,
uint64_t now) const {
for (auto& animation : inflightAnimations_) {
if (animation.surfaceId != surfaceId) {
continue;
}
if (animation.completed) {
continue;
}
int incompleteAnimations = 0;
for (auto& keyframe : animation.keyFrames) {
if (keyframe.invalidated) {
continue;
}
const auto& baselineShadowView = keyframe.viewStart;
const auto& finalShadowView = keyframe.viewEnd;
// The contract with the "keyframes generation" phase is that any animated
// node will have a valid configuration.
const auto layoutAnimationConfig = animation.layoutAnimationConfig;
const auto& mutationConfig =
(keyframe.type == AnimationConfigurationType::Delete
? layoutAnimationConfig.deleteConfig
: (keyframe.type == AnimationConfigurationType::Create
? layoutAnimationConfig.createConfig
: layoutAnimationConfig.updateConfig));
// Interpolate
auto progress =
calculateAnimationProgress(now, animation, mutationConfig);
auto animationTimeProgressLinear = progress.first;
auto animationInterpolationFactor = progress.second;
auto mutatedShadowView = createInterpolatedShadowView(
animationInterpolationFactor, baselineShadowView, finalShadowView);
// Create the mutation instruction
mutationsList.emplace_back(
ShadowViewMutation::UpdateMutation(
keyframe.viewPrev, mutatedShadowView, keyframe.parentTag));
PrintMutationInstruction("Animation Progress:", mutationsList.back());
keyframe.viewPrev = std::move(mutatedShadowView);
if (animationTimeProgressLinear < 1) {
incompleteAnimations++;
}
}
// Are there no ongoing mutations left in this animation?
if (incompleteAnimations == 0) {
animation.completed = true;
}
}
// Clear out finished animations
for (auto it = inflightAnimations_.begin();
it != inflightAnimations_.end();) {
const auto& animation = *it;
if (animation.completed) {
callCallback(animation.successCallback);
// Queue up "final" mutations for all keyframes in the completed animation
for (const auto& keyframe : animation.keyFrames) {
if (keyframe.invalidated) {
continue;
}
queueFinalMutationsForCompletedKeyFrame(
keyframe,
mutationsList,
false,
"LayoutAnimationDriver: Animation Completed");
}
it = inflightAnimations_.erase(it);
} else {
it++;
}
}
// Final step: make sure that all operations execute in the proper order.
// REMOVE operations with highest indices must operate first.
std::stable_sort(
mutationsList.begin(),
mutationsList.end(),
&shouldFirstComeBeforeSecondMutation);
}
} // namespace facebook::react

View File

@@ -0,0 +1,31 @@
/*
* 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/animations/LayoutAnimationKeyFrameManager.h>
#include <react/renderer/core/ReactPrimitives.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
namespace facebook::react {
class LayoutAnimationDriver : public LayoutAnimationKeyFrameManager {
public:
LayoutAnimationDriver(
RuntimeExecutor runtimeExecutor,
std::shared_ptr<const ContextContainer> &contextContainer,
LayoutAnimationStatusDelegate *delegate)
: LayoutAnimationKeyFrameManager(runtimeExecutor, contextContainer, delegate)
{
}
protected:
virtual void animationMutationsForFrame(SurfaceId surfaceId, ShadowViewMutation::List &mutationsList, uint64_t now)
const override;
};
} // namespace facebook::react

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,174 @@
/*
* 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 <ReactCommon/RuntimeExecutor.h>
#include <react/renderer/animations/LayoutAnimationCallbackWrapper.h>
#include <react/renderer/animations/primitives.h>
#include <react/renderer/core/RawValue.h>
#include <react/renderer/debug/flags.h>
#include <react/renderer/mounting/MountingOverrideDelegate.h>
#include <react/renderer/mounting/MountingTransaction.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <react/renderer/uimanager/LayoutAnimationStatusDelegate.h>
#include <react/renderer/uimanager/UIManagerAnimationDelegate.h>
#include <optional>
#include <unordered_set>
namespace facebook::react {
#ifdef LAYOUT_ANIMATION_VERBOSE_LOGGING
void PrintMutationInstruction(std::string message, const ShadowViewMutation &mutation);
void PrintMutationInstructionRelative(
std::string message,
const ShadowViewMutation &mutation,
const ShadowViewMutation &relativeMutation);
#else
#define PrintMutationInstruction(a, b)
#define PrintMutationInstructionRelative(a, b, c)
#endif
class LayoutAnimationKeyFrameManager : public UIManagerAnimationDelegate, public MountingOverrideDelegate {
public:
LayoutAnimationKeyFrameManager(
RuntimeExecutor runtimeExecutor,
std::shared_ptr<const ContextContainer> &contextContainer,
LayoutAnimationStatusDelegate *delegate);
#pragma mark - UIManagerAnimationDelegate methods
void uiManagerDidConfigureNextLayoutAnimation(
jsi::Runtime &runtime,
const RawValue &config,
const jsi::Value &successCallbackValue,
const jsi::Value &failureCallbackValue) const override;
void setComponentDescriptorRegistry(const SharedComponentDescriptorRegistry &componentDescriptorRegistry) override;
// TODO: add SurfaceId to this API as well
bool shouldAnimateFrame() const override;
void stopSurface(SurfaceId surfaceId) override;
#pragma mark - MountingOverrideDelegate methods
bool shouldOverridePullTransaction() const override;
// This is used to "hijack" the diffing process to figure out which mutations
// should be animated. The mutations returned by this function will be
// executed immediately.
std::optional<MountingTransaction> pullTransaction(
SurfaceId surfaceId,
MountingTransaction::Number number,
const TransactionTelemetry &telemetry,
ShadowViewMutationList mutations) const override;
// Exposed for testing.
void uiManagerDidConfigureNextLayoutAnimation(LayoutAnimation layoutAnimation) const;
// LayoutAnimationStatusDelegate - this is for the platform to get
// signal when animations start and complete. Setting and resetting this
// delegate is protected by a mutex; ALL method calls into this delegate are
// also protected by the mutex! The only way to set this without a mutex is
// via a constructor.
void setLayoutAnimationStatusDelegate(LayoutAnimationStatusDelegate *delegate) const;
void setClockNow(std::function<uint64_t()> now);
protected:
SharedComponentDescriptorRegistry componentDescriptorRegistry_;
mutable std::optional<LayoutAnimation> currentAnimation_{};
mutable std::mutex currentAnimationMutex_;
/**
* All mutations of inflightAnimations_ are thread-safe as long as
* we keep the contract of: only mutate it within the context of
* `pullTransaction`. If that contract is held, this is implicitly protected
* by the MountingCoordinator's mutex.
*/
mutable std::vector<LayoutAnimation> inflightAnimations_{};
bool hasComponentDescriptorForShadowView(const ShadowView &shadowView) const;
const ComponentDescriptor &getComponentDescriptorForShadowView(const ShadowView &shadowView) const;
/**
* Given a `progress` between 0 and 1, a mutation and LayoutAnimation config,
* return a ShadowView with mutated props and/or LayoutMetrics.
*
* @param progress the current progress for the animation
* @param startingView the initial configuration of the ShadowView
* @param finalView the final configuration of the ShadowView
* @return the current ShadowView
*/
ShadowView createInterpolatedShadowView(Float progress, const ShadowView &startingView, const ShadowView &finalView)
const;
void callCallback(const LayoutAnimationCallbackWrapper &callback) const;
virtual void animationMutationsForFrame(SurfaceId surfaceId, ShadowViewMutation::List &mutationsList, uint64_t now)
const = 0;
/**
* Queue (and potentially synthesize) final mutations for a finished keyframe.
* Keyframe animation may have timed-out, or be canceled due to a conflict.
*/
void queueFinalMutationsForCompletedKeyFrame(
const AnimationKeyFrame &keyframe,
ShadowViewMutation::List &mutationsList,
bool interrupted,
const std::string &logPrefix) const;
private:
RuntimeExecutor runtimeExecutor_;
std::shared_ptr<const ContextContainer> contextContainer_;
mutable std::mutex layoutAnimationStatusDelegateMutex_;
mutable LayoutAnimationStatusDelegate *layoutAnimationStatusDelegate_{};
mutable std::mutex surfaceIdsToStopMutex_;
mutable std::unordered_set<SurfaceId> surfaceIdsToStop_{};
// Function that returns current time in milliseconds
std::function<uint64_t()> now_;
void adjustImmediateMutationIndicesForDelayedMutations(
SurfaceId surfaceId,
ShadowViewMutation &mutation,
bool skipLastAnimation = false,
bool lastAnimationOnly = false) const;
void adjustDelayedMutationIndicesForMutation(
SurfaceId surfaceId,
const ShadowViewMutation &mutation,
bool skipLastAnimation = false) const;
void getAndEraseConflictingAnimations(
SurfaceId surfaceId,
const ShadowViewMutationList &mutations,
std::vector<AnimationKeyFrame> &conflictingAnimations) const;
/*
* Removes animations from `inflightAnimations_` for stopped surfaces.
*/
void deleteAnimationsForStoppedSurfaces() const;
void simulateImagePropsMemoryAccess(const ShadowViewMutationList &mutations) const;
/**
* Interpolates the props values.
*/
Props::Shared interpolateProps(
const ComponentDescriptor &componentDescriptor,
const PropsParserContext &context,
Float animationProgress,
const Props::Shared &props,
const Props::Shared &newProps,
const Size &size) const;
};
} // namespace facebook::react

View File

@@ -0,0 +1,203 @@
/*
* 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 <glog/logging.h>
#include <react/renderer/animations/primitives.h>
#include <optional>
namespace facebook::react {
static inline std::optional<AnimationType> parseAnimationType(std::string param)
{
if (param == "spring") {
return AnimationType::Spring;
}
if (param == "linear") {
return AnimationType::Linear;
}
if (param == "easeInEaseOut") {
return AnimationType::EaseInEaseOut;
}
if (param == "easeIn") {
return AnimationType::EaseIn;
}
if (param == "easeOut") {
return AnimationType::EaseOut;
}
if (param == "keyboard") {
return AnimationType::Keyboard;
}
LOG(ERROR) << "Error parsing animation type: " << param;
return {};
}
static inline std::optional<AnimationProperty> parseAnimationProperty(std::string param)
{
if (param == "opacity") {
return AnimationProperty::Opacity;
}
if (param == "scaleX") {
return AnimationProperty::ScaleX;
}
if (param == "scaleY") {
return AnimationProperty::ScaleY;
}
if (param == "scaleXY") {
return AnimationProperty::ScaleXY;
}
LOG(ERROR) << "Error parsing animation property: " << param;
return {};
}
static inline std::optional<AnimationConfig>
parseAnimationConfig(const folly::dynamic &config, double defaultDuration, bool parsePropertyType)
{
if (config.empty() || !config.isObject()) {
return AnimationConfig{
.animationType = AnimationType::Linear,
.animationProperty = AnimationProperty::NotApplicable,
.duration = defaultDuration,
.delay = 0,
.springDamping = 0,
.initialVelocity = 0};
}
const auto typeIt = config.find("type");
if (typeIt == config.items().end()) {
LOG(ERROR) << "Error parsing animation config: could not find field `type`";
return {};
}
const auto animationTypeParam = typeIt->second;
if (animationTypeParam.empty() || !animationTypeParam.isString()) {
LOG(ERROR) << "Error parsing animation config: could not unwrap field `type`";
return {};
}
const auto animationType = parseAnimationType(animationTypeParam.asString());
if (!animationType) {
LOG(ERROR) << "Error parsing animation config: could not parse field `type`";
return {};
}
AnimationProperty animationProperty = AnimationProperty::NotApplicable;
if (parsePropertyType) {
const auto propertyIt = config.find("property");
if (propertyIt == config.items().end()) {
LOG(ERROR) << "Error parsing animation config: could not find field `property`";
return {};
}
const auto animationPropertyParam = propertyIt->second;
if (animationPropertyParam.empty() || !animationPropertyParam.isString()) {
LOG(ERROR) << "Error parsing animation config: could not unwrap field `property`";
return {};
}
const auto animationPropertyParsed = parseAnimationProperty(animationPropertyParam.asString());
if (!animationPropertyParsed) {
LOG(ERROR) << "Error parsing animation config: could not parse field `property`";
return {};
}
animationProperty = *animationPropertyParsed;
}
double duration = defaultDuration;
double delay = 0;
Float springDamping = 0.5;
Float initialVelocity = 0;
const auto durationIt = config.find("duration");
if (durationIt != config.items().end()) {
if (durationIt->second.isDouble()) {
duration = durationIt->second.asDouble();
} else {
LOG(ERROR) << "Error parsing animation config: field `duration` must be a number";
return {};
}
}
const auto delayIt = config.find("delay");
if (delayIt != config.items().end()) {
if (delayIt->second.isDouble()) {
delay = delayIt->second.asDouble();
} else {
LOG(ERROR) << "Error parsing animation config: field `delay` must be a number";
return {};
}
}
const auto springDampingIt = config.find("springDamping");
if (springDampingIt != config.items().end() && springDampingIt->second.isDouble()) {
if (springDampingIt->second.isDouble()) {
springDamping = (Float)springDampingIt->second.asDouble();
} else {
LOG(ERROR) << "Error parsing animation config: field `springDamping` must be a number";
return {};
}
}
const auto initialVelocityIt = config.find("initialVelocity");
if (initialVelocityIt != config.items().end()) {
if (initialVelocityIt->second.isDouble()) {
initialVelocity = (Float)initialVelocityIt->second.asDouble();
} else {
LOG(ERROR) << "Error parsing animation config: field `initialVelocity` must be a number";
return {};
}
}
return std::optional<AnimationConfig>(AnimationConfig{
.animationType = *animationType,
.animationProperty = animationProperty,
.duration = duration,
.delay = delay,
.springDamping = springDamping,
.initialVelocity = initialVelocity});
}
// Parse animation config from JS
static inline std::optional<LayoutAnimationConfig> parseLayoutAnimationConfig(const folly::dynamic &config)
{
if (config.empty() || !config.isObject()) {
return {};
}
const auto durationIt = config.find("duration");
if (durationIt == config.items().end() || !durationIt->second.isDouble()) {
return {};
}
const double duration = durationIt->second.asDouble();
const auto createConfigIt = config.find("create");
const auto createConfig = createConfigIt == config.items().end()
? std::optional<AnimationConfig>(AnimationConfig{})
: parseAnimationConfig(createConfigIt->second, duration, true);
const auto updateConfigIt = config.find("update");
const auto updateConfig = updateConfigIt == config.items().end()
? std::optional<AnimationConfig>(AnimationConfig{})
: parseAnimationConfig(updateConfigIt->second, duration, false);
const auto deleteConfigIt = config.find("delete");
const auto deleteConfig = deleteConfigIt == config.items().end()
? std::optional<AnimationConfig>(AnimationConfig{})
: parseAnimationConfig(deleteConfigIt->second, duration, true);
if (!createConfig || !updateConfig || !deleteConfig) {
return {};
}
return LayoutAnimationConfig{
.duration = duration,
.createConfig = *createConfig,
.updateConfig = *updateConfig,
.deleteConfig = *deleteConfig};
}
} // namespace facebook::react

View File

@@ -0,0 +1,96 @@
/*
* 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/animations/LayoutAnimationCallbackWrapper.h>
#include <react/renderer/core/ReactPrimitives.h>
#include <react/renderer/graphics/Float.h>
#include <react/renderer/mounting/ShadowView.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <vector>
namespace facebook::react {
// This corresponds exactly with JS.
enum class AnimationType {
None = 0,
Spring = 1,
Linear = 2,
EaseInEaseOut = 4,
EaseIn = 8,
EaseOut = 16,
Keyboard = 32
};
enum class AnimationProperty { NotApplicable = 0, Opacity = 1, ScaleX = 2, ScaleY = 4, ScaleXY = 8 };
// This corresponds exactly with JS.
struct AnimationConfig {
AnimationType animationType = AnimationType::None;
AnimationProperty animationProperty = AnimationProperty::NotApplicable;
double duration = 0; // these are perhaps better represented as uint64_t, but they
// come from JS as doubles
double delay = 0;
Float springDamping = 0;
Float initialVelocity = 0;
};
// This corresponds exactly with JS.
struct LayoutAnimationConfig {
double duration; // ms
AnimationConfig createConfig;
AnimationConfig updateConfig;
AnimationConfig deleteConfig;
};
enum class AnimationConfigurationType { Create = 1, Update = 2, Delete = 4 };
struct AnimationKeyFrame {
// The mutation(s) that should be executed once the animation completes.
// This maybe empty.
// For CREATE/INSERT this will contain CREATE, INSERT in that order.
// For REMOVE/DELETE, same.
std::vector<ShadowViewMutation> finalMutationsForKeyFrame;
// The type of animation this is (for configuration purposes)
AnimationConfigurationType type;
// Tag representing the node being animated.
Tag tag;
Tag parentTag;
// ShadowView representing the start and end points of this animation.
ShadowView viewStart;
ShadowView viewEnd;
// ShadowView representing the previous frame of the animation.
ShadowView viewPrev;
// If an animation interrupts an existing one, the starting state may actually
// be halfway through the intended transition.
double initialProgress;
bool invalidated{false};
// In the case where some mutation conflicts with this keyframe,
// should we generate final synthetic UPDATE mutations for this keyframe?
bool generateFinalSyntheticMutations{true};
};
struct LayoutAnimation {
SurfaceId surfaceId;
uint64_t startTime;
bool completed = false;
LayoutAnimationConfig layoutAnimationConfig;
LayoutAnimationCallbackWrapper successCallback;
LayoutAnimationCallbackWrapper failureCallback;
std::vector<AnimationKeyFrame> keyFrames;
};
} // namespace facebook::react

View File

@@ -0,0 +1,557 @@
/*
* 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 <vector>
#include <glog/logging.h>
#include <gtest/gtest.h>
#include <ReactCommon/RuntimeExecutor.h>
#include <react/renderer/animations/LayoutAnimationDriver.h>
#include <react/renderer/componentregistry/ComponentDescriptorProvider.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <react/renderer/componentregistry/ComponentDescriptorRegistry.h>
#include <react/renderer/components/root/RootComponentDescriptor.h>
#include <react/renderer/components/view/ViewComponentDescriptor.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/mounting/Differentiator.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <react/renderer/mounting/stubs/stubs.h>
#include <react/test_utils/Entropy.h>
#include <react/test_utils/MockClock.h>
#include <react/test_utils/shadowTreeGeneration.h>
// Uncomment when random test blocks are uncommented below.
// #include <algorithm>
// #include <random>
MockClock::time_point MockClock::time_ = {};
namespace facebook::react {
static void testShadowNodeTreeLifeCycleLayoutAnimations(
uint_fast32_t seed,
int treeSize,
int repeats,
int stages,
int animation_duration,
int animation_frames,
int delay_ms_between_frames,
int delay_ms_between_stages,
int delay_ms_between_repeats,
bool commits_conflicting_mutations = false,
int final_animation_delay = 0) {
auto entropy = seed == 0 ? Entropy() : Entropy(seed);
auto eventDispatcher = EventDispatcher::Shared{};
auto contextContainer = std::make_shared<const ContextContainer>();
auto componentDescriptorParameters = ComponentDescriptorParameters{
.eventDispatcher = eventDispatcher,
.contextContainer = contextContainer,
.flavor = nullptr};
auto viewComponentDescriptor =
ViewComponentDescriptor(componentDescriptorParameters);
auto rootComponentDescriptor =
RootComponentDescriptor(componentDescriptorParameters);
PropsParserContext parserContext{-1, *contextContainer};
// Create a RuntimeExecutor
RuntimeExecutor runtimeExecutor =
[](const std::function<void(jsi::Runtime&)>& /*unused*/) {};
// Create component descriptor registry for animation driver
auto providerRegistry =
std::make_shared<ComponentDescriptorProviderRegistry>();
auto componentDescriptorRegistry =
providerRegistry->createComponentDescriptorRegistry(
componentDescriptorParameters);
providerRegistry->add(
concreteComponentDescriptorProvider<ViewComponentDescriptor>());
// Create Animation Driver
auto animationDriver = std::make_shared<LayoutAnimationDriver>(
runtimeExecutor, contextContainer, nullptr);
animationDriver->setComponentDescriptorRegistry(componentDescriptorRegistry);
// Mock animation timers
animationDriver->setClockNow([]() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
MockClock::now().time_since_epoch())
.count();
});
auto allNodes = std::vector<std::shared_ptr<const ShadowNode>>{};
for (int i = 0; i < repeats; i++) {
allNodes.clear();
int surfaceIdInt = 1;
auto surfaceId = SurfaceId(surfaceIdInt);
auto family = rootComponentDescriptor.createFamily(
{.tag = Tag(surfaceIdInt),
.surfaceId = surfaceId,
.instanceHandle = nullptr});
// Creating an initial root shadow node.
auto emptyRootNode = std::const_pointer_cast<RootShadowNode>(
std::static_pointer_cast<const RootShadowNode>(
rootComponentDescriptor.createShadowNode(
ShadowNodeFragment{
.props = RootShadowNode::defaultSharedProps()},
family)));
// Applying size constraints.
emptyRootNode = emptyRootNode->clone(
parserContext,
LayoutConstraints{
.minimumSize = Size{.width = 512, .height = 0},
.maximumSize =
Size{
.width = 512,
.height = std::numeric_limits<Float>::infinity()}},
LayoutContext{});
// Generation of a random tree.
auto singleRootChildNode =
generateShadowNodeTree(entropy, viewComponentDescriptor, treeSize);
// Injecting a tree into the root node.
auto currentRootNode = std::static_pointer_cast<const RootShadowNode>(
emptyRootNode->ShadowNode::clone(
ShadowNodeFragment{
.props = ShadowNodeFragment::propsPlaceholder(),
.children = std::make_shared<
std::vector<std::shared_ptr<const ShadowNode>>>(
std::vector<std::shared_ptr<const ShadowNode>>{
singleRootChildNode})}));
// Building an initial view hierarchy.
auto viewTree = StubViewTree(ShadowView(*emptyRootNode));
viewTree.mutate(
calculateShadowViewMutations(*emptyRootNode, *currentRootNode));
for (int j = 0; j < stages; j++) {
auto nextRootNode = currentRootNode;
// Mutating the tree.
alterShadowTree(
entropy,
nextRootNode,
{
&messWithChildren,
&messWithYogaStyles,
&messWithLayoutableOnlyFlag,
});
std::vector<const LayoutableShadowNode*> affectedLayoutableNodes{};
affectedLayoutableNodes.reserve(1024);
// Laying out the tree.
std::const_pointer_cast<RootShadowNode>(nextRootNode)
->layoutIfNeeded(&affectedLayoutableNodes);
nextRootNode->sealRecursive();
allNodes.push_back(nextRootNode);
// Calculating mutations.
auto originalMutations =
calculateShadowViewMutations(*currentRootNode, *nextRootNode);
// If tree randomization produced no changes in the form of mutations,
// don't bother trying to animate because this violates a bunch of our
// assumptions in this test
if (originalMutations.empty()) {
continue;
}
// If we only mutated the root... also don't bother
if (originalMutations.size() == 1 &&
(originalMutations[0].oldChildShadowView.tag == 1 ||
originalMutations[0].newChildShadowView.tag == 1)) {
continue;
}
// Configure animation
animationDriver->uiManagerDidConfigureNextLayoutAnimation(
{.surfaceId = surfaceId,
.startTime = 0,
.completed = false,
.layoutAnimationConfig =
{.duration = (double)animation_duration,
.createConfig = {
/* Create */ .animationType = AnimationType::EaseInEaseOut,
.animationProperty = AnimationProperty::Opacity,
.duration = (double)animation_duration,
.delay = 0,
.springDamping = 0,
.initialVelocity = 0},
.updateConfig = {
/* Update */ .animationType = AnimationType::EaseInEaseOut,
.animationProperty = AnimationProperty::ScaleXY,
.duration = (double)animation_duration,
.delay = 0,
.springDamping = 0,
.initialVelocity = 0},
.deleteConfig = {
/* Delete */ .animationType = AnimationType::EaseInEaseOut,
.animationProperty = AnimationProperty::Opacity,
.duration = (double)animation_duration,
.delay = 0,
.springDamping = 0,
.initialVelocity = 0}},
.successCallback = {},
.failureCallback = {},
.keyFrames = {}});
// Get mutations for each frame
for (int k = 0; k < animation_frames + 2; k++) {
auto mutationsInput = ShadowViewMutation::List{};
if (k == 0) {
mutationsInput = originalMutations;
}
if (k != (animation_frames + 1)) {
EXPECT_TRUE(animationDriver->shouldOverridePullTransaction());
} else if (!commits_conflicting_mutations) {
EXPECT_FALSE(animationDriver->shouldOverridePullTransaction());
}
auto telemetry = TransactionTelemetry{};
telemetry.willLayout();
telemetry.willCommit();
telemetry.willDiff();
auto transaction = animationDriver->pullTransaction(
surfaceId, 0, telemetry, mutationsInput);
EXPECT_TRUE(transaction.has_value() || k == animation_frames);
// We have something to validate.
if (transaction.has_value()) {
auto mutations = transaction->getMutations();
// Mutating the view tree.
viewTree.mutate(mutations);
// We don't do any validation on this until all animations are
// finished!
}
MockClock::advance_by(
std::chrono::milliseconds(delay_ms_between_frames));
}
// After the animation is completed...
// Build a view tree to compare with.
// After all the synthetic mutations, at the end of the animation,
// the mutated and newly-constructed trees should be identical.
if (!commits_conflicting_mutations) {
auto rebuiltViewTree =
buildStubViewTreeWithoutUsingDifferentiator(*nextRootNode);
// Comparing the newly built tree with the updated one.
if (rebuiltViewTree != viewTree) {
// Something went wrong.
LOG(ERROR)
<< "Entropy seed: " << entropy.getSeed()
<< ". To see why trees are different, define STUB_VIEW_TREE_VERBOSE and see logging in StubViewTree.cpp.\n";
EXPECT_TRUE(false);
}
}
currentRootNode = nextRootNode;
MockClock::advance_by(std::chrono::milliseconds(delay_ms_between_stages));
}
// Flush all remaining animations before validating trees
if (final_animation_delay > 0) {
MockClock::advance_by(std::chrono::milliseconds(final_animation_delay));
auto telemetry = TransactionTelemetry{};
telemetry.willLayout();
telemetry.willCommit();
telemetry.willDiff();
auto transaction =
animationDriver->pullTransaction(surfaceId, 0, telemetry, {});
// We have something to validate.
if (transaction.has_value()) {
auto mutations = transaction->getMutations();
// Mutating the view tree.
viewTree.mutate(mutations);
// We don't do any validation on this until all animations are
// finished!
}
}
// After all animations are completed...
// Build a view tree to compare with.
// After all the synthetic mutations, at the end of the animation,
// the mutated and newly-constructed trees should be identical.
if (commits_conflicting_mutations) {
auto rebuiltViewTree =
buildStubViewTreeWithoutUsingDifferentiator(*currentRootNode);
// Comparing the newly built tree with the updated one.
if (rebuiltViewTree != viewTree) {
// Something went wrong.
LOG(ERROR)
<< "Entropy seed: " << entropy.getSeed()
<< ". To see why trees are different, define STUB_VIEW_TREE_VERBOSE and see logging in StubViewTree.cpp.\n";
EXPECT_TRUE(false);
}
}
MockClock::advance_by(std::chrono::milliseconds(delay_ms_between_repeats));
}
SUCCEED();
}
} // namespace facebook::react
using namespace facebook::react;
TEST(
LayoutAnimationTest,
stableSmallerTreeFewRepeatsFewStages_NonOverlapping_2029343357) {
testShadowNodeTreeLifeCycleLayoutAnimations(
/* seed */ 2029343357, /* working seed found 5-10-2021 */
/* size */ 128,
/* repeats */ 128,
/* stages */ 10,
/* animation_duration */ 1000,
/* animation_frames*/ 10,
/* delay_ms_between_frames */ 100,
/* delay_ms_between_stages */ 100,
/* delay_ms_between_repeats */ 2000);
}
TEST(
LayoutAnimationTest,
stableSmallerTreeFewRepeatsFewStages_NonOverlapping_3619914559) {
testShadowNodeTreeLifeCycleLayoutAnimations(
/* seed */ 3619914559, /* working seed found 5-10-2021 */
/* size */ 128,
/* repeats */ 128,
/* stages */ 10,
/* animation_duration */ 1000,
/* animation_frames*/ 10,
/* delay_ms_between_frames */ 100,
/* delay_ms_between_stages */ 100,
/* delay_ms_between_repeats */ 2000);
}
TEST(
LayoutAnimationTest,
stableSmallerTreeFewRepeatsFewStages_NonOverlapping_597132284) {
testShadowNodeTreeLifeCycleLayoutAnimations(
/* seed */ 597132284, /* failing seed found 5-10-2021 */
/* size */ 128,
/* repeats */ 128,
/* stages */ 10,
/* animation_duration */ 1000,
/* animation_frames*/ 10,
/* delay_ms_between_frames */ 100,
/* delay_ms_between_stages */ 100,
/* delay_ms_between_repeats */ 2000);
}
TEST(
LayoutAnimationTest,
stableSmallerTreeFewRepeatsFewStages_NonOverlapping_774986518) {
testShadowNodeTreeLifeCycleLayoutAnimations(
/* seed */ 774986518, /* failing seed found 5-10-2021 */
/* size */ 128,
/* repeats */ 128,
/* stages */ 10,
/* animation_duration */ 1000,
/* animation_frames*/ 10,
/* delay_ms_between_frames */ 100,
/* delay_ms_between_stages */ 100,
/* delay_ms_between_repeats */ 2000);
}
TEST(
LayoutAnimationTest,
stableSmallerTreeFewRepeatsFewStages_NonOverlapping_1450614414) {
testShadowNodeTreeLifeCycleLayoutAnimations(
/* seed */ 1450614414, /* failing seed found 5-10-2021 */
/* size */ 128,
/* repeats */ 128,
/* stages */ 10,
/* animation_duration */ 1000,
/* animation_frames*/ 10,
/* delay_ms_between_frames */ 100,
/* delay_ms_between_stages */ 100,
/* delay_ms_between_repeats */ 2000);
}
TEST(LayoutAnimationTest, stableBiggerTreeFewRepeatsFewStages_NonOverlapping) {
testShadowNodeTreeLifeCycleLayoutAnimations(
/* seed */ 2029343357,
/* size */ 512,
/* repeats */ 32,
/* stages */ 10,
/* animation_duration */ 1000,
/* animation_frames*/ 10,
/* delay_ms_between_frames */ 100,
/* delay_ms_between_stages */ 100,
/* delay_ms_between_repeats */ 2000);
}
TEST(LayoutAnimationTest, stableBiggerTreeFewRepeatsManyStages_NonOverlapping) {
testShadowNodeTreeLifeCycleLayoutAnimations(
/* seed */ 2029343357,
/* size */ 512,
/* repeats */ 32,
/* stages */ 128,
/* animation_duration */ 1000,
/* animation_frames*/ 10,
/* delay_ms_between_frames */ 100,
/* delay_ms_between_stages */ 100,
/* delay_ms_between_repeats */ 2000);
}
// You may uncomment this - locally only! - to generate failing seeds.
// TEST(LayoutAnimationTest, stableSmallerTreeFewRepeatsFewStages_Random) {
// std::random_device device;
// for (int i = 0; i < 10; i++) {
// uint_fast32_t seed = device();
// LOG(ERROR) << "Seed: " << seed;
// testShadowNodeTreeLifeCycleLayoutAnimations(
// /* seed */ seed,
// /* size */ 128,
// /* repeats */ 128,
// /* stages */ 10,
// /* animation_duration */ 1000,
// /* animation_frames*/ 10,
// /* delay_ms_between_frames */ 100,
// /* delay_ms_between_stages */ 100,
// /* delay_ms_between_repeats */ 2000);
// }
// // Fail if you want output to get seeds
// LOG(ERROR) << "ALL RUNS SUCCESSFUL";
// // react_native_assert(false);
// }
//
// These tests are "overlapping", meaning that mutations will be committed
// before the previous animation completes.
//
TEST(
LayoutAnimationTest,
stableSmallerTreeFewRepeatsFewStages_Overlapping_2029343357) {
testShadowNodeTreeLifeCycleLayoutAnimations(
/* seed */ 2029343357,
/* size */ 128,
/* repeats */ 128,
/* stages */ 10,
/* animation_duration */ 1000,
/* animation_frames*/ 9, // an animation completes in 10 frames, so this
// causes conflicts
/* delay_ms_between_frames */ 100,
/* delay_ms_between_stages */ 100,
/* delay_ms_between_repeats */ 2000,
/* commits_conflicting_mutations */ true,
/* final_animation_delay */ 10000 + 1);
}
TEST(
LayoutAnimationTest,
stableSmallerTreeFewRepeatsFewStages_Overlapping_597132284) {
testShadowNodeTreeLifeCycleLayoutAnimations(
/* seed */ 597132284,
/* size */ 128,
/* repeats */ 128,
/* stages */ 10,
/* animation_duration */ 1000,
/* animation_frames*/ 9, // an animation completes in 10 frames, so this
// causes conflicts
/* delay_ms_between_frames */ 100,
/* delay_ms_between_stages */ 100,
/* delay_ms_between_repeats */ 2000,
/* commits_conflicting_mutations */ true,
/* final_animation_delay */ 10000 + 1);
}
TEST(
LayoutAnimationTest,
stableSmallerTreeFewRepeatsFewStages_Overlapping_ManyConflicts_597132284) {
GTEST_SKIP();
testShadowNodeTreeLifeCycleLayoutAnimations(
/* seed */ 597132284,
/* size */ 128,
/* repeats */ 128,
/* stages */ 50,
/* animation_duration */ 1000,
/* animation_frames*/ 5, // an animation completes in 10 frames, so this
// causes conflicts. We only animate 5 frames,
// but have 50 stages, so conflicts stack up
// quickly.
/* delay_ms_between_frames */ 100,
/* delay_ms_between_stages */ 100,
/* delay_ms_between_repeats */ 2000,
/* commits_conflicting_mutations */ true,
/* final_animation_delay */ 50000 + 1);
}
TEST(
LayoutAnimationTest,
stableBiggerTreeFewRepeatsManyStages_Overlapping_ManyConflicts_2029343357) {
GTEST_SKIP();
testShadowNodeTreeLifeCycleLayoutAnimations(
/* seed */ 2029343357,
/* size */ 512,
/* repeats */ 32,
/* stages */ 128,
/* animation_duration */ 1000,
/* animation_frames*/ 10,
/* delay_ms_between_frames */ 10,
/* delay_ms_between_stages */ 10,
/* delay_ms_between_repeats */ 2000,
/* commits_conflicting_mutations */ true,
/* final_animation_delay */ (128 * 1000 + 100));
}
// You may uncomment this -
// locally only !-to generate failing seeds.
// TEST(
// LayoutAnimationTest,
// stableSmallerTreeFewRepeatsFewStages_Overlapping_Random) {
// std::random_device device;
// for (int i = 0; i < 10; i++) {
// uint_fast32_t seed = device();
// LOG(ERROR) << "Seed: " << seed;
// testShadowNodeTreeLifeCycleLayoutAnimations(
// /* seed */ seed,
// /* size */ 512,
// /* repeats */ 32,
// /* stages */ 128,
// /* animation_duration */ 1000,
// /* animation_frames*/ 10,
// /* delay_ms_between_frames */ 10,
// /* delay_ms_between_stages */ 10,
// /* delay_ms_between_repeats */ 2000,
// /* commits_conflicting_mutations */ true,
// /* final_animation_delay */ (128 * 1000 + 100));
// }
// // Fail if you want output to get seeds
// LOG(ERROR) << "ALL RUNS SUCCESSFUL";
// // react_native_assert(false);
// }

View File

@@ -0,0 +1,69 @@
/*
* 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 "utils.h"
#include <cmath>
namespace facebook::react {
std::pair<Float, Float> calculateAnimationProgress(
uint64_t now,
const LayoutAnimation& animation,
const AnimationConfig& mutationConfig) {
if (mutationConfig.animationType == AnimationType::None) {
return {1, 1};
}
uint64_t startTime = animation.startTime;
auto delay = (uint64_t)mutationConfig.delay;
uint64_t endTime = startTime + delay + (uint64_t)mutationConfig.duration;
if (now >= endTime) {
return {1, 1};
}
if (now < startTime + delay) {
return {0, 0};
}
double linearTimeProgression = 1 -
(double)(endTime - delay - now) / (double)(endTime - animation.startTime);
if (mutationConfig.animationType == AnimationType::Linear) {
return {linearTimeProgression, linearTimeProgression};
} else if (mutationConfig.animationType == AnimationType::EaseIn) {
// This is an accelerator-style interpolator.
// In the future, this parameter (2.0) could be adjusted. This has been the
// default for Classic RN forever.
return {linearTimeProgression, pow(linearTimeProgression, 2.0)};
} else if (mutationConfig.animationType == AnimationType::EaseOut) {
// This is an decelerator-style interpolator.
// In the future, this parameter (2.0) could be adjusted. This has been the
// default for Classic RN forever.
return {linearTimeProgression, 1.0 - pow(1 - linearTimeProgression, 2.0)};
} else if (mutationConfig.animationType == AnimationType::EaseInEaseOut) {
// This is a combination of accelerate+decelerate.
// The animation starts and ends slowly, and speeds up in the middle.
return {
linearTimeProgression,
cos((linearTimeProgression + 1.0) * M_PI) / 2 + 0.5};
} else if (mutationConfig.animationType == AnimationType::Spring) {
// Using mSpringDamping in this equation is not really the exact
// mathematical springDamping, but a good approximation We need to replace
// this equation with the right Factor that accounts for damping and
// friction
double damping = mutationConfig.springDamping;
return {
linearTimeProgression,
(1 +
pow(2, -10 * linearTimeProgression) *
sin((linearTimeProgression - damping / 4) * M_PI * 2 / damping))};
} else {
return {linearTimeProgression, linearTimeProgression};
}
}
} // namespace facebook::react

View File

@@ -0,0 +1,105 @@
/*
* 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/animations/primitives.h>
#include <react/renderer/graphics/Float.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
namespace facebook::react {
static inline bool shouldFirstComeBeforeSecondRemovesOnly(
const ShadowViewMutation &lhs,
const ShadowViewMutation &rhs) noexcept
{
// Make sure that removes on the same level are sorted - highest indices must
// come first.
return (lhs.type == ShadowViewMutation::Type::Remove && lhs.type == rhs.type) && (lhs.parentTag == rhs.parentTag) &&
(lhs.index > rhs.index);
}
static inline void handleShouldFirstComeBeforeSecondRemovesOnly(ShadowViewMutation::List &list) noexcept
{
std::unordered_map<std::string, std::vector<ShadowViewMutation>> removeMutationsByTag;
ShadowViewMutation::List finalList;
for (auto &mutation : list) {
if (mutation.type == ShadowViewMutation::Type::Remove) {
auto key = std::to_string(mutation.parentTag);
removeMutationsByTag[key].push_back(mutation);
} else {
finalList.push_back(mutation);
}
}
if (removeMutationsByTag.empty()) {
return;
}
for (auto &mutationsPair : removeMutationsByTag) {
if (mutationsPair.second.size() > 1) {
std::stable_sort(
mutationsPair.second.begin(), mutationsPair.second.end(), &shouldFirstComeBeforeSecondRemovesOnly);
}
finalList.insert(finalList.begin(), mutationsPair.second.begin(), mutationsPair.second.end());
}
list = finalList;
}
static inline bool shouldFirstComeBeforeSecondMutation(
const ShadowViewMutation &lhs,
const ShadowViewMutation &rhs) noexcept
{
if (lhs.type != rhs.type) {
// Deletes always come last
if (lhs.type == ShadowViewMutation::Type::Delete) {
return false;
}
if (rhs.type == ShadowViewMutation::Type::Delete) {
return true;
}
// Remove comes before insert
if (lhs.type == ShadowViewMutation::Type::Remove && rhs.type == ShadowViewMutation::Type::Insert) {
return true;
}
if (rhs.type == ShadowViewMutation::Type::Remove && lhs.type == ShadowViewMutation::Type::Insert) {
return false;
}
// Create comes before insert
if (lhs.type == ShadowViewMutation::Type::Create && rhs.type == ShadowViewMutation::Type::Insert) {
return true;
}
if (rhs.type == ShadowViewMutation::Type::Create && lhs.type == ShadowViewMutation::Type::Insert) {
return false;
}
// Remove comes before Update
if (lhs.type == ShadowViewMutation::Type::Remove && rhs.type == ShadowViewMutation::Type::Update) {
return true;
}
if (rhs.type == ShadowViewMutation::Type::Remove && lhs.type == ShadowViewMutation::Type::Update) {
return false;
}
} else {
// Make sure that removes on the same level are sorted - highest indices
// must come first.
if (lhs.type == ShadowViewMutation::Type::Remove && lhs.parentTag == rhs.parentTag) {
return lhs.index > rhs.index;
}
}
return &lhs < &rhs;
}
std::pair<Float, Float>
calculateAnimationProgress(uint64_t now, const LayoutAnimation &animation, const AnimationConfig &mutationConfig);
} // namespace facebook::react