From b38a7986032b5ee1f7a76cd3064f8b3dc1fb37c6 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Wed, 8 Dec 2021 11:01:54 -0500 Subject: [PATCH 1/3] Adds Rendering driver option to NativeAnimated Currently, NativeAnimated compiles the animation graph into CompositionAnimations from UI.Composition. While this approach likely provides the ideal performance for native animations on Windows, it suffers a few insurmountable limitations: 1. #8419: Creating a new animation on an animated value after stopping a previous animation on the same value does not retain the current value. a. Even with a fix like #9190, because UI.Composition values cannot be queried synchronously, starting a new animation on an animated value immediately after stopping a previous animation causes jitter in the animation, because it takes 1-2 frames for the completion callback to fire, signaling that the animated value has an up-to-date value. 2. #3283: UI.Composition can only animate supported properties, like opacity and transforms. NativeAnimated provides a prop "hook" (the prop name is `progress`) to allow arbitrary views to subscribe to animation value changes synchronously. This is not possible with UI.Composition. 3. #4311: Similar to the limitation for starting new animations synchronously after stopping a previous animation on the same animated value (#8419), animated value listeners will always be 1-2 frames out of sync while waiting for an up-to-date composition value. This feature is currently not implemented for the UI.Composition approach, but I suspect it would require a frame callback via CompositionTarget::Rendering, so not only would the values be out of sync, but the approach would have a similar performance profile to the CompositionTarget::Rendering driven animations. 4. #9255: Similar to #3283, it's actually possible to animate arbitrary props with Animated (e.g., `borderRadius`). It's unlikely that it would be possible to support such an animation with UI.Composition. There are also a few bugs that are likely possible to workaround for UI.Composition, but would be fixed immediately by this CompositionTarget::Rendering approach: 1. #3460: Animated values persist even after the animation is unmounted with UI.Composition. The CompositionTarget::Rendering approach uses the view's shadow node as the source of truth for the prop value, and resets the prop to null when the animation is unmounted. 2. #9256: The UI.Composition implementation for NativeAnimated commandeers the `Offset` value for animated nodes, using it to drive progress on an animation. Animated value offsets are not intended for this purpose and can cause bugs as demonstrated in the linked issue. 3. #9251: Calling `setValue` on an animated value should stop any active animations and update the animation graph to reflect the value that was set, but in the UI.Composition implementation, the animation is only stopped in place. 4. #9252: Animated.diffClamp nodes are intended to clamp the difference between the last value and the current value, but the UI.Composition `clamp` expression only has the capability to clamp the current value. 5. #9250: Calling `getValue` on an animated value does not return the current value for the UI.Composition approach (because values are not available until the animation has been stopped and the completion callback fires). 6. A more minor issue that I haven't filed an issue for, is that Animated.decay animations work slightly differently in NativeAnimated vs. JS-driven animations. The NativeAnimated approach is a bit more "accurate", in that it stops the animation when its value is within 0.1 of the final value if the decay ran infinitely. The JS-driven approach stops the animation more eagerly when the value differential (the difference between the current value and the previous value) is 0.1. This change allows each animation node and driver to be used in either composition mode or frame callback / CompositionTarget::Rendering mode. The latter approach is largely derived from the Android implementation of NativeAnimated (and the C# implementation in react-native-windows v0.59 and earlier). This change will leverage WIP changes to the Animated APIs in RN core that pass a property bag to each AnimationDriver and AnimatedNode signaling which mode to use. The API surface will look something like the following: ```js const value = Animated.value(0); Animated.timing(value, { ..., useNativeDriver: true, platformConfig: { useComposition: false, }); ``` We will also likely complement this API change with a way to set default `platformConfig` values for all Animated APIs using `useNativeDriver: true`: ```js Animated.setDefaultPlatformConfig({useComposition: false}); ``` For now, in order to not regress performance on existing react-native-windows applications, using the CompositionTarget::Rendering approach will be strictly opt-in. As of this commit, these two approaches cannot be blended together. I.e., you cannot connect a node using UI.Composition to a node using CompositionTarget::Rendering, and it's unlikely these two approaches could be combined until the UI.Composition approach supports synchronous queries of values (at which point, quite a few of the justifications for the CompositionTarget::Rendering approach will be resolved). Fixes #3283 Fixes #3460 Fixes #8419 Fixes #9250 Fixes #9251 Fixes #9252 Fixes #9255 Fixes #9256 --- .../Microsoft.ReactNative.vcxproj | 3 + .../Microsoft.ReactNative.vcxproj.filters | 9 + .../Modules/Animated/AdditionAnimatedNode.cpp | 45 ++- .../Modules/Animated/AdditionAnimatedNode.h | 2 + .../Modules/Animated/AnimatedNode.cpp | 19 +- .../Modules/Animated/AnimatedNode.h | 20 +- .../Animated/AnimatedPlatformConfig.cpp | 23 ++ .../Modules/Animated/AnimatedPlatformConfig.h | 17 + .../Modules/Animated/AnimationDriver.cpp | 50 ++- .../Modules/Animated/AnimationDriver.h | 23 ++ .../Modules/Animated/AnimationUtils.h | 45 +++ .../Animated/CalculatedAnimationDriver.cpp | 5 +- .../Animated/CalculatedAnimationDriver.h | 2 - .../Modules/Animated/DecayAnimationDriver.cpp | 30 +- .../Modules/Animated/DecayAnimationDriver.h | 2 + .../Animated/DiffClampAnimatedNode.cpp | 37 ++- .../Modules/Animated/DiffClampAnimatedNode.h | 3 + .../Modules/Animated/DivisionAnimatedNode.cpp | 59 ++-- .../Modules/Animated/DivisionAnimatedNode.h | 2 + .../Modules/Animated/FrameAnimationDriver.cpp | 39 ++- .../Modules/Animated/FrameAnimationDriver.h | 5 + .../Animated/InterpolationAnimatedNode.cpp | 100 ++++-- .../Animated/InterpolationAnimatedNode.h | 8 +- .../Modules/Animated/ModulusAnimatedNode.cpp | 36 ++- .../Modules/Animated/ModulusAnimatedNode.h | 2 + .../Animated/MultiplicationAnimatedNode.cpp | 45 ++- .../Animated/MultiplicationAnimatedNode.h | 2 + .../Modules/Animated/NativeAnimatedModule.cpp | 31 +- .../Animated/NativeAnimatedNodeManager.cpp | 298 ++++++++++++++++-- .../Animated/NativeAnimatedNodeManager.h | 17 + .../Modules/Animated/PropsAnimatedNode.cpp | 110 +++++-- .../Modules/Animated/PropsAnimatedNode.h | 5 +- .../Animated/SpringAnimationDriver.cpp | 67 +++- .../Modules/Animated/SpringAnimationDriver.h | 4 + .../Modules/Animated/StyleAnimatedNode.cpp | 21 +- .../Modules/Animated/StyleAnimatedNode.h | 3 +- .../Animated/SubtractionAnimatedNode.cpp | 58 ++-- .../Animated/SubtractionAnimatedNode.h | 2 + .../Modules/Animated/TrackingAnimatedNode.cpp | 25 +- .../Modules/Animated/TrackingAnimatedNode.h | 5 +- .../Animated/TransformAnimatedNode.cpp | 34 +- .../Modules/Animated/TransformAnimatedNode.h | 3 + .../Modules/Animated/ValueAnimatedNode.cpp | 90 ++++-- .../Modules/Animated/ValueAnimatedNode.h | 10 +- 44 files changed, 1145 insertions(+), 271 deletions(-) create mode 100644 vnext/Microsoft.ReactNative/Modules/Animated/AnimatedPlatformConfig.cpp create mode 100644 vnext/Microsoft.ReactNative/Modules/Animated/AnimatedPlatformConfig.h create mode 100644 vnext/Microsoft.ReactNative/Modules/Animated/AnimationUtils.h diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj index 5bd2e634cdb..10d83b86500 100644 --- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj +++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj @@ -233,9 +233,11 @@ + + @@ -466,6 +468,7 @@ + diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters index c1d94412580..ff71e587beb 100644 --- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters +++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters @@ -65,6 +65,9 @@ Modules\Animated + + Modules\Animated + Modules\Animated @@ -412,6 +415,9 @@ Modules\Animated + + Modules\Animated + Modules\Animated @@ -421,6 +427,9 @@ Modules\Animated + + Modules\Animated + Modules\Animated diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/AdditionAnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/AdditionAnimatedNode.cpp index ef5c1637e88..92a94208148 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/AdditionAnimatedNode.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/AdditionAnimatedNode.cpp @@ -12,24 +12,41 @@ AdditionAnimatedNode::AdditionAnimatedNode( int64_t tag, const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager) - : ValueAnimatedNode(tag, manager) { + : ValueAnimatedNode(tag, config, manager) { for (const auto &inputNode : config[s_inputName].AsArray()) { - m_inputNodes.insert(static_cast(inputNode.AsDouble())); + const auto inputTag = inputNode.AsInt64(); + assert(HasCompatibleAnimationDriver(inputTag)); + m_inputNodes.insert(inputTag); } - m_propertySet.StartAnimation(s_valueName, [nodes = m_inputNodes, manager]() { - const auto anim = Microsoft::ReactNative::GetCompositor().CreateExpressionAnimation(); + if (m_useComposition) { + m_propertySet.StartAnimation(s_valueName, [nodes = m_inputNodes, manager]() { + const auto anim = Microsoft::ReactNative::GetCompositor().CreateExpressionAnimation(); - anim.Expression([nodes, manager, anim]() { - winrt::hstring expr = L"0"; - for (const auto tag : nodes) { - const auto identifier = L"n" + std::to_wstring(tag); - anim.SetReferenceParameter(identifier, manager->GetValueAnimatedNode(tag)->PropertySet()); - expr = expr + L" + " + identifier + L"." + s_valueName + L" + " + identifier + L"." + s_offsetName; - } - return expr; + anim.Expression([nodes, manager, anim]() { + winrt::hstring expr = L"0"; + for (const auto tag : nodes) { + const auto identifier = L"n" + std::to_wstring(tag); + anim.SetReferenceParameter(identifier, manager->GetValueAnimatedNode(tag)->PropertySet()); + expr = expr + L" + " + identifier + L"." + s_valueName + L" + " + identifier + L"." + s_offsetName; + } + return expr; + }()); + return anim; }()); - return anim; - }()); + } +} + +void AdditionAnimatedNode::Update() { + assert(!m_useComposition); + auto rawValue = 0.0; + if (const auto manager = m_manager.lock()) { + for (const auto tag : m_inputNodes) { + if (const auto node = manager->GetValueAnimatedNode(tag)) { + rawValue += node->Value(); + } + } + } + RawValue(rawValue); } } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/AdditionAnimatedNode.h b/vnext/Microsoft.ReactNative/Modules/Animated/AdditionAnimatedNode.h index c2fc087640b..053bb0303e0 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/AdditionAnimatedNode.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/AdditionAnimatedNode.h @@ -12,6 +12,8 @@ class AdditionAnimatedNode final : public ValueAnimatedNode { const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager); + virtual void Update() override; + private: std::unordered_set m_inputNodes{}; }; diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedNode.cpp index 3b65c3dcb2a..f7c09340af7 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedNode.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedNode.cpp @@ -4,12 +4,18 @@ #include "pch.h" #include "AnimatedNode.h" +#include "AnimatedPlatformConfig.h" #include "NativeAnimatedNodeManager.h" namespace Microsoft::ReactNative { -AnimatedNode::AnimatedNode(int64_t tag, const std::shared_ptr &manager) - : m_tag(tag), m_manager(manager) {} +AnimatedNode::AnimatedNode( + int64_t tag, + const winrt::Microsoft::ReactNative::JSValueObject &config, + const std::shared_ptr &manager) + : m_tag(tag), m_manager(manager) { + m_useComposition = AnimatedPlatformConfig::ShouldUseComposition(config); +} int64_t AnimatedNode::Tag() { return m_tag; @@ -36,4 +42,13 @@ AnimatedNode *AnimatedNode::GetChildNode(int64_t tag) { return static_cast(nullptr); } + +bool AnimatedNode::HasCompatibleAnimationDriver(int64_t tag) { +#if DEBUG + if (const auto manager = m_manager.lock()) { + return manager->GetAnimatedNode(tag)->UseComposition() == m_useComposition; + } +#endif + return true; +} } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedNode.h b/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedNode.h index 3be03bcf815..4b94ffd49ce 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedNode.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedNode.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include @@ -11,11 +12,22 @@ namespace Microsoft::ReactNative { class NativeAnimatedNodeManager; class AnimatedNode { public: - AnimatedNode(int64_t tag, const std::shared_ptr &manager); + AnimatedNode( + int64_t tag, + const winrt::Microsoft::ReactNative::JSValueObject &config, + const std::shared_ptr &manager); int64_t Tag(); void AddChild(int64_t animatedNode); void RemoveChild(int64_t animatedNode); + std::vector &Children() { + return m_children; + } + + virtual bool UseComposition() const noexcept { + return m_useComposition; + } + virtual void Update(){}; virtual void OnDetachedFromNode(int64_t /*animatedNodeTag*/){}; virtual void OnAttachToNode(int64_t /*animatedNodeTag*/){}; @@ -26,5 +38,11 @@ class AnimatedNode { int64_t m_tag{0}; const std::weak_ptr m_manager; std::vector m_children{}; + bool m_useComposition{false}; + + bool HasCompatibleAnimationDriver(int64_t tag); + + static constexpr std::string_view s_platformConfigName{"platformConfig"}; + static constexpr std::string_view s_useCompositionName{"useComposition"}; }; } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedPlatformConfig.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedPlatformConfig.cpp new file mode 100644 index 00000000000..c940b16d23a --- /dev/null +++ b/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedPlatformConfig.cpp @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "AnimatedPlatformConfig.h" + +namespace Microsoft::ReactNative { + +// We could consider converting this value to a quirk setting in the future if +// we want to change the default behavior to use the frame rendering approach. +static constexpr auto DEFAULT_USE_COMPOSITION = true; + +/*static*/ bool AnimatedPlatformConfig::ShouldUseComposition( + const winrt::Microsoft::ReactNative::JSValueObject &config) { + if (config.count(s_platformConfigName)) { + const auto &platformConfig = config[s_platformConfigName].AsObject(); + if (platformConfig.count(s_useCompositionName)) { + return platformConfig[s_useCompositionName].AsBoolean(); + } + } + return DEFAULT_USE_COMPOSITION; +} + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedPlatformConfig.h b/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedPlatformConfig.h new file mode 100644 index 00000000000..2d81fa21a4e --- /dev/null +++ b/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedPlatformConfig.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include + +namespace Microsoft::ReactNative { +class AnimatedPlatformConfig { + public: + static bool ShouldUseComposition(const winrt::Microsoft::ReactNative::JSValueObject &config); + + private: + static constexpr std::string_view s_platformConfigName{"platformConfig"}; + static constexpr std::string_view s_useCompositionName{"useComposition"}; +}; +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/AnimationDriver.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/AnimationDriver.cpp index 6a694d05ea0..376da7b5876 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/AnimationDriver.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/AnimationDriver.cpp @@ -4,6 +4,7 @@ #include "pch.h" #include +#include "AnimatedPlatformConfig.h" #include "AnimationDriver.h" namespace Microsoft::ReactNative { @@ -20,6 +21,7 @@ AnimationDriver::AnimationDriver( m_endCallback(endCallback), m_manager(manager) { m_iterations = config.find("iterations") == config.end() ? 1 : config["iterations"].AsInt64(); + m_useComposition = AnimatedPlatformConfig::ShouldUseComposition(config); } void AnimationDriver::DoCallback(bool value) { @@ -36,11 +38,12 @@ void AnimationDriver::DoCallback(bool value) { } AnimationDriver::~AnimationDriver() { - if (m_scopedBatch) + if (m_useComposition && m_scopedBatch) m_scopedBatch.Completed(m_scopedBatchCompletedToken); } void AnimationDriver::StartAnimation() { + assert(m_useComposition); m_started = true; const auto [animation, scopedBatch] = MakeAnimation(m_config); if (auto const animatedValue = GetAnimatedValue()) { @@ -76,15 +79,44 @@ void AnimationDriver::StartAnimation() { } void AnimationDriver::StopAnimation(bool ignoreCompletedHandlers) { - if (!m_started) { - // The animation may have been deferred and never started. In this case, - // we will never get a scoped batch completion, so we need to fire the - // callback synchronously. + if (m_useComposition && m_started) { + if (const auto animatedValue = GetAnimatedValue()) { + animatedValue->PropertySet().StopAnimation(ValueAnimatedNode::s_valueName); + m_stopped = true; + m_ignoreCompletedHandlers = ignoreCompletedHandlers; + } + } else { + // For composition animations, the animation may have been deferred and + // never started. In this case, we will never get a scoped batch + // completion, so we need to fire the callback synchronously. For rendering + // animations, we always fire the callback synchronously. DoCallback(false); - } else if (const auto animatedValue = GetAnimatedValue()) { - animatedValue->PropertySet().StopAnimation(ValueAnimatedNode::s_valueName); - m_stopped = true; - m_ignoreCompletedHandlers = ignoreCompletedHandlers; + } +} + +void AnimationDriver::RunAnimationStep(winrt::TimeSpan renderingTime) { + assert(!m_useComposition); + if (m_isComplete) { + return; + } + + // winrt::TimeSpan ticks are 100 nanoseconds, divide by 10000 to get milliseconds. + const auto frameTimeMs = renderingTime.count() / 10000.0; + auto restarting = false; + if (m_startFrameTimeMs < 0) { + m_startFrameTimeMs = frameTimeMs; + restarting = true; + } + + const auto timeDeltaMs = frameTimeMs - m_startFrameTimeMs; + const auto isComplete = Update(timeDeltaMs, restarting); + + if (isComplete) { + if (m_iterations == -1 || ++m_iteration < m_iterations) { + m_startFrameTimeMs = -1; + } else { + m_isComplete = true; + } } } diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/AnimationDriver.h b/vnext/Microsoft.ReactNative/Modules/Animated/AnimationDriver.h index 6f5b3b477bf..36ff9bbd95c 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/AnimationDriver.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/AnimationDriver.h @@ -47,11 +47,23 @@ class AnimationDriver : public std::enable_shared_from_this { }; virtual std::vector Frames() { + assert(m_useComposition); return std::vector(); } void DoCallback(bool value); + bool UseComposition() const noexcept { + return m_useComposition; + } + + bool IsComplete() { + assert(!m_useComposition); + return m_isComplete; + } + + void RunAnimationStep(winrt::TimeSpan renderingTime); + private: Callback m_endCallback{}; #ifdef DEBUG @@ -60,13 +72,22 @@ class AnimationDriver : public std::enable_shared_from_this { protected: ValueAnimatedNode *GetAnimatedValue(); + virtual bool Update(double timeDeltaMs, bool restarting) { + return true; + }; + bool m_useComposition{}; int64_t m_id{0}; int64_t m_animatedValueTag{}; int64_t m_iterations{0}; winrt::Microsoft::ReactNative::JSValueObject m_config{}; std::weak_ptr m_manager{}; + bool m_isComplete{false}; + int64_t m_iteration{0}; + double m_startFrameTimeMs{-1}; + std::optional m_originalValue{}; + comp::CompositionAnimation m_animation{nullptr}; comp::CompositionScopedBatch m_scopedBatch{nullptr}; // auto revoker for scopedBatch.Completed is broken, tracked by internal bug @@ -75,5 +96,7 @@ class AnimationDriver : public std::enable_shared_from_this { bool m_started{false}; bool m_stopped{false}; bool m_ignoreCompletedHandlers{false}; + + static constexpr double s_frameDurationMs = 1000.0 / 60.0; }; } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/AnimationUtils.h b/vnext/Microsoft.ReactNative/Modules/Animated/AnimationUtils.h new file mode 100644 index 00000000000..170a8de39dd --- /dev/null +++ b/vnext/Microsoft.ReactNative/Modules/Animated/AnimationUtils.h @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +static constexpr std::string_view ExtrapolateTypeIdentity = "identity"; +static constexpr std::string_view ExtrapolateTypeClamp = "clamp"; +static constexpr std::string_view ExtrapolateTypeExtend = "extend"; + +static double Interpolate( + double value, + double inputMin, + double inputMax, + double outputMin, + double outputMax, + std::string_view const &extrapolateLeft, + std::string_view const &extrapolateRight) { + auto result = value; + + // Extrapolate + if (result < inputMin) { + if (extrapolateLeft == ExtrapolateTypeIdentity) { + return result; + } else if (extrapolateLeft == ExtrapolateTypeClamp) { + result = inputMin; + } + } + + if (result > inputMax) { + if (extrapolateRight == ExtrapolateTypeIdentity) { + return result; + } else if (extrapolateRight == ExtrapolateTypeClamp) { + result = inputMax; + } + } + + if (inputMin == inputMax) { + if (value <= inputMin) { + return outputMin; + } + return outputMax; + } + + return outputMin + (outputMax - outputMin) * (result - inputMin) / (inputMax - inputMin); +} diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/CalculatedAnimationDriver.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/CalculatedAnimationDriver.cpp index 8e6efb5d69f..08e1fe08ae2 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/CalculatedAnimationDriver.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/CalculatedAnimationDriver.cpp @@ -10,6 +10,7 @@ namespace Microsoft::ReactNative { std::tuple CalculatedAnimationDriver::MakeAnimation( const winrt::Microsoft::ReactNative::JSValueObject & /*config*/) { + assert(m_useComposition); const auto [scopedBatch, animation, easingFunction] = []() { const auto compositor = Microsoft::ReactNative::GetCompositor(); return std::make_tuple( @@ -18,7 +19,7 @@ std::tuple CalculatedA compositor.CreateLinearEasingFunction()); }(); - m_startValue = GetAnimatedValue()->RawValue(); + m_originalValue = GetAnimatedValue()->RawValue(); std::vector keyFrames = [this]() { std::vector keyFrames; bool done = false; @@ -39,7 +40,7 @@ std::tuple CalculatedA std::chrono::milliseconds duration(static_cast(keyFrames.size() / 60.0f * 1000.0f)); animation.Duration(duration); auto normalizedProgress = 0.0f; - auto fromValue = static_cast(m_startValue); + auto fromValue = static_cast(m_originalValue.value()); animation.InsertKeyFrame(normalizedProgress, fromValue, easingFunction); for (const auto keyFrame : keyFrames) { normalizedProgress = std::min(normalizedProgress + 1.0f / keyFrames.size(), 1.0f); diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/CalculatedAnimationDriver.h b/vnext/Microsoft.ReactNative/Modules/Animated/CalculatedAnimationDriver.h index 41e17722113..884f975a693 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/CalculatedAnimationDriver.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/CalculatedAnimationDriver.h @@ -17,8 +17,6 @@ class CalculatedAnimationDriver : public AnimationDriver { protected: virtual std::tuple GetValueAndVelocityForTime(double time) = 0; - virtual bool IsAnimationDone(double currentValue, std::optional previousValue, double currentVelocity) = 0; - double m_startValue{0}; }; } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/DecayAnimationDriver.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/DecayAnimationDriver.cpp index 44b3bfbd299..42a5da62c3d 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/DecayAnimationDriver.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/DecayAnimationDriver.cpp @@ -20,8 +20,8 @@ DecayAnimationDriver::DecayAnimationDriver( } std::tuple DecayAnimationDriver::GetValueAndVelocityForTime(double time) { - const auto value = - m_startValue + m_velocity / (1 - m_deceleration) * (1 - std::exp(-(1 - m_deceleration) * (1000 * time))); + const auto value = m_originalValue.value() + + m_velocity / (1 - m_deceleration) * (1 - std::exp(-(1 - m_deceleration) * (1000 * time))); return std::make_tuple(static_cast(value), 42.0f); // we don't need the velocity, so set it to a dummy value } @@ -33,4 +33,30 @@ bool DecayAnimationDriver::IsAnimationDone( return previousValue.has_value() && std::abs(currentValue - previousValue.value()) < 0.1; } +bool DecayAnimationDriver::Update(double timeDeltaMs, bool restarting) { + if (const auto node = GetAnimatedValue()) { + if (restarting) { + const auto value = node->RawValue(); + if (!m_originalValue) { + // First iteration, assign m_fromValue based on AnimatedValue + m_originalValue = value; + } else { + // Not the first iteration, reset AnimatedValue based on m_originalValue + node->RawValue(m_originalValue.value()); + } + + m_lastValue = value; + } + + const auto [value, velocity] = GetValueAndVelocityForTime(timeDeltaMs / 1000.0); + if (restarting || IsAnimationDone(value, m_lastValue, 0.0 /* ignored */)) { + m_lastValue = value; + node->RawValue(value); + return false; + } + } + + return true; +} + } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/DecayAnimationDriver.h b/vnext/Microsoft.ReactNative/Modules/Animated/DecayAnimationDriver.h index 5af9d6f51ee..d67200f319f 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/DecayAnimationDriver.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/DecayAnimationDriver.h @@ -17,12 +17,14 @@ class DecayAnimationDriver : public CalculatedAnimationDriver { const std::shared_ptr &manager); protected: + bool Update(double timeDeltaMs, bool restarting) override; std::tuple GetValueAndVelocityForTime(double time) override; bool IsAnimationDone(double currentValue, std::optional previousValue, double currentVelocity) override; private: double m_velocity{0}; double m_deceleration{0}; + double m_lastValue{0}; static constexpr std::string_view s_velocityName{"velocity"}; static constexpr std::string_view s_decelerationName{"deceleration"}; diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/DiffClampAnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/DiffClampAnimatedNode.cpp index 18d6fd4d351..4c8647fa56d 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/DiffClampAnimatedNode.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/DiffClampAnimatedNode.cpp @@ -11,20 +11,35 @@ DiffClampAnimatedNode::DiffClampAnimatedNode( int64_t tag, const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager) - : ValueAnimatedNode(tag, manager) { + : ValueAnimatedNode(tag, config, manager) { m_inputNodeTag = static_cast(config[s_inputName].AsDouble()); + assert(HasCompatibleAnimationDriver(m_inputNodeTag)); m_min = config[s_minName].AsDouble(); m_max = config[s_maxName].AsDouble(); - m_propertySet.StartAnimation(s_valueName, [node = m_inputNodeTag, min = m_min, max = m_max, manager]() { - const auto anim = Microsoft::ReactNative::GetCompositor().CreateExpressionAnimation(); - anim.SetReferenceParameter(s_inputParameterName, manager->GetValueAnimatedNode(node)->PropertySet()); - anim.SetScalarParameter(s_minParameterName, static_cast(min)); - anim.SetScalarParameter(s_maxParameterName, static_cast(max)); - anim.Expression( - static_cast(L"Clamp(") + s_inputParameterName + L"." + s_valueName + L" + " + - s_inputParameterName + L"." + s_offsetName + L", " + s_minParameterName + L", " + s_maxParameterName + L")"); - return anim; - }()); + if (m_useComposition) { + m_propertySet.StartAnimation(s_valueName, [node = m_inputNodeTag, min = m_min, max = m_max, manager]() { + const auto anim = Microsoft::ReactNative::GetCompositor().CreateExpressionAnimation(); + anim.SetReferenceParameter(s_inputParameterName, manager->GetValueAnimatedNode(node)->PropertySet()); + anim.SetScalarParameter(s_minParameterName, static_cast(min)); + anim.SetScalarParameter(s_maxParameterName, static_cast(max)); + anim.Expression( + static_cast(L"Clamp(") + s_inputParameterName + L"." + s_valueName + L" + " + + s_inputParameterName + L"." + s_offsetName + L", " + s_minParameterName + L", " + s_maxParameterName + L")"); + return anim; + }()); + } +} + +void DiffClampAnimatedNode::Update() { + assert(!m_useComposition); + if (const auto manager = m_manager.lock()) { + if (const auto node = manager->GetValueAnimatedNode(m_inputNodeTag)) { + const auto value = node->Value(); + const auto diff = value - m_lastValue; + m_lastValue = value; + RawValue(std::clamp(Value() + diff, m_min, m_max)); + } + } } } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/DiffClampAnimatedNode.h b/vnext/Microsoft.ReactNative/Modules/Animated/DiffClampAnimatedNode.h index 29948a32e5f..9e5b3569ed8 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/DiffClampAnimatedNode.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/DiffClampAnimatedNode.h @@ -12,10 +12,13 @@ class DiffClampAnimatedNode final : public ValueAnimatedNode { const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager); + virtual void Update() override; + private: int64_t m_inputNodeTag{}; double m_min{}; double m_max{}; + double m_lastValue{}; static constexpr std::string_view s_minName{"min"}; static constexpr std::string_view s_maxName{"max"}; diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/DivisionAnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/DivisionAnimatedNode.cpp index 644ae8aa074..5b72fe9febc 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/DivisionAnimatedNode.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/DivisionAnimatedNode.cpp @@ -11,30 +11,53 @@ DivisionAnimatedNode::DivisionAnimatedNode( int64_t tag, const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager) - : ValueAnimatedNode(tag, manager) { + : ValueAnimatedNode(tag, config, manager) { for (const auto &inputNode : config[s_inputName].AsArray()) { + const auto inputTag = inputNode.AsInt64(); + assert(HasCompatibleAnimationDriver(inputTag)); if (m_firstInput == s_firstInputUnset) { - m_firstInput = static_cast(inputNode.AsDouble()); + m_firstInput = inputTag; } else { - m_inputNodes.insert(static_cast(inputNode.AsDouble())); + m_inputNodes.insert(inputTag); } } - m_propertySet.StartAnimation(s_valueName, [firstNode = m_firstInput, nodes = m_inputNodes, manager]() { - const auto anim = Microsoft::ReactNative::GetCompositor().CreateExpressionAnimation(); - - anim.Expression([firstNode, nodes, manager, anim]() { - anim.SetReferenceParameter(s_baseName, manager->GetValueAnimatedNode(firstNode)->PropertySet()); - winrt::hstring expr = static_cast(L"(") + s_baseName + L"." + s_valueName + L" + " + s_baseName + - L"." + s_offsetName + L")"; - for (const auto tag : nodes) { - const auto identifier = L"n" + std::to_wstring(tag); - anim.SetReferenceParameter(identifier, manager->GetValueAnimatedNode(tag)->PropertySet()); - expr = expr + L" / (" + identifier + L"." + s_valueName + L" + " + identifier + L"." + s_offsetName + L")"; - } - return expr; + if (m_useComposition) { + m_propertySet.StartAnimation(s_valueName, [firstNode = m_firstInput, nodes = m_inputNodes, manager]() { + const auto anim = Microsoft::ReactNative::GetCompositor().CreateExpressionAnimation(); + + anim.Expression([firstNode, nodes, manager, anim]() { + anim.SetReferenceParameter(s_baseName, manager->GetValueAnimatedNode(firstNode)->PropertySet()); + winrt::hstring expr = static_cast(L"(") + s_baseName + L"." + s_valueName + L" + " + + s_baseName + L"." + s_offsetName + L")"; + for (const auto tag : nodes) { + const auto identifier = L"n" + std::to_wstring(tag); + anim.SetReferenceParameter(identifier, manager->GetValueAnimatedNode(tag)->PropertySet()); + expr = expr + L" / (" + identifier + L"." + s_valueName + L" + " + identifier + L"." + s_offsetName + L")"; + } + return expr; + }()); + return anim; }()); - return anim; - }()); + } +} + +void DivisionAnimatedNode::Update() { + assert(!m_useComposition); + if (const auto manager = m_manager.lock()) { + auto rawValue = 0.0; + if (const auto firstNode = manager->GetValueAnimatedNode(m_firstInput)) { + rawValue = firstNode->Value(); + } + + for (const auto tag : m_inputNodes) { + if (const auto node = manager->GetValueAnimatedNode(tag)) { + const auto value = node->Value(); + rawValue /= value; + } + } + + RawValue(rawValue); + } } } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/DivisionAnimatedNode.h b/vnext/Microsoft.ReactNative/Modules/Animated/DivisionAnimatedNode.h index e9edb663671..9e4c89a1dc7 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/DivisionAnimatedNode.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/DivisionAnimatedNode.h @@ -12,6 +12,8 @@ class DivisionAnimatedNode final : public ValueAnimatedNode { const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager); + virtual void Update() override; + private: int64_t m_firstInput{s_firstInputUnset}; std::unordered_set m_inputNodes{}; diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/FrameAnimationDriver.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/FrameAnimationDriver.cpp index 5dd9b5a7053..a42407f3b3d 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/FrameAnimationDriver.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/FrameAnimationDriver.cpp @@ -3,6 +3,7 @@ #include "pch.h" +#include "AnimationUtils.h" #include "FrameAnimationDriver.h" #include "Utils/Helpers.h" @@ -22,6 +23,7 @@ FrameAnimationDriver::FrameAnimationDriver( std::tuple FrameAnimationDriver::MakeAnimation( const winrt::Microsoft::ReactNative::JSValueObject & /*config*/) { + assert(m_useComposition); const auto [scopedBatch, animation] = []() { const auto compositor = Microsoft::ReactNative::GetCompositor(); return std::make_tuple( @@ -32,7 +34,7 @@ std::tuple FrameAnimat // Frames contains 60 values per second of duration of the animation, convert // the size of frames to duration in ms. - std::chrono::milliseconds duration(static_cast(m_frames.size() * 1000.0 / 60.0)); + std::chrono::milliseconds duration(static_cast(m_frames.size() * s_frameDurationMs)); animation.Duration(duration); auto normalizedProgress = 0.0f; @@ -57,4 +59,39 @@ double FrameAnimationDriver::ToValue() { return m_toValue; } +bool FrameAnimationDriver::Update(double timeDeltaMs, bool restarting) { + assert(!m_useComposition); + if (const auto node = GetAnimatedValue()) { + if (!m_startValue) { + m_startValue = node->RawValue(); + } + + const auto startValue = m_startValue.value(); + const auto startIndex = static_cast(timeDeltaMs / s_frameDurationMs); + assert(startIndex >= 0); + const auto nextIndex = startIndex + 1; + + double nextValue; + auto isComplete = false; + if (nextIndex >= m_frames.size()) { + nextValue = m_toValue; + isComplete = true; + } else { + const auto fromInterval = startIndex * s_frameDurationMs; + const auto toInterval = nextIndex * s_frameDurationMs; + const auto fromValue = m_frames[startIndex]; + const auto toValue = m_frames[nextIndex]; + const auto frameOutput = Interpolate( + timeDeltaMs, fromInterval, toInterval, fromValue, toValue, ExtrapolateTypeExtend, ExtrapolateTypeExtend); + nextValue = Interpolate(frameOutput, 0, 1, startValue, m_toValue, ExtrapolateTypeExtend, ExtrapolateTypeExtend); + } + + node->RawValue(nextValue); + + return isComplete; + } + + return true; +} + } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/FrameAnimationDriver.h b/vnext/Microsoft.ReactNative/Modules/Animated/FrameAnimationDriver.h index d0ae2fa7b3c..01203085cf0 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/FrameAnimationDriver.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/FrameAnimationDriver.h @@ -22,11 +22,16 @@ class FrameAnimationDriver : public AnimationDriver { double ToValue() override; inline std::vector Frames() override { + assert(m_useComposition); return m_frames; } + protected: + bool Update(double timeDeltaMs, bool restarting) override; + private: std::vector m_frames{}; double m_toValue{0}; + std::optional m_startValue{}; }; } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/InterpolationAnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/InterpolationAnimatedNode.cpp index 3b4089a31a4..dfd3db790db 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/InterpolationAnimatedNode.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/InterpolationAnimatedNode.cpp @@ -3,6 +3,7 @@ #include "pch.h" +#include "AnimationUtils.h" #include "ExtrapolationType.h" #include "InterpolationAnimatedNode.h" #include "NativeAnimatedNodeManager.h" @@ -12,7 +13,7 @@ InterpolationAnimatedNode::InterpolationAnimatedNode( int64_t tag, const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager) - : ValueAnimatedNode(tag, manager) { + : ValueAnimatedNode(tag, config, manager) { for (const auto &rangeValue : config[s_inputRangeName].AsArray()) { m_inputRanges.push_back(rangeValue.AsDouble()); } @@ -24,49 +25,66 @@ InterpolationAnimatedNode::InterpolationAnimatedNode( m_extrapolateRight = config[s_extrapolateRightName].AsString(); } -void InterpolationAnimatedNode::Update() {} +void InterpolationAnimatedNode::Update() { + assert(!m_useComposition); + if (m_parentTag == s_parentTagUnset) { + return; + } + + if (const auto manager = m_manager.lock()) { + if (const auto node = manager->GetValueAnimatedNode(m_parentTag)) { + RawValue(InterpolateValue(node->Value())); + } + } +} void InterpolationAnimatedNode::OnDetachedFromNode([[maybe_unused]] int64_t animatedNodeTag) { assert(m_parentTag == animatedNodeTag); m_parentTag = s_parentTagUnset; - m_propertySet.StopAnimation(s_valueName); - m_propertySet.StopAnimation(s_offsetName); - m_rawValueAnimation = nullptr; - m_offsetAnimation = nullptr; + + if (m_useComposition) { + m_propertySet.StopAnimation(s_valueName); + m_propertySet.StopAnimation(s_offsetName); + m_rawValueAnimation = nullptr; + m_offsetAnimation = nullptr; + } } void InterpolationAnimatedNode::OnAttachToNode(int64_t animatedNodeTag) { + assert(HasCompatibleAnimationDriver(animatedNodeTag)); assert(m_parentTag == s_parentTagUnset); m_parentTag = animatedNodeTag; - const auto [rawValueAnimation, offsetAnimation] = [this]() { - if (const auto manager = m_manager.lock()) { - if (const auto parent = manager->GetValueAnimatedNode(m_parentTag)) { - const auto compositor = Microsoft::ReactNative::GetCompositor(); - - const auto rawValueAnimation = CreateExpressionAnimation(compositor, *parent); - rawValueAnimation.Expression( - GetExpression(s_parentPropsName + static_cast(L".") + s_valueName)); - - const auto offsetAnimation = CreateExpressionAnimation(compositor, *parent); - offsetAnimation.Expression( - L"(" + - GetExpression( - s_parentPropsName + static_cast(L".") + s_offsetName + L" + " + s_parentPropsName + - L"." + s_valueName) + - L") - this.target." + s_valueName); - - return std::make_tuple(rawValueAnimation, offsetAnimation); + if (m_useComposition) { + const auto [rawValueAnimation, offsetAnimation] = [this]() { + if (const auto manager = m_manager.lock()) { + if (const auto parent = manager->GetValueAnimatedNode(m_parentTag)) { + const auto compositor = Microsoft::ReactNative::GetCompositor(); + + const auto rawValueAnimation = CreateExpressionAnimation(compositor, *parent); + rawValueAnimation.Expression( + GetExpression(s_parentPropsName + static_cast(L".") + s_valueName)); + + const auto offsetAnimation = CreateExpressionAnimation(compositor, *parent); + offsetAnimation.Expression( + L"(" + + GetExpression( + s_parentPropsName + static_cast(L".") + s_offsetName + L" + " + s_parentPropsName + + L"." + s_valueName) + + L") - this.target." + s_valueName); + + return std::make_tuple(rawValueAnimation, offsetAnimation); + } } - } - return std::tuple(nullptr, nullptr); - }(); + return std::tuple(nullptr, nullptr); + }(); - m_propertySet.StartAnimation(s_valueName, rawValueAnimation); - m_propertySet.StartAnimation(s_offsetName, offsetAnimation); + m_propertySet.StartAnimation(s_valueName, rawValueAnimation); + m_propertySet.StartAnimation(s_offsetName, offsetAnimation); - m_rawValueAnimation = rawValueAnimation; - m_offsetAnimation = offsetAnimation; + m_rawValueAnimation = rawValueAnimation; + m_offsetAnimation = offsetAnimation; + } } comp::ExpressionAnimation InterpolationAnimatedNode::CreateExpressionAnimation( @@ -168,4 +186,24 @@ winrt::hstring InterpolationAnimatedNode::GetRightExpression( } } +double InterpolationAnimatedNode::InterpolateValue(double value) { + // Compute range index + size_t index = 1; + for (; index < m_inputRanges.size() - 1; ++index) { + if (m_inputRanges[index] >= value) { + break; + } + } + index--; + + return Interpolate( + value, + m_inputRanges[index], + m_inputRanges[index + 1], + m_outputRanges[index], + m_outputRanges[index + 1], + m_extrapolateLeft, + m_extrapolateRight); +} + } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/InterpolationAnimatedNode.h b/vnext/Microsoft.ReactNative/Modules/Animated/InterpolationAnimatedNode.h index dc0d06450ef..84321c4ee91 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/InterpolationAnimatedNode.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/InterpolationAnimatedNode.h @@ -17,9 +17,9 @@ class InterpolationAnimatedNode final : public ValueAnimatedNode { virtual void OnDetachedFromNode(int64_t animatedNodeTag) override; virtual void OnAttachToNode(int64_t animatedNodeTag) override; - static constexpr std::wstring_view ExtrapolateTypeIdentity = L"identity"; - static constexpr std::wstring_view ExtrapolateTypeClamp = L"clamp"; - static constexpr std::wstring_view ExtrapolateTypeExtend = L"extend"; + static constexpr std::string_view ExtrapolateTypeIdentity = "identity"; + static constexpr std::string_view ExtrapolateTypeClamp = "clamp"; + static constexpr std::string_view ExtrapolateTypeExtend = "extend"; private: comp::ExpressionAnimation CreateExpressionAnimation(const winrt::Compositor &compositor, ValueAnimatedNode &parent); @@ -34,6 +34,8 @@ class InterpolationAnimatedNode final : public ValueAnimatedNode { winrt::hstring GetLeftExpression(const winrt::hstring &value, const winrt::hstring &leftInterpolateExpression); winrt::hstring GetRightExpression(const winrt::hstring &, const winrt::hstring &rightInterpolateExpression); + double InterpolateValue(double value); + comp::ExpressionAnimation m_rawValueAnimation{nullptr}; comp::ExpressionAnimation m_offsetAnimation{nullptr}; std::vector m_inputRanges; diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/ModulusAnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/ModulusAnimatedNode.cpp index 68475ebd96e..e46aa4df206 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/ModulusAnimatedNode.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/ModulusAnimatedNode.cpp @@ -11,18 +11,30 @@ ModulusAnimatedNode::ModulusAnimatedNode( int64_t tag, const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager) - : ValueAnimatedNode(tag, manager) { - m_inputNodeTag = static_cast(config[s_inputName].AsDouble()); - m_modulus = static_cast(config[s_modulusName].AsDouble()); + : ValueAnimatedNode(tag, config, manager) { + m_inputNodeTag = config[s_inputName].AsInt64(); + assert(HasCompatibleAnimationDriver(m_inputNodeTag)); + m_modulus = config[s_modulusName].AsInt64(); - m_propertySet.StartAnimation(s_valueName, [node = m_inputNodeTag, mod = m_modulus, manager]() { - const auto anim = Microsoft::ReactNative::GetCompositor().CreateExpressionAnimation(); - anim.SetReferenceParameter(s_inputParameterName, manager->GetValueAnimatedNode(node)->PropertySet()); - anim.SetScalarParameter(s_modName, static_cast(mod)); - anim.Expression( - static_cast(L"(") + s_inputParameterName + L"." + s_valueName + L" + " + s_inputParameterName + - L"." + s_offsetName + L") % " + s_modName); - return anim; - }()); + if (m_useComposition) { + m_propertySet.StartAnimation(s_valueName, [node = m_inputNodeTag, mod = m_modulus, manager]() { + const auto anim = Microsoft::ReactNative::GetCompositor().CreateExpressionAnimation(); + anim.SetReferenceParameter(s_inputParameterName, manager->GetValueAnimatedNode(node)->PropertySet()); + anim.SetScalarParameter(s_modName, static_cast(mod)); + anim.Expression( + static_cast(L"(") + s_inputParameterName + L"." + s_valueName + L" + " + + s_inputParameterName + L"." + s_offsetName + L") % " + s_modName); + return anim; + }()); + } +} + +void ModulusAnimatedNode::Update() { + assert(!m_useComposition); + if (const auto manager = m_manager.lock()) { + if (const auto node = manager->GetValueAnimatedNode(m_inputNodeTag)) { + RawValue(std::fmod(node->Value(), m_modulus)); + } + } } } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/ModulusAnimatedNode.h b/vnext/Microsoft.ReactNative/Modules/Animated/ModulusAnimatedNode.h index bcb90a33daa..53b96232e45 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/ModulusAnimatedNode.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/ModulusAnimatedNode.h @@ -13,6 +13,8 @@ class ModulusAnimatedNode final : public ValueAnimatedNode { const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager); + virtual void Update() override; + private: int64_t m_inputNodeTag{}; int64_t m_modulus{}; diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/MultiplicationAnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/MultiplicationAnimatedNode.cpp index 84767dc3dfc..c70b02e0f11 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/MultiplicationAnimatedNode.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/MultiplicationAnimatedNode.cpp @@ -11,24 +11,41 @@ MultiplicationAnimatedNode::MultiplicationAnimatedNode( int64_t tag, const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager) - : ValueAnimatedNode(tag, manager) { + : ValueAnimatedNode(tag, config, manager) { for (const auto &inputNode : config[s_inputName].AsArray()) { - m_inputNodes.insert(static_cast(inputNode.AsDouble())); + const auto inputTag = inputNode.AsInt64(); + assert(HasCompatibleAnimationDriver(inputTag)); + m_inputNodes.insert(inputTag); } - m_propertySet.StartAnimation(s_valueName, [nodes = m_inputNodes, manager]() { - const auto anim = Microsoft::ReactNative::GetCompositor().CreateExpressionAnimation(); + if (m_useComposition) { + m_propertySet.StartAnimation(s_valueName, [nodes = m_inputNodes, manager]() { + const auto anim = Microsoft::ReactNative::GetCompositor().CreateExpressionAnimation(); - anim.Expression([nodes, manager, anim]() { - winrt::hstring expr = L"1"; - for (const auto tag : nodes) { - auto identifier = L"n" + std::to_wstring(tag); - anim.SetReferenceParameter(identifier, manager->GetValueAnimatedNode(tag)->PropertySet()); - expr = expr + L" * (" + identifier + L"." + s_valueName + L" + " + identifier + L"." + s_offsetName + L")"; - } - return expr; + anim.Expression([nodes, manager, anim]() { + winrt::hstring expr = L"1"; + for (const auto tag : nodes) { + auto identifier = L"n" + std::to_wstring(tag); + anim.SetReferenceParameter(identifier, manager->GetValueAnimatedNode(tag)->PropertySet()); + expr = expr + L" * (" + identifier + L"." + s_valueName + L" + " + identifier + L"." + s_offsetName + L")"; + } + return expr; + }()); + return anim; }()); - return anim; - }()); + } +} + +void MultiplicationAnimatedNode::Update() { + assert(!m_useComposition); + auto rawValue = 1.0; + if (const auto manager = m_manager.lock()) { + for (const auto tag : m_inputNodes) { + if (const auto node = manager->GetValueAnimatedNode(tag)) { + rawValue *= node->Value(); + } + } + } + RawValue(rawValue); } } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/MultiplicationAnimatedNode.h b/vnext/Microsoft.ReactNative/Modules/Animated/MultiplicationAnimatedNode.h index 5c61227a4db..1c60d95f71c 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/MultiplicationAnimatedNode.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/MultiplicationAnimatedNode.h @@ -12,6 +12,8 @@ class MultiplicationAnimatedNode final : public ValueAnimatedNode { const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager); + virtual void Update() override; + private: std::unordered_set m_inputNodes{}; }; diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedModule.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedModule.cpp index ecf11402948..1511744b88c 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedModule.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedModule.cpp @@ -54,11 +54,29 @@ void NativeAnimatedModule::getValue(double tag, std::function cons } void NativeAnimatedModule::startListeningToAnimatedNodeValue(double tag) noexcept { - // NYI + winrt::Microsoft::ReactNative::implementation::ReactCoreInjection::PostToUIBatchingQueue( + m_context.Handle(), [wkThis = std::weak_ptr(this->shared_from_this()), nodeTag = static_cast(tag)]() { + if (auto pThis = wkThis.lock()) { + pThis->m_nodesManager->StartListeningToAnimatedNodeValue( + nodeTag, [context = pThis->m_context, nodeTag](double value) { + // This event could be coalesced, however it doesn't appear to be + // coalesced on Android or iOS, so leaving it without coalescing. + context.EmitJSEvent( + L"RCTDeviceEventEmitter", + L"onAnimatedValueUpdate", + ::React::JSValueObject{{"tag", nodeTag}, {"value", value}}); + }); + } + }); } void NativeAnimatedModule::stopListeningToAnimatedNodeValue(double tag) noexcept { - // NYI + winrt::Microsoft::ReactNative::implementation::ReactCoreInjection::PostToUIBatchingQueue( + m_context.Handle(), [wkThis = std::weak_ptr(this->shared_from_this()), nodeTag = static_cast(tag)]() { + if (auto pThis = wkThis.lock()) { + pThis->m_nodesManager->StopListeningToAnimatedNodeValue(nodeTag); + } + }); } void NativeAnimatedModule::connectAnimatedNodes(double parentTag, double childTag) noexcept { @@ -183,13 +201,20 @@ void NativeAnimatedModule::disconnectAnimatedNodeFromView(double nodeTag, double nodeTag = static_cast(nodeTag), viewTag = static_cast(viewTag)]() { if (auto pThis = wkThis.lock()) { + pThis->m_nodesManager->RestoreDefaultValues(viewTag); pThis->m_nodesManager->DisconnectAnimatedNodeToView(nodeTag, viewTag); } }); } void NativeAnimatedModule::restoreDefaultValues(double nodeTag) noexcept { - // NYI + winrt::Microsoft::ReactNative::implementation::ReactCoreInjection::PostToUIBatchingQueue( + m_context.Handle(), + [wkThis = std::weak_ptr(this->shared_from_this()), nodeTag = static_cast(nodeTag)]() { + if (auto pThis = wkThis.lock()) { + pThis->m_nodesManager->RestoreDefaultValues(nodeTag); + } + }); } void NativeAnimatedModule::dropAnimatedNode(double tag) noexcept { diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.cpp index 3601eeff957..6331e2f1638 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.cpp @@ -25,6 +25,7 @@ #include #include #include +#include namespace Microsoft::ReactNative { void NativeAnimatedNodeManager::CreateAnimatedNode( @@ -103,22 +104,41 @@ void NativeAnimatedNodeManager::GetValue( } void NativeAnimatedNodeManager::ConnectAnimatedNodeToView(int64_t propsNodeTag, int64_t viewTag) { - m_propsNodes.at(propsNodeTag)->ConnectToView(viewTag); + const auto &propsNode = m_propsNodes.at(propsNodeTag); + propsNode->ConnectToView(viewTag); + if (!propsNode->UseComposition()) { + m_updatedNodes.insert(propsNodeTag); + EnsureRendering(); + } } void NativeAnimatedNodeManager::DisconnectAnimatedNodeToView(int64_t propsNodeTag, int64_t viewTag) { m_propsNodes.at(propsNodeTag)->DisconnectFromView(viewTag); } +void NativeAnimatedNodeManager::RestoreDefaultValues(int64_t tag) { + if (const auto propsNode = GetPropsAnimatedNode(tag)) { + propsNode->RestoreDefaultValues(); + } +} + void NativeAnimatedNodeManager::ConnectAnimatedNode(int64_t parentNodeTag, int64_t childNodeTag) { if (const auto parentNode = GetAnimatedNode(parentNodeTag)) { parentNode->AddChild(childNodeTag); + if (!parentNode->UseComposition()) { + m_updatedNodes.insert(childNodeTag); + EnsureRendering(); + } } } void NativeAnimatedNodeManager::DisconnectAnimatedNode(int64_t parentNodeTag, int64_t childNodeTag) { if (const auto parentNode = GetAnimatedNode(parentNodeTag)) { parentNode->RemoveChild(childNodeTag); + if (!parentNode->UseComposition()) { + m_updatedNodes.insert(childNodeTag); + EnsureRendering(); + } } } @@ -127,33 +147,38 @@ void NativeAnimatedNodeManager::StopAnimation(int64_t animationId, bool isTracki if (const auto animation = m_activeAnimations.at(animationId)) { animation->StopAnimation(isTrackingAnimation); - // Insert the animation into the pending completion set to ensure it is - // not destroyed before the callback occurs. It's safe to assume the - // scoped batch completion callback has not run, since if it had, the - // animation would have been removed from the set of active animations. - m_pendingCompletionAnimations.insert({animationId, animation}); - - const auto nodeTag = animation->AnimatedValueTag(); - if (nodeTag != -1) { - const auto deferredAnimation = m_deferredAnimationForValues.find(nodeTag); - if (deferredAnimation != m_deferredAnimationForValues.end()) { - // We can assume that the currently deferred animation is the one - // being stopped given the constraint that only one animation can - // be active for a given value node. - assert(deferredAnimation->second == animationId); - // If the animation is deferred, just remove the deferred animation - // entry as two animations cannot animate the same value concurrently. - m_deferredAnimationForValues.erase(nodeTag); - } else { - // Since only one animation can be active at a time, there shouldn't - // be any stopped animations for the value node if the animation has - // not been deferred. - assert(!m_valuesWithStoppedAnimation.count(nodeTag)); - // In this case, add the value tag to the set of values with stopped - // animations. This is used to optimize the lookup when determining - // if an animation needs to be deferred (rather than iterating over - // the map of pending completion animations). - m_valuesWithStoppedAnimation.insert(nodeTag); + // We need to update the node manager state for composition animations + // to ensure new animations on the same animated value do not start until + // the completion callback has fired for the stopped animation. + if (animation->UseComposition()) { + // Insert the animation into the pending completion set to ensure it is + // not destroyed before the callback occurs. It's safe to assume the + // scoped batch completion callback has not run, since if it had, the + // animation would have been removed from the set of active animations. + m_pendingCompletionAnimations.insert({animationId, animation}); + + const auto nodeTag = animation->AnimatedValueTag(); + if (nodeTag != -1) { + const auto deferredAnimation = m_deferredAnimationForValues.find(nodeTag); + if (deferredAnimation != m_deferredAnimationForValues.end()) { + // We can assume that the currently deferred animation is the one + // being stopped given the constraint that only one animation can + // be active for a given value node. + assert(deferredAnimation->second == animationId); + // If the animation is deferred, just remove the deferred animation + // entry as two animations cannot animate the same value concurrently. + m_deferredAnimationForValues.erase(nodeTag); + } else { + // Since only one animation can be active at a time, there shouldn't + // be any stopped animations for the value node if the animation has + // not been deferred. + assert(!m_valuesWithStoppedAnimation.count(nodeTag)); + // In this case, add the value tag to the set of values with stopped + // animations. This is used to optimize the lookup when determining + // if an animation needs to be deferred (rather than iterating over + // the map of pending completion animations). + m_valuesWithStoppedAnimation.insert(nodeTag); + } } } @@ -168,6 +193,7 @@ void NativeAnimatedNodeManager::RestartTrackingAnimatedNode( const std::shared_ptr &manager) { if (m_activeAnimations.count(animationId)) { if (const auto animation = m_activeAnimations.at(animationId).get()) { + assert(animation->UseComposition()); auto const animatedValueTag = animation->AnimatedValueTag(); auto const &animationConfig = animation->AnimationConfig(); auto const endCallback = animation->EndCallback(); @@ -273,14 +299,19 @@ void NativeAnimatedNodeManager::StartAnimatingNode( // If the animated value node has any stopped animations, defer start until // all stopped animations fire completion callback and have latest values. if (m_activeAnimations.count(animationId)) { - if (m_valuesWithStoppedAnimation.count(animatedNodeTag)) { - // Since only one animation can be active per value at a time, there will - // not be any other deferred animations for the value node. - assert(!m_deferredAnimationForValues.count(animatedNodeTag)); - // Add the animation to the deferred animation map for the value tag. - m_deferredAnimationForValues.insert({animatedNodeTag, animationId}); + const auto &animation = m_activeAnimations.at(animationId); + if (animation->UseComposition()) { + if (m_valuesWithStoppedAnimation.count(animatedNodeTag)) { + // Since only one animation can be active per value at a time, there will + // not be any other deferred animations for the value node. + assert(!m_deferredAnimationForValues.count(animatedNodeTag)); + // Add the animation to the deferred animation map for the value tag. + m_deferredAnimationForValues.insert({animatedNodeTag, animationId}); + } else { + StartAnimationAndTrackingNodes(animationId, animatedNodeTag, manager); + } } else { - StartAnimationAndTrackingNodes(animationId, animatedNodeTag, manager); + EnsureRendering(); } } } @@ -290,17 +321,27 @@ void NativeAnimatedNodeManager::DropAnimatedNode(int64_t tag) { m_propsNodes.erase(tag); m_styleNodes.erase(tag); m_transformNodes.erase(tag); + m_updatedNodes.erase(tag); } void NativeAnimatedNodeManager::SetAnimatedNodeValue(int64_t tag, double value) { if (const auto valueNode = m_valueNodes.at(tag).get()) { valueNode->RawValue(static_cast(value)); + if (!valueNode->UseComposition()) { + StopAnimationsForNode(tag); + m_updatedNodes.insert(tag); + EnsureRendering(); + } } } void NativeAnimatedNodeManager::SetAnimatedNodeOffset(int64_t tag, double offset) { if (const auto valueNode = m_valueNodes.at(tag).get()) { valueNode->Offset(static_cast(offset)); + if (!valueNode->UseComposition()) { + m_updatedNodes.insert(tag); + EnsureRendering(); + } } } @@ -316,6 +357,18 @@ void NativeAnimatedNodeManager::ExtractAnimatedNodeOffset(int64_t tag) { } } +void NativeAnimatedNodeManager::StartListeningToAnimatedNodeValue(int64_t tag, const ValueListenerCallback &callback) { + if (const auto valueNode = m_valueNodes.at(tag).get()) { + valueNode->ValueListener(callback); + } +} + +void NativeAnimatedNodeManager::StopListeningToAnimatedNodeValue(int64_t tag) { + if (const auto valueNode = m_valueNodes.at(tag).get()) { + valueNode->ValueListener(nullptr); + } +} + void NativeAnimatedNodeManager::AddAnimatedEventToView( int64_t viewTag, const std::string &eventName, @@ -374,6 +427,9 @@ void NativeAnimatedNodeManager::ProcessDelayedPropsNodes() { void NativeAnimatedNodeManager::AddDelayedPropsNode( int64_t propsNodeTag, const winrt::Microsoft::ReactNative::ReactContext &context) { +#if DEBUG + assert(m_propsNodes.at(propsNodeTag)->UseComposition()); +#endif m_delayedPropsNodes.push_back(propsNodeTag); if (m_delayedPropsNodes.size() <= 1) { if (const auto uiManger = Microsoft::ReactNative::GetNativeUIManager(context).lock()) { @@ -478,4 +534,176 @@ void NativeAnimatedNodeManager::StartAnimationAndTrackingNodes( } } } + +void NativeAnimatedNodeManager::RunUpdates(winrt::TimeSpan renderingTime) { + auto hasFinishedAnimations = false; + std::unordered_set updatingNodes{}; + updatingNodes = std::move(m_updatedNodes); + + // Increment animation drivers + for (auto id : m_activeAnimationIds) { + auto &animation = m_activeAnimations.at(id); + animation->RunAnimationStep(renderingTime); + updatingNodes.insert(animation->AnimatedValueTag()); + if (animation->IsComplete()) { + hasFinishedAnimations = true; + } + } + + UpdateNodes(updatingNodes); + + if (hasFinishedAnimations) { + for (auto id : m_activeAnimationIds) { + auto &animation = m_activeAnimations.at(id); + if (animation->IsComplete()) { + animation->DoCallback(true); + m_activeAnimations.erase(id); + } + } + } +} + +void NativeAnimatedNodeManager::EnsureRendering() { + m_renderingRevoker = + xaml::Media::CompositionTarget::Rendering(winrt::auto_revoke, {this, &NativeAnimatedNodeManager::OnRendering}); +} + +void NativeAnimatedNodeManager::OnRendering(winrt::IInspectable const &sender, winrt::IInspectable const &args) { + // The `UpdateActiveAnimationIds` method only tracks animations where + // composition is not used, so if only UI.Composition animations are active, + // this rendering callback will not run. + UpdateActiveAnimationIds(); + if (m_activeAnimationIds.size() > 0 || m_updatedNodes.size() > 0) { + if (const auto renderingArgs = args.try_as()) { + RunUpdates(renderingArgs.RenderingTime()); + } + } else { + m_renderingRevoker.revoke(); + } +} + +void NativeAnimatedNodeManager::StopAnimationsForNode(int64_t tag) { + UpdateActiveAnimationIds(); + for (auto id : m_activeAnimationIds) { + auto &animation = m_activeAnimations.at(id); + if (tag == animation->AnimatedValueTag()) { + animation->DoCallback(false); + m_activeAnimations.erase(id); + } + } +} + +void NativeAnimatedNodeManager::UpdateActiveAnimationIds() { + m_activeAnimationIds.clear(); + for (const auto &pair : m_activeAnimations) { + if (!pair.second->UseComposition()) { + m_activeAnimationIds.push_back(pair.first); + } + } +} + +void NativeAnimatedNodeManager::UpdateNodes(std::unordered_set &nodes) { + auto activeNodesCount = 0; + auto updatedNodesCount = 0; + + // BFS state + std::unordered_map bfsColors; + std::unordered_map incomingNodeCounts; + + // STEP 1. + // BFS over graph of nodes starting from IDs in `nodes` argument and IDs that are attached to + // active animations (from `m_activeAnimations)`. Update `incomingNodeCounts` map for each node + // during that BFS. Store number of visited nodes in `activeNodesCount`. We "execute" active + // animations as a part of this step. + + m_animatedGraphBFSColor++; /* use new color */ + if (m_animatedGraphBFSColor == 0) { + // value "0" is used as an initial color for a new node, using it in BFS may cause some nodes to be skipped. + m_animatedGraphBFSColor++; + } + + std::queue nodesQueue{}; + for (auto id : nodes) { + if (!bfsColors.count(id) || bfsColors.at(id) != m_animatedGraphBFSColor) { + bfsColors[id] = m_animatedGraphBFSColor; + activeNodesCount++; + nodesQueue.push(id); + } + } + + while (nodesQueue.size() > 0) { + auto id = nodesQueue.front(); + nodesQueue.pop(); + if (auto node = GetAnimatedNode(id)) { + for (auto &childId : node->Children()) { + if (!incomingNodeCounts.count(childId)) { + incomingNodeCounts[childId] = 1; + } else { + incomingNodeCounts.at(childId)++; + } + + if (!bfsColors.count(childId) || bfsColors.at(childId) != m_animatedGraphBFSColor) { + bfsColors[childId] = m_animatedGraphBFSColor; + activeNodesCount++; + nodesQueue.push(childId); + } + } + } + } + + // STEP 2 + // BFS over the graph of active nodes in topological order -> visit node only when all its + // "predecessors" in the graph have already been visited. It is important to visit nodes in that + // order as they may often use values of their predecessors in order to calculate "next state" + // of their own. We start by determining the starting set of nodes by looking for nodes with + // `incomingNodeCounts[id] = 0` (those can only be the ones that we start BFS in the previous + // step). We store number of visited nodes in this step in `updatedNodesCount` + + m_animatedGraphBFSColor++; + if (m_animatedGraphBFSColor == 0) { + // see reasoning for this check a few lines above + m_animatedGraphBFSColor++; + } + + // find nodes with zero "incoming nodes", those can be either nodes from `m_updatedNodes` or + // ones connected to active animations + for (auto id : nodes) { + if (!incomingNodeCounts.count(id) || + incomingNodeCounts.at(id) == 0 && bfsColors.at(id) != m_animatedGraphBFSColor) { + bfsColors[id] = m_animatedGraphBFSColor; + updatedNodesCount++; + nodesQueue.push(id); + } + } + + // Run main "update" loop + while (nodesQueue.size() > 0) { + auto id = nodesQueue.front(); + nodesQueue.pop(); + if (auto node = GetAnimatedNode(id)) { + node->Update(); + if (auto propsNode = GetPropsAnimatedNode(id)) { + propsNode->UpdateView(); + } else if (auto valueNode = GetValueAnimatedNode(id)) { + valueNode->OnValueUpdate(); + } + + for (auto &childId : node->Children()) { + auto &incomingCount = incomingNodeCounts.at(childId); + auto &bfsColor = bfsColors.at(childId); + incomingCount--; + if (bfsColor != m_animatedGraphBFSColor && incomingCount == 0) { + bfsColor = m_animatedGraphBFSColor; + updatedNodesCount++; + nodesQueue.push(childId); + } + } + } + } + + // Verify that we've visited *all* active nodes. Throw otherwise as this would mean there is a + // cycle in animated node graph. We also take advantage of the fact that all active nodes are + // visited in the step above so that `incomingNodeCounts` for all node IDs are set to zero + assert(activeNodesCount == updatedNodesCount); +} } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.h b/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.h index e5198b24de1..3a0d1fcdb1c 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.h @@ -5,6 +5,7 @@ // Licensed under the MIT License. #include +#include #include #include #include "AnimatedNode.h" @@ -30,6 +31,7 @@ namespace Microsoft::ReactNative { /// typedef std::function EndCallback; +typedef std::function ValueListenerCallback; class AnimatedNode; class StyleAnimatedNode; @@ -49,6 +51,7 @@ class NativeAnimatedNodeManager { void GetValue(int64_t animatedNodeTag, std::function const &endCallback); void ConnectAnimatedNodeToView(int64_t propsNodeTag, int64_t viewTag); void DisconnectAnimatedNodeToView(int64_t propsNodeTag, int64_t viewTag); + void RestoreDefaultValues(int64_t tag); void ConnectAnimatedNode(int64_t parentNodeTag, int64_t childNodeTag); void DisconnectAnimatedNode(int64_t parentNodeTag, int64_t childNodeTag); void StopAnimation(int64_t animationId, bool isTrackingAnimation = false); @@ -75,6 +78,8 @@ class NativeAnimatedNodeManager { void SetAnimatedNodeOffset(int64_t tag, double offset); void FlattenAnimatedNodeOffset(int64_t tag); void ExtractAnimatedNodeOffset(int64_t tag); + void StartListeningToAnimatedNodeValue(int64_t tag, const ValueListenerCallback &callback); + void StopListeningToAnimatedNodeValue(int64_t tag); void AddAnimatedEventToView( int64_t viewTag, const std::string &eventName, @@ -102,6 +107,13 @@ class NativeAnimatedNodeManager { const std::shared_ptr &manager); private: + void EnsureRendering(); + void OnRendering(winrt::IInspectable const &sender, winrt::IInspectable const &args); + void RunUpdates(winrt::TimeSpan renderingTime); + void StopAnimationsForNode(int64_t tag); + void UpdateActiveAnimationIds(); + void UpdateNodes(std::unordered_set &nodes); + std::unordered_map> m_valueNodes{}; std::unordered_map> m_propsNodes{}; std::unordered_map> m_styleNodes{}; @@ -116,6 +128,11 @@ class NativeAnimatedNodeManager { std::vector> m_trackingAndLeadNodeTags{}; std::vector m_delayedPropsNodes{}; + std::unordered_set m_updatedNodes{}; + std::vector m_activeAnimationIds{}; + int64_t m_animatedGraphBFSColor{}; + xaml::Media::CompositionTarget::Rendering_revoker m_renderingRevoker; + static constexpr std::string_view s_toValueIdName{"toValue"}; static constexpr std::string_view s_framesName{"frames"}; static constexpr std::string_view s_dynamicToValuesName{"dynamicToValues"}; diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/PropsAnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/PropsAnimatedNode.cpp index a0748d68243..daa0f63f79d 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/PropsAnimatedNode.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/PropsAnimatedNode.cpp @@ -18,25 +18,29 @@ PropsAnimatedNode::PropsAnimatedNode( const winrt::Microsoft::ReactNative::JSValueObject &config, const winrt::Microsoft::ReactNative::ReactContext &context, const std::shared_ptr &manager) - : AnimatedNode(tag, manager), m_context(context) { + : AnimatedNode(tag, config, manager), m_context(context) { for (const auto &entry : config["props"].AsObject()) { - m_propMapping.insert({entry.first, static_cast(entry.second.AsDouble())}); + const auto inputTag = entry.second.AsInt64(); + m_propMapping.insert({entry.first, inputTag}); } - auto compositor = Microsoft::ReactNative::GetCompositor(); - m_subchannelPropertySet = compositor.CreatePropertySet(); - m_subchannelPropertySet.InsertScalar(L"TranslationX", 0.0f); - m_subchannelPropertySet.InsertScalar(L"TranslationY", 0.0f); - m_subchannelPropertySet.InsertScalar(L"ScaleX", 1.0f); - m_subchannelPropertySet.InsertScalar(L"ScaleY", 1.0f); - m_translationCombined = - compositor.CreateExpressionAnimation(L"Vector3(subchannels.TranslationX, subchannels.TranslationY, 0.0)"); - m_translationCombined.SetReferenceParameter(L"subchannels", m_subchannelPropertySet); - m_translationCombined.Target(L"Translation"); + if (m_useComposition) { + auto compositor = Microsoft::ReactNative::GetCompositor(); + m_subchannelPropertySet = compositor.CreatePropertySet(); + m_subchannelPropertySet.InsertScalar(L"TranslationX", 0.0f); + m_subchannelPropertySet.InsertScalar(L"TranslationY", 0.0f); + m_subchannelPropertySet.InsertScalar(L"ScaleX", 1.0f); + m_subchannelPropertySet.InsertScalar(L"ScaleY", 1.0f); - m_scaleCombined = compositor.CreateExpressionAnimation(L"Vector3(subchannels.ScaleX, subchannels.ScaleY, 1.0)"); - m_scaleCombined.SetReferenceParameter(L"subchannels", m_subchannelPropertySet); - m_scaleCombined.Target(L"Scale"); + m_translationCombined = + compositor.CreateExpressionAnimation(L"Vector3(subchannels.TranslationX, subchannels.TranslationY, 0.0)"); + m_translationCombined.SetReferenceParameter(L"subchannels", m_subchannelPropertySet); + m_translationCombined.Target(L"Translation"); + + m_scaleCombined = compositor.CreateExpressionAnimation(L"Vector3(subchannels.ScaleX, subchannels.ScaleY, 1.0)"); + m_scaleCombined.SetReferenceParameter(L"subchannels", m_subchannelPropertySet); + m_scaleCombined.Target(L"Scale"); + } } void PropsAnimatedNode::ConnectToView(int64_t viewTag) { @@ -46,7 +50,10 @@ void PropsAnimatedNode::ConnectToView(int64_t viewTag) { return; } m_connectedViewTag = viewTag; - UpdateView(); + + if (m_useComposition) { + UpdateView(); + } } void PropsAnimatedNode::DisconnectFromView(int64_t viewTag) { @@ -58,26 +65,36 @@ void PropsAnimatedNode::DisconnectFromView(int64_t viewTag) { return; } - std::vector keys{}; - for (const auto &anim : m_expressionAnimations) { - keys.push_back(anim.first); - } - for (const auto &key : keys) { - DisposeCompletedAnimation(key); - } + if (m_useComposition) { + std::vector keys{}; + for (const auto &anim : m_expressionAnimations) { + keys.push_back(anim.first); + } + for (const auto &key : keys) { + DisposeCompletedAnimation(key); + } - if (m_centerPointAnimation) { - if (const auto target = GetUIElement()) { - target.StopAnimation(m_centerPointAnimation); + if (m_centerPointAnimation) { + if (const auto target = GetUIElement()) { + target.StopAnimation(m_centerPointAnimation); + } + m_centerPointAnimation = nullptr; } - m_centerPointAnimation = nullptr; + m_needsCenterPointAnimation = false; } m_connectedViewTag = s_connectedViewTagUnset; - m_needsCenterPointAnimation = false; } -void PropsAnimatedNode::RestoreDefaultValues() {} +void PropsAnimatedNode::RestoreDefaultValues() { + if (!m_useComposition) { + for (const auto &entry : m_props) { + m_props[entry.first] = nullptr; + } + + CommitProps(); + } +} void PropsAnimatedNode::UpdateView() { if (m_connectedViewTag == s_connectedViewTagUnset) { @@ -87,19 +104,31 @@ void PropsAnimatedNode::UpdateView() { if (const auto manager = std::shared_ptr(m_manager)) { for (const auto &entry : m_propMapping) { if (const auto &styleNode = manager->GetStyleAnimatedNode(entry.second)) { - for (const auto &styleEntry : styleNode->GetMapping()) { - MakeAnimation(styleEntry.second, styleEntry.first); + if (m_useComposition) { + for (const auto &styleEntry : styleNode->GetMapping()) { + MakeAnimation(styleEntry.second, styleEntry.first); + } + } else { + styleNode->CollectViewUpdates(m_props); } } else if (const auto &valueNode = manager->GetValueAnimatedNode(entry.second)) { - const auto &facade = StringToFacadeType(entry.first); - if (facade != FacadeType::None) { - MakeAnimation(entry.second, facade); + if (m_useComposition) { + const auto &facade = StringToFacadeType(entry.first); + if (facade != FacadeType::None) { + MakeAnimation(entry.second, facade); + } + } else { + m_props[entry.first] = valueNode->Value(); } } } } - StartAnimations(); + if (m_useComposition) { + StartAnimations(); + } else { + CommitProps(); + } } static void EnsureUIElementDirtyForRender(xaml::UIElement uiElement) { @@ -117,6 +146,7 @@ static void EnsureUIElementDirtyForRender(xaml::UIElement uiElement) { } void PropsAnimatedNode::StartAnimations() { + assert(m_useComposition); if (m_expressionAnimations.size()) { if (const auto uiElement = GetUIElement()) { // Work around for https://github.com/microsoft/microsoft-ui-xaml/issues/2511 @@ -161,6 +191,7 @@ void PropsAnimatedNode::StartAnimations() { } void PropsAnimatedNode::DisposeCompletedAnimation(int64_t valueTag) { + assert(m_useComposition); /* if (m_expressionAnimations.count(valueTag)) { if (const auto target = GetUIElement()) { @@ -180,6 +211,7 @@ void PropsAnimatedNode::DisposeCompletedAnimation(int64_t valueTag) { } void PropsAnimatedNode::ResumeSuspendedAnimations(int64_t valueTag) { + assert(m_useComposition); /* const auto iterator = std::find(m_suspendedExpressionAnimationTags.begin(), m_suspendedExpressionAnimationTags.end(), valueTag); @@ -286,4 +318,12 @@ xaml::UIElement PropsAnimatedNode::GetUIElement() { } return nullptr; } + +void PropsAnimatedNode::CommitProps() { + if (const auto node = GetShadowNodeBase()) { + if (!node->m_zombie) { + node->updateProperties(m_props); + } + } +} } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/PropsAnimatedNode.h b/vnext/Microsoft.ReactNative/Modules/Animated/PropsAnimatedNode.h index b88836b8acc..d9cc7bf60aa 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/PropsAnimatedNode.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/PropsAnimatedNode.h @@ -7,8 +7,8 @@ #include #include #include "AnimatedNode.h" - #include "FacadeType.h" +#include "JSValue.h" namespace Microsoft::ReactNative { struct ShadowNodeBase; @@ -31,13 +31,14 @@ class PropsAnimatedNode final : public AnimatedNode { void ResumeSuspendedAnimations(int64_t valueTag); private: + void CommitProps(); void MakeAnimation(int64_t valueNodeTag, FacadeType facadeType); Microsoft::ReactNative::ShadowNodeBase *GetShadowNodeBase(); xaml::UIElement GetUIElement(); winrt::Microsoft::ReactNative::ReactContext m_context; std::map m_propMapping{}; - folly::dynamic m_propMap{}; + winrt::Microsoft::ReactNative::JSValueObject m_props{}; int64_t m_connectedViewTag{s_connectedViewTagUnset}; std::unordered_map m_expressionAnimations{}; diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/SpringAnimationDriver.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/SpringAnimationDriver.cpp index 0cd73f2fd48..b563ab0e42c 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/SpringAnimationDriver.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/SpringAnimationDriver.cpp @@ -8,6 +8,9 @@ #include "SpringAnimationDriver.h" namespace Microsoft::ReactNative { + +static constexpr auto MAX_DELTA_TIME_MS = 4.0 * 1000.0 / 60.0; + SpringAnimationDriver::SpringAnimationDriver( int64_t id, int64_t animatedValueTag, @@ -38,10 +41,13 @@ bool SpringAnimationDriver::IsAnimationDone( } std::tuple SpringAnimationDriver::GetValueAndVelocityForTime(double time) { - const auto toValue = [this, time]() { - const auto frameFromTime = static_cast(time * 60.0); - if (frameFromTime < static_cast(m_dynamicToValues.size())) { - return m_startValue + (m_dynamicToValues[frameFromTime].AsDouble() * (m_endValue - m_startValue)); + const auto startValue = m_originalValue.value(); + const auto toValue = [this, startValue, time]() { + if (m_useComposition) { + const auto frameFromTime = static_cast(time * 60.0); + if (frameFromTime < static_cast(m_dynamicToValues.size())) { + return startValue + (m_dynamicToValues[frameFromTime].AsDouble() * (m_endValue - startValue)); + } } return m_endValue; }(); @@ -53,7 +59,7 @@ std::tuple SpringAnimationDriver::GetValueAndVelocityForTime(doub const auto zeta = c / (2 * std::sqrt(k * m)); const auto omega0 = std::sqrt(k / m); const auto omega1 = omega0 * std::sqrt(1.0 - (zeta * zeta)); - const auto x0 = toValue - m_startValue; + const auto x0 = toValue - startValue; if (zeta < 1) { const auto envelope = std::exp(-zeta * omega0 * time); @@ -72,15 +78,62 @@ std::tuple SpringAnimationDriver::GetValueAndVelocityForTime(doub } } +bool SpringAnimationDriver::Update(double timeDeltaMs, bool restarting) { + assert(!m_useComposition); + if (const auto node = GetAnimatedValue()) { + if (restarting) { + if (!m_originalValue) { + m_originalValue = node->RawValue(); + } else { + node->RawValue(m_originalValue.value()); + } + + // Spring animations run a frame behind JS driven animations if we do + // not start the first frame at 16ms. + m_lastTime = timeDeltaMs - s_frameDurationMs; + m_timeAccumulator = 0.0; + } + + // clamp the amount of timeDeltaMs to avoid stuttering in the UI. + // We should be able to catch up in a subsequent advance if necessary. + auto adjustedDeltaTime = timeDeltaMs - m_lastTime; + if (adjustedDeltaTime > MAX_DELTA_TIME_MS) { + adjustedDeltaTime = MAX_DELTA_TIME_MS; + } + m_timeAccumulator += adjustedDeltaTime; + m_lastTime = timeDeltaMs; + + auto [value, velocity] = GetValueAndVelocityForTime(m_timeAccumulator / 1000.0); + + auto isComplete = false; + if (IsAnimationDone(value, std::nullopt, velocity)) { + if (m_springStiffness > 0) { + value = static_cast(m_endValue); + } else { + m_endValue = value; + } + + isComplete = true; + } + + node->RawValue(value); + + return isComplete; + } + + return true; +} + bool SpringAnimationDriver::IsAtRest(double currentVelocity, double currentValue, double endValue) { return std::abs(currentVelocity) <= m_restSpeedThreshold && (std::abs(currentValue - endValue) <= m_displacementFromRestThreshold || m_springStiffness == 0); } bool SpringAnimationDriver::IsOvershooting(double currentValue) { + const auto startValue = m_originalValue.value(); return m_springStiffness > 0 && - ((m_startValue < m_endValue && currentValue > m_endValue) || - (m_startValue > m_endValue && currentValue < m_endValue)); + ((startValue < m_endValue && currentValue > m_endValue) || + (startValue > m_endValue && currentValue < m_endValue)); } double SpringAnimationDriver::ToValue() { diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/SpringAnimationDriver.h b/vnext/Microsoft.ReactNative/Modules/Animated/SpringAnimationDriver.h index a8e447df1e5..838ac2a69df 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/SpringAnimationDriver.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/SpringAnimationDriver.h @@ -21,6 +21,7 @@ class SpringAnimationDriver : public CalculatedAnimationDriver { double ToValue() override; protected: + bool Update(double timeDeltaMs, bool restarting) override; std::tuple GetValueAndVelocityForTime(double time) override; bool IsAnimationDone(double currentValue, std::optional previousValue, double currentVelocity) override; @@ -39,6 +40,9 @@ class SpringAnimationDriver : public CalculatedAnimationDriver { int m_iterations{0}; winrt::Microsoft::ReactNative::JSValueArray m_dynamicToValues{}; + double m_lastTime{0}; + double m_timeAccumulator{0}; + static constexpr std::string_view s_springStiffnessParameterName{"stiffness"}; static constexpr std::string_view s_springDampingParameterName{"damping"}; static constexpr std::string_view s_springMassParameterName{"mass"}; diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/StyleAnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/StyleAnimatedNode.cpp index 78617dfafb5..ef5499166d6 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/StyleAnimatedNode.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/StyleAnimatedNode.cpp @@ -11,15 +11,30 @@ StyleAnimatedNode::StyleAnimatedNode( int64_t tag, const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager) - : AnimatedNode(tag, manager) { + : AnimatedNode(tag, config, manager) { for (const auto &entry : config[s_styleName].AsObject()) { - m_propMapping.insert({entry.first, static_cast(entry.second.AsDouble())}); + const auto inputTag = entry.second.AsInt64(); + assert(HasCompatibleAnimationDriver(inputTag)); + m_propMapping.insert({entry.first, inputTag}); } } -void StyleAnimatedNode::CollectViewUpdates(const folly::dynamic & /*propsMap*/) {} +void StyleAnimatedNode::CollectViewUpdates(winrt::Microsoft::ReactNative::JSValueObject &propsMap) { + assert(!m_useComposition); + auto rawValue = 0.0; + for (const auto &propMapping : m_propMapping) { + if (const auto manager = m_manager.lock()) { + if (const auto transformNode = manager->GetTransformAnimatedNode(propMapping.second)) { + transformNode->CollectViewUpdates(propsMap); + } else if (const auto node = manager->GetValueAnimatedNode(propMapping.second)) { + propsMap[propMapping.first] = node->Value(); + } + } + } +} std::unordered_map StyleAnimatedNode::GetMapping() { + assert(m_useComposition); std::unordered_map mapping; for (const auto &prop : m_propMapping) { if (const auto manager = m_manager.lock()) { diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/StyleAnimatedNode.h b/vnext/Microsoft.ReactNative/Modules/Animated/StyleAnimatedNode.h index 8adc94d60c6..0bd79154ffb 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/StyleAnimatedNode.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/StyleAnimatedNode.h @@ -5,6 +5,7 @@ #include #include "AnimatedNode.h" #include "FacadeType.h" +#include "JSValue.h" namespace Microsoft::ReactNative { class StyleAnimatedNode final : public AnimatedNode { @@ -13,7 +14,7 @@ class StyleAnimatedNode final : public AnimatedNode { int64_t tag, const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager); - void CollectViewUpdates(const folly::dynamic &propsMap); + void CollectViewUpdates(winrt::Microsoft::ReactNative::JSValueObject &propsMap); std::unordered_map GetMapping(); diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/SubtractionAnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/SubtractionAnimatedNode.cpp index 3fef02c348c..51ce24f1680 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/SubtractionAnimatedNode.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/SubtractionAnimatedNode.cpp @@ -11,30 +11,52 @@ SubtractionAnimatedNode::SubtractionAnimatedNode( int64_t tag, const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager) - : ValueAnimatedNode(tag, manager) { + : ValueAnimatedNode(tag, config, manager) { for (const auto &inputNode : config[s_inputName].AsArray()) { + const auto inputTag = inputNode.AsInt64(); + assert(HasCompatibleAnimationDriver(inputTag)); if (m_firstInput == s_firstInputUnset) { - m_firstInput = static_cast(inputNode.AsDouble()); + m_firstInput = inputTag; } else { - m_inputNodes.insert(static_cast(inputNode.AsDouble())); + m_inputNodes.insert(inputTag); } } - m_propertySet.StartAnimation(s_valueName, [firstNode = m_firstInput, nodes = m_inputNodes, manager]() { - const auto anim = Microsoft::ReactNative::GetCompositor().CreateExpressionAnimation(); - - anim.Expression([firstNode, nodes, manager, anim]() { - anim.SetReferenceParameter(s_baseName, manager->GetValueAnimatedNode(firstNode)->PropertySet()); - winrt::hstring expr = static_cast(L"(") + s_baseName + L"." + s_valueName + L" + " + s_baseName + - L"." + s_offsetName + L")"; - for (const auto tag : nodes) { - const auto identifier = L"n" + std::to_wstring(tag); - anim.SetReferenceParameter(identifier, manager->GetValueAnimatedNode(tag)->PropertySet()); - expr = expr + L" - (" + identifier + L"." + s_valueName + L" + " + identifier + L"." + s_offsetName + L")"; - } - return expr; + if (m_useComposition) { + m_propertySet.StartAnimation(s_valueName, [firstNode = m_firstInput, nodes = m_inputNodes, manager]() { + const auto anim = Microsoft::ReactNative::GetCompositor().CreateExpressionAnimation(); + + anim.Expression([firstNode, nodes, manager, anim]() { + anim.SetReferenceParameter(s_baseName, manager->GetValueAnimatedNode(firstNode)->PropertySet()); + winrt::hstring expr = static_cast(L"(") + s_baseName + L"." + s_valueName + L" + " + + s_baseName + L"." + s_offsetName + L")"; + for (const auto tag : nodes) { + const auto identifier = L"n" + std::to_wstring(tag); + anim.SetReferenceParameter(identifier, manager->GetValueAnimatedNode(tag)->PropertySet()); + expr = expr + L" - (" + identifier + L"." + s_valueName + L" + " + identifier + L"." + s_offsetName + L")"; + } + return expr; + }()); + return anim; }()); - return anim; - }()); + } +} + +void SubtractionAnimatedNode::Update() { + assert(!m_useComposition); + if (const auto manager = m_manager.lock()) { + auto rawValue = 0.0; + if (const auto firstNode = manager->GetValueAnimatedNode(m_firstInput)) { + rawValue = firstNode->Value(); + } + + for (const auto &tag : m_inputNodes) { + if (const auto node = manager->GetValueAnimatedNode(tag)) { + rawValue -= node->Value(); + } + } + + RawValue(rawValue); + } } } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/SubtractionAnimatedNode.h b/vnext/Microsoft.ReactNative/Modules/Animated/SubtractionAnimatedNode.h index 359cde6dac3..65e8febe1d2 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/SubtractionAnimatedNode.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/SubtractionAnimatedNode.h @@ -13,6 +13,8 @@ class SubtractionAnimatedNode final : public ValueAnimatedNode { const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager); + virtual void Update() override; + private: int64_t m_firstInput{s_firstInputUnset}; std::unordered_set m_inputNodes{}; diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/TrackingAnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/TrackingAnimatedNode.cpp index c0879f3c6a1..3de098e810f 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/TrackingAnimatedNode.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/TrackingAnimatedNode.cpp @@ -11,13 +11,18 @@ TrackingAnimatedNode::TrackingAnimatedNode( int64_t tag, const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager) - : AnimatedNode(tag, manager) { - m_animationId = static_cast(config[s_animationIdName].AsDouble()); - m_toValueId = static_cast(config[s_toValueIdName].AsDouble()); - m_valueId = static_cast(config[s_valueIdName].AsDouble()); + : AnimatedNode(tag, config, manager) { + m_animationId = config[s_animationIdName].AsInt64(); + m_toValueId = config[s_toValueIdName].AsInt64(); + m_valueId = config[s_valueIdName].AsInt64(); m_animationConfig = std::move(config[s_animationConfigName].AsObject().Copy()); + if (config.count(s_platformConfigName) && !m_animationConfig.count(s_platformConfigName)) { + m_animationConfig[s_platformConfigName] = std::move(config[s_platformConfigName].Copy()); + } - StartAnimation(); + if (m_useComposition) { + StartAnimation(); + } } void TrackingAnimatedNode::Update() { @@ -30,10 +35,14 @@ void TrackingAnimatedNode::StartAnimation() { // In case the animation is already running, we need to stop it to free up the // animationId key in the active animations map in the animation manager. strongManager->StopAnimation(m_animationId, true); - toValueNode->AddActiveTrackingNode(m_tag); m_animationConfig[s_toValueIdName] = toValueNode->Value(); - strongManager->StartTrackingAnimatedNode( - m_animationId, m_valueId, m_toValueId, m_animationConfig, nullptr, strongManager); + if (m_useComposition) { + toValueNode->AddActiveTrackingNode(m_tag); + strongManager->StartTrackingAnimatedNode( + m_animationId, m_valueId, m_toValueId, m_animationConfig, nullptr, strongManager); + } else { + strongManager->StartAnimatingNode(m_animationId, m_valueId, m_animationConfig, nullptr, strongManager); + } } } } diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/TrackingAnimatedNode.h b/vnext/Microsoft.ReactNative/Modules/Animated/TrackingAnimatedNode.h index 609f1a67300..7b88740086e 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/TrackingAnimatedNode.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/TrackingAnimatedNode.h @@ -13,11 +13,10 @@ class TrackingAnimatedNode final : public AnimatedNode { const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager); - void Update() override; - - private: + virtual void Update() override; void StartAnimation(); + private: int64_t m_animationId{}; int64_t m_toValueId{}; int64_t m_valueId{}; diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/TransformAnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/TransformAnimatedNode.cpp index ac6413b1ceb..4fb2ae95536 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/TransformAnimatedNode.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/TransformAnimatedNode.cpp @@ -4,6 +4,7 @@ #include "pch.h" #include "FacadeType.h" +#include "NativeAnimatedNodeManager.h" #include "TransformAnimatedNode.h" namespace Microsoft::ReactNative { @@ -11,12 +12,13 @@ TransformAnimatedNode::TransformAnimatedNode( int64_t tag, const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager) - : AnimatedNode(tag, manager) { + : AnimatedNode(tag, config, manager) { for (const auto &transform : config[s_transformsName].AsArray()) { const auto property = transform[s_propertyName].AsString(); if (transform[s_typeName].AsString() == s_animatedName) { - m_transformConfigs.push_back( - TransformConfig{property, static_cast(transform[s_nodeTagName].AsDouble()), 0}); + const auto inputTag = transform[s_nodeTagName].AsInt64(); + assert(HasCompatibleAnimationDriver(inputTag)); + m_transformConfigs.push_back(TransformConfig{property, inputTag, 0}); } else { m_transformConfigs.push_back(TransformConfig{property, s_unsetNodeTag, transform[s_valueName].AsDouble()}); } @@ -24,6 +26,7 @@ TransformAnimatedNode::TransformAnimatedNode( } std::unordered_map TransformAnimatedNode::GetMapping() { + assert(m_useComposition); std::unordered_map mapping; for (const auto &config : m_transformConfigs) { if (config.nodeTag != s_unsetNodeTag) { @@ -35,4 +38,29 @@ std::unordered_map TransformAnimatedNode::GetMapping() { } return mapping; } + +void TransformAnimatedNode::CollectViewUpdates(winrt::Microsoft::ReactNative::JSValueObject &props) { + assert(!m_useComposition); + winrt::Microsoft::ReactNative::JSValueArray transforms; + if (const auto manager = m_manager.lock()) { + for (const auto &transformConfig : m_transformConfigs) { + std::optional value; + if (transformConfig.nodeTag == s_unsetNodeTag) { + value = transformConfig.value; + } else { + if (const auto node = manager->GetValueAnimatedNode(transformConfig.nodeTag)) { + value = node->Value(); + } + } + + if (value) { + transforms.emplace_back( + winrt::Microsoft::ReactNative::JSValueObject{{transformConfig.property, value.value()}}); + } + } + } + + props[s_transformPropName] = std::move(transforms); +} + } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/TransformAnimatedNode.h b/vnext/Microsoft.ReactNative/Modules/Animated/TransformAnimatedNode.h index 70e37e5462c..ff8fe05bb9c 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/TransformAnimatedNode.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/TransformAnimatedNode.h @@ -5,6 +5,7 @@ #include #include "AnimatedNode.h" #include "FacadeType.h" +#include "JSValue.h" namespace Microsoft::ReactNative { struct TransformConfig { @@ -21,6 +22,7 @@ class TransformAnimatedNode final : public AnimatedNode { const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager); std::unordered_map GetMapping(); + void CollectViewUpdates(winrt::Microsoft::ReactNative::JSValueObject &props); private: std::vector m_transformConfigs; @@ -33,5 +35,6 @@ class TransformAnimatedNode final : public AnimatedNode { static constexpr std::string_view s_animatedName{"animated"}; static constexpr std::string_view s_nodeTagName{"nodeTag"}; static constexpr std::string_view s_valueName{"value"}; + static constexpr std::string_view s_transformPropName{"transform"}; }; } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/ValueAnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/ValueAnimatedNode.cpp index 13aee59c5cd..306f2ae1f04 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/ValueAnimatedNode.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/ValueAnimatedNode.cpp @@ -11,55 +11,70 @@ ValueAnimatedNode::ValueAnimatedNode( int64_t tag, const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager) - : AnimatedNode(tag, manager) { - // TODO: Islands - need to get the XamlView associated with this animation in order to - // use the compositor Microsoft::ReactNative::GetCompositor(xamlView) - m_propertySet = Microsoft::ReactNative::GetCompositor().CreatePropertySet(); - m_propertySet.InsertScalar(s_valueName, static_cast(config[s_jsValueName].AsDouble())); - m_propertySet.InsertScalar(s_offsetName, static_cast(config[s_jsOffsetName].AsDouble())); -} + : AnimatedNode(tag, config, manager) { + auto value = 0.0; + auto offset = 0.0; + if (config.count(s_jsValueName) && config.count(s_jsOffsetName)) { + value = config[s_jsValueName].AsDouble(); + offset = config[s_jsOffsetName].AsDouble(); + } -ValueAnimatedNode::ValueAnimatedNode(int64_t tag, const std::shared_ptr &manager) - : AnimatedNode(tag, manager) { - // TODO: Islands - need to get the XamlView associated with this animation in order to - // use the compositor Microsoft::ReactNative::GetCompositor(xamlView) - m_propertySet = Microsoft::ReactNative::GetCompositor().CreatePropertySet(); - m_propertySet.InsertScalar(s_valueName, 0.0); - m_propertySet.InsertScalar(s_offsetName, 0.0); + if (m_useComposition) { + // TODO: Islands - need to get the XamlView associated with this animation in order to + // use the compositor Microsoft::ReactNative::GetCompositor(xamlView) + m_propertySet = Microsoft::ReactNative::GetCompositor().CreatePropertySet(); + m_propertySet.InsertScalar(s_valueName, static_cast(value)); + m_propertySet.InsertScalar(s_offsetName, static_cast(offset)); + } else { + m_value = value; + m_offset = offset; + } } double ValueAnimatedNode::RawValue() { - auto rawValue = 0.0f; - m_propertySet.TryGetScalar(s_valueName, rawValue); - return rawValue; + if (m_useComposition) { + auto rawValue = 0.0f; + m_propertySet.TryGetScalar(s_valueName, rawValue); + return rawValue; + } else { + return m_value; + } } void ValueAnimatedNode::RawValue(double value) { if (RawValue() != value) { - m_propertySet.InsertScalar(s_valueName, static_cast(value)); - UpdateTrackingNodes(); + if (m_useComposition) { + m_propertySet.InsertScalar(s_valueName, static_cast(value)); + UpdateTrackingNodes(); + } else { + m_value = value; + } } } double ValueAnimatedNode::Offset() { - auto offset = 0.0f; - m_propertySet.TryGetScalar(s_offsetName, offset); - return offset; + if (m_useComposition) { + auto offset = 0.0f; + m_propertySet.TryGetScalar(s_offsetName, offset); + return offset; + } else { + return m_offset; + } } void ValueAnimatedNode::Offset(double offset) { if (Offset() != offset) { - m_propertySet.InsertScalar(s_offsetName, static_cast(offset)); - UpdateTrackingNodes(); + if (m_useComposition) { + m_propertySet.InsertScalar(s_offsetName, static_cast(offset)); + UpdateTrackingNodes(); + } else { + m_offset = offset; + } } } double ValueAnimatedNode::Value() { - auto rawValue = 0.0f; - auto offset = 0.0f; - m_propertySet.TryGetScalar(s_valueName, rawValue); - m_propertySet.TryGetScalar(s_offsetName, offset); - return static_cast(rawValue) + static_cast(offset); + return RawValue() + Offset(); } void ValueAnimatedNode::FlattenOffset() { @@ -72,15 +87,28 @@ void ValueAnimatedNode::ExtractOffset() { RawValue(0.0f); } +void ValueAnimatedNode::OnValueUpdate() { + if (m_valueListener) { + m_valueListener(Value()); + } +} + +void ValueAnimatedNode::ValueListener(const ValueListenerCallback &callback) { + m_valueListener = callback; +} + void ValueAnimatedNode::AddDependentPropsNode(int64_t propsNodeTag) { + assert(m_useComposition); m_dependentPropsNodes.insert(propsNodeTag); } void ValueAnimatedNode::RemoveDependentPropsNode(int64_t propsNodeTag) { + assert(m_useComposition); m_dependentPropsNodes.erase(propsNodeTag); } void ValueAnimatedNode::AddActiveAnimation(int64_t animationTag) { + assert(m_useComposition); m_activeAnimations.insert(animationTag); if (m_activeAnimations.size() == 1) { if (const auto manager = m_manager.lock()) { @@ -93,6 +121,7 @@ void ValueAnimatedNode::AddActiveAnimation(int64_t animationTag) { } void ValueAnimatedNode::RemoveActiveAnimation(int64_t animationTag) { + assert(m_useComposition); m_activeAnimations.erase(animationTag); if (!m_activeAnimations.size()) { if (const auto manager = m_manager.lock()) { @@ -105,14 +134,17 @@ void ValueAnimatedNode::RemoveActiveAnimation(int64_t animationTag) { } void ValueAnimatedNode::AddActiveTrackingNode(int64_t trackingNodeTag) { + assert(m_useComposition); m_activeTrackingNodes.insert(trackingNodeTag); } void ValueAnimatedNode::RemoveActiveTrackingNode(int64_t trackingNodeTag) { + assert(m_useComposition); m_activeTrackingNodes.erase(trackingNodeTag); } void ValueAnimatedNode::UpdateTrackingNodes() { + assert(m_useComposition); if (auto const manager = m_manager.lock()) { for (auto trackingNodeTag : m_activeTrackingNodes) { if (auto trackingNode = manager->GetTrackingAnimatedNode(trackingNodeTag)) { diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/ValueAnimatedNode.h b/vnext/Microsoft.ReactNative/Modules/Animated/ValueAnimatedNode.h index d73361188c6..6d373641933 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/ValueAnimatedNode.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/ValueAnimatedNode.h @@ -12,13 +12,14 @@ using namespace comp; } namespace Microsoft::ReactNative { +typedef std::function ValueListenerCallback; + class ValueAnimatedNode : public AnimatedNode { public: ValueAnimatedNode( int64_t tag, const winrt::Microsoft::ReactNative::JSValueObject &config, const std::shared_ptr &manager); - ValueAnimatedNode(int64_t tag, const std::shared_ptr &manager); double Value(); double RawValue(); void RawValue(double value); @@ -26,6 +27,9 @@ class ValueAnimatedNode : public AnimatedNode { void Offset(double offset); void FlattenOffset(); void ExtractOffset(); + void OnValueUpdate(); + void ValueListener(const ValueListenerCallback &callback); + comp::CompositionPropertySet PropertySet() { return m_propertySet; }; @@ -42,7 +46,6 @@ class ValueAnimatedNode : public AnimatedNode { protected: comp::CompositionPropertySet m_propertySet{nullptr}; - static constexpr std::string_view s_inputName{"input"}; static constexpr std::string_view s_jsValueName{"value"}; @@ -53,5 +56,8 @@ class ValueAnimatedNode : public AnimatedNode { std::unordered_set m_dependentPropsNodes{}; std::unordered_set m_activeAnimations{}; std::unordered_set m_activeTrackingNodes{}; + double m_value{0.0}; + double m_offset{0.0}; + ValueListenerCallback m_valueListener{}; }; } // namespace Microsoft::ReactNative From bbb2c4874ee05e7bf949c6d16908ca42c0550896 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Wed, 15 Dec 2021 11:50:42 -0500 Subject: [PATCH 2/3] Change files --- ...ative-windows-bf20335c-eccd-487b-bbcf-9fcc46a3b849.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-bf20335c-eccd-487b-bbcf-9fcc46a3b849.json diff --git a/change/react-native-windows-bf20335c-eccd-487b-bbcf-9fcc46a3b849.json b/change/react-native-windows-bf20335c-eccd-487b-bbcf-9fcc46a3b849.json new file mode 100644 index 00000000000..7e385668313 --- /dev/null +++ b/change/react-native-windows-bf20335c-eccd-487b-bbcf-9fcc46a3b849.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Adds Rendering driver option to NativeAnimated", + "packageName": "react-native-windows", + "email": "erozell@outlook.com", + "dependentChangeType": "patch" +} From 5e55a55bfb7c10880ebf8dfa859999e8647693cd Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Tue, 2 Aug 2022 16:50:58 -0400 Subject: [PATCH 3/3] Adds RNTester example demonstrating bugs in UI Composition Animated --- .../NativeAnimation/CompositionBugsExample.js | 476 ++++++++++++++++++ .../src/js/utils/RNTesterList.windows.js | 5 + 2 files changed, 481 insertions(+) create mode 100644 packages/@react-native-windows/tester/src/js/examples-win/NativeAnimation/CompositionBugsExample.js diff --git a/packages/@react-native-windows/tester/src/js/examples-win/NativeAnimation/CompositionBugsExample.js b/packages/@react-native-windows/tester/src/js/examples-win/NativeAnimation/CompositionBugsExample.js new file mode 100644 index 00000000000..cb0cf6dbdf0 --- /dev/null +++ b/packages/@react-native-windows/tester/src/js/examples-win/NativeAnimation/CompositionBugsExample.js @@ -0,0 +1,476 @@ +/** + * 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. + * + * @format + * @flow + */ + +'use strict'; + +const React = require('react'); + +const { + View, + Text, + Animated, + StyleSheet, + TouchableWithoutFeedback, +} = require('react-native'); + +class Tester extends React.Component<$FlowFixMeProps, $FlowFixMeState> { + state = { + native: new Animated.Value(0), + tick: new Animated.Value(0), + js: new Animated.Value(0), + }; + + current = 0; + + onPress = () => { + const animConfig = + this.current && this.props.reverseConfig + ? this.props.reverseConfig + : this.props.config; + this.current = this.current ? 0 : 1; + const config: Object = { + ...animConfig, + toValue: this.current, + }; + + Animated[this.props.type](this.state.native, { + ...config, + useNativeDriver: true, + }).start(); + Animated[this.props.type](this.state.tick, { + ...config, + useNativeDriver: true, + platformConfig: { + useComposition: false, + }, + }).start(); + Animated[this.props.type](this.state.js, { + ...config, + useNativeDriver: false, + }).start(); + + if (this.props.onPress) { + this.props.onPress(this.state.native); + this.props.onPress(this.state.tick); + this.props.onPress(this.state.js); + } + }; + + render() { + return ( + + + + Composition: + + + {this.props.children(this.state.native)} + + + UI Tick{':'} + + {this.props.children(this.state.tick)} + + JavaScript{':'} + + {this.props.children(this.state.js)} + + + ); + } +} + +class ValueListenerTester extends Tester { + state = { + native: new Animated.Value(0), + tick: new Animated.Value(0), + js: new Animated.Value(0), + nativeValue: 0, + tickValue: 0, + jsValue: 0, + }; + + componentDidMount() { + this.state.native.addListener(e => this.setState({nativeValue: e.value})); + this.state.tick.addListener(e => this.setState({tickValue: e.value})); + this.state.js.addListener(e => this.setState({jsValue: e.value})); + } + + componentWillUnmount() { + this.state.native.removeAllListeners(); + this.state.tick.removeAllListeners(); + this.state.js.removeAllListeners(); + } + + render() { + return ( + + + + Composition: + + + {this.props.children(this.state.native)} + + + Value: {this.state.nativeValue} + {'\n'} + + + UI Tick{':'} + + {this.props.children(this.state.tick)} + + Value: {this.state.tickValue} + {'\n'} + + + JavaScript{':'} + + {this.props.children(this.state.js)} + + Value: {this.state.jsValue} + {'\n'} + + + + ); + } +} + +class RevertToStaticPropsExample extends React.Component< + $FlowFixMeProps, + $FlowFixMeState, +> { + state = { + native: new Animated.Value(0), + tick: new Animated.Value(0), + js: new Animated.Value(0), + resetProp: false, + }; + + current = 0; + + onPress = () => { + if (this.current) { + this.state.native.setValue(0); + this.state.tick.setValue(0); + this.state.js.setValue(0); + this.setState({resetProp: true}); + } else { + this.setState({resetProp: false}); + const config: Object = { + ...this.props.config, + toValue: 50, + }; + + Animated[this.props.type](this.state.native, { + ...config, + useNativeDriver: true, + }).start(); + Animated[this.props.type](this.state.tick, { + ...config, + useNativeDriver: true, + platformConfig: { + useComposition: false, + }, + }).start(); + Animated[this.props.type](this.state.js, { + ...config, + useNativeDriver: false, + }).start(); + + if (this.props.onPress) { + this.props.onPress(this.state.native); + this.props.onPress(this.state.tick); + this.props.onPress(this.state.js); + } + } + this.current = this.current ? 0 : 1; + }; + + render() { + return ( + + + + Composition: + + + {this.props.children( + this.state.resetProp ? undefined : this.state.native, + )} + + + UI Tick{':'} + + + {this.props.children( + this.state.resetProp ? undefined : this.state.tick, + )} + + + JavaScript{':'} + + + {this.props.children( + this.state.resetProp ? undefined : this.state.js, + )} + + + + ); + } +} + +class StopAnimationCallbackTester extends Tester { + onPress = () => { + const animConfig = + this.current && this.props.reverseConfig + ? this.props.reverseConfig + : this.props.config; + this.current = this.current ? 0 : 1; + const config: Object = { + ...animConfig, + toValue: this.current, + }; + + Animated[this.props.type](this.state.native, { + ...config, + useNativeDriver: true, + }).start(); + Animated[this.props.type](this.state.tick, { + ...config, + useNativeDriver: true, + platformConfig: { + useComposition: false, + }, + }).start(); + Animated[this.props.type](this.state.js, { + ...config, + useNativeDriver: false, + }).start(); + + setTimeout(() => { + this.state.native.stopAnimation(nativeValue => + this.setState({nativeValue}), + ); + this.state.tick.stopAnimation(tickValue => this.setState({tickValue})); + this.state.js.stopAnimation(jsValue => this.setState({jsValue})); + }, config.duration / 2); + }; + + render() { + return ( + + + + Composition: + + + {this.props.children(this.state.native)} + + + Final Value: {this.state.nativeValue} + {'\n'} + + + UI Tick{':'} + + {this.props.children(this.state.tick)} + + Final Value: {this.state.tickValue} + {'\n'} + + + JavaScript{':'} + + {this.props.children(this.state.js)} + + Final Value: {this.state.jsValue} + {'\n'} + + + + ); + } +} + +const styles = StyleSheet.create({ + row: { + padding: 10, + zIndex: 1, + }, + block: { + width: 50, + height: 50, + backgroundColor: 'blue', + }, + line: { + position: 'absolute', + left: 35, + top: 0, + bottom: 0, + width: 1, + backgroundColor: 'red', + }, +}); + +exports.framework = 'React'; +exports.title = 'Composition Bugs Example'; +exports.category = 'UI'; +exports.description = 'See bugs in UI.Composition driven native animations'; + +exports.examples = [ + { + title: 'Animated value listener', + render: function (): React.Node { + return ( + + {anim => ( + + )} + + ); + }, + }, + { + title: "Arbitrary props (e.g., 'borderRadius')", + render: function (): React.Node { + return ( + + {anim => ( + + )} + + ); + }, + }, + { + title: 'diffClamp', + render: function (): React.Node { + return ( + + {anim => ( + + )} + + ); + }, + }, + { + title: 'setValue in active animation', + render: function (): React.Node { + return ( + setTimeout(() => anim.setValue(0.5), 500)}> + {anim => { + return ( + + ); + }} + + ); + }, + }, + { + title: 'stopAnimation callback', + render: function (): React.Node { + return ( + + {anim => { + return ( + + ); + }} + + ); + }, + }, + { + title: "Animated 'transform' prop value persisted", + render: function (): React.Node { + return ( + + {value => { + return ( + + ); + }} + + ); + }, + }, +]; diff --git a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js index 2c662135d7f..40b8a6c7531 100644 --- a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js +++ b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js @@ -299,6 +299,11 @@ const APIs: Array = [ category: 'UI', module: require('../examples/NativeAnimation/NativeAnimationsExample'), }, + { + key: 'CompositionBugsExample', + category: 'UI', + module: require('../examples-win/NativeAnimation/CompositionBugsExample'), + }, { key: 'PanResponderExample', category: 'Basic',