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" +} 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', 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