diff --git a/React/Fabric/RCTScheduler.mm b/React/Fabric/RCTScheduler.mm index 25821a72e6..8c876c379f 100644 --- a/React/Fabric/RCTScheduler.mm +++ b/React/Fabric/RCTScheduler.mm @@ -70,7 +70,7 @@ class SchedulerDelegateProxy : public SchedulerDelegate { { if (self = [super init]) { _delegateProxy = std::make_shared((__bridge void *)self); - _scheduler = std::make_shared(toolbox, _delegateProxy.get()); + _scheduler = std::make_shared(toolbox, nullptr, _delegateProxy.get()); } return self; diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp index 95ea4b0e50..54830fe1ac 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp @@ -288,7 +288,7 @@ void Binding::installFabricUIManager( toolbox.runtimeExecutor = runtimeExecutor; toolbox.synchronousEventBeatFactory = synchronousBeatFactory; toolbox.asynchronousEventBeatFactory = asynchronousBeatFactory; - scheduler_ = std::make_shared(toolbox, this); + scheduler_ = std::make_shared(toolbox, nullptr, this); } void Binding::uninstallFabricUIManager() { diff --git a/ReactCommon/fabric/animations/BUCK b/ReactCommon/fabric/animations/BUCK new file mode 100644 index 0000000000..fde3bc7dec --- /dev/null +++ b/ReactCommon/fabric/animations/BUCK @@ -0,0 +1,95 @@ +load("@fbsource//tools/build_defs/apple:flag_defs.bzl", "get_preprocessor_flags_for_build_mode") +load( + "//tools/build_defs/oss:rn_defs.bzl", + "ANDROID", + "APPLE", + "CXX", + "fb_xplat_cxx_test", + "get_apple_compiler_flags", + "get_apple_inspector_flags", + "react_native_xplat_target", + "rn_xplat_cxx_library", + "subdir_glob", +) + +APPLE_COMPILER_FLAGS = get_apple_compiler_flags() + +rn_xplat_cxx_library( + name = "animations", + srcs = glob( + ["**/*.cpp"], + exclude = glob(["tests/**/*.cpp"]), + ), + headers = glob( + ["**/*.h"], + exclude = glob(["tests/**/*.h"]), + ), + header_namespace = "", + exported_headers = subdir_glob( + [ + ("", "*.h"), + ], + prefix = "react/animations", + ), + compiler_flags = [ + "-fexceptions", + "-frtti", + "-std=c++14", + "-Wall", + ], + fbobjc_compiler_flags = APPLE_COMPILER_FLAGS, + fbobjc_preprocessor_flags = get_preprocessor_flags_for_build_mode() + get_apple_inspector_flags(), + force_static = True, + labels = ["supermodule:xplat/default/public.react_native.infra"], + macosx_tests_override = [], + platforms = (ANDROID, APPLE, CXX), + preprocessor_flags = [ + "-DLOG_TAG=\"ReactNative\"", + "-DWITH_FBSYSTRACE=1", + ], + tests = [":tests"], + visibility = ["PUBLIC"], + deps = [ + "//third-party/glog:glog", + "//xplat/fbsystrace:fbsystrace", + "//xplat/folly:headers_only", + "//xplat/folly:memory", + "//xplat/folly:molly", + "//xplat/jsi:JSIDynamic", + "//xplat/jsi:jsi", + react_native_xplat_target("config:config"), + react_native_xplat_target("fabric/componentregistry:componentregistry"), + react_native_xplat_target("fabric/components/view:view"), + react_native_xplat_target("fabric/core:core"), + react_native_xplat_target("fabric/debug:debug"), + react_native_xplat_target("fabric/mounting:mounting"), + react_native_xplat_target("fabric/uimanager:uimanager"), + react_native_xplat_target("runtimeexecutor:runtimeexecutor"), + ], +) + +fb_xplat_cxx_test( + name = "tests", + srcs = glob(["tests/**/*.cpp"]), + headers = glob(["tests/**/*.h"]), + compiler_flags = [ + "-fexceptions", + "-frtti", + "-std=c++14", + "-Wall", + ], + contacts = ["oncall+react_native@xmail.facebook.com"], + platforms = (ANDROID, APPLE, CXX), + deps = [ + ":animations", + "//xplat/folly:molly", + "//xplat/third-party/gmock:gtest", + react_native_xplat_target("config:config"), + react_native_xplat_target("fabric/components/activityindicator:activityindicator"), + react_native_xplat_target("fabric/components/image:image"), + react_native_xplat_target("fabric/components/root:root"), + react_native_xplat_target("fabric/components/scrollview:scrollview"), + react_native_xplat_target("fabric/components/view:view"), + "//xplat/js/react-native-github:generated_components-rncore", + ], +) diff --git a/ReactCommon/fabric/animations/LayoutAnimationDriver.cpp b/ReactCommon/fabric/animations/LayoutAnimationDriver.cpp new file mode 100644 index 0000000000..a1e1da9564 --- /dev/null +++ b/ReactCommon/fabric/animations/LayoutAnimationDriver.cpp @@ -0,0 +1,197 @@ +/* + * Copyright (c) Facebook, Inc. and its 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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace facebook { +namespace react { + +static double +getProgressFromValues(double start, double end, double currentValue) { + auto opacityMinmax = std::minmax({start, end}); + auto min = opacityMinmax.first; + auto max = opacityMinmax.second; + return ( + currentValue < min + ? 0 + : (currentValue > max ? 0 : ((max - currentValue) / (max - min)))); +} + +/** + * Given an animation and a ShadowView with properties set on it, detect how + * far through the animation the ShadowView has progressed. + * + * @param mutationsList + * @param now + */ +double LayoutAnimationDriver::getProgressThroughAnimation( + AnimationKeyFrame const &keyFrame, + LayoutAnimation const *layoutAnimation, + ShadowView const &animationStateView) const { + auto layoutAnimationConfig = layoutAnimation->layoutAnimationConfig; + auto const mutationConfig = + *(keyFrame.type == AnimationConfigurationType::Delete + ? layoutAnimationConfig.deleteConfig + : (keyFrame.type == AnimationConfigurationType::Create + ? layoutAnimationConfig.createConfig + : layoutAnimationConfig.updateConfig)); + + auto initialProps = keyFrame.viewStart.props; + auto finalProps = keyFrame.viewEnd.props; + + if (mutationConfig.animationProperty == AnimationProperty::Opacity) { + // Detect progress through opacity animation. + const auto &oldViewProps = + dynamic_cast(initialProps.get()); + const auto &newViewProps = + dynamic_cast(finalProps.get()); + const auto &animationStateViewProps = + dynamic_cast(animationStateView.props.get()); + if (oldViewProps != nullptr && newViewProps != nullptr && + animationStateViewProps != nullptr) { + return getProgressFromValues( + oldViewProps->opacity, + newViewProps->opacity, + animationStateViewProps->opacity); + } + } else if ( + mutationConfig.animationProperty != AnimationProperty::NotApplicable) { + // Detect progress through layout animation. + LayoutMetrics const &finalLayoutMetrics = keyFrame.viewEnd.layoutMetrics; + LayoutMetrics const &baselineLayoutMetrics = + keyFrame.viewStart.layoutMetrics; + LayoutMetrics const &animationStateLayoutMetrics = + animationStateView.layoutMetrics; + + if (baselineLayoutMetrics.frame.size.height != + finalLayoutMetrics.frame.size.height) { + return getProgressFromValues( + baselineLayoutMetrics.frame.size.height, + finalLayoutMetrics.frame.size.height, + animationStateLayoutMetrics.frame.size.height); + } + if (baselineLayoutMetrics.frame.size.width != + finalLayoutMetrics.frame.size.width) { + return getProgressFromValues( + baselineLayoutMetrics.frame.size.width, + finalLayoutMetrics.frame.size.width, + animationStateLayoutMetrics.frame.size.width); + } + if (baselineLayoutMetrics.frame.origin.x != + finalLayoutMetrics.frame.origin.x) { + return getProgressFromValues( + baselineLayoutMetrics.frame.origin.x, + finalLayoutMetrics.frame.origin.x, + animationStateLayoutMetrics.frame.origin.x); + } + if (baselineLayoutMetrics.frame.origin.y != + finalLayoutMetrics.frame.origin.y) { + return getProgressFromValues( + baselineLayoutMetrics.frame.origin.y, + finalLayoutMetrics.frame.origin.y, + animationStateLayoutMetrics.frame.origin.y); + } + } + + return 0; +} + +void LayoutAnimationDriver::animationMutationsForFrame( + SurfaceId surfaceId, + ShadowViewMutation::List &mutationsList, + uint64_t now) const { + for (auto &animation : inflightAnimations_) { + if (animation.surfaceId != surfaceId) { + continue; + } + + int incompleteAnimations = 0; + for (const auto &keyframe : animation.keyFrames) { + if (keyframe.type == AnimationConfigurationType::Noop) { + continue; + } + + auto const &baselineShadowView = keyframe.viewStart; + auto const &finalShadowView = keyframe.viewEnd; + + // The contract with the "keyframes generation" phase is that any animated + // node will have a valid configuration. + auto const layoutAnimationConfig = animation.layoutAnimationConfig; + auto const mutationConfig = + (keyframe.type == AnimationConfigurationType::Delete + ? layoutAnimationConfig.deleteConfig + : (keyframe.type == AnimationConfigurationType::Create + ? layoutAnimationConfig.createConfig + : layoutAnimationConfig.updateConfig)); + + // Interpolate + std::pair progress = + calculateAnimationProgress(now, animation, *mutationConfig); + double animationTimeProgressLinear = progress.first; + double animationInterpolationFactor = progress.second; + + auto mutatedShadowView = createInterpolatedShadowView( + animationInterpolationFactor, + *mutationConfig, + baselineShadowView, + finalShadowView); + + // Create the mutation instruction + mutationsList.push_back(ShadowViewMutation::UpdateMutation( + keyframe.parentView, baselineShadowView, mutatedShadowView, -1)); + + 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) { + // Queue up "final" mutations for all keyframes in the completed animation + for (auto const &keyframe : animation.keyFrames) { + if (keyframe.finalMutationForKeyFrame.hasValue()) { + mutationsList.push_back(*keyframe.finalMutationForKeyFrame); + } + } + + it = inflightAnimations_.erase(it); + } else { + it++; + } + } +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/animations/LayoutAnimationDriver.h b/ReactCommon/fabric/animations/LayoutAnimationDriver.h new file mode 100644 index 0000000000..2a22157752 --- /dev/null +++ b/ReactCommon/fabric/animations/LayoutAnimationDriver.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include "LayoutAnimationKeyFrameManager.h" + +namespace facebook { +namespace react { + +class LayoutAnimationDriver : public LayoutAnimationKeyFrameManager { + public: + virtual ~LayoutAnimationDriver() {} + + protected: + virtual void animationMutationsForFrame( + SurfaceId surfaceId, + ShadowViewMutation::List &mutationsList, + uint64_t now) const override; + virtual double getProgressThroughAnimation( + AnimationKeyFrame const &keyFrame, + LayoutAnimation const *layoutAnimation, + ShadowView const &animationStateView) const override; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/animations/LayoutAnimationKeyFrameManager.cpp b/ReactCommon/fabric/animations/LayoutAnimationKeyFrameManager.cpp new file mode 100644 index 0000000000..4f617bf71f --- /dev/null +++ b/ReactCommon/fabric/animations/LayoutAnimationKeyFrameManager.cpp @@ -0,0 +1,847 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "LayoutAnimationKeyFrameManager.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace facebook { +namespace react { + +static better::optional parseAnimationType(std::string param) { + if (param == "spring") { + return better::optional(AnimationType::Spring); + } + if (param == "linear") { + return better::optional(AnimationType::Linear); + } + if (param == "easeInEaseOut") { + return better::optional(AnimationType::EaseInEaseOut); + } + if (param == "easeIn") { + return better::optional(AnimationType::EaseIn); + } + if (param == "easeOut") { + return better::optional(AnimationType::EaseOut); + } + if (param == "keyboard") { + return better::optional(AnimationType::Keyboard); + } + + return {}; +} + +static better::optional parseAnimationProperty( + std::string param) { + if (param == "opacity") { + return better::optional(AnimationProperty::Opacity); + } + if (param == "scaleX") { + return better::optional(AnimationProperty::ScaleX); + } + if (param == "scaleY") { + return better::optional(AnimationProperty::ScaleY); + } + if (param == "scaleXY") { + return better::optional(AnimationProperty::ScaleXY); + } + + return {}; +} + +static better::optional parseAnimationConfig( + folly::dynamic const &config, + double defaultDuration) { + if (config.empty() || !config.isObject()) { + return better::optional( + AnimationConfig{AnimationType::Linear, + AnimationProperty::NotApplicable, + defaultDuration, + 0, + 0, + 0}); + } + + folly::dynamic const &animationTypeParam = config["type"]; + if (animationTypeParam.empty() || !animationTypeParam.isString()) { + return {}; + } + const auto animationType = parseAnimationType(animationTypeParam.asString()); + if (!animationType) { + return {}; + } + + folly::dynamic const &animationPropertyParam = config["property"]; + if (animationPropertyParam.empty() || !animationPropertyParam.isString()) { + return {}; + } + const auto animationProperty = + parseAnimationProperty(animationPropertyParam.asString()); + if (!animationProperty) { + return {}; + } + + double duration = defaultDuration; + double delay = 0; + double springDamping = 0; + double initialVelocity = 0; + + auto const durationIt = config.find("duration"); + if (durationIt != config.items().end()) { + if (durationIt->second.isDouble()) { + duration = durationIt->second.asDouble(); + } else { + return {}; + } + } + + auto const delayIt = config.find("delay"); + if (delayIt != config.items().end()) { + if (delayIt->second.isDouble()) { + delay = delayIt->second.asDouble(); + } else { + return {}; + } + } + + auto const springDampingIt = config.find("springDamping"); + if (springDampingIt != config.items().end() && + springDampingIt->second.isDouble()) { + if (springDampingIt->second.isDouble()) { + springDamping = springDampingIt->second.asDouble(); + } else { + return {}; + } + } + + auto const initialVelocityIt = config.find("initialVelocity"); + if (initialVelocityIt != config.items().end()) { + if (initialVelocityIt->second.isDouble()) { + initialVelocity = initialVelocityIt->second.asDouble(); + } else { + return {}; + } + } + + return better::optional(AnimationConfig{*animationType, + *animationProperty, + duration, + delay, + springDamping, + initialVelocity}); +} + +// Parse animation config from JS +static better::optional parseLayoutAnimationConfig( + folly::dynamic const &config) { + if (config.empty() || !config.isObject()) { + return {}; + } + + auto const durationIt = config.find("duration"); + if (durationIt == config.items().end() || !durationIt->second.isDouble()) { + return {}; + } + const double duration = durationIt->second.asDouble(); + + const auto createConfig = parseAnimationConfig(config["create"], duration); + if (!createConfig) { + return {}; + } + + const auto updateConfig = parseAnimationConfig(config["update"], duration); + if (!updateConfig) { + return {}; + } + + const auto deleteConfig = parseAnimationConfig(config["delete"], duration); + if (!deleteConfig) { + return {}; + } + + return better::optional(LayoutAnimationConfig{ + duration, *createConfig, *updateConfig, *deleteConfig}); +} + +/** + * Globally configure next LayoutAnimation. + */ +void LayoutAnimationKeyFrameManager::uiManagerDidConfigureNextLayoutAnimation( + RawValue const &config, + std::shared_ptr successCallback, + std::shared_ptr errorCallback) const { + auto layoutAnimationConfig = + parseLayoutAnimationConfig((folly::dynamic)config); + + if (layoutAnimationConfig) { + std::lock_guard lock(currentAnimationMutex_); + currentAnimation_ = better::optional{ + LayoutAnimation{-1, + 0, + false, + *layoutAnimationConfig, + successCallback, + errorCallback, + {}}}; + } else { + // TODO: call errorCallback + LOG(ERROR) << "Parsing LayoutAnimationConfig failed: " + << (folly::dynamic)config; + } +} + +bool LayoutAnimationKeyFrameManager::shouldOverridePullTransaction() const { + return shouldAnimateFrame(); +} + +bool LayoutAnimationKeyFrameManager::shouldAnimateFrame() const { + // There is potentially a race here between getting and setting + // `currentMutation_`. We don't want to lock around this because then we're + // creating contention between pullTransaction and the JS thread. + return currentAnimation_ || !inflightAnimations_.empty(); +} + +static inline const float +interpolateFloats(float coefficient, float oldValue, float newValue) { + return oldValue + (newValue - oldValue) * coefficient; +} + +std::pair +LayoutAnimationKeyFrameManager::calculateAnimationProgress( + uint64_t now, + const LayoutAnimation &animation, + const AnimationConfig &mutationConfig) const { + uint64_t startTime = animation.startTime; + uint64_t delay = mutationConfig.delay; + uint64_t endTime = startTime + delay + mutationConfig.duration; + double progress = (now >= endTime) + ? 1 + : ((now < startTime + delay) ? 0 + : 1 - + (double)(endTime - delay - now) / + (double)(endTime - animation.startTime)); + return {progress, progress}; +} + +void LayoutAnimationKeyFrameManager::adjustDelayedMutationIndicesForMutation( + SurfaceId surfaceId, + ShadowViewMutation const &mutation) const { + bool isRemoveMutation = mutation.type == ShadowViewMutation::Type::Remove; + bool isInsertMutation = mutation.type == ShadowViewMutation::Type::Insert; + assert(isRemoveMutation || isInsertMutation); + + for (auto &inflightAnimation : inflightAnimations_) { + if (inflightAnimation.surfaceId != surfaceId) { + continue; + } + + for (auto it = inflightAnimation.keyFrames.begin(); + it != inflightAnimation.keyFrames.end(); + it++) { + auto &animatedKeyFrame = *it; + + // Detect if they're in the same view hierarchy, but not equivalent + // (We've already detected direct conflicts and handled them above) + if (animatedKeyFrame.parentView.tag != mutation.parentShadowView.tag) { + continue; + } + + if (animatedKeyFrame.type != AnimationConfigurationType::Noop) { + continue; + } + if (!animatedKeyFrame.finalMutationForKeyFrame.has_value()) { + continue; + } + ShadowViewMutation &finalAnimationMutation = + *animatedKeyFrame.finalMutationForKeyFrame; + + if (finalAnimationMutation.type != ShadowViewMutation::Type::Remove) { + continue; + } + + // Do we need to adjust the index of this operation? + if (isRemoveMutation && mutation.index <= finalAnimationMutation.index) { + finalAnimationMutation.index--; + } else if ( + isInsertMutation && mutation.index <= finalAnimationMutation.index) { + finalAnimationMutation.index++; + } + } + } +} + +better::optional +LayoutAnimationKeyFrameManager::pullTransaction( + SurfaceId surfaceId, + MountingTransaction::Number transactionNumber, + MountingTelemetry const &telemetry, + ShadowViewMutationList mutations) const { + // Current time in milliseconds + uint64_t now = + std::chrono::duration_cast( + std::chrono::high_resolution_clock::now().time_since_epoch()) + .count(); + + if (!mutations.empty()) { +#ifdef RN_SHADOW_TREE_INTROSPECTION + { + std::stringstream ss(getDebugDescription(mutations, {})); + std::string to; + while (std::getline(ss, to, '\n')) { + LOG(ERROR) + << "LayoutAnimationKeyFrameManager.cpp: got mutation list: Line: " + << to; + } + }; +#endif + + // What to do if we detect a conflict? Get current value and make + // that the baseline of the next animation. Scale the remaining time + // in the animation + // Types of conflicts and how we handle them: + // Update -> update: remove the previous update, make it the baseline of the + // next update (with current progress) Update -> remove: same, with final + // mutation being a remove Insert -> update: treat as update->update Insert + // -> remove: same, as update->remove Remove -> update/insert: not possible + // We just collect pairs here of and delete them + // from active animations. If another animation is queued up from the + // current mutations then these deleted mutations will serve as the baseline + // for the next animation. If not, the current mutations are executed + // immediately without issues. + std::vector< + std::tuple> + conflictingAnimations{}; + for (auto &mutation : mutations) { + auto const &baselineShadowView = + (mutation.type == ShadowViewMutation::Type::Insert) + ? mutation.newChildShadowView + : mutation.oldChildShadowView; + + for (auto &inflightAnimation : inflightAnimations_) { + if (inflightAnimation.surfaceId != surfaceId) { + continue; + } + + for (auto it = inflightAnimation.keyFrames.begin(); + it != inflightAnimation.keyFrames.end();) { + auto &animatedKeyFrame = *it; + + // Conflicting animation detected + if (animatedKeyFrame.tag == baselineShadowView.tag) { + auto const layoutAnimationConfig = + inflightAnimation.layoutAnimationConfig; + + auto const mutationConfig = + (animatedKeyFrame.type == AnimationConfigurationType::Delete + ? layoutAnimationConfig.deleteConfig + : (animatedKeyFrame.type == + AnimationConfigurationType::Create + ? layoutAnimationConfig.createConfig + : layoutAnimationConfig.updateConfig)); + + conflictingAnimations.push_back(std::make_tuple( + animatedKeyFrame, *mutationConfig, &inflightAnimation)); + + // Delete from existing animation + it = inflightAnimation.keyFrames.erase(it); + } else { + it++; + } + } + } + } + + // Are we animating this list of mutations? + better::optional currentAnimation{}; + { + std::lock_guard lock(currentAnimationMutex_); + if (currentAnimation_) { + currentAnimation = currentAnimation_; + currentAnimation_ = {}; + } + } + + if (currentAnimation) { + LayoutAnimation animation = currentAnimation.value(); + animation.surfaceId = surfaceId; + animation.startTime = now; + + // Pre-process list to: + // Catch remove+reinsert (reorders) + // Catch delete+create (reparenting) (this should be optimized away at + // the diffing level eventually?) + // TODO: to prevent this step we could tag Remove/Insert mutations as + // being moves on the Differ level, since we know that there? We could use + // TinyMap here, but it's not exposed by Differentiator (yet). + std::vector insertedTags; + std::vector createdTags; + std::unordered_map movedTags; + std::vector reparentedTags; + for (const auto &mutation : mutations) { + if (mutation.type == ShadowViewMutation::Type::Insert) { + insertedTags.push_back(mutation.newChildShadowView.tag); + } + if (mutation.type == ShadowViewMutation::Type::Create) { + createdTags.push_back(mutation.newChildShadowView.tag); + } + } + + // Process mutations list into operations that can be sent to platform + // immediately, and those that need to be animated Deletions, removals, + // updates are delayed and animated. Creations and insertions are sent to + // platform and then "animated in" with opacity updates. Upon completion, + // removals and deletions are sent to platform + ShadowViewMutation::List immediateMutations; + + // Remove operations that are actually moves should be copied to + // "immediate mutations". The corresponding "insert" will also be executed + // immediately and animated as an update. + std::vector keyFramesToAnimate; + std::vector movesToAnimate; + auto const layoutAnimationConfig = animation.layoutAnimationConfig; + for (auto &mutation : mutations) { + ShadowView baselineShadowView = + (mutation.type == ShadowViewMutation::Type::Delete || + mutation.type == ShadowViewMutation::Type::Remove + ? mutation.oldChildShadowView + : mutation.newChildShadowView); + auto const &componentDescriptor = + getComponentDescriptorForShadowView(baselineShadowView); + + auto mutationConfig = + (mutation.type == ShadowViewMutation::Type::Delete + ? layoutAnimationConfig.deleteConfig + : (mutation.type == ShadowViewMutation::Type::Insert + ? layoutAnimationConfig.createConfig + : layoutAnimationConfig.updateConfig)); + + bool isRemoveReinserted = + mutation.type == ShadowViewMutation::Type::Remove && + std::find( + insertedTags.begin(), + insertedTags.end(), + mutation.oldChildShadowView.tag) != insertedTags.end(); + + // Reparenting can result in a node being removed, inserted (moved) and + // also deleted and created in the same frame, with the same props etc. + // This should eventually be optimized out of the diffing algorithm, but + // for now we detect reparenting and prevent the corresponding + // Delete/Create instructions from being animated. + bool isReparented = + (mutation.type == ShadowViewMutation::Delete && + std::find( + createdTags.begin(), + createdTags.end(), + mutation.oldChildShadowView.tag) != createdTags.end()) || + (mutation.type == ShadowViewMutation::Create && + std::find( + reparentedTags.begin(), + reparentedTags.end(), + mutation.newChildShadowView.tag) != reparentedTags.end()); + + if (isRemoveReinserted) { + movedTags.insert({mutation.oldChildShadowView.tag, mutation}); + } + + if (isReparented && mutation.type == ShadowViewMutation::Delete) { + reparentedTags.push_back(mutation.oldChildShadowView.tag); + } + + // Inserts that follow a "remove" of the same tag should be treated as + // an update (move) animation. + bool wasInsertedTagRemoved = false; + bool haveConfiguration = mutationConfig.has_value(); + if (mutation.type == ShadowViewMutation::Type::Insert) { + // If this is a move, we actually don't want to copy this insert + // instruction to animated instructions - we want to + // generate an Update mutation for Remove+Insert pairs to animate + // the layout. + // The corresponding Remove and Insert instructions will instead + // be treated as "immediate" instructions. + auto movedIt = movedTags.find(mutation.newChildShadowView.tag); + wasInsertedTagRemoved = movedIt != movedTags.end(); + if (wasInsertedTagRemoved) { + mutationConfig = layoutAnimationConfig.updateConfig; + } + haveConfiguration = mutationConfig.has_value(); + + if (wasInsertedTagRemoved && haveConfiguration) { + movesToAnimate.push_back( + AnimationKeyFrame{{}, + AnimationConfigurationType::Update, + mutation.newChildShadowView.tag, + mutation.parentShadowView, + movedIt->second.oldChildShadowView, + mutation.newChildShadowView}); + } + } + + // Creates and inserts should also be executed immediately. + // Mutations that would otherwise be animated, but have no + // configuration, are also executed immediately. + if (isRemoveReinserted || !haveConfiguration || isReparented || + mutation.type == ShadowViewMutation::Type::Create || + mutation.type == ShadowViewMutation::Type::Insert) { + immediateMutations.push_back(mutation); + + // Adjust indices for any non-directly-conflicting animations that + // affect the same parent view by inserting or removing anything + // from the hierarchy. + if (mutation.type == ShadowViewMutation::Type::Insert || + mutation.type == ShadowViewMutation::Type::Remove) { + adjustDelayedMutationIndicesForMutation(surfaceId, mutation); + } + } + + // Deletes, non-move inserts, updates get animated + if (!wasInsertedTagRemoved && !isRemoveReinserted && !isReparented && + haveConfiguration && + mutation.type != ShadowViewMutation::Type::Create) { + ShadowView viewStart = ShadowView( + mutation.type == ShadowViewMutation::Type::Insert + ? mutation.newChildShadowView + : mutation.oldChildShadowView); + ShadowView viewFinal = ShadowView( + mutation.type == ShadowViewMutation::Type::Update + ? mutation.newChildShadowView + : viewStart); + ShadowView parent = mutation.parentShadowView; + Tag tag = viewStart.tag; + Tag parentTag = mutation.parentShadowView.tag; + + AnimationKeyFrame keyFrame{}; + if (mutation.type == ShadowViewMutation::Type::Insert) { + if (mutationConfig->animationProperty == + AnimationProperty::Opacity) { + auto props = componentDescriptor.cloneProps(viewStart.props, {}); + const auto viewProps = + dynamic_cast(props.get()); + if (viewProps != nullptr) { + const_cast(viewProps)->opacity = 0; + } + viewStart.props = props; + } + bool isScaleX = mutationConfig->animationProperty == + AnimationProperty::ScaleX || + mutationConfig->animationProperty == AnimationProperty::ScaleXY; + bool isScaleY = mutationConfig->animationProperty == + AnimationProperty::ScaleY || + mutationConfig->animationProperty == AnimationProperty::ScaleXY; + if (isScaleX || isScaleY) { + auto props = componentDescriptor.cloneProps(viewStart.props, {}); + const auto viewProps = + dynamic_cast(props.get()); + if (viewProps != nullptr) { + const_cast(viewProps)->transform = + Transform::Scale(isScaleX ? 0 : 1, isScaleY ? 0 : 1, 1); + } + viewStart.props = props; + } + + keyFrame = AnimationKeyFrame{{}, + AnimationConfigurationType::Create, + tag, + parent, + viewStart, + viewFinal, + 0}; + } else if (mutation.type == ShadowViewMutation::Type::Delete) { + if (mutationConfig->animationProperty == + AnimationProperty::Opacity) { + auto props = componentDescriptor.cloneProps(viewFinal.props, {}); + const auto viewProps = + dynamic_cast(props.get()); + if (viewProps != nullptr) { + const_cast(viewProps)->opacity = 0; + } + viewFinal.props = props; + } + bool isScaleX = mutationConfig->animationProperty == + AnimationProperty::ScaleX || + mutationConfig->animationProperty == AnimationProperty::ScaleXY; + bool isScaleY = mutationConfig->animationProperty == + AnimationProperty::ScaleY || + mutationConfig->animationProperty == AnimationProperty::ScaleXY; + if (isScaleX || isScaleY) { + auto props = componentDescriptor.cloneProps(viewFinal.props, {}); + const auto viewProps = + dynamic_cast(props.get()); + if (viewProps != nullptr) { + const_cast(viewProps)->transform = + Transform::Scale(isScaleX ? 0 : 1, isScaleY ? 0 : 1, 1); + } + viewFinal.props = props; + } + + keyFrame = AnimationKeyFrame{ + better::optional(mutation), + AnimationConfigurationType::Delete, + tag, + parent, + viewStart, + viewFinal, + 0}; + } else if (mutation.type == ShadowViewMutation::Type::Update) { + viewFinal = ShadowView(mutation.newChildShadowView); + + keyFrame = AnimationKeyFrame{ + better::optional(mutation), + AnimationConfigurationType::Update, + tag, + parent, + viewStart, + viewFinal, + 0}; + } else { + // This should just be "Remove" instructions that are not animated + // (either this is a "move", or there's a corresponding "Delete" + // that is animated). We configure it as a Noop animation so it is + // executed when all the other animations are completed. + assert(mutation.type == ShadowViewMutation::Type::Remove); + + // For remove instructions: since the execution of the Remove + // instruction will be delayed and therefore may execute outside of + // otherwise-expected order, other views may be inserted before the + // Remove is executed, requiring index adjustment. + { + int adjustedIndex = mutation.index; + for (const auto &otherMutation : mutations) { + if (otherMutation.type == ShadowViewMutation::Type::Insert && + otherMutation.parentShadowView.tag == parentTag) { + if (otherMutation.index <= adjustedIndex) { + adjustedIndex++; + } + } + } + + mutation = ShadowViewMutation::RemoveMutation( + mutation.parentShadowView, + mutation.oldChildShadowView, + adjustedIndex); + } + + keyFrame = AnimationKeyFrame{ + better::optional(mutation), + AnimationConfigurationType::Noop, + tag, + parent, + {}, + {}, + 0}; + } + + // Handle conflicting animations + for (auto &conflictingKeyframeTuple : conflictingAnimations) { + auto &conflictingKeyFrame = std::get<0>(conflictingKeyframeTuple); + auto const &conflictingMutationBaselineShadowView = + conflictingKeyFrame.viewStart; + + // We've found a conflict. + if (conflictingMutationBaselineShadowView.tag == tag) { + // What's the progress of this ongoing animation? + double conflictingAnimationProgress = + calculateAnimationProgress( + now, + *std::get<2>(conflictingKeyframeTuple), + std::get<1>(conflictingKeyframeTuple)) + .first; + + // Get a baseline ShadowView at the current progress of the + // inflight animation. TODO: handle multiple properties being + // animated separately? + auto interpolatedInflightShadowView = + createInterpolatedShadowView( + conflictingAnimationProgress, + std::get<1>(conflictingKeyframeTuple), + conflictingKeyFrame.viewStart, + conflictingKeyFrame.viewEnd); + + // Pick a Prop or layout property, depending on the current + // animation configuration. Figure out how much progress we've + // already made in the current animation, and start the animation + // from this point. + keyFrame.viewStart = interpolatedInflightShadowView; + keyFrame.initialProgress = getProgressThroughAnimation( + keyFrame, &animation, interpolatedInflightShadowView); + + // We're guaranteed that a tag only has one animation associated + // with it, so we can break here. If we support multiple + // animations and animation curves over the same tag in the + // future, this will need to be modified to support that. + break; + } + } + + keyFramesToAnimate.push_back(keyFrame); + } + } + +#ifdef RN_SHADOW_TREE_INTROSPECTION + { + std::stringstream ss(getDebugDescription(immediateMutations, {})); + std::string to; + while (std::getline(ss, to, '\n')) { + LOG(ERROR) + << "LayoutAnimationKeyFrameManager.cpp: got IMMEDIATE list: Line: " + << to; + } + } + + { + std::stringstream ss(getDebugDescription(mutationsToAnimate, {})); + std::string to; + while (std::getline(ss, to, '\n')) { + LOG(ERROR) + << "LayoutAnimationKeyFrameManager.cpp: got FINAL list: Line: " + << to; + } + } +#endif + + animation.keyFrames = keyFramesToAnimate; + inflightAnimations_.push_back(animation); + + // These will be executed immediately. + mutations = immediateMutations; + } /* if (currentAnimation) */ else { + // If there's no "next" animation, make sure we queue up "final" + // operations from all ongoing animations. + ShadowViewMutationList finalMutationsForConflictingAnimations{}; + for (auto &conflictingKeyframeTuple : conflictingAnimations) { + auto &keyFrame = std::get<0>(conflictingKeyframeTuple); + if (keyFrame.finalMutationForKeyFrame.hasValue()) { + finalMutationsForConflictingAnimations.push_back( + *keyFrame.finalMutationForKeyFrame); + } + } + + // Append mutations to this list and swap - so that the final + // conflicting mutations happen before any other mutations + finalMutationsForConflictingAnimations.insert( + finalMutationsForConflictingAnimations.end(), + mutations.begin(), + mutations.end()); + mutations = finalMutationsForConflictingAnimations; + + // Adjust pending mutation indices base on these operations + for (auto &mutation : mutations) { + if (mutation.type == ShadowViewMutation::Type::Insert || + mutation.type == ShadowViewMutation::Type::Remove) { + adjustDelayedMutationIndicesForMutation(surfaceId, mutation); + } + } + } + } // if (mutations) + + // We never commit a different root or modify anything - + // we just send additional mutations to the mounting layer until the + // animations are finished and the mounting layer (view) represents exactly + // what is in the most recent shadow tree + // Add animation mutations to the end of our existing mutations list in this + // function. + ShadowViewMutationList mutationsForAnimation{}; + animationMutationsForFrame(surfaceId, mutationsForAnimation, now); + + // Adjust pending mutation indices base on these operations + // For example: if a final "remove" mutation has been performed, and there is + // another that has not yet been executed because it is a part of an ongoing + // animation, its index may need to be adjusted. + for (auto const &animatedMutation : mutationsForAnimation) { + if (animatedMutation.type == ShadowViewMutation::Type::Insert || + animatedMutation.type == ShadowViewMutation::Type::Remove) { + adjustDelayedMutationIndicesForMutation(surfaceId, animatedMutation); + } + } + + mutations.insert( + mutations.end(), + mutationsForAnimation.begin(), + mutationsForAnimation.end()); + + // TODO: fill in telemetry + return MountingTransaction{ + surfaceId, transactionNumber, std::move(mutations), {}}; +} + +ComponentDescriptor const & +LayoutAnimationKeyFrameManager::getComponentDescriptorForShadowView( + ShadowView const &shadowView) const { + return componentDescriptorRegistry_->at(shadowView.componentHandle); +} + +void LayoutAnimationKeyFrameManager::setComponentDescriptorRegistry( + const SharedComponentDescriptorRegistry &componentDescriptorRegistry) { + componentDescriptorRegistry_ = componentDescriptorRegistry; +} + +/** + * Given a `progress` between 0 and 1, a mutation and LayoutAnimation config, + * return a ShadowView with mutated props and/or LayoutMetrics. + * + * @param progress + * @param layoutAnimation + * @param animatedMutation + * @return + */ +ShadowView LayoutAnimationKeyFrameManager::createInterpolatedShadowView( + double progress, + AnimationConfig const &animationConfig, + ShadowView startingView, + ShadowView finalView) const { + ComponentDescriptor const &componentDescriptor = + getComponentDescriptorForShadowView(startingView); + auto mutatedShadowView = ShadowView(startingView); + + // Animate opacity or scale/transform + mutatedShadowView.props = componentDescriptor.interpolateProps( + progress, startingView.props, finalView.props); + + // Interpolate LayoutMetrics + LayoutMetrics const &finalLayoutMetrics = finalView.layoutMetrics; + LayoutMetrics const &baselineLayoutMetrics = startingView.layoutMetrics; + LayoutMetrics interpolatedLayoutMetrics = finalLayoutMetrics; + interpolatedLayoutMetrics.frame.origin.x = interpolateFloats( + progress, + baselineLayoutMetrics.frame.origin.x, + finalLayoutMetrics.frame.origin.x); + interpolatedLayoutMetrics.frame.origin.y = interpolateFloats( + progress, + baselineLayoutMetrics.frame.origin.y, + finalLayoutMetrics.frame.origin.y); + interpolatedLayoutMetrics.frame.size.width = interpolateFloats( + progress, + baselineLayoutMetrics.frame.size.width, + finalLayoutMetrics.frame.size.width); + interpolatedLayoutMetrics.frame.size.height = interpolateFloats( + progress, + baselineLayoutMetrics.frame.size.height, + finalLayoutMetrics.frame.size.height); + mutatedShadowView.layoutMetrics = interpolatedLayoutMetrics; + + return mutatedShadowView; +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/animations/LayoutAnimationKeyFrameManager.h b/ReactCommon/fabric/animations/LayoutAnimationKeyFrameManager.h new file mode 100644 index 0000000000..f286bd27a3 --- /dev/null +++ b/ReactCommon/fabric/animations/LayoutAnimationKeyFrameManager.h @@ -0,0 +1,164 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook { +namespace react { + +// This corresponds exactly with JS. +enum class AnimationType { + Spring, + Linear, + EaseInEaseOut, + EaseIn, + EaseOut, + Keyboard +}; +enum class AnimationProperty { + NotApplicable, + Opacity, + ScaleX, + ScaleY, + ScaleXY +}; +enum class AnimationConfigurationType { + Noop, // for animation placeholders that are not animated, and should be + // executed once other animations have completed + Create, + Update, + Delete +}; + +// This corresponds exactly with JS. +struct AnimationConfig { + AnimationType animationType; + AnimationProperty animationProperty; + double duration; // these are perhaps better represented as uint64_t, but they + // come from JS as doubles + double delay; + double springDamping; + double initialVelocity; +}; + +// This corresponds exactly with JS. +struct LayoutAnimationConfig { + double duration; // ms + better::optional createConfig; + better::optional updateConfig; + better::optional deleteConfig; +}; + +struct AnimationKeyFrame { + // The mutation that should be executed once the animation completes + // (optional). + better::optional finalMutationForKeyFrame; + + // The type of animation this is (for configuration purposes) + AnimationConfigurationType type; + + // Tag representing the node being animated. + Tag tag; + + ShadowView parentView; + + // ShadowView representing the start and end points of this animation. + ShadowView viewStart; + ShadowView viewEnd; + + // If an animation interrupts an existing one, the starting state may actually + // be halfway through the intended transition. + double initialProgress; +}; + +struct LayoutAnimation { + SurfaceId surfaceId; + uint64_t startTime; + bool completed = false; + LayoutAnimationConfig layoutAnimationConfig; + std::shared_ptr successCallback; + std::shared_ptr errorCallback; + std::vector keyFrames; +}; + +class LayoutAnimationKeyFrameManager : public UIManagerAnimationDelegate, + public MountingOverrideDelegate { + public: + void uiManagerDidConfigureNextLayoutAnimation( + RawValue const &config, + std::shared_ptr successCallback, + std::shared_ptr errorCallback) const override; + void setComponentDescriptorRegistry(SharedComponentDescriptorRegistry const & + componentDescriptorRegistry) override; + + // TODO: add SurfaceId to this API as well + bool shouldAnimateFrame() const override; + + 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. + better::optional pullTransaction( + SurfaceId surfaceId, + MountingTransaction::Number number, + MountingTelemetry const &telemetry, + ShadowViewMutationList mutations) const override; + + private: + void adjustDelayedMutationIndicesForMutation( + SurfaceId surfaceId, + ShadowViewMutation const &mutation) const; + + protected: + ComponentDescriptor const &getComponentDescriptorForShadowView( + ShadowView const &shadowView) const; + std::pair calculateAnimationProgress( + uint64_t now, + LayoutAnimation const &animation, + AnimationConfig const &mutationConfig) const; + + ShadowView createInterpolatedShadowView( + double progress, + AnimationConfig const &animationConfig, + ShadowView startingView, + ShadowView finalView) const; + + virtual void animationMutationsForFrame( + SurfaceId surfaceId, + ShadowViewMutation::List &mutationsList, + uint64_t now) const = 0; + + virtual double getProgressThroughAnimation( + AnimationKeyFrame const &keyFrame, + LayoutAnimation const *layoutAnimation, + ShadowView const &animationStateView) const = 0; + + SharedComponentDescriptorRegistry componentDescriptorRegistry_; + mutable better::optional 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 inflightAnimations_{}; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/core/primitives/RawValue.h b/ReactCommon/fabric/core/primitives/RawValue.h index 67f6ac05dc..75c2e9527a 100644 --- a/ReactCommon/fabric/core/primitives/RawValue.h +++ b/ReactCommon/fabric/core/primitives/RawValue.h @@ -64,6 +64,7 @@ class RawValue { private: friend class RawProps; friend class RawPropsParser; + friend class UIManagerBinding; /* * Arbitrary constructors are private only for RawProps and internal usage. @@ -73,9 +74,9 @@ class RawValue { RawValue(folly::dynamic &&dynamic) noexcept : dynamic_(std::move(dynamic)){}; /* - * Copy constructor and copy assignment operator are private and only for - * internal use. Basically, it's implementation details. Other particular - * implementations of the `RawValue` interface may not have them. + * Copy constructor and copy assignment operator would be private and only for + * internal use, but it's needed for user-code that does `auto val = + * (better::map)rawVal;` */ RawValue(RawValue const &other) noexcept : dynamic_(other.dynamic_) {} diff --git a/ReactCommon/fabric/mounting/MountingCoordinator.cpp b/ReactCommon/fabric/mounting/MountingCoordinator.cpp index 158959d40d..ad1552f156 100644 --- a/ReactCommon/fabric/mounting/MountingCoordinator.cpp +++ b/ReactCommon/fabric/mounting/MountingCoordinator.cpp @@ -19,9 +19,12 @@ namespace facebook { namespace react { -MountingCoordinator::MountingCoordinator(ShadowTreeRevision baseRevision) +MountingCoordinator::MountingCoordinator( + ShadowTreeRevision baseRevision, + MountingOverrideDelegate *delegate) : surfaceId_(baseRevision.getRootShadowNode().getSurfaceId()), - baseRevision_(baseRevision) { + baseRevision_(baseRevision), + mountingOverrideDelegate_(delegate) { #ifdef RN_SHADOW_TREE_INTROSPECTION stubViewTree_ = stubViewTreeFromShadowNode(baseRevision_.getRootShadowNode()); #endif @@ -66,31 +69,30 @@ bool MountingCoordinator::waitForTransaction( lock, timeout, [this]() { return lastRevision_.has_value(); }); } -better::optional MountingCoordinator::pullTransaction() - const { - std::lock_guard lock(mutex_); +void MountingCoordinator::updateBaseRevision( + ShadowTreeRevision const &baseRevision) const { + baseRevision_ = std::move(baseRevision); +} - if (!lastRevision_.has_value()) { - return {}; - } - - number_++; - - auto telemetry = lastRevision_->getTelemetry(); - telemetry.willDiff(); - - auto mutations = calculateShadowViewMutations( - baseRevision_.getRootShadowNode(), lastRevision_->getRootShadowNode()); - - telemetry.didDiff(); +void MountingCoordinator::resetLatestRevision() const { + lastRevision_.reset(); +} #ifdef RN_SHADOW_TREE_INTROSPECTION +void MountingCoordinator::validateTransactionAgainstStubViewTree( + ShadowViewMutationList const &mutations, + bool assertEquality) const { + std::string line; + + std::stringstream ssMutations(getDebugDescription(mutations, {})); + while (std::getline(ssMutations, line, '\n')) { + LOG(ERROR) << "Mutations:" << line; + } + stubViewTree_.mutate(mutations); auto stubViewTree = stubViewTreeFromShadowNode(lastRevision_->getRootShadowNode()); - std::string line; - std::stringstream ssOldTree( baseRevision_.getRootShadowNode().getDebugDescription()); while (std::getline(ssOldTree, line, '\n')) { @@ -103,19 +105,65 @@ better::optional MountingCoordinator::pullTransaction() LOG(ERROR) << "New tree:" << line; } - std::stringstream ssMutations(getDebugDescription(mutations, {})); - while (std::getline(ssMutations, line, '\n')) { - LOG(ERROR) << "Mutations:" << line; + if (assertEquality) { + assert(stubViewTree_ == stubViewTree); } - - assert(stubViewTree_ == stubViewTree); +} #endif - baseRevision_ = std::move(*lastRevision_); - lastRevision_.reset(); +better::optional MountingCoordinator::pullTransaction() + const { + std::lock_guard lock(mutex_); - return MountingTransaction{ - surfaceId_, number_, std::move(mutations), telemetry}; + bool shouldOverridePullTransaction = mountingOverrideDelegate_ != nullptr && + mountingOverrideDelegate_->shouldOverridePullTransaction(); + + if (!shouldOverridePullTransaction && !lastRevision_.has_value()) { + return {}; + } + + number_++; + + ShadowViewMutation::List diffMutations{}; + auto telemetry = + (lastRevision_.hasValue() ? lastRevision_->getTelemetry() + : MountingTelemetry{}); + if (lastRevision_.hasValue()) { + telemetry.willDiff(); + + diffMutations = calculateShadowViewMutations( + baseRevision_.getRootShadowNode(), lastRevision_->getRootShadowNode()); + + telemetry.didDiff(); + } + + better::optional transaction{}; + + // The override delegate can provide custom mounting instructions, + // even if there's no `lastRevision_`. Consider cases of animation frames + // in between React tree updates. + if (shouldOverridePullTransaction) { + transaction = mountingOverrideDelegate_->pullTransaction( + surfaceId_, number_, telemetry, std::move(diffMutations)); + } else if (lastRevision_.hasValue()) { + transaction = MountingTransaction{ + surfaceId_, number_, std::move(diffMutations), telemetry}; + } + + if (lastRevision_.hasValue()) { +#ifdef RN_SHADOW_TREE_INTROSPECTION + // Only validate non-animated transactions - it's garbage to validate + // animated transactions, since the stub view tree likely won't match + // the committed tree during an animation. + this->validateTransactionAgainstStubViewTree( + transaction->getMutations(), !shouldOverridePullTransaction); +#endif + + baseRevision_ = std::move(*lastRevision_); + lastRevision_.reset(); + } + + return transaction; } } // namespace react diff --git a/ReactCommon/fabric/mounting/MountingCoordinator.h b/ReactCommon/fabric/mounting/MountingCoordinator.h index e4389d6421..6976f35f48 100644 --- a/ReactCommon/fabric/mounting/MountingCoordinator.h +++ b/ReactCommon/fabric/mounting/MountingCoordinator.h @@ -11,8 +11,10 @@ #include #include +#include #include #include +#include "ShadowTreeRevision.h" #ifdef RN_SHADOW_TREE_INTROSPECTION #include @@ -33,10 +35,12 @@ class MountingCoordinator final { using Shared = std::shared_ptr; /* - * The constructor is ment to be used only inside `ShadowTree`, and it's + * The constructor is meant to be used only inside `ShadowTree`, and it's * `public` only to enable using with `std::make_shared<>`. */ - MountingCoordinator(ShadowTreeRevision baseRevision); + MountingCoordinator( + ShadowTreeRevision baseRevision, + MountingOverrideDelegate *delegate); /* * Returns the id of the surface that the coordinator belongs to. @@ -65,12 +69,20 @@ class MountingCoordinator final { */ bool waitForTransaction(std::chrono::duration timeout) const; - private: - friend class ShadowTree; + /* + * Methods from this section are meant to be used by + * `MountingOverrideDelegate` only. + */ + public: + void updateBaseRevision(ShadowTreeRevision const &baseRevision) const; + void resetLatestRevision() const; /* * Methods from this section are meant to be used by `ShadowTree` only. */ + private: + friend class ShadowTree; + void push(ShadowTreeRevision &&revision) const; /* @@ -78,7 +90,7 @@ class MountingCoordinator final { * Generating a `MountingTransaction` requires some resources which the * `MountingCoordinator` does not own (e.g. `ComponentDescriptor`s). Revoking * committed revisions allows the owner (a Shadow Tree) to make sure that - * those resources will not be accessed (e.g. by the Mouting Layer). + * those resources will not be accessed (e.g. by the Mounting Layer). */ void revoke() const; @@ -90,8 +102,12 @@ class MountingCoordinator final { mutable better::optional lastRevision_{}; mutable MountingTransaction::Number number_{0}; mutable std::condition_variable signal_; + mutable MountingOverrideDelegate *mountingOverrideDelegate_{nullptr}; #ifdef RN_SHADOW_TREE_INTROSPECTION + void validateTransactionAgainstStubViewTree( + ShadowViewMutationList const &mutations, + bool assertEquality) const; mutable StubViewTree stubViewTree_; // Protected by `mutex_`. #endif }; diff --git a/ReactCommon/fabric/mounting/MountingOverrideDelegate.h b/ReactCommon/fabric/mounting/MountingOverrideDelegate.h new file mode 100644 index 0000000000..2641a449dc --- /dev/null +++ b/ReactCommon/fabric/mounting/MountingOverrideDelegate.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include + +#pragma once + +namespace facebook { +namespace react { + +class MountingCoordinator; + +/** + * Generic interface for anything that needs to override specific + * MountingCoordinator methods. This is for platform-specific escape hatches + * like animations. + */ +class MountingOverrideDelegate { + public: + virtual bool shouldOverridePullTransaction() const = 0; + virtual ~MountingOverrideDelegate() {}; + + /** + * Delegates that override this method are responsible for: + * + * - Returning a MountingTransaction with mutations + * - Calling + * - Telemetry, if appropriate + * + * @param surfaceId + * @param number + * @param mountingCoordinator + * @return + */ + virtual better::optional pullTransaction( + SurfaceId surfaceId, + MountingTransaction::Number number, + MountingTelemetry const &telemetry, + ShadowViewMutationList mutations) const = 0; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/mounting/MountingTransaction.h b/ReactCommon/fabric/mounting/MountingTransaction.h index a13526f8a9..0a0c68ab6d 100644 --- a/ReactCommon/fabric/mounting/MountingTransaction.h +++ b/ReactCommon/fabric/mounting/MountingTransaction.h @@ -18,7 +18,7 @@ namespace react { * particularly list of mutations and meta-data associated with the commit. * Movable and copyable, but moving is strongly encouraged. * Beware: A moved-from object of this type has unspecified value and accessing - * that is UB. + * that is UB (Undefined Behaviour). */ class MountingTransaction final { public: diff --git a/ReactCommon/fabric/mounting/ShadowTree.cpp b/ReactCommon/fabric/mounting/ShadowTree.cpp index a6e4c2c58e..d6a40be917 100644 --- a/ReactCommon/fabric/mounting/ShadowTree.cpp +++ b/ReactCommon/fabric/mounting/ShadowTree.cpp @@ -221,7 +221,8 @@ ShadowTree::ShadowTree( LayoutConstraints const &layoutConstraints, LayoutContext const &layoutContext, RootComponentDescriptor const &rootComponentDescriptor, - ShadowTreeDelegate const &delegate) + ShadowTreeDelegate const &delegate, + MountingOverrideDelegate *mountingOverrideDelegate) : surfaceId_(surfaceId), delegate_(delegate) { const auto noopEventEmitter = std::make_shared( nullptr, -1, std::shared_ptr()); @@ -240,7 +241,7 @@ ShadowTree::ShadowTree( family)); mountingCoordinator_ = std::make_shared( - ShadowTreeRevision{rootShadowNode_, 0, {}}); + ShadowTreeRevision{rootShadowNode_, 0, {}}, mountingOverrideDelegate); } ShadowTree::~ShadowTree() { @@ -357,7 +358,7 @@ bool ShadowTree::tryCommit( mountingCoordinator_->push( ShadowTreeRevision{newRootShadowNode, revisionNumber, telemetry}); - delegate_.shadowTreeDidFinishTransaction(*this, mountingCoordinator_); + notifyDelegatesOfUpdates(); return true; } @@ -398,5 +399,9 @@ void ShadowTree::emitLayoutEvents( } } +void ShadowTree::notifyDelegatesOfUpdates() const { + delegate_.shadowTreeDidFinishTransaction(*this, mountingCoordinator_); +} + } // namespace react } // namespace facebook diff --git a/ReactCommon/fabric/mounting/ShadowTree.h b/ReactCommon/fabric/mounting/ShadowTree.h index f0c904d515..a4606ba9a8 100644 --- a/ReactCommon/fabric/mounting/ShadowTree.h +++ b/ReactCommon/fabric/mounting/ShadowTree.h @@ -18,6 +18,7 @@ #include #include #include +#include "MountingOverrideDelegate.h" namespace facebook { namespace react { @@ -38,7 +39,8 @@ class ShadowTree final { LayoutConstraints const &layoutConstraints, LayoutContext const &layoutContext, RootComponentDescriptor const &rootComponentDescriptor, - ShadowTreeDelegate const &delegate); + ShadowTreeDelegate const &delegate, + MountingOverrideDelegate *mountingOverrideDelegate); ~ShadowTree(); @@ -69,6 +71,13 @@ class ShadowTree final { */ void commitEmptyTree() const; + /** + * Forces the ShadowTree to ping its delegate that an update is available. + * Useful for animations on Android. + * @return + */ + void notifyDelegatesOfUpdates() const; + MountingCoordinator::Shared getMountingCoordinator() const; /* diff --git a/ReactCommon/fabric/mounting/ShadowTreeRevision.cpp b/ReactCommon/fabric/mounting/ShadowTreeRevision.cpp index 7e13c85540..46f59d0418 100644 --- a/ReactCommon/fabric/mounting/ShadowTreeRevision.cpp +++ b/ReactCommon/fabric/mounting/ShadowTreeRevision.cpp @@ -22,6 +22,10 @@ MountingTelemetry const &ShadowTreeRevision::getTelemetry() const { return telemetry_; } +ShadowNode::Shared ShadowTreeRevision::getSharedRootShadowNode() { + return rootShadowNode_; +} + ShadowNode const &ShadowTreeRevision::getRootShadowNode() { return *rootShadowNode_; } diff --git a/ReactCommon/fabric/mounting/ShadowTreeRevision.h b/ReactCommon/fabric/mounting/ShadowTreeRevision.h index 71f68d0ed6..ba89dc784f 100644 --- a/ReactCommon/fabric/mounting/ShadowTreeRevision.h +++ b/ReactCommon/fabric/mounting/ShadowTreeRevision.h @@ -9,6 +9,7 @@ #include +#include #include #include #include @@ -42,14 +43,21 @@ class ShadowTreeRevision final { */ MountingTelemetry const &getTelemetry() const; - private: - friend class MountingCoordinator; + /* + * Methods from this section are meant to be used by + * `MountingOverrideDelegate` only. + */ + public: + ShadowNode const &getRootShadowNode(); + ShadowNode::Shared getSharedRootShadowNode(); /* * Methods from this section are meant to be used by `MountingCoordinator` * only. */ - ShadowNode const &getRootShadowNode(); + private: + friend class MountingCoordinator; + Number getNumber() const; private: diff --git a/ReactCommon/fabric/mounting/ShadowView.cpp b/ReactCommon/fabric/mounting/ShadowView.cpp index 800308add5..a5436df7dc 100644 --- a/ReactCommon/fabric/mounting/ShadowView.cpp +++ b/ReactCommon/fabric/mounting/ShadowView.cpp @@ -13,10 +13,10 @@ namespace facebook { namespace react { static LayoutMetrics layoutMetricsFromShadowNode(ShadowNode const &shadowNode) { - auto layotableShadowNode = + auto layoutableShadowNode = traitCast(&shadowNode); - return layotableShadowNode ? layotableShadowNode->getLayoutMetrics() - : EmptyLayoutMetrics; + return layoutableShadowNode ? layoutableShadowNode->getLayoutMetrics() + : EmptyLayoutMetrics; } ShadowView::ShadowView(const ShadowNode &shadowNode) diff --git a/ReactCommon/fabric/mounting/ShadowView.h b/ReactCommon/fabric/mounting/ShadowView.h index 1372b44928..fc51f9e9b1 100644 --- a/ReactCommon/fabric/mounting/ShadowView.h +++ b/ReactCommon/fabric/mounting/ShadowView.h @@ -66,7 +66,7 @@ struct ShadowViewNodePair final { ShadowNode const *shadowNode; /* - * The stored pointer to `ShadowNode` represents an indentity of the pair. + * The stored pointer to `ShadowNode` represents an identity of the pair. */ bool operator==(const ShadowViewNodePair &rhs) const; bool operator!=(const ShadowViewNodePair &rhs) const; diff --git a/ReactCommon/fabric/mounting/tests/StateReconciliationTest.cpp b/ReactCommon/fabric/mounting/tests/StateReconciliationTest.cpp index 19ba086ca2..665f18bcf6 100644 --- a/ReactCommon/fabric/mounting/tests/StateReconciliationTest.cpp +++ b/ReactCommon/fabric/mounting/tests/StateReconciliationTest.cpp @@ -103,7 +103,8 @@ TEST(StateReconciliationTest, testStateReconciliation) { LayoutConstraints{}, LayoutContext{}, rootComponentDescriptor, - shadowTreeDelegate}; + shadowTreeDelegate, + nullptr}; shadowTree.commit( [&](RootShadowNode::Shared const &oldRootShadowNode) { diff --git a/ReactCommon/fabric/scheduler/Scheduler.cpp b/ReactCommon/fabric/scheduler/Scheduler.cpp index 1acdf65a17..d4dafd716b 100644 --- a/ReactCommon/fabric/scheduler/Scheduler.cpp +++ b/ReactCommon/fabric/scheduler/Scheduler.cpp @@ -13,15 +13,23 @@ #include #include #include +#include +#include #include #include #include +#ifdef RN_SHADOW_TREE_INTROSPECTION +#include +#include +#endif + namespace facebook { namespace react { Scheduler::Scheduler( SchedulerToolbox schedulerToolbox, + UIManagerAnimationDelegate *animationDelegate, SchedulerDelegate *delegate) { runtimeExecutor_ = schedulerToolbox.runtimeExecutor; @@ -90,6 +98,12 @@ Scheduler::Scheduler( delegate_ = delegate; uiManager_ = uiManager; + if (animationDelegate != nullptr) { + animationDelegate->setComponentDescriptorRegistry( + componentDescriptorRegistry_); + } + uiManager_->setAnimationDelegate(animationDelegate); + #ifdef ANDROID enableNewStateReconciliation_ = reactNativeConfig_->getBool( "react_fabric:enable_new_state_reconciliation_android"); @@ -158,7 +172,8 @@ void Scheduler::startSurface( const std::string &moduleName, const folly::dynamic &initialProps, const LayoutConstraints &layoutConstraints, - const LayoutContext &layoutContext) const { + const LayoutContext &layoutContext, + MountingOverrideDelegate *mountingOverrideDelegate) const { SystraceSection s("Scheduler::startSurface"); auto shadowTree = std::make_unique( @@ -166,7 +181,8 @@ void Scheduler::startSurface( layoutConstraints, layoutContext, *rootComponentDescriptor_, - *uiManager_); + *uiManager_, + mountingOverrideDelegate); shadowTree->setEnableNewStateReconciliation(enableNewStateReconciliation_); @@ -310,6 +326,12 @@ SchedulerDelegate *Scheduler::getDelegate() const { return delegate_; } +#pragma mark - UIManagerAnimationDelegate + +void Scheduler::animationTick() const { + uiManager_->animationTick(); +} + #pragma mark - UIManagerDelegate void Scheduler::uiManagerDidFinishTransaction( @@ -320,7 +342,6 @@ void Scheduler::uiManagerDidFinishTransaction( delegate_->schedulerDidFinishTransaction(mountingCoordinator); } } - void Scheduler::uiManagerDidCreateShadowNode( const ShadowNode::Shared &shadowNode) { SystraceSection s("Scheduler::uiManagerDidCreateShadowNode"); diff --git a/ReactCommon/fabric/scheduler/Scheduler.h b/ReactCommon/fabric/scheduler/Scheduler.h index 243db58a0e..9ee114ec21 100644 --- a/ReactCommon/fabric/scheduler/Scheduler.h +++ b/ReactCommon/fabric/scheduler/Scheduler.h @@ -12,13 +12,14 @@ #include #include -#include #include #include #include #include +#include #include #include +#include #include #include #include @@ -31,7 +32,10 @@ namespace react { */ class Scheduler final : public UIManagerDelegate { public: - Scheduler(SchedulerToolbox schedulerToolbox, SchedulerDelegate *delegate); + Scheduler( + SchedulerToolbox schedulerToolbox, + UIManagerAnimationDelegate *animationDelegate, + SchedulerDelegate *delegate); ~Scheduler(); #pragma mark - Surface Management @@ -41,7 +45,8 @@ class Scheduler final : public UIManagerDelegate { const std::string &moduleName, const folly::dynamic &initialProps, const LayoutConstraints &layoutConstraints = {}, - const LayoutContext &layoutContext = {}) const; + const LayoutContext &layoutContext = {}, + MountingOverrideDelegate *mountingOverrideDelegate = nullptr) const; void renderTemplateToSurface( SurfaceId surfaceId, @@ -88,6 +93,13 @@ class Scheduler final : public UIManagerDelegate { void setDelegate(SchedulerDelegate *delegate); SchedulerDelegate *getDelegate() const; +#pragma mark - UIManagerAnimationDelegate + // This is not needed on iOS or any platform that has a "pull" instead of + // "push" MountingCoordinator model. This just tells the delegate an update + // is available and that it should `pullTransaction`; we may want to rename + // this to be more generic and not animation-specific. + void animationTick() const; + #pragma mark - UIManagerDelegate void uiManagerDidFinishTransaction( diff --git a/ReactCommon/fabric/uimanager/UIManager.cpp b/ReactCommon/fabric/uimanager/UIManager.cpp index 761a87c3ef..b4910cee67 100644 --- a/ReactCommon/fabric/uimanager/UIManager.cpp +++ b/ReactCommon/fabric/uimanager/UIManager.cpp @@ -268,9 +268,15 @@ void UIManager::dispatchCommand( } void UIManager::configureNextLayoutAnimation( - const folly::dynamic config, + RawValue const &config, SharedEventTarget successCallback, - SharedEventTarget errorCallback) const {} + SharedEventTarget errorCallback) const { + if (animationDelegate_) { + animationDelegate_->uiManagerDidConfigureNextLayoutAnimation( + config, successCallback, errorCallback); + } +} + void UIManager::setComponentDescriptorRegistry( const SharedComponentDescriptorRegistry &componentDescriptorRegistry) { componentDescriptorRegistry_ = componentDescriptorRegistry; @@ -310,5 +316,22 @@ void UIManager::shadowTreeDidFinishTransaction( } } +#pragma mark - UIManagerAnimationDelegate + +void UIManager::setAnimationDelegate( + UIManagerAnimationDelegate *delegate) const { + animationDelegate_ = delegate; +} + +void UIManager::animationTick() { + if (animationDelegate_ != nullptr && + animationDelegate_->shouldAnimateFrame()) { + shadowTreeRegistry_.enumerate( + [&](ShadowTree const &shadowTree, bool &stop) { + shadowTree.notifyDelegatesOfUpdates(); + }); + } +} + } // namespace react } // namespace facebook diff --git a/ReactCommon/fabric/uimanager/UIManager.h b/ReactCommon/fabric/uimanager/UIManager.h index 51131dd98d..543bf97cfa 100644 --- a/ReactCommon/fabric/uimanager/UIManager.h +++ b/ReactCommon/fabric/uimanager/UIManager.h @@ -12,11 +12,13 @@ #include #include +#include #include #include #include #include #include +#include #include namespace facebook { @@ -39,6 +41,15 @@ class UIManager final : public ShadowTreeDelegate { void setDelegate(UIManagerDelegate *delegate); UIManagerDelegate *getDelegate(); + /** + * Sets and gets the UIManager's Animation APIs delegate. + * The delegate is stored as a raw pointer, so the owner must null + * the pointer before being destroyed. + */ + void setAnimationDelegate(UIManagerAnimationDelegate *delegate) const; + + void animationTick(); + /* * Provides access to a UIManagerBindging. * The `callback` methods will not be called if the internal pointer to @@ -121,13 +132,15 @@ class UIManager final : public ShadowTreeDelegate { * This API configures a global LayoutAnimation starting from the root node. */ void configureNextLayoutAnimation( - const folly::dynamic config, + RawValue const &config, SharedEventTarget successCallback, SharedEventTarget errorCallback) const; + ShadowTreeRegistry const &getShadowTreeRegistry() const; SharedComponentDescriptorRegistry componentDescriptorRegistry_; UIManagerDelegate *delegate_; + mutable UIManagerAnimationDelegate *animationDelegate_{nullptr}; UIManagerBinding *uiManagerBinding_; ShadowTreeRegistry shadowTreeRegistry_{}; }; diff --git a/ReactCommon/fabric/uimanager/UIManagerAnimationDelegate.h b/ReactCommon/fabric/uimanager/UIManagerAnimationDelegate.h new file mode 100644 index 0000000000..70ad75879e --- /dev/null +++ b/ReactCommon/fabric/uimanager/UIManagerAnimationDelegate.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook { +namespace react { + +class UIManagerAnimationDelegate { + public: + virtual ~UIManagerAnimationDelegate() {}; + + /* + * Configure a LayoutAnimation. + * TODO: need SurfaceId here + */ + virtual void uiManagerDidConfigureNextLayoutAnimation( + RawValue const &config, + SharedEventTarget successCallback, + SharedEventTarget errorCallback) const = 0; + + /** + * Set ComponentDescriptor registry. + * + * @param componentDescriptorRegistry + */ + virtual void setComponentDescriptorRegistry( + const SharedComponentDescriptorRegistry &componentDescriptorRegistry) = 0; + + /** + * Only needed on Android to drive animations. + */ + virtual bool shouldAnimateFrame() const = 0; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/uimanager/UIManagerBinding.cpp b/ReactCommon/fabric/uimanager/UIManagerBinding.cpp index 4c50095e44..92d20a915c 100644 --- a/ReactCommon/fabric/uimanager/UIManagerBinding.cpp +++ b/ReactCommon/fabric/uimanager/UIManagerBinding.cpp @@ -619,16 +619,14 @@ jsi::Value UIManagerBinding::get( const jsi::Value *arguments, size_t count) -> jsi::Value { uiManager->configureNextLayoutAnimation( - commandArgsFromValue( - runtime, - arguments[0]), // TODO T66507273: do a better job of parsing - // these arguments into a real struct / use a - // C++ typed object instead of folly::dynamic + // TODO: pass in JSI value instead of folly::dynamic to RawValue + RawValue(commandArgsFromValue(runtime, arguments[0])), eventTargetFromValue(runtime, arguments[1], -1), eventTargetFromValue(runtime, arguments[2], -1)); return jsi::Value::undefined(); }); } + return jsi::Value::undefined(); } diff --git a/ReactCommon/fabric/uimanager/UIManagerBinding.h b/ReactCommon/fabric/uimanager/UIManagerBinding.h index 75485e6c8c..0ecf00c9dd 100644 --- a/ReactCommon/fabric/uimanager/UIManagerBinding.h +++ b/ReactCommon/fabric/uimanager/UIManagerBinding.h @@ -9,6 +9,7 @@ #include #include +#include #include #include