diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/definitions.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/definitions.h index 1b9f6292641a..fe4af931cd45 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/definitions.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/definitions.h @@ -9,15 +9,7 @@ namespace reanimated::css { using namespace facebook; -using PropertyNames = std::vector; using PropertyPath = std::vector; -/** - * If nullopt - all style properties can trigger transition - * If empty vector - no style property can trigger transition - * Otherwise - only specified style properties can trigger transition - */ -using TransitionProperties = std::optional; - using EasingFunction = std::function; using ColorChannels = std::array; diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/configs/CSSTransitionConfig.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/configs/CSSTransitionConfig.cpp index f9b52ed3ef3b..26504c7ed642 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/configs/CSSTransitionConfig.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/configs/CSSTransitionConfig.cpp @@ -1,55 +1,89 @@ #include -#include +#include namespace reanimated::css { -std::optional getTransitionPropertySettings( - const CSSTransitionPropertiesSettings &propertiesSettings, - const std::string &propName) { - // Try to use property specific settings first - const auto &propIt = propertiesSettings.find(propName); - if (propIt != propertiesSettings.end()) { - return propIt->second; - } - // Fallback to "all" settings if no property specific settings are available - const auto &allIt = propertiesSettings.find("all"); - if (allIt != propertiesSettings.end()) { - return allIt->second; +CSSTransitionPropertyUpdates parsePropertyUpdates(jsi::Runtime &rt, const jsi::Object &diffs) { + CSSTransitionPropertyUpdates result; + const auto propertyNames = diffs.getPropertyNames(rt); + const auto propertyCount = propertyNames.size(rt); + + for (size_t i = 0; i < propertyCount; ++i) { + const auto propertyName = propertyNames.getValueAtIndex(rt, i).asString(rt).utf8(rt); + const auto diffValue = diffs.getProperty(rt, jsi::PropNameID::forUtf8(rt, propertyName)); + + if (diffValue.isNull()) { + result.emplace(propertyName, std::nullopt); + continue; + } + + if (!diffValue.isObject()) { + continue; + } + + const auto diffArray = diffValue.asObject(rt).asArray(rt); + if (diffArray.size(rt) != 2) { + continue; + } + + result.emplace( + propertyName, + std::make_optional(std::make_pair(diffArray.getValueAtIndex(rt, 0), diffArray.getValueAtIndex(rt, 1)))); } - // Or return nullopt if no settings are available - return std::nullopt; + + return result; } -TransitionProperties getProperties(jsi::Runtime &rt, const jsi::Object &config) { - const auto transitionProperty = config.getProperty(rt, "properties"); +PartialCSSTransitionPropertySettings parsePartialPropertySettings(jsi::Runtime &rt, const jsi::Object &settings) { + PartialCSSTransitionPropertySettings result; - if (transitionProperty.isObject()) { - PropertyNames properties; + if (settings.hasProperty(rt, "duration")) { + result.duration = getDuration(rt, settings); + } - const auto propertiesArray = transitionProperty.asObject(rt).asArray(rt); - const auto propertiesCount = propertiesArray.size(rt); - for (size_t i = 0; i < propertiesCount; ++i) { - properties.emplace_back(propertiesArray.getValueAtIndex(rt, i).asString(rt).utf8(rt)); - } + if (settings.hasProperty(rt, "timingFunction")) { + result.easingFunction = getTimingFunction(rt, settings); + } - return properties; + if (settings.hasProperty(rt, "delay")) { + result.delay = getDelay(rt, settings); } - return std::nullopt; + if (settings.hasProperty(rt, "allowDiscrete")) { + result.allowDiscrete = settings.getProperty(rt, "allowDiscrete").getBool(); + } + + return result; } -bool getAllowDiscrete(jsi::Runtime &rt, const jsi::Object &config) { - return config.getProperty(rt, "allowDiscrete").asBool(); +CSSTransitionPropertySettingsUpdates parseSettingsUpdates(jsi::Runtime &rt, const jsi::Object &settings) { + CSSTransitionPropertySettingsUpdates result; + const auto propertyNames = settings.getPropertyNames(rt); + const auto propertyCount = propertyNames.size(rt); + + for (size_t i = 0; i < propertyCount; ++i) { + const auto propertyName = propertyNames.getValueAtIndex(rt, i).asString(rt).utf8(rt); + const auto propertyValue = settings.getProperty(rt, jsi::PropNameID::forUtf8(rt, propertyName)); + + if (!propertyValue.isObject()) { + continue; + } + + const auto propertySettings = propertyValue.asObject(rt); + auto parsedSettings = parsePartialPropertySettings(rt, propertySettings); + result.emplace(propertyName, std::move(parsedSettings)); + } + + return result; } -CSSTransitionPropertiesSettings parseCSSTransitionPropertiesSettings(jsi::Runtime &rt, const jsi::Object &settings) { +CSSTransitionPropertiesSettings parseSettings(jsi::Runtime &rt, const jsi::Object &settings) { CSSTransitionPropertiesSettings result; - const auto propertyNames = settings.getPropertyNames(rt); - const auto propertiesCount = propertyNames.size(rt); + const auto propertyCount = propertyNames.size(rt); - for (size_t i = 0; i < propertiesCount; ++i) { + for (size_t i = 0; i < propertyCount; ++i) { const auto propertyName = propertyNames.getValueAtIndex(rt, i).asString(rt).utf8(rt); const auto propertySettings = settings.getProperty(rt, jsi::PropNameID::forUtf8(rt, propertyName)).asObject(rt); @@ -59,29 +93,51 @@ CSSTransitionPropertiesSettings parseCSSTransitionPropertiesSettings(jsi::Runtim getDuration(rt, propertySettings), getTimingFunction(rt, propertySettings), getDelay(rt, propertySettings), - getAllowDiscrete(rt, propertySettings)}); + propertySettings.getProperty(rt, "allowDiscrete").asBool()}); } return result; } +std::optional getTransitionPropertySettings( + const CSSTransitionPropertiesSettings &propertiesSettings, + const std::string &propName) { + const auto &propIt = propertiesSettings.find(propName); + if (propIt != propertiesSettings.end()) { + return propIt->second; + } + + const auto &allIt = propertiesSettings.find("all"); + if (allIt != propertiesSettings.end()) { + return allIt->second; + } + + return std::nullopt; +} + CSSTransitionConfig parseCSSTransitionConfig(jsi::Runtime &rt, const jsi::Value &config) { const auto configObj = config.asObject(rt); - return CSSTransitionConfig{ - getProperties(rt, configObj), - parseCSSTransitionPropertiesSettings(rt, configObj.getProperty(rt, "settings").asObject(rt))}; -} -PartialCSSTransitionConfig parsePartialCSSTransitionConfig(jsi::Runtime &rt, const jsi::Value &partialConfig) { - const auto partialObj = partialConfig.asObject(rt); + CSSTransitionConfig result{ + .properties = parsePropertyUpdates(rt, configObj.getProperty(rt, "properties").asObject(rt)), + .settings = parseSettings(rt, configObj.getProperty(rt, "settings").asObject(rt)), + }; - PartialCSSTransitionConfig result; + return result; +} - if (partialObj.hasProperty(rt, "properties")) { - result.properties = getProperties(rt, partialObj); - } - if (partialObj.hasProperty(rt, "settings")) { - result.settings = parseCSSTransitionPropertiesSettings(rt, partialObj.getProperty(rt, "settings").asObject(rt)); +CSSTransitionUpdates parseCSSTransitionUpdates(jsi::Runtime &rt, const jsi::Value &updates) { + const auto updatesObj = updates.asObject(rt); + CSSTransitionUpdates result{ + .properties = parsePropertyUpdates(rt, updatesObj.getProperty(rt, "properties").asObject(rt)), + }; + + if (updatesObj.hasProperty(rt, "settings")) { + const auto settingsValue = updatesObj.getProperty(rt, "settings"); + auto settingsUpdates = parseSettingsUpdates(rt, settingsValue.asObject(rt)); + if (!settingsUpdates.empty()) { + result.settings = std::move(settingsUpdates); + } } return result; diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/configs/CSSTransitionConfig.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/configs/CSSTransitionConfig.h index ca78dd547963..d75f3882e22b 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/configs/CSSTransitionConfig.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/configs/CSSTransitionConfig.h @@ -4,11 +4,24 @@ #include #include +#include #include #include +#include namespace reanimated::css { +enum class TransitionPropertyStatus : uint8_t { + Updated, + Removed, + Reversed, +}; + +struct TransitionPropertyUpdate { + std::string name; + TransitionPropertyStatus status; +}; + struct CSSTransitionPropertySettings { double duration; EasingFunction easingFunction; @@ -18,26 +31,32 @@ struct CSSTransitionPropertySettings { using CSSTransitionPropertiesSettings = std::unordered_map; +using CSSTransitionPropertyUpdates = std::unordered_map>>; + struct CSSTransitionConfig { - TransitionProperties properties; + CSSTransitionPropertyUpdates properties; CSSTransitionPropertiesSettings settings; }; -struct PartialCSSTransitionConfig { - std::optional properties; - std::optional settings; +struct PartialCSSTransitionPropertySettings { + std::optional duration; + std::optional easingFunction; + std::optional delay; + std::optional allowDiscrete; +}; + +using CSSTransitionPropertySettingsUpdates = std::unordered_map; + +struct CSSTransitionUpdates { + CSSTransitionPropertyUpdates properties; + std::optional settings; }; std::optional getTransitionPropertySettings( const CSSTransitionPropertiesSettings &propertiesSettings, const std::string &propName); -TransitionProperties getProperties(jsi::Runtime &rt, const jsi::Object &config); - -CSSTransitionPropertiesSettings parseCSSTransitionPropertiesSettings(jsi::Runtime &rt, const jsi::Object &settings); - CSSTransitionConfig parseCSSTransitionConfig(jsi::Runtime &rt, const jsi::Value &config); - -PartialCSSTransitionConfig parsePartialCSSTransitionConfig(jsi::Runtime &rt, const jsi::Value &partialConfig); +CSSTransitionUpdates parseCSSTransitionUpdates(jsi::Runtime &rt, const jsi::Value &updates); } // namespace reanimated::css diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/core/CSSTransition.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/core/CSSTransition.cpp index 315b3b652c20..44c6892d1b92 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/core/CSSTransition.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/core/CSSTransition.cpp @@ -1,9 +1,11 @@ #include #include +#include #include #include #include +#include namespace reanimated::css { @@ -13,16 +15,9 @@ CSSTransition::CSSTransition( const std::shared_ptr &viewStylesRepository) : shadowNode_(std::move(shadowNode)), viewStylesRepository_(viewStylesRepository), - properties_(config.properties), settings_(config.settings), styleInterpolator_(TransitionStyleInterpolator(shadowNode_->getComponentName(), viewStylesRepository)), - progressProvider_(TransitionProgressProvider()) { - updateAllowedDiscreteProperties(); -} - -Tag CSSTransition::getViewTag() const { - return shadowNode_->getTag(); -} + progressProvider_(TransitionProgressProvider()) {} std::shared_ptr CSSTransition::getShadowNode() const { return shadowNode_; @@ -36,72 +31,52 @@ TransitionProgressState CSSTransition::getState() const { return progressProvider_.getState(); } -folly::dynamic CSSTransition::getCurrentInterpolationStyle() const { - return styleInterpolator_.interpolate(shadowNode_, progressProvider_, allowDiscreteProperties_); -} - -TransitionProperties CSSTransition::getProperties() const { - return properties_; -} - -PropertyNames CSSTransition::getAllowedProperties(const folly::dynamic &oldProps, const folly::dynamic &newProps) { - if (!oldProps.isObject() || !newProps.isObject()) { - return {}; - } - - // If specific properties are set, process only those - if (properties_.has_value()) { - PropertyNames allowedProps; - const auto &properties = properties_.value(); - allowedProps.reserve(properties.size()); +void CSSTransition::updateSettings(const CSSTransitionPropertySettingsUpdates &settingsUpdates) { + for (const auto &[property, partialSettings] : settingsUpdates) { + auto &existingSettings = settings_[property]; - for (const auto &prop : properties) { - if (isAllowedProperty(prop)) { - allowedProps.push_back(prop); - } + if (partialSettings.duration.has_value()) { + existingSettings.duration = partialSettings.duration.value(); } - - return allowedProps; - } - - // Process all properties from both old and new props - std::unordered_set allAllowedProps; - - for (const auto &props : {oldProps, newProps}) { - for (const auto &propertyName : props.keys()) { - if (isAllowedProperty(propertyName.asString())) { - allAllowedProps.insert(propertyName.asString()); - } + if (partialSettings.easingFunction.has_value()) { + existingSettings.easingFunction = partialSettings.easingFunction.value(); + } + if (partialSettings.delay.has_value()) { + existingSettings.delay = partialSettings.delay.value(); + } + if (partialSettings.allowDiscrete.has_value()) { + existingSettings.allowDiscrete = partialSettings.allowDiscrete.value(); } } - - return {allAllowedProps.begin(), allAllowedProps.end()}; } -void CSSTransition::updateSettings(const PartialCSSTransitionConfig &config) { - if (config.properties.has_value()) { - updateTransitionProperties(config.properties.value()); +std::optional> CSSTransition::getProperties() const { + const auto allIt = settings_.find("all"); + if (allIt != settings_.end()) { + return std::nullopt; // "all" means no specific properties } - if (config.settings.has_value()) { - settings_ = config.settings.value(); - updateAllowedDiscreteProperties(); + + std::unordered_set properties; + for (const auto &[property, _] : settings_) { + properties.insert(property); } + return properties; } -folly::dynamic -CSSTransition::run(const ChangedProps &changedProps, const folly::dynamic &lastUpdateValue, const double timestamp) { - progressProvider_.runProgressProviders( - timestamp, - settings_, - changedProps.changedPropertyNames, - styleInterpolator_.getReversedPropertyNames(changedProps.newProps)); - styleInterpolator_.updateInterpolatedProperties(changedProps, lastUpdateValue); +folly::dynamic CSSTransition::run( + jsi::Runtime &rt, + const CSSTransitionPropertyUpdates &propertyUpdates, + const jsi::Value &lastUpdates, + const double timestamp) { + const auto updatedProperties = + styleInterpolator_.updateInterpolatedProperties(rt, propertyUpdates, lastUpdates, settings_); + progressProvider_.runProgressProviders(timestamp, updatedProperties, settings_); return update(timestamp); } folly::dynamic CSSTransition::update(const double timestamp) { progressProvider_.update(timestamp); - auto result = styleInterpolator_.interpolate(shadowNode_, progressProvider_, allowDiscreteProperties_); + auto result = styleInterpolator_.interpolate(shadowNode_, progressProvider_); // Remove interpolators for which interpolation has finished // (we won't need them anymore in the current transition) styleInterpolator_.discardFinishedInterpolators(progressProvider_); @@ -111,35 +86,4 @@ folly::dynamic CSSTransition::update(const double timestamp) { return result; } -void CSSTransition::updateTransitionProperties(const TransitionProperties &properties) { - properties_ = properties; - - const auto isAllPropertiesTransition = !properties_.has_value(); - if (isAllPropertiesTransition) { - return; - } - - const std::unordered_set transitionPropertyNames(properties_->begin(), properties_->end()); - - styleInterpolator_.discardIrrelevantInterpolators(transitionPropertyNames); - progressProvider_.discardIrrelevantProgressProviders(transitionPropertyNames); -} - -void CSSTransition::updateAllowedDiscreteProperties() { - allowDiscreteProperties_.clear(); - for (const auto &[propertyName, propertySettings] : settings_) { - if (propertySettings.allowDiscrete) { - allowDiscreteProperties_.insert(propertyName); - } - } -} - -bool CSSTransition::isAllowedProperty(const std::string &propertyName) const { - if (!isDiscreteProperty(propertyName, shadowNode_->getComponentName())) { - return true; - } - - return allowDiscreteProperties_.contains(propertyName) || allowDiscreteProperties_.contains("all"); -} - } // namespace reanimated::css diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/core/CSSTransition.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/core/CSSTransition.h index 1ba76f6a648a..05be4a49b9ad 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/core/CSSTransition.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/core/CSSTransition.h @@ -18,30 +18,24 @@ class CSSTransition { const CSSTransitionConfig &config, const std::shared_ptr &viewStylesRepository); - Tag getViewTag() const; std::shared_ptr getShadowNode() const; double getMinDelay(double timestamp) const; TransitionProgressState getState() const; - folly::dynamic getCurrentInterpolationStyle() const; - TransitionProperties getProperties() const; - PropertyNames getAllowedProperties(const folly::dynamic &oldProps, const folly::dynamic &newProps); - - void updateSettings(const PartialCSSTransitionConfig &config); - folly::dynamic run(const ChangedProps &changedProps, const folly::dynamic &lastUpdateValue, double timestamp); + void updateSettings(const CSSTransitionPropertySettingsUpdates &settingsUpdates); + std::optional> getProperties() const; + folly::dynamic run( + jsi::Runtime &rt, + const CSSTransitionPropertyUpdates &propertyUpdates, + const jsi::Value &lastUpdates, + double timestamp); folly::dynamic update(double timestamp); private: const std::shared_ptr shadowNode_; const std::shared_ptr viewStylesRepository_; - std::unordered_set allowDiscreteProperties_; - TransitionProperties properties_; CSSTransitionPropertiesSettings settings_; TransitionStyleInterpolator styleInterpolator_; TransitionProgressProvider progressProvider_; - - void updateTransitionProperties(const TransitionProperties &properties); - void updateAllowedDiscreteProperties(); - bool isAllowedProperty(const std::string &propertyName) const; }; } // namespace reanimated::css diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/InterpolatorFactory.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/InterpolatorFactory.cpp index 1e86264b81d1..f64999b76e91 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/InterpolatorFactory.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/InterpolatorFactory.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -12,6 +13,11 @@ class RecordInterpolatorFactory : public PropertyInterpolatorFactory { explicit RecordInterpolatorFactory(const InterpolatorFactoriesRecord &factories) : PropertyInterpolatorFactory(), factories_(factories) {} + bool isDiscrete() const override { + return std::all_of( + factories_.begin(), factories_.end(), [](const auto &pair) { return pair.second->isDiscrete(); }); + } + const CSSValue &getDefaultValue() const override { static EmptyObjectValue emptyObjectValue; return emptyObjectValue; @@ -63,6 +69,10 @@ class ArrayInterpolatorFactory : public ArrayLikeInterpolatorFactory { explicit ArrayInterpolatorFactory(const InterpolatorFactoriesArray &factories) : ArrayLikeInterpolatorFactory(), factories_(factories) {} + bool isDiscrete() const override { + return std::all_of(factories_.begin(), factories_.end(), [](const auto &factory) { return factory->isDiscrete(); }); + } + std::shared_ptr create( const PropertyPath &propertyPath, const std::shared_ptr &viewStylesRepository) const override { @@ -78,6 +88,10 @@ class FiltersInterpolatorFactory : public ArrayLikeInterpolatorFactory { explicit FiltersInterpolatorFactory(const std::shared_ptr &interpolators) : ArrayLikeInterpolatorFactory(), interpolators_(interpolators) {} + bool isDiscrete() const override { + return false; + } + std::shared_ptr create( const PropertyPath &propertyPath, const std::shared_ptr &viewStylesRepository) const override { @@ -93,6 +107,10 @@ class TransformsInterpolatorFactory : public PropertyInterpolatorFactory { explicit TransformsInterpolatorFactory(const std::shared_ptr &interpolators) : PropertyInterpolatorFactory(), interpolators_(interpolators) {} + bool isDiscrete() const override { + return false; + } + const CSSValue &getDefaultValue() const override { static EmptyTransformsValue emptyTransformsValue; return emptyTransformsValue; diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/InterpolatorFactory.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/InterpolatorFactory.h index f19515ff2e00..085ef228a537 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/InterpolatorFactory.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/InterpolatorFactory.h @@ -28,9 +28,7 @@ class SimpleValueInterpolatorFactory : public PropertyInterpolatorFactory { explicit SimpleValueInterpolatorFactory(const TValue &defaultValue) : PropertyInterpolatorFactory(), defaultValue_(defaultValue) {} - bool isDiscreteProperty() const override { - // The property is considered discrete if all of the allowed types are - // discrete + bool isDiscrete() const override { return (Discrete && ...); } @@ -56,6 +54,10 @@ class ResolvableValueInterpolatorFactory : public PropertyInterpolatorFactory { explicit ResolvableValueInterpolatorFactory(const TValue &defaultValue, ResolvableValueInterpolatorConfig config) : PropertyInterpolatorFactory(), defaultValue_(defaultValue), config_(std::move(config)) {} + bool isDiscrete() const override { + return (Discrete && ...); + } + const CSSValue &getDefaultValue() const override { return defaultValue_; } diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/PropertyInterpolator.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/PropertyInterpolator.cpp index 34fd6922f490..f07632ed5d1e 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/PropertyInterpolator.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/PropertyInterpolator.cpp @@ -12,10 +12,6 @@ PropertyInterpolator::PropertyInterpolator( const std::shared_ptr &viewStylesRepository) : propertyPath_(std::move(propertyPath)), viewStylesRepository_(viewStylesRepository) {} -bool PropertyInterpolatorFactory::isDiscreteProperty() const { - return false; -} - std::string PropertyInterpolator::getPropertyPathString() const { if (propertyPath_.empty()) { return ""; diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/PropertyInterpolator.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/PropertyInterpolator.h index 04647f4c52f0..ab712c1badec 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/PropertyInterpolator.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/PropertyInterpolator.h @@ -23,13 +23,12 @@ class PropertyInterpolator { virtual folly::dynamic getResetStyle(const std::shared_ptr &shadowNode) const = 0; virtual folly::dynamic getFirstKeyframeValue() const = 0; virtual folly::dynamic getLastKeyframeValue() const = 0; - virtual bool equalsReversingAdjustedStartValue(const folly::dynamic &propertyValue) const = 0; virtual void updateKeyframes(jsi::Runtime &rt, const jsi::Value &keyframes) = 0; - virtual void updateKeyframesFromStyleChange( - const folly::dynamic &oldStyleValue, - const folly::dynamic &newStyleValue, - const folly::dynamic &lastUpdateValue) = 0; + virtual bool updateKeyframesFromStyleChange( + jsi::Runtime &rt, + const jsi::Value &oldStyleValue, + const jsi::Value &newStyleValue) = 0; virtual folly::dynamic interpolate( const std::shared_ptr &shadowNode, @@ -49,7 +48,7 @@ class PropertyInterpolatorFactory { PropertyInterpolatorFactory() = default; virtual ~PropertyInterpolatorFactory() = default; - virtual bool isDiscreteProperty() const; + virtual bool isDiscrete() const = 0; virtual const CSSValue &getDefaultValue() const = 0; virtual std::shared_ptr create( diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/groups/ArrayPropertiesInterpolator.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/groups/ArrayPropertiesInterpolator.cpp index 4e75120ee27d..cd9b635e2d49 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/groups/ArrayPropertiesInterpolator.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/groups/ArrayPropertiesInterpolator.cpp @@ -11,25 +11,6 @@ ArrayPropertiesInterpolator::ArrayPropertiesInterpolator( const std::shared_ptr &viewStylesRepository) : GroupPropertiesInterpolator(propertyPath, viewStylesRepository), factories_(factories) {} -bool ArrayPropertiesInterpolator::equalsReversingAdjustedStartValue(const folly::dynamic &propertyValue) const { - if (!propertyValue.isArray()) { - return false; - } - - const auto valuesCount = propertyValue.size(); - if (valuesCount != interpolators_.size()) { - return false; - } - - for (size_t i = 0; i < valuesCount; ++i) { - if (!interpolators_[i]->equalsReversingAdjustedStartValue(propertyValue[i])) { - return false; - } - } - - return true; -} - void ArrayPropertiesInterpolator::updateKeyframes(jsi::Runtime &rt, const jsi::Value &keyframes) { const jsi::Array keyframesArray = keyframes.asObject(rt).asArray(rt); const size_t valuesCount = keyframesArray.size(rt); @@ -41,32 +22,31 @@ void ArrayPropertiesInterpolator::updateKeyframes(jsi::Runtime &rt, const jsi::V } } -void ArrayPropertiesInterpolator::updateKeyframesFromStyleChange( - const folly::dynamic &oldStyleValue, - const folly::dynamic &newStyleValue, - const folly::dynamic &lastUpdateValue) { - const auto emptyArray = folly::dynamic::array(); - const auto null = folly::dynamic(); +bool ArrayPropertiesInterpolator::updateKeyframesFromStyleChange( + jsi::Runtime &rt, + const jsi::Value &oldStyleValue, + const jsi::Value &newStyleValue) { + const auto oldStyleArray = oldStyleValue.isUndefined() ? jsi::Array(rt, 0) : oldStyleValue.asObject(rt).asArray(rt); + const auto newStyleArray = newStyleValue.isUndefined() ? jsi::Array(rt, 0) : newStyleValue.asObject(rt).asArray(rt); - const auto &oldStyleArray = oldStyleValue.empty() ? emptyArray : oldStyleValue; - const auto &newStyleArray = newStyleValue.empty() ? emptyArray : newStyleValue; - const auto &lastUpdateArray = lastUpdateValue.empty() ? emptyArray : lastUpdateValue; - - const size_t oldSize = oldStyleArray.size(); - const size_t newSize = newStyleArray.size(); - const size_t lastSize = lastUpdateArray.size(); + const size_t oldSize = oldStyleArray.size(rt); + const size_t newSize = newStyleArray.size(rt); const size_t valuesCount = std::max(oldSize, newSize); resizeInterpolators(valuesCount); + bool areAllPropsReversed = true; + for (size_t i = 0; i < valuesCount; ++i) { // These index checks ensure that interpolation works between 2 arrays // with different lengths - interpolators_[i]->updateKeyframesFromStyleChange( - i < oldSize ? oldStyleArray[i] : null, - i < newSize ? newStyleArray[i] : null, - i < lastSize ? lastUpdateArray[i] : null); + areAllPropsReversed &= interpolators_[i]->updateKeyframesFromStyleChange( + rt, + i < oldSize ? oldStyleArray.getValueAtIndex(rt, i) : jsi::Value::undefined(), + i < newSize ? newStyleArray.getValueAtIndex(rt, i) : jsi::Value::undefined()); } + + return areAllPropsReversed; } folly::dynamic ArrayPropertiesInterpolator::mapInterpolators( diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/groups/ArrayPropertiesInterpolator.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/groups/ArrayPropertiesInterpolator.h index 0284399e1e9c..baba29a8e9d3 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/groups/ArrayPropertiesInterpolator.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/groups/ArrayPropertiesInterpolator.h @@ -15,13 +15,11 @@ class ArrayPropertiesInterpolator : public GroupPropertiesInterpolator { const std::shared_ptr &viewStylesRepository); virtual ~ArrayPropertiesInterpolator() = default; - bool equalsReversingAdjustedStartValue(const folly::dynamic &propertyValue) const override; - void updateKeyframes(jsi::Runtime &rt, const jsi::Value &keyframes) override; - void updateKeyframesFromStyleChange( - const folly::dynamic &oldStyleValue, - const folly::dynamic &newStyleValue, - const folly::dynamic &lastUpdateValue) override; + bool updateKeyframesFromStyleChange( + jsi::Runtime &rt, + const jsi::Value &oldStyleValue, + const jsi::Value &newStyleValue) override; protected: folly::dynamic mapInterpolators(const std::function &callback) const override; diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/groups/RecordPropertiesInterpolator.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/groups/RecordPropertiesInterpolator.cpp index 10a519054a32..a8368551cfc1 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/groups/RecordPropertiesInterpolator.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/groups/RecordPropertiesInterpolator.cpp @@ -12,14 +12,6 @@ RecordPropertiesInterpolator::RecordPropertiesInterpolator( const std::shared_ptr &viewStylesRepository) : GroupPropertiesInterpolator(propertyPath, viewStylesRepository), factories_(factories) {} -bool RecordPropertiesInterpolator::equalsReversingAdjustedStartValue(const folly::dynamic &propertyValue) const { - return std::ranges::all_of(propertyValue.items(), [this](const auto &item) { - const auto &[propName, propValue] = item; - const auto it = interpolators_.find(propName.getString()); - return it != interpolators_.end() && it->second->equalsReversingAdjustedStartValue(propValue); - }); -} - void RecordPropertiesInterpolator::updateKeyframes(jsi::Runtime &rt, const jsi::Value &keyframes) { // TODO - maybe add a possibility to remove interpolators that are no longer // used (for now, for simplicity, we only add new ones) @@ -37,34 +29,42 @@ void RecordPropertiesInterpolator::updateKeyframes(jsi::Runtime &rt, const jsi:: } } -void RecordPropertiesInterpolator::updateKeyframesFromStyleChange( - const folly::dynamic &oldStyleValue, - const folly::dynamic &newStyleValue, - const folly::dynamic &lastUpdateValue) { +bool RecordPropertiesInterpolator::updateKeyframesFromStyleChange( + jsi::Runtime &rt, + const jsi::Value &oldStyleValue, + const jsi::Value &newStyleValue) { // TODO - maybe add a possibility to remove interpolators that are no longer - // used (for now, for simplicity, we only add new ones) - const folly::dynamic emptyObject = folly::dynamic::object(); - const auto null = folly::dynamic(); + // used (for now, for simplicity, we only add new ones) + const auto oldStyleObject = oldStyleValue.isUndefined() ? jsi::Object(rt) : oldStyleValue.asObject(rt); + const auto newStyleObject = newStyleValue.isUndefined() ? jsi::Object(rt) : newStyleValue.asObject(rt); - const auto &oldStyleObject = oldStyleValue.empty() ? emptyObject : oldStyleValue; - const auto &newStyleObject = newStyleValue.empty() ? emptyObject : newStyleValue; - const auto &lastUpdateObject = lastUpdateValue.empty() ? emptyObject : lastUpdateValue; + const auto oldPropertyNames = oldStyleObject.getPropertyNames(rt); + const auto newPropertyNames = newStyleObject.getPropertyNames(rt); + const auto oldSize = oldPropertyNames.size(rt); + const auto newSize = newPropertyNames.size(rt); std::unordered_set propertyNamesSet; - for (const auto &key : oldStyleObject.keys()) { - propertyNamesSet.insert(key.asString()); + for (size_t i = 0; i < oldSize; ++i) { + propertyNamesSet.insert(oldPropertyNames.getValueAtIndex(rt, i).asString(rt).utf8(rt)); } - for (const auto &key : newStyleObject.keys()) { - propertyNamesSet.insert(key.asString()); + for (size_t i = 0; i < newSize; ++i) { + propertyNamesSet.insert(newPropertyNames.getValueAtIndex(rt, i).asString(rt).utf8(rt)); } + bool areAllPropsReversed = true; + for (const auto &propertyName : propertyNamesSet) { maybeCreateInterpolator(propertyName); - interpolators_[propertyName]->updateKeyframesFromStyleChange( - oldStyleObject.getDefault(propertyName, null), - newStyleObject.getDefault(propertyName, null), - lastUpdateObject.getDefault(propertyName, null)); + const auto propNameID = jsi::PropNameID::forUtf8(rt, propertyName); + areAllPropsReversed &= interpolators_[propertyName]->updateKeyframesFromStyleChange( + rt, + oldStyleObject.hasProperty(rt, propNameID) ? oldStyleObject.getProperty(rt, propNameID) + : jsi::Value::undefined(), + newStyleObject.hasProperty(rt, propNameID) ? newStyleObject.getProperty(rt, propNameID) + : jsi::Value::undefined()); } + + return areAllPropsReversed; } folly::dynamic RecordPropertiesInterpolator::mapInterpolators( diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/groups/RecordPropertiesInterpolator.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/groups/RecordPropertiesInterpolator.h index b743e9fa1436..964234a53dd4 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/groups/RecordPropertiesInterpolator.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/groups/RecordPropertiesInterpolator.h @@ -16,13 +16,11 @@ class RecordPropertiesInterpolator : public GroupPropertiesInterpolator { const std::shared_ptr &viewStylesRepository); virtual ~RecordPropertiesInterpolator() = default; - bool equalsReversingAdjustedStartValue(const folly::dynamic &propertyValue) const override; - void updateKeyframes(jsi::Runtime &rt, const jsi::Value &keyframes) override; - void updateKeyframesFromStyleChange( - const folly::dynamic &oldStyleValue, - const folly::dynamic &newStyleValue, - const folly::dynamic &lastUpdateValue) override; + bool updateKeyframesFromStyleChange( + jsi::Runtime &rt, + const jsi::Value &oldStyleValue, + const jsi::Value &newStyleValue) override; protected: folly::dynamic mapInterpolators(const std::function &callback) const override; diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/operations/OperationsStyleInterpolator.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/operations/OperationsStyleInterpolator.cpp index 012fddbc81b1..427833d7e70b 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/operations/OperationsStyleInterpolator.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/operations/OperationsStyleInterpolator.cpp @@ -40,31 +40,6 @@ folly::dynamic OperationsStyleInterpolator::getLastKeyframeValue() const { return toOperations.has_value() ? convertOperationsToDynamic(toOperations.value()) : defaultStyleValueDynamic_; } -bool OperationsStyleInterpolator::equalsReversingAdjustedStartValue(const folly::dynamic &propertyValue) const { - const auto propertyOperations = parseStyleOperations(propertyValue); - - if (!reversingAdjustedStartValue_.has_value()) { - return !propertyOperations.has_value(); - } else if (!propertyOperations.has_value()) { - return false; - } - - const auto &reversingAdjustedOperationsValue = reversingAdjustedStartValue_.value(); - const auto &propertyOperationsValue = propertyOperations.value(); - - if (reversingAdjustedOperationsValue.size() != propertyOperationsValue.size()) { - return false; - } - - for (size_t i = 0; i < reversingAdjustedOperationsValue.size(); ++i) { - if (*reversingAdjustedOperationsValue[i] != *propertyOperationsValue[i]) { - return false; - } - } - - return true; -} - folly::dynamic OperationsStyleInterpolator::interpolate( const std::shared_ptr &shadowNode, const std::shared_ptr &progressProvider, @@ -126,25 +101,25 @@ void OperationsStyleInterpolator::updateKeyframes(jsi::Runtime &rt, const jsi::V } } -void OperationsStyleInterpolator::updateKeyframesFromStyleChange( - const folly::dynamic &oldStyleValue, - const folly::dynamic &newStyleValue, - const folly::dynamic &lastUpdateValue) { - if (oldStyleValue.isNull()) { - reversingAdjustedStartValue_ = std::nullopt; - } else { - reversingAdjustedStartValue_ = parseStyleOperations(oldStyleValue); - } - - const auto &prevStyleValue = lastUpdateValue.isNull() ? oldStyleValue : lastUpdateValue; - +bool OperationsStyleInterpolator::updateKeyframesFromStyleChange( + jsi::Runtime &rt, + const jsi::Value &oldStyleValue, + const jsi::Value &newStyleValue) { keyframes_.clear(); keyframes_.reserve(1); keyframes_.emplace_back(createStyleOperationsKeyframe( 0, 1, - parseStyleOperations(prevStyleValue).value_or(StyleOperations{}), - parseStyleOperations(newStyleValue).value_or(StyleOperations{}))); + parseStyleOperations(rt, oldStyleValue).value_or(StyleOperations{}), + parseStyleOperations(rt, newStyleValue).value_or(StyleOperations{}))); + + const auto &toOperations = keyframes_.back()->toOperations; + bool equalsReversingAdjustedStartValue = toOperations.has_value() == reversingAdjustedStartValue_.has_value() && + (!toOperations.has_value() || toOperations.value() == reversingAdjustedStartValue_.value()); + + reversingAdjustedStartValue_ = keyframes_.back()->fromOperations; + + return equalsReversingAdjustedStartValue; } std::optional OperationsStyleInterpolator::parseStyleOperations( diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/operations/OperationsStyleInterpolator.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/operations/OperationsStyleInterpolator.h index 9c1835c4a667..716526958c32 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/operations/OperationsStyleInterpolator.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/operations/OperationsStyleInterpolator.h @@ -37,18 +37,17 @@ class OperationsStyleInterpolator : public PropertyInterpolator { folly::dynamic getResetStyle(const std::shared_ptr &shadowNode) const override; folly::dynamic getFirstKeyframeValue() const override; folly::dynamic getLastKeyframeValue() const override; - bool equalsReversingAdjustedStartValue(const folly::dynamic &propertyValue) const override; folly::dynamic interpolate( const std::shared_ptr &shadowNode, const std::shared_ptr &progressProvider, - const double fallbackInterpolateThreshold) const override; + double fallbackInterpolateThreshold) const override; void updateKeyframes(jsi::Runtime &rt, const jsi::Value &keyframes) override; - void updateKeyframesFromStyleChange( - const folly::dynamic &oldStyleValue, - const folly::dynamic &newStyleValue, - const folly::dynamic &lastUpdateValue) override; + bool updateKeyframesFromStyleChange( + jsi::Runtime &rt, + const jsi::Value &oldStyleValue, + const jsi::Value &newStyleValue) override; protected: const std::shared_ptr interpolators_; diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/styles/TransitionStyleInterpolator.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/styles/TransitionStyleInterpolator.cpp index 902df9500c3d..cefc31b3c322 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/styles/TransitionStyleInterpolator.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/styles/TransitionStyleInterpolator.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include @@ -9,43 +10,21 @@ namespace reanimated::css { TransitionStyleInterpolator::TransitionStyleInterpolator( const std::string &componentName, const std::shared_ptr &viewStylesRepository) - : componentName_(componentName), viewStylesRepository_(viewStylesRepository) {} - -std::unordered_set TransitionStyleInterpolator::getReversedPropertyNames( - const folly::dynamic &newPropertyValues) const { - std::unordered_set reversedProperties; - - if (!newPropertyValues.isObject()) { - return reversedProperties; - } - - for (const auto &[propertyName, propertyValue] : newPropertyValues.items()) { - const auto propertyNameStr = propertyName.getString(); - const auto it = interpolators_.find(propertyNameStr); - if (it != interpolators_.end() && - // First keyframe value of the previous transition is the reversing - // adjusted start value - it->second->equalsReversingAdjustedStartValue(propertyValue)) { - reversedProperties.insert(propertyNameStr); - } - } - - return reversedProperties; -} + : viewStylesRepository_(viewStylesRepository), interpolatorFactories_(getComponentInterpolators(componentName)) {} folly::dynamic TransitionStyleInterpolator::interpolate( const std::shared_ptr &shadowNode, - const TransitionProgressProvider &transitionProgressProvider, - const std::unordered_set &allowDiscreteProperties) const { + const TransitionProgressProvider &transitionProgressProvider) const { folly::dynamic result = folly::dynamic::object; - const auto allFallbackInterpolateThreshold = allowDiscreteProperties.contains("all") ? 0.5 : 0; - for (const auto &[propertyName, progressProvider] : transitionProgressProvider.getPropertyProgressProviders()) { - const auto &interpolator = interpolators_.at(propertyName); - const auto fallbackInterpolateThreshold = - (allowDiscreteProperties.contains(propertyName)) ? 0.5 : allFallbackInterpolateThreshold; - result[propertyName] = interpolator->interpolate(shadowNode, progressProvider, fallbackInterpolateThreshold); + const auto interpolatorIt = interpolators_.find(propertyName); + if (interpolatorIt == interpolators_.end()) { + continue; + } + + result[propertyName] = interpolatorIt->second->interpolate( + shadowNode, progressProvider, progressProvider->getFallbackInterpolateThreshold()); } return result; @@ -71,33 +50,69 @@ void TransitionStyleInterpolator::discardIrrelevantInterpolators( } } -void TransitionStyleInterpolator::updateInterpolatedProperties( - const ChangedProps &changedProps, - const folly::dynamic &lastUpdateValue) { - const auto &oldPropsObj = changedProps.oldProps; - const auto &newPropsObj = changedProps.newProps; - - const auto empty = folly::dynamic(); +std::vector TransitionStyleInterpolator::updateInterpolatedProperties( + jsi::Runtime &rt, + const CSSTransitionPropertyUpdates &propertyUpdates, + const jsi::Value &lastUpdates, + const CSSTransitionPropertiesSettings &settings) { + std::vector result; + + // Check if lastUpdates is null or undefined (converted from empty folly::dynamic()) + const bool hasLastUpdates = !lastUpdates.isNull() && !lastUpdates.isUndefined(); + const jsi::Object lastUpdatesObject = hasLastUpdates ? lastUpdates.asObject(rt) : jsi::Object(rt); + + for (const auto &[propertyName, diffPair] : propertyUpdates) { + if (!diffPair.has_value() || !isAllowedProperty(propertyName, settings)) { + // If the diffPair is not present (this means that the property was removed and should no + // longer be transitioned) or if the property is not allowed to be transitioned, we should + // remove the interpolator for this property. + interpolators_.erase(propertyName); + result.emplace_back(TransitionPropertyUpdate{propertyName, TransitionPropertyStatus::Removed}); + continue; + } - for (const auto &propertyName : changedProps.changedPropertyNames) { auto it = interpolators_.find(propertyName); - const auto shouldCreateInterpolator = it == interpolators_.end(); - - if (shouldCreateInterpolator) { - const auto newInterpolator = createPropertyInterpolator( - propertyName, {}, getComponentInterpolators(componentName_), viewStylesRepository_); + if (it == interpolators_.end()) { + const auto newInterpolator = + createPropertyInterpolator(propertyName, {}, interpolatorFactories_, viewStylesRepository_); it = interpolators_.emplace(propertyName, newInterpolator).first; } - const auto &oldValue = oldPropsObj.getDefault(propertyName, empty); - const auto &newValue = newPropsObj.getDefault(propertyName, empty); - // Pass lastValue only if the interpolator is updated (no new interpolator - // was created), otherwise pass an empty value - const auto &lastValue = - (shouldCreateInterpolator || lastUpdateValue.empty()) ? empty : lastUpdateValue.getDefault(propertyName, empty); + // Try to get the last update value from the lastUpdates object + // If lastUpdates is null/undefined (from empty folly::dynamic()), use diffPair->first + std::optional lastUpdateValueOpt; + if (hasLastUpdates) { + auto lastUpdateValue = lastUpdatesObject.getProperty(rt, propertyName.c_str()); + if (!lastUpdateValue.isUndefined()) { + lastUpdateValueOpt.emplace(std::move(lastUpdateValue)); + } + } - it->second->updateKeyframesFromStyleChange(oldValue, newValue, lastValue); + const jsi::Value &oldStyleValue = lastUpdateValueOpt.has_value() ? *lastUpdateValueOpt : diffPair->first; + auto isPropertyReversed = it->second->updateKeyframesFromStyleChange(rt, oldStyleValue, diffPair->second); + const auto status = isPropertyReversed ? TransitionPropertyStatus::Reversed : TransitionPropertyStatus::Updated; + result.emplace_back(TransitionPropertyUpdate{propertyName, status}); } + + return result; +} + +bool TransitionStyleInterpolator::isAllowedProperty( + const std::string &propertyName, + const CSSTransitionPropertiesSettings &settings) const { + const auto it = interpolatorFactories_.find(propertyName); + if (it == interpolatorFactories_.end()) { + return false; + } + + // Non-discrete properties are always allowed + if (!it->second->isDiscrete()) { + return true; + } + + // Discrete properties require allowDiscrete to be true + const auto propertySettings = getTransitionPropertySettings(settings, propertyName); + return propertySettings.has_value() && propertySettings->allowDiscrete; } } // namespace reanimated::css diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/styles/TransitionStyleInterpolator.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/styles/TransitionStyleInterpolator.h index 02e121d20e3f..32794a927c7e 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/styles/TransitionStyleInterpolator.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/styles/TransitionStyleInterpolator.h @@ -2,13 +2,16 @@ #include #include +#include #include #include +#include #include #include #include #include +#include namespace reanimated::css { @@ -22,20 +25,25 @@ class TransitionStyleInterpolator { folly::dynamic interpolate( const std::shared_ptr &shadowNode, - const TransitionProgressProvider &transitionProgressProvider, - const std::unordered_set &allowDiscreteProperties) const; + const TransitionProgressProvider &transitionProgressProvider) const; void discardFinishedInterpolators(const TransitionProgressProvider &transitionProgressProvider); void discardIrrelevantInterpolators(const std::unordered_set &transitionPropertyNames); - void updateInterpolatedProperties(const ChangedProps &changedProps, const folly::dynamic &lastUpdateValue); + std::vector updateInterpolatedProperties( + jsi::Runtime &rt, + const CSSTransitionPropertyUpdates &propertyUpdates, + const jsi::Value &lastUpdates, + const CSSTransitionPropertiesSettings &settings); private: + bool isAllowedProperty(const std::string &propertyName, const CSSTransitionPropertiesSettings &settings) const; + using MapInterpolatorsCallback = std::function< folly::dynamic(const std::shared_ptr &, const std::shared_ptr &)>; - const std::string componentName_; const std::shared_ptr viewStylesRepository_; + const InterpolatorFactoriesRecord &interpolatorFactories_; PropertyInterpolatorsRecord interpolators_; }; diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/SimpleValueInterpolator.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/SimpleValueInterpolator.h index 17c168bf00ed..27de1b1d2b7d 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/SimpleValueInterpolator.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/SimpleValueInterpolator.h @@ -27,7 +27,6 @@ class SimpleValueInterpolator : public ValueInterpolator { protected: std::shared_ptr createValue(jsi::Runtime &rt, const jsi::Value &value) const override; - std::shared_ptr createValue(const folly::dynamic &value) const override; folly::dynamic interpolateValue( diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/ValueInterpolator.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/ValueInterpolator.cpp index 2628915de724..ee55c519ed30 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/ValueInterpolator.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/ValueInterpolator.cpp @@ -35,13 +35,6 @@ folly::dynamic ValueInterpolator::getLastKeyframeValue() const { return convertOptionalToDynamic(keyframes_.back().value); } -bool ValueInterpolator::equalsReversingAdjustedStartValue(const folly::dynamic &propertyValue) const { - if (reversingAdjustedStartValue_.isNull()) { - return propertyValue.isNull(); - } - return reversingAdjustedStartValue_ == propertyValue; -} - void ValueInterpolator::updateKeyframes(jsi::Runtime &rt, const jsi::Value &keyframes) { const auto parsedKeyframes = parseJSIKeyframes(rt, keyframes); @@ -57,28 +50,18 @@ void ValueInterpolator::updateKeyframes(jsi::Runtime &rt, const jsi::Value &keyf } } -void ValueInterpolator::updateKeyframesFromStyleChange( - const folly::dynamic &oldStyleValue, - const folly::dynamic &newStyleValue, - const folly::dynamic &lastUpdateValue) { - ValueKeyframe firstKeyframe, lastKeyframe; - - if (!lastUpdateValue.isNull()) { - firstKeyframe = ValueKeyframe{0, createValue(lastUpdateValue)}; - } else if (!oldStyleValue.isNull()) { - firstKeyframe = ValueKeyframe{0, createValue(oldStyleValue)}; - } else { - firstKeyframe = ValueKeyframe{0, defaultStyleValue_}; - } +bool ValueInterpolator::updateKeyframesFromStyleChange( + jsi::Runtime &rt, + const jsi::Value &oldStyleValue, + const jsi::Value &newStyleValue) { + keyframes_ = { + ValueKeyframe{0, oldStyleValue.isUndefined() ? defaultStyleValue_ : createValue(rt, oldStyleValue)}, + ValueKeyframe{1, newStyleValue.isUndefined() ? defaultStyleValue_ : createValue(rt, newStyleValue)}}; - if (newStyleValue.isNull()) { - lastKeyframe = ValueKeyframe{1, defaultStyleValue_}; - } else { - lastKeyframe = ValueKeyframe{1, createValue(newStyleValue)}; - } + auto equalsReversingAdjustedStartValue = keyframes_[1].value.value() == reversingAdjustedStartValue_; + reversingAdjustedStartValue_ = keyframes_[0].value.value(); - keyframes_ = {std::move(firstKeyframe), std::move(lastKeyframe)}; - reversingAdjustedStartValue_ = oldStyleValue; + return equalsReversingAdjustedStartValue; } folly::dynamic ValueInterpolator::interpolate( diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/ValueInterpolator.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/ValueInterpolator.h index d638191faf20..f5e3c7ee8ecf 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/ValueInterpolator.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/ValueInterpolator.h @@ -32,13 +32,12 @@ class ValueInterpolator : public PropertyInterpolator { folly::dynamic getResetStyle(const std::shared_ptr &shadowNode) const override; folly::dynamic getFirstKeyframeValue() const override; folly::dynamic getLastKeyframeValue() const override; - bool equalsReversingAdjustedStartValue(const folly::dynamic &propertyValue) const override; void updateKeyframes(jsi::Runtime &rt, const jsi::Value &keyframes) override; - void updateKeyframesFromStyleChange( - const folly::dynamic &oldStyleValue, - const folly::dynamic &newStyleValue, - const folly::dynamic &lastUpdateValue) override; + bool updateKeyframesFromStyleChange( + jsi::Runtime &rt, + const jsi::Value &oldStyleValue, + const jsi::Value &newStyleValue) override; folly::dynamic interpolate( const std::shared_ptr &shadowNode, @@ -47,9 +46,7 @@ class ValueInterpolator : public PropertyInterpolator { protected: std::vector keyframes_; - std::shared_ptr defaultStyleValue_; folly::dynamic defaultStyleValueDynamic_; - folly::dynamic reversingAdjustedStartValue_; virtual std::shared_ptr createValue(jsi::Runtime &rt, const jsi::Value &value) const = 0; virtual std::shared_ptr createValue(const folly::dynamic &value) const = 0; @@ -60,6 +57,9 @@ class ValueInterpolator : public PropertyInterpolator { const ValueInterpolationContext &context) const = 0; private: + std::shared_ptr defaultStyleValue_; + std::shared_ptr reversingAdjustedStartValue_; + folly::dynamic convertOptionalToDynamic(const std::optional> &value) const; std::shared_ptr getFallbackValue(const std::shared_ptr &shadowNode) const; size_t getToKeyframeIndex(const std::shared_ptr &progressProvider) const; diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/progress/TransitionProgressProvider.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/progress/TransitionProgressProvider.cpp index b100b248549e..2cf8d65fde05 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/progress/TransitionProgressProvider.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/progress/TransitionProgressProvider.cpp @@ -8,23 +8,24 @@ namespace reanimated::css { // TransitionPropertyProgressProvider - TransitionPropertyProgressProvider::TransitionPropertyProgressProvider( const double timestamp, const double duration, const double delay, - const EasingFunction &easingFunction) - : RawProgressProvider(timestamp, duration, delay), easingFunction_(easingFunction) {} - + const EasingFunction &easingFunction, + const bool allowDiscrete) + : RawProgressProvider(timestamp, duration, delay), easingFunction_(easingFunction), allowDiscrete_(allowDiscrete) {} TransitionPropertyProgressProvider::TransitionPropertyProgressProvider( const double timestamp, const double duration, const double delay, const EasingFunction &easingFunction, + const bool allowDiscrete, const double reversingShorteningFactor) : RawProgressProvider(timestamp, duration, delay), easingFunction_(easingFunction), - reversingShorteningFactor_(reversingShorteningFactor) {} + reversingShorteningFactor_(reversingShorteningFactor), + allowDiscrete_(allowDiscrete) {} double TransitionPropertyProgressProvider::getGlobalProgress() const { return rawProgress_.value_or(0); @@ -45,6 +46,10 @@ double TransitionPropertyProgressProvider::getReversingShorteningFactor() const return reversingShorteningFactor_; } +double TransitionPropertyProgressProvider::getFallbackInterpolateThreshold() const { + return allowDiscrete_ ? 0.5 : 0; +} + TransitionProgressState TransitionPropertyProgressProvider::getState() const { if (!rawProgress_.has_value()) { return TransitionProgressState::Pending; @@ -127,14 +132,21 @@ void TransitionProgressProvider::discardIrrelevantProgressProviders( void TransitionProgressProvider::runProgressProviders( const double timestamp, - const CSSTransitionPropertiesSettings &propertiesSettings, - const PropertyNames &changedPropertyNames, - const std::unordered_set &reversedPropertyNames) { - for (const auto &propertyName : changedPropertyNames) { - const auto propertySettingsOptional = getTransitionPropertySettings(propertiesSettings, propertyName); + const std::vector &propertyUpdates, + const CSSTransitionPropertiesSettings &settings) { + for (const auto &propertyUpdate : propertyUpdates) { + const auto &propertyName = propertyUpdate.name; + const auto status = propertyUpdate.status; + + // Handle removed properties + if (status == TransitionPropertyStatus::Removed) { + propertyProgressProviders_.erase(propertyName); + continue; + } + const auto propertySettingsOptional = getTransitionPropertySettings(settings, propertyName); if (!propertySettingsOptional.has_value()) { - throw std::invalid_argument("[Reanimated] Property '" + propertyName + "' is not a valid transition property"); + throw std::runtime_error("[Reanimated] Settings not found for CSS transition property: '" + propertyName + "'"); } const auto &propertySettings = propertySettingsOptional.value(); @@ -144,21 +156,24 @@ void TransitionProgressProvider::runProgressProviders( const auto &progressProvider = it->second; progressProvider->update(timestamp); - if (reversedPropertyNames.find(propertyName) != reversedPropertyNames.end() && + if (status == TransitionPropertyStatus::Reversed && progressProvider->getState() != TransitionProgressState::Finished) { - // Create reversing shortening progress provider for interrupted - // reversing transition + // Create reversing shortening progress provider for interrupted reversing transition propertyProgressProviders_.insert_or_assign( propertyName, createReversingShorteningProgressProvider(timestamp, propertySettings, *progressProvider)); continue; } } - // Create progress provider with the new settings + // Create a new progress provider with the latest settings propertyProgressProviders_.insert_or_assign( propertyName, std::make_shared( - timestamp, propertySettings.duration, propertySettings.delay, propertySettings.easingFunction)); + timestamp, + propertySettings.duration, + propertySettings.delay, + propertySettings.easingFunction, + propertySettings.allowDiscrete)); } } @@ -187,6 +202,7 @@ TransitionProgressProvider::createReversingShorteningProgressProvider( propertySettings.duration * newReversingShorteningFactor, propertySettings.delay < 0 ? newReversingShorteningFactor * propertySettings.delay : propertySettings.delay, propertySettings.easingFunction, + propertySettings.allowDiscrete, newReversingShorteningFactor); } diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/progress/TransitionProgressProvider.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/progress/TransitionProgressProvider.h index 53ce6e626647..68bd28556945 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/progress/TransitionProgressProvider.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/progress/TransitionProgressProvider.h @@ -3,12 +3,12 @@ #include #include #include -#include #include #include #include #include +#include namespace reanimated::css { @@ -20,18 +20,21 @@ class TransitionPropertyProgressProvider final : public KeyframeProgressProvider double timestamp, double duration, double delay, - const EasingFunction &easingFunction); + const EasingFunction &easingFunction, + bool allowDiscrete); TransitionPropertyProgressProvider( double timestamp, double duration, double delay, const EasingFunction &easingFunction, + bool allowDiscrete, double reversingShorteningFactor); double getGlobalProgress() const override; double getKeyframeProgress(double fromOffset, double toOffset) const override; double getRemainingDelay(double timestamp) const; double getReversingShorteningFactor() const; + double getFallbackInterpolateThreshold() const; TransitionProgressState getState() const; protected: @@ -40,6 +43,7 @@ class TransitionPropertyProgressProvider final : public KeyframeProgressProvider private: EasingFunction easingFunction_; double reversingShorteningFactor_ = 1; + bool allowDiscrete_; double getElapsedTime(double timestamp) const; }; @@ -58,9 +62,8 @@ class TransitionProgressProvider final { void discardIrrelevantProgressProviders(const std::unordered_set &transitionPropertyNames); void runProgressProviders( double timestamp, - const CSSTransitionPropertiesSettings &propertiesSettings, - const PropertyNames &changedPropertyNames, - const std::unordered_set &reversedPropertyNames); + const std::vector &propertyUpdates, + const CSSTransitionPropertiesSettings &settings); void update(double timestamp); private: diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/registries/CSSAnimationsRegistry.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/registries/CSSAnimationsRegistry.h index a0b161511222..5a20c08360ec 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/registries/CSSAnimationsRegistry.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/registries/CSSAnimationsRegistry.h @@ -3,7 +3,6 @@ #include #include #include -#include #include #include diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/registries/CSSTransitionsRegistry.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/registries/CSSTransitionsRegistry.cpp index cafc6c10f161..9c123e990d37 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/registries/CSSTransitionsRegistry.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/registries/CSSTransitionsRegistry.cpp @@ -5,10 +5,8 @@ namespace reanimated::css { -CSSTransitionsRegistry::CSSTransitionsRegistry( - const std::shared_ptr &staticPropsRegistry, - const GetAnimationTimestampFunction &getCurrentTimestamp) - : getCurrentTimestamp_(getCurrentTimestamp), staticPropsRegistry_(staticPropsRegistry) {} +CSSTransitionsRegistry::CSSTransitionsRegistry(const GetAnimationTimestampFunction &getCurrentTimestamp) + : getCurrentTimestamp_(getCurrentTimestamp) {} bool CSSTransitionsRegistry::isEmpty() const { // The registry is empty if has no registered animations and no updates @@ -20,32 +18,35 @@ bool CSSTransitionsRegistry::hasUpdates() const { return !runningTransitionTags_.empty() || !delayedTransitionsManager_.empty(); } -void CSSTransitionsRegistry::add(const std::shared_ptr &transition) { - const auto &shadowNode = transition->getShadowNode(); - const auto viewTag = shadowNode->getTag(); - +void CSSTransitionsRegistry::add( + jsi::Runtime &rt, + std::shared_ptr transition, + const CSSTransitionPropertyUpdates &propertyUpdates) { + const auto viewTag = transition->getShadowNode()->getTag(); registry_.insert({viewTag, transition}); - PropsObserver observer = createPropsObserver(viewTag); - staticPropsRegistry_->setObserver(viewTag, std::move(observer)); + runTransition(rt, transition, propertyUpdates); } void CSSTransitionsRegistry::remove(const Tag viewTag) { removeFromUpdatesRegistry(viewTag); - staticPropsRegistry_->removeObserver(viewTag); delayedTransitionsManager_.remove(viewTag); runningTransitionTags_.erase(viewTag); registry_.erase(viewTag); } -void CSSTransitionsRegistry::updateSettings(const Tag viewTag, const PartialCSSTransitionConfig &config) { - const auto &transition = registry_[viewTag]; - transition->updateSettings(config); +void CSSTransitionsRegistry::update(jsi::Runtime &rt, const Tag viewTag, const CSSTransitionUpdates &updates) { + const auto transitionIt = registry_.find(viewTag); + if (transitionIt == registry_.end()) { + return; + } - // Replace style overrides with the new ones if transition properties were - // updated (we want to keep overrides only for transitioned properties) - if (config.properties.has_value()) { - updateInUpdatesRegistry(transition, transition->getCurrentInterpolationStyle()); + const auto &transition = transitionIt->second; + + if (updates.settings.has_value()) { + transition->updateSettings(*updates.settings); } + + runTransition(rt, transition, updates.properties); } void CSSTransitionsRegistry::update(const double timestamp) { @@ -89,7 +90,7 @@ void CSSTransitionsRegistry::activateDelayedTransitions(const double timestamp) } void CSSTransitionsRegistry::scheduleOrActivateTransition(const std::shared_ptr &transition) { - const auto viewTag = transition->getViewTag(); + const auto viewTag = transition->getShadowNode()->getTag(); const auto currentTimestamp = getCurrentTimestamp_(); const auto minDelay = transition->getMinDelay(currentTimestamp); @@ -104,34 +105,6 @@ void CSSTransitionsRegistry::scheduleOrActivateTransition(const std::shared_ptr< } } -PropsObserver CSSTransitionsRegistry::createPropsObserver(const Tag viewTag) { - return [weakThis = weak_from_this(), viewTag](const folly::dynamic &oldProps, const folly::dynamic &newProps) { - auto strongThis = weakThis.lock(); - if (!strongThis) { - return; - } - - const auto &transition = strongThis->registry_[viewTag]; - const auto allowedProperties = transition->getAllowedProperties(oldProps, newProps); - - const auto changedProps = getChangedProps(oldProps, newProps, allowedProperties); - - if (changedProps.changedPropertyNames.empty()) { - return; - } - - { - std::lock_guard lock{strongThis->mutex_}; - - const auto &shadowNode = transition->getShadowNode(); - const auto &lastUpdates = strongThis->getUpdatesFromRegistry(shadowNode->getTag()); - const auto &transitionStartStyle = transition->run(changedProps, lastUpdates, strongThis->getCurrentTimestamp_()); - strongThis->updateInUpdatesRegistry(transition, transitionStartStyle); - strongThis->scheduleOrActivateTransition(transition); - } - }; -} - void CSSTransitionsRegistry::updateInUpdatesRegistry( const std::shared_ptr &transition, const folly::dynamic &updates) { @@ -164,4 +137,15 @@ void CSSTransitionsRegistry::updateInUpdatesRegistry( setInUpdatesRegistry(shadowNode, filteredUpdates); } +void CSSTransitionsRegistry::runTransition( + jsi::Runtime &rt, + const std::shared_ptr &transition, + const CSSTransitionPropertyUpdates &propertyUpdates) { + const auto &lastUpdates = getUpdatesFromRegistry(transition->getShadowNode()->getTag()); + const auto startStyle = + transition->run(rt, propertyUpdates, valueFromDynamic(rt, lastUpdates), getCurrentTimestamp_()); + updateInUpdatesRegistry(transition, startStyle); + scheduleOrActivateTransition(transition); +} + } // namespace reanimated::css diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/registries/CSSTransitionsRegistry.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/registries/CSSTransitionsRegistry.h index dc497118f33b..553717428b1a 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/registries/CSSTransitionsRegistry.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/registries/CSSTransitionsRegistry.h @@ -1,32 +1,26 @@ #pragma once #include -#include #include -#include #include #include #include -#include #include #include -#include -#include namespace reanimated::css { -class CSSTransitionsRegistry : public UpdatesRegistry, public std::enable_shared_from_this { +class CSSTransitionsRegistry : public UpdatesRegistry { public: - CSSTransitionsRegistry( - const std::shared_ptr &staticPropsRegistry, - const GetAnimationTimestampFunction &getCurrentTimestamp); + explicit CSSTransitionsRegistry(const GetAnimationTimestampFunction &getCurrentTimestamp); bool isEmpty() const override; bool hasUpdates() const; - void add(const std::shared_ptr &transition); - void updateSettings(Tag viewTag, const PartialCSSTransitionConfig &config); + void + add(jsi::Runtime &rt, std::shared_ptr transition, const CSSTransitionPropertyUpdates &propertyUpdates); + void update(jsi::Runtime &rt, Tag viewTag, const CSSTransitionUpdates &updates); void remove(Tag viewTag) override; void update(double timestamp); @@ -35,8 +29,6 @@ class CSSTransitionsRegistry : public UpdatesRegistry, public std::enable_shared using Registry = std::unordered_map>; const GetAnimationTimestampFunction &getCurrentTimestamp_; - const std::shared_ptr staticPropsRegistry_; - Registry registry_; std::unordered_set runningTransitionTags_; @@ -44,7 +36,10 @@ class CSSTransitionsRegistry : public UpdatesRegistry, public std::enable_shared void activateDelayedTransitions(double timestamp); void scheduleOrActivateTransition(const std::shared_ptr &transition); - PropsObserver createPropsObserver(Tag viewTag); + void runTransition( + jsi::Runtime &rt, + const std::shared_ptr &transition, + const CSSTransitionPropertyUpdates &propertyUpdates); void updateInUpdatesRegistry(const std::shared_ptr &transition, const folly::dynamic &updates); }; diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/utils/props.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/utils/props.cpp deleted file mode 100644 index ac6ff0fc78fd..000000000000 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/utils/props.cpp +++ /dev/null @@ -1,149 +0,0 @@ -#include - -#include -#include - -namespace reanimated::css { - -bool isDiscreteProperty(const std::string &propName, const std::string &componentName) { - const auto &interpolators = getComponentInterpolators(componentName); - const auto it = interpolators.find(propName); - if (it == interpolators.end()) { - return false; - } - return it->second->isDiscreteProperty(); -} - -bool areArraysDifferentRecursive(const folly::dynamic &oldArray, const folly::dynamic &newArray) { - if (oldArray.size() != newArray.size()) { - return true; - } - - for (size_t i = 0; i < oldArray.size(); i++) { - const auto [oldChangedProp, newChangedProp] = getChangedPropsRecursive(oldArray[i], newArray[i]); - - if (!oldChangedProp.isNull() || !newChangedProp.isNull()) { - return true; - } - } - - return false; -} - -std::pair getChangedPropsRecursive( - const folly::dynamic &oldProp, - const folly::dynamic &newProp) { - if (!oldProp.isObject() || !newProp.isObject()) { - // Primitive values comparison - if (oldProp != newProp) { - return {oldProp, newProp}; - } - return {folly::dynamic(), folly::dynamic()}; - } - - if (oldProp.isArray() && newProp.isArray()) { - // Arrays comparison - if (areArraysDifferentRecursive(oldProp, newProp)) { - return {oldProp, newProp}; - } - return {folly::dynamic(), folly::dynamic()}; - } - - folly::dynamic oldResult = folly::dynamic::object; - folly::dynamic newResult = folly::dynamic::object; - bool oldHasChanges = false; - bool newHasChanges = false; - - // Check for removed properties - for (const auto &item : oldProp.items()) { - const auto &propName = item.first.asString(); - if (!newProp.count(propName)) { - const auto &oldValue = item.second; - oldResult[propName] = oldValue; - oldHasChanges = true; - } - } - - // Check for new and changed properties - for (const auto &item : newProp.items()) { - const auto &propName = item.first.asString(); - const auto &newValue = item.second; - - if (oldProp.count(propName)) { - const auto &oldValue = oldProp[propName]; - auto [oldChangedProp, newChangedProp] = getChangedPropsRecursive(oldValue, newValue); - - if (!oldChangedProp.isNull() && !newChangedProp.isNull()) { - oldResult[propName] = std::move(oldChangedProp); - newResult[propName] = std::move(newChangedProp); - oldHasChanges = true; - newHasChanges = true; - } - } else { - newResult[propName] = newValue; - newHasChanges = true; - } - } - - return { - oldHasChanges ? std::move(oldResult) : folly::dynamic(), newHasChanges ? std::move(newResult) : folly::dynamic()}; -} - -std::pair -getChangedValueForProp(const folly::dynamic &oldObject, const folly::dynamic &newObject, const std::string &propName) { - const bool oldHasProperty = oldObject.count(propName); - const bool newHasProperty = newObject.count(propName); - - if (oldHasProperty && newHasProperty) { - const auto &oldVal = oldObject[propName]; - const auto &newVal = newObject[propName]; - - if (oldVal.isObject() && newVal.isObject()) { - return getChangedPropsRecursive(oldVal, newVal); - } else if (oldVal != newVal) { - return {oldVal, newVal}; - } - - return {folly::dynamic(), folly::dynamic()}; - } - - if (oldHasProperty) { - const auto &oldVal = oldObject[propName]; - return {oldVal, folly::dynamic()}; - } else if (newHasProperty) { - const auto &newVal = newObject[propName]; - return {folly::dynamic(), newVal}; - } - - return {folly::dynamic(), folly::dynamic()}; -} - -ChangedProps getChangedProps( - const folly::dynamic &oldProps, - const folly::dynamic &newProps, - const PropertyNames &allowedProperties) { - folly::dynamic oldResult = folly::dynamic::object; - folly::dynamic newResult = folly::dynamic::object; - PropertyNames changedPropertyNames; - - for (const auto &propName : allowedProperties) { - auto [oldChangedProp, newChangedProp] = getChangedValueForProp(oldProps, newProps, propName); - - const auto hasOldChangedProp = !oldChangedProp.isNull(); - const auto hasNewChangedProp = !newChangedProp.isNull(); - - if (hasOldChangedProp) { - oldResult[propName] = std::move(oldChangedProp); - } - if (hasNewChangedProp) { - newResult[propName] = std::move(newChangedProp); - } - if (hasOldChangedProp || hasNewChangedProp) { - changedPropertyNames.push_back(propName); - } - } - - return {std::move(oldResult), std::move(newResult), std::move(changedPropertyNames)}; -} - -} // namespace reanimated::css diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/utils/props.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/utils/props.h deleted file mode 100644 index fa21ec606eea..000000000000 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/utils/props.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include -#include - -namespace reanimated::css { - -struct ChangedProps { - const folly::dynamic oldProps; - const folly::dynamic newProps; - const PropertyNames changedPropertyNames; -}; - -bool isDiscreteProperty(const std::string &propName, const std::string &componentName); - -// We need to specify it here because there are 2 methods referencing -// each other in the recursion and areArraysDifferentRecursive must be -// aware that getChangedPropsRecursive exists -std::pair getChangedPropsRecursive( - const folly::dynamic &oldProp, - const folly::dynamic &newProp); - -ChangedProps -getChangedProps(const folly::dynamic &oldProps, const folly::dynamic &newProps, const PropertyNames &allowedProperties); - -} // namespace reanimated::css diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp index 454eeb1a4cc0..045ed54413a6 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp @@ -57,7 +57,7 @@ ReanimatedModuleProxy::ReanimatedModuleProxy( viewStylesRepository_(std::make_shared(staticPropsRegistry_, animatedPropsRegistry_)), cssAnimationKeyframesRegistry_(std::make_shared(viewStylesRepository_)), cssAnimationsRegistry_(std::make_shared()), - cssTransitionsRegistry_(std::make_shared(staticPropsRegistry_, getAnimationTimestamp_)), + cssTransitionsRegistry_(std::make_shared(getAnimationTimestamp_)), synchronouslyUpdateUIPropsFunction_(platformDepMethodsHolder.synchronouslyUpdateUIPropsFunction), #ifdef ANDROID filterUnmountedTagsFunction_(platformDepMethodsHolder.filterUnmountedTagsFunction), @@ -421,23 +421,30 @@ void ReanimatedModuleProxy::registerCSSTransition( const jsi::Value &shadowNodeWrapper, const jsi::Value &transitionConfig) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeWrapper); + const auto config = parseCSSTransitionConfig(rt, transitionConfig); - auto transition = std::make_shared( - std::move(shadowNode), parseCSSTransitionConfig(rt, transitionConfig), viewStylesRepository_); + auto transition = std::make_shared(std::move(shadowNode), config, viewStylesRepository_); { auto lock = cssTransitionsRegistry_->lock(); - cssTransitionsRegistry_->add(transition); + cssTransitionsRegistry_->add(rt, transition, config.properties); } + maybeRunCSSLoop(); } void ReanimatedModuleProxy::updateCSSTransition( jsi::Runtime &rt, const jsi::Value &viewTag, - const jsi::Value &configUpdates) { - auto lock = cssTransitionsRegistry_->lock(); - cssTransitionsRegistry_->updateSettings(viewTag.asNumber(), parsePartialCSSTransitionConfig(rt, configUpdates)); + const jsi::Value &transitionUpdates) { + const auto tag = viewTag.asNumber(); + const auto updates = parseCSSTransitionUpdates(rt, transitionUpdates); + + { + auto lock = cssTransitionsRegistry_->lock(); + cssTransitionsRegistry_->update(rt, tag, updates); + } + maybeRunCSSLoop(); } diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.h b/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.h index 599a0157f8c6..e25b60b758cc 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.h @@ -119,7 +119,7 @@ class ReanimatedModuleProxy : public ReanimatedModuleProxySpec, void registerCSSTransition(jsi::Runtime &rt, const jsi::Value &shadowNodeWrapper, const jsi::Value &transitionConfig) override; - void updateCSSTransition(jsi::Runtime &rt, const jsi::Value &viewTag, const jsi::Value &configUpdates) override; + void updateCSSTransition(jsi::Runtime &rt, const jsi::Value &viewTag, const jsi::Value &transitionUpdates) override; void unregisterCSSTransition(jsi::Runtime &rt, const jsi::Value &viewTag) override; void cssLoopCallback(const double /*timestampMs*/); diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxySpec.h b/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxySpec.h index 21304293a29e..fae7c8350fd5 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxySpec.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxySpec.h @@ -84,7 +84,8 @@ class JSI_EXPORT ReanimatedModuleProxySpec : public TurboModule { // CSS transitions virtual void registerCSSTransition(jsi::Runtime &rt, const jsi::Value &shadowNodeWrapper, const jsi::Value &transitionConfig) = 0; - virtual void updateCSSTransition(jsi::Runtime &rt, const jsi::Value &viewTag, const jsi::Value &configUpdates) = 0; + virtual void + updateCSSTransition(jsi::Runtime &rt, const jsi::Value &viewTag, const jsi::Value &transitionUpdates) = 0; virtual void unregisterCSSTransition(jsi::Runtime &rt, const jsi::Value &viewTag) = 0; }; diff --git a/packages/react-native-reanimated/src/ReanimatedModule/NativeReanimated.ts b/packages/react-native-reanimated/src/ReanimatedModule/NativeReanimated.ts index 165db69cb7ef..269aa71d23bb 100644 --- a/packages/react-native-reanimated/src/ReanimatedModule/NativeReanimated.ts +++ b/packages/react-native-reanimated/src/ReanimatedModule/NativeReanimated.ts @@ -23,8 +23,9 @@ import type { } from '../commonTypes'; import type { CSSAnimationUpdates, + CSSTransitionUpdates, NormalizedCSSAnimationKeyframesConfig, - NormalizedCSSTransitionConfig, + NormalizedNewCSSTransitionConfig, } from '../css/native'; import { getShadowNodeWrapperFromRef } from '../fabricUtils'; import { checkCppVersion } from '../platform-specific/checkCppVersion'; @@ -186,7 +187,7 @@ See https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooti this.#reanimatedModuleProxy.unsubscribeFromKeyboardEvents(listenerId); } - setViewStyle(viewTag: number, style: StyleProps) { + setViewStyle(viewTag: number, style: StyleProps | null) { this.#reanimatedModuleProxy.setViewStyle(viewTag, style); } @@ -230,7 +231,7 @@ See https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooti registerCSSTransition( shadowNodeWrapper: ShadowNodeWrapper, - transitionConfig: NormalizedCSSTransitionConfig + transitionConfig: NormalizedNewCSSTransitionConfig ) { this.#reanimatedModuleProxy.registerCSSTransition( shadowNodeWrapper, @@ -240,9 +241,9 @@ See https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooti updateCSSTransition( viewTag: number, - configUpdates: Partial + transitionUpdates: CSSTransitionUpdates ) { - this.#reanimatedModuleProxy.updateCSSTransition(viewTag, configUpdates); + this.#reanimatedModuleProxy.updateCSSTransition(viewTag, transitionUpdates); } unregisterCSSTransition(viewTag: number) { diff --git a/packages/react-native-reanimated/src/ReanimatedModule/js-reanimated/JSReanimated.ts b/packages/react-native-reanimated/src/ReanimatedModule/js-reanimated/JSReanimated.ts index 00cd647af6d9..68798b93dfd6 100644 --- a/packages/react-native-reanimated/src/ReanimatedModule/js-reanimated/JSReanimated.ts +++ b/packages/react-native-reanimated/src/ReanimatedModule/js-reanimated/JSReanimated.ts @@ -23,8 +23,9 @@ import type { import { SensorType } from '../../commonTypes'; import type { CSSAnimationUpdates, + CSSTransitionUpdates, NormalizedCSSAnimationKeyframesConfig, - NormalizedCSSTransitionConfig, + NormalizedNewCSSTransitionConfig, } from '../../css/native'; import { assertWorkletsVersion } from '../../platform-specific/workletsVersion'; import type { IReanimatedModule } from '../reanimatedModuleProxy'; @@ -270,7 +271,7 @@ class JSReanimated implements IReanimatedModule { // noop } - setViewStyle(_viewTag: number, _style: StyleProps): void { + setViewStyle(_viewTag: number, _style: StyleProps | null): void { throw new ReanimatedError('setViewStyle is not available in JSReanimated.'); } @@ -319,7 +320,7 @@ class JSReanimated implements IReanimatedModule { registerCSSTransition( _shadowNodeWrapper: ShadowNodeWrapper, - _transitionConfig: NormalizedCSSTransitionConfig + _transitionConfig: NormalizedNewCSSTransitionConfig ): void { throw new ReanimatedError( '`registerCSSTransition` is not available in JSReanimated.' @@ -328,7 +329,7 @@ class JSReanimated implements IReanimatedModule { updateCSSTransition( _viewTag: number, - _settingsUpdates: Partial + _transitionUpdates: CSSTransitionUpdates ): void { throw new ReanimatedError( '`updateCSSTransition` is not available in JSReanimated.' diff --git a/packages/react-native-reanimated/src/ReanimatedModule/reanimatedModuleProxy.ts b/packages/react-native-reanimated/src/ReanimatedModule/reanimatedModuleProxy.ts index 7eb2a09904b8..a97370b18535 100644 --- a/packages/react-native-reanimated/src/ReanimatedModule/reanimatedModuleProxy.ts +++ b/packages/react-native-reanimated/src/ReanimatedModule/reanimatedModuleProxy.ts @@ -12,8 +12,9 @@ import type { } from '../commonTypes'; import type { CSSAnimationUpdates, + CSSTransitionUpdates, NormalizedCSSAnimationKeyframesConfig, - NormalizedCSSTransitionConfig, + NormalizedNewCSSTransitionConfig, } from '../css/native'; /** Type of `__reanimatedModuleProxy` injected with JSI. */ @@ -59,7 +60,7 @@ export interface ReanimatedModuleProxy { setShouldAnimateExitingForTag(viewTag: number, shouldAnimate: boolean): void; - setViewStyle(viewTag: number, style: StyleProps): void; + setViewStyle(viewTag: number, style: StyleProps | null): void; markNodeAsRemovable(shadowNodeWrapper: ShadowNodeWrapper): void; unmarkNodeAsRemovable(viewTag: number): void; @@ -81,12 +82,12 @@ export interface ReanimatedModuleProxy { registerCSSTransition( shadowNodeWrapper: ShadowNodeWrapper, - transitionConfig: NormalizedCSSTransitionConfig + transitionConfig: NormalizedNewCSSTransitionConfig ): void; updateCSSTransition( viewTag: number, - settingsUpdates: Partial + transitionUpdates: CSSTransitionUpdates ): void; unregisterCSSTransition(viewTag: number): void; diff --git a/packages/react-native-reanimated/src/common/types/helpers.ts b/packages/react-native-reanimated/src/common/types/helpers.ts index 6a924c7633d0..2e9e4ba25a37 100644 --- a/packages/react-native-reanimated/src/common/types/helpers.ts +++ b/packages/react-native-reanimated/src/common/types/helpers.ts @@ -13,6 +13,7 @@ export type Maybe = T | null | undefined; export type NonMutable = T extends object ? Readonly : T; export type AnyRecord = Record; +export type UnknownRecord = Record; export type AnyComponent = ComponentType; diff --git a/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts b/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts index 6c1b9f4d8bb2..4ada19c44dc1 100644 --- a/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts +++ b/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts @@ -18,7 +18,7 @@ export default class CSSManager implements ICSSManager { private readonly viewTag: number; private readonly viewName: string; private readonly styleBuilder: StyleBuilder | null = null; - private isFirstUpdate: boolean = true; + private isStyleSet = false; constructor({ shadowNodeWrapper, viewTag, viewName = 'RCTView' }: ViewInfo) { const tag = (this.viewTag = viewTag as number); @@ -41,30 +41,26 @@ export default class CSSManager implements ICSSManager { filterCSSAndStyleProperties(style); if (!this.styleBuilder && (animationProperties || transitionProperties)) { + const kind = animationProperties ? 'CSS animations' : 'a cSS transition'; throw new ReanimatedError( - `Tried to apply CSS animations to ${this.viewName} which is not supported` + `Tried to apply ${kind} to ${this.viewName} which is not supported` ); } - const normalizedStyle = this.styleBuilder?.buildFrom(filteredStyle); + const normalizedStyle = this.styleBuilder?.buildFrom(filteredStyle) ?? null; - // If the update is called during the first css style update, we won't - // trigger CSS transitions and set styles before attaching CSS transitions - if (this.isFirstUpdate && normalizedStyle) { - setViewStyle(this.viewTag, normalizedStyle); - } - - this.cssTransitionsManager.update(transitionProperties); + this.cssTransitionsManager.update(transitionProperties, normalizedStyle); this.cssAnimationsManager.update(animationProperties); - // If the current update is not the fist one, we want to update CSS - // animations and transitions first and update the style then to make - // sure that the new transition is fired with new settings (like duration) - if (!this.isFirstUpdate && normalizedStyle) { - setViewStyle(this.viewTag, normalizedStyle); + if (normalizedStyle) { + if (animationProperties) { + setViewStyle(this.viewTag, normalizedStyle); + this.isStyleSet = true; + } + } else if (this.isStyleSet) { + setViewStyle(this.viewTag, null); + this.isStyleSet = false; } - - this.isFirstUpdate = false; } unmountCleanup(): void { diff --git a/packages/react-native-reanimated/src/css/native/managers/CSSTransitionsManager.ts b/packages/react-native-reanimated/src/css/native/managers/CSSTransitionsManager.ts index 4f530ff4aa13..019f89f9f0da 100644 --- a/packages/react-native-reanimated/src/css/native/managers/CSSTransitionsManager.ts +++ b/packages/react-native-reanimated/src/css/native/managers/CSSTransitionsManager.ts @@ -1,57 +1,84 @@ 'use strict'; +import type { UnknownRecord } from '../../../common'; import type { ShadowNodeWrapper } from '../../../commonTypes'; import type { CSSTransitionProperties, ICSSTransitionsManager, } from '../../types'; -import { - getNormalizedCSSTransitionConfigUpdates, - normalizeCSSTransitionProperties, -} from '../normalization'; +import { getChangedProps } from '../../utils'; +import { normalizeCSSTransitionProperties } from '../normalization'; import { registerCSSTransition, unregisterCSSTransition, updateCSSTransition, } from '../proxy'; -import type { NormalizedCSSTransitionConfig } from '../types'; +import type { + CSSTransitionUpdates, + NormalizedCSSTransitionConfig, + NormalizedSingleCSSTransitionSettings, +} from '../types'; export default class CSSTransitionsManager implements ICSSTransitionsManager { private readonly viewTag: number; private readonly shadowNodeWrapper: ShadowNodeWrapper; private transitionConfig: NormalizedCSSTransitionConfig | null = null; + private previousStyle: UnknownRecord | null = null; constructor(shadowNodeWrapper: ShadowNodeWrapper, viewTag: number) { this.viewTag = viewTag; this.shadowNodeWrapper = shadowNodeWrapper; } - update(transitionProperties: CSSTransitionProperties | null): void { - if (!transitionProperties) { + update( + transitionProperties: CSSTransitionProperties | null, + style: UnknownRecord | null + ): void { + const previousStyle = this.previousStyle; + const previousConfig = this.transitionConfig; + + this.previousStyle = style; + const transitionConfig = transitionProperties + ? normalizeCSSTransitionProperties(transitionProperties) + : null; + + if (!transitionConfig) { this.detach(); return; } - const transitionConfig = - normalizeCSSTransitionProperties(transitionProperties); - if (!transitionConfig) { - this.detach(); + const propertyDiff = this.getPropertyDiff( + previousStyle, + style, + previousConfig, + transitionConfig + ); + + if (!propertyDiff) { return; } - if (this.transitionConfig) { - const configUpdates = getNormalizedCSSTransitionConfigUpdates( - this.transitionConfig, - transitionConfig - ); + const settingsDiff = previousConfig + ? this.getSettingsUpdates(previousConfig, transitionConfig) + : null; - if (Object.keys(configUpdates).length > 0) { - this.transitionConfig = transitionConfig; - updateCSSTransition(this.viewTag, configUpdates); - } + if (!previousConfig) { + registerCSSTransition(this.shadowNodeWrapper, { + properties: propertyDiff, + settings: transitionConfig.settings, + }); } else { - this.attachTransition(transitionConfig); + const updates: CSSTransitionUpdates = { + properties: propertyDiff, + }; + + if (settingsDiff) { + updates.settings = settingsDiff; + } + + updateCSSTransition(this.viewTag, updates); } + this.transitionConfig = transitionConfig; } unmountCleanup(): void { @@ -61,14 +88,120 @@ export default class CSSTransitionsManager implements ICSSTransitionsManager { private detach() { if (this.transitionConfig) { unregisterCSSTransition(this.viewTag); - this.transitionConfig = null; } + this.transitionConfig = null; + this.previousStyle = null; + } + + private getPropertyDiff( + previousStyle: UnknownRecord | null, + nextStyle: UnknownRecord | null, + previousConfig: NormalizedCSSTransitionConfig | null, + nextConfig: NormalizedCSSTransitionConfig + ): Record | null { + const trackedProperties = + nextConfig.properties === 'all' ? undefined : nextConfig.properties; + + const changedProps = getChangedProps( + previousStyle, + nextStyle, + trackedProperties + ); + + const removedPropsDiff = this.getRemovedPropertiesDiff( + previousStyle, + previousConfig, + nextConfig + ); + + const combinedDiff = { + ...changedProps, + ...removedPropsDiff, + }; + + return Object.keys(combinedDiff).length > 0 ? combinedDiff : null; + } + + private getRemovedPropertiesDiff( + previousStyle: UnknownRecord | null, + previousConfig: NormalizedCSSTransitionConfig | null, + nextConfig: NormalizedCSSTransitionConfig + ): Record { + if (!previousConfig || !previousStyle) { + return {}; + } + + const nextProperties = nextConfig.properties; + if (nextProperties === 'all') { + return {}; + } + + const previousProperties = + previousConfig.properties === 'all' + ? Object.keys(previousStyle) + : previousConfig.properties; + + const diff: Record = {}; + + for (const property of previousProperties) { + if (previousStyle[property] !== undefined) { + diff[property] = null; + } + } + + for (const property of nextProperties) { + delete diff[property]; + } + + return diff; + } + + private getSettingsUpdates( + oldConfig: NormalizedCSSTransitionConfig, + newConfig: NormalizedCSSTransitionConfig + ): Record> | null { + const diff: Record< + string, + Partial + > = {}; + + for (const [property, newSettings] of Object.entries(newConfig.settings)) { + const settingsDiff = this.getPropertySettingsDiff( + oldConfig.settings[property], + newSettings + ); + + if (settingsDiff) { + diff[property] = settingsDiff; + } + } + + return Object.keys(diff).length > 0 ? diff : null; } - private attachTransition(transitionConfig: NormalizedCSSTransitionConfig) { - if (!this.transitionConfig) { - registerCSSTransition(this.shadowNodeWrapper, transitionConfig); - this.transitionConfig = transitionConfig; + private getPropertySettingsDiff( + oldSettings: NormalizedSingleCSSTransitionSettings | undefined, + newSettings: NormalizedSingleCSSTransitionSettings + ): Partial | null { + if (!oldSettings) { + return newSettings; + } + + const settingsDiff: Partial = {}; + + if (oldSettings.duration !== newSettings.duration) { + settingsDiff.duration = newSettings.duration; + } + if (oldSettings.delay !== newSettings.delay) { + settingsDiff.delay = newSettings.delay; } + if (oldSettings.allowDiscrete !== newSettings.allowDiscrete) { + settingsDiff.allowDiscrete = newSettings.allowDiscrete; + } + if (oldSettings.timingFunction !== newSettings.timingFunction) { + settingsDiff.timingFunction = newSettings.timingFunction; + } + + return Object.keys(settingsDiff).length > 0 ? settingsDiff : null; } } diff --git a/packages/react-native-reanimated/src/css/native/managers/__tests__/CSSTransitionsManager.test.ts b/packages/react-native-reanimated/src/css/native/managers/__tests__/CSSTransitionsManager.test.ts index 6781259c30c9..fabf23b62c59 100644 --- a/packages/react-native-reanimated/src/css/native/managers/__tests__/CSSTransitionsManager.test.ts +++ b/packages/react-native-reanimated/src/css/native/managers/__tests__/CSSTransitionsManager.test.ts @@ -26,92 +26,252 @@ describe('CSSTransitionsManager', () => { }); describe('update', () => { + const initialStyle = { opacity: 0 }; + describe('attaching transition', () => { - test('registers a transition if there is no existing transition', () => { + test('registers native transition only after tracked style property changes', () => { const transitionProperties: CSSTransitionProperties = { transitionProperty: 'opacity', + transitionDuration: '200ms', }; + const nextStyle = { opacity: 0.5 }; - manager.update(transitionProperties); + manager.update(transitionProperties, initialStyle); + expect(registerCSSTransition).not.toHaveBeenCalled(); - expect(registerCSSTransition).toHaveBeenCalledWith( - shadowNodeWrapper, - normalizeCSSTransitionProperties(transitionProperties) - ); + manager.update(transitionProperties, nextStyle); + + const normalizedConfig = + normalizeCSSTransitionProperties(transitionProperties); + + expect(registerCSSTransition).toHaveBeenCalledWith(shadowNodeWrapper, { + properties: { opacity: [0, 0.5] }, + settings: normalizedConfig?.settings ?? {}, + }); expect(unregisterCSSTransition).not.toHaveBeenCalled(); expect(updateCSSTransition).not.toHaveBeenCalled(); }); }); describe('updating transition', () => { - test("doesn't update transition if method was called with the same config", () => { + test("doesn't send native updates when style and settings remain unchanged", () => { const transitionProperties: CSSTransitionProperties = { transitionProperty: 'opacity', + transitionDuration: '150ms', }; + const secondStyle = { opacity: 0.4 }; - manager.update(transitionProperties); + manager.update(transitionProperties, initialStyle); + manager.update(transitionProperties, secondStyle); expect(registerCSSTransition).toHaveBeenCalledTimes(1); expect(unregisterCSSTransition).not.toHaveBeenCalled(); expect(updateCSSTransition).not.toHaveBeenCalled(); - manager.update(transitionProperties); - expect(registerCSSTransition).toHaveBeenCalledTimes(1); + jest.clearAllMocks(); + + manager.update(transitionProperties, secondStyle); + expect(registerCSSTransition).not.toHaveBeenCalled(); expect(unregisterCSSTransition).not.toHaveBeenCalled(); expect(updateCSSTransition).not.toHaveBeenCalled(); }); - test('updates transition if method was called with different config', () => { - const transitionProperties: CSSTransitionProperties = { + test('does not invoke native updates when only settings or unrelated props change', () => { + const baseTransition: CSSTransitionProperties = { transitionProperty: 'opacity', + transitionDuration: '180ms', }; - const newTransitionConfig: CSSTransitionProperties = { - transitionProperty: 'transform', - transitionDuration: '1.5s', + const changedStyle = { opacity: 0.6 }; + + manager.update(baseTransition, initialStyle); + manager.update(baseTransition, changedStyle); + + jest.clearAllMocks(); + + const settingsOnlyUpdate: CSSTransitionProperties = { + transitionProperty: 'opacity', + transitionDuration: '220ms', }; - manager.update(transitionProperties); - expect(registerCSSTransition).toHaveBeenCalledTimes(1); + manager.update(settingsOnlyUpdate, changedStyle); + expect(registerCSSTransition).not.toHaveBeenCalled(); expect(unregisterCSSTransition).not.toHaveBeenCalled(); expect(updateCSSTransition).not.toHaveBeenCalled(); - manager.update(newTransitionConfig); - expect(registerCSSTransition).toHaveBeenCalledTimes(1); + const unrelatedStyleChange = { + opacity: 0.6, + transform: 'scale(1.1)', + }; + + manager.update(baseTransition, unrelatedStyleChange); + expect(registerCSSTransition).not.toHaveBeenCalled(); + expect(unregisterCSSTransition).not.toHaveBeenCalled(); + expect(updateCSSTransition).not.toHaveBeenCalled(); + }); + + test('updates native transition when tracked style property changes with new settings', () => { + const transitionProperties: CSSTransitionProperties = { + transitionProperty: 'opacity', + transitionDuration: '100ms', + }; + const updatedTransitionConfig: CSSTransitionProperties = { + transitionProperty: 'opacity', + transitionDuration: '200ms', + }; + const secondStyle = { opacity: 0.25 }; + const thirdStyle = { opacity: 0.75 }; + + manager.update(transitionProperties, initialStyle); + manager.update(transitionProperties, secondStyle); + + jest.clearAllMocks(); + + manager.update(updatedTransitionConfig, thirdStyle); + expect(registerCSSTransition).not.toHaveBeenCalled(); expect(unregisterCSSTransition).not.toHaveBeenCalled(); - expect(updateCSSTransition).toHaveBeenCalledTimes(1); expect(updateCSSTransition).toHaveBeenCalledWith(viewTag, { - properties: ['transform'], - settings: { - transform: { - duration: 1500, - delay: 0, - timingFunction: 'ease', - allowDiscrete: false, - }, - }, + properties: { opacity: [0.25, 0.75] }, + settings: { opacity: { duration: 200 } }, }); }); + + test('removes transitioned properties immediately when transitionProperty no longer includes them', () => { + const transitionProperties: CSSTransitionProperties = { + transitionProperty: ['opacity', 'transform'], + transitionDuration: '150ms', + }; + const initialTransitionStyle = { + opacity: 0, + transform: 'scale(1)', + }; + const updatedStyle = { + opacity: 1, + transform: 'scale(1.1)', + }; + + manager.update(transitionProperties, initialTransitionStyle); + manager.update(transitionProperties, updatedStyle); + + expect(registerCSSTransition).toHaveBeenCalledTimes(1); + + jest.clearAllMocks(); + + const narrowedTransition: CSSTransitionProperties = { + transitionProperty: 'transform', + transitionDuration: '150ms', + }; + + manager.update(narrowedTransition, updatedStyle); + + expect(registerCSSTransition).not.toHaveBeenCalled(); + expect(unregisterCSSTransition).not.toHaveBeenCalled(); + expect(updateCSSTransition).toHaveBeenCalledWith( + viewTag, + expect.objectContaining({ + properties: { opacity: null }, + }) + ); + }); + + test('removes properties when previous config tracked all properties and new config is narrower', () => { + const transitionProperties: CSSTransitionProperties = { + transitionProperty: 'all', + transitionDuration: '120ms', + }; + const initialTransitionStyle = { + opacity: 0, + transform: 'scale(1)', + }; + const updatedStyle = { + opacity: 0.4, + transform: 'scale(1.1)', + }; + + manager.update(transitionProperties, initialTransitionStyle); + manager.update(transitionProperties, updatedStyle); + + jest.clearAllMocks(); + + const narrowedConfig: CSSTransitionProperties = { + transitionProperty: 'transform', + transitionDuration: '120ms', + }; + + manager.update(narrowedConfig, updatedStyle); + + expect(registerCSSTransition).not.toHaveBeenCalled(); + expect(unregisterCSSTransition).not.toHaveBeenCalled(); + expect(updateCSSTransition).toHaveBeenCalledWith( + viewTag, + expect.objectContaining({ + properties: { opacity: null }, + }) + ); + }); + + test('sends combined diff when style changes and property is removed in the same update', () => { + const transitionProperties: CSSTransitionProperties = { + transitionProperty: ['opacity', 'transform'], + transitionDuration: '160ms', + }; + const firstStyle = { + opacity: 0.2, + transform: 'scale(1)', + }; + const secondStyle = { + opacity: 0.5, + transform: 'scale(1.2)', + }; + + manager.update(transitionProperties, firstStyle); + manager.update(transitionProperties, secondStyle); + + jest.clearAllMocks(); + + const narrowedConfig: CSSTransitionProperties = { + transitionProperty: 'transform', + transitionDuration: '160ms', + }; + const thirdStyle = { + opacity: 0.7, + transform: 'scale(1.4)', + }; + + manager.update(narrowedConfig, thirdStyle); + + expect(updateCSSTransition).toHaveBeenCalledWith( + viewTag, + expect.objectContaining({ + properties: { + opacity: null, + transform: ['scale(1.2)', 'scale(1.4)'], + }, + }) + ); + }); }); describe('detaching transition', () => { - test('detaches transition if method was called with null config and there is existing transition', () => { + test('unregisters native transition when config is cleared even if there are no style changes', () => { const transitionProperties: CSSTransitionProperties = { transitionProperty: 'opacity', + transitionDuration: '80ms', }; + const secondStyle = { opacity: 0.6 }; - manager.update(transitionProperties); - expect(registerCSSTransition).toHaveBeenCalledTimes(1); - expect(unregisterCSSTransition).not.toHaveBeenCalled(); - expect(updateCSSTransition).not.toHaveBeenCalled(); + manager.update(transitionProperties, initialStyle); + manager.update(transitionProperties, secondStyle); - manager.update(null); - expect(registerCSSTransition).toHaveBeenCalledTimes(1); - expect(unregisterCSSTransition).toHaveBeenCalledTimes(1); + jest.clearAllMocks(); + + manager.update(null, secondStyle); + expect(registerCSSTransition).not.toHaveBeenCalled(); + expect(unregisterCSSTransition).toHaveBeenCalledWith(viewTag); expect(updateCSSTransition).not.toHaveBeenCalled(); }); }); test("doesn't call detach if there is no existing transition", () => { - manager.update(null); + manager.update(null, null); expect(registerCSSTransition).not.toHaveBeenCalled(); expect(unregisterCSSTransition).not.toHaveBeenCalled(); expect(updateCSSTransition).not.toHaveBeenCalled(); diff --git a/packages/react-native-reanimated/src/css/native/normalization/transition/__tests__/config.test.ts b/packages/react-native-reanimated/src/css/native/normalization/transition/__tests__/config.test.ts index e91398133265..ae40a6becd97 100644 --- a/packages/react-native-reanimated/src/css/native/normalization/transition/__tests__/config.test.ts +++ b/packages/react-native-reanimated/src/css/native/normalization/transition/__tests__/config.test.ts @@ -4,17 +4,8 @@ import { cubicBezier } from '../../../../easing'; import type { CSSTransitionProperties, CSSTransitionProperty, - Repeat, } from '../../../../types'; -import type { - NormalizedCSSTransitionConfig, - NormalizedCSSTransitionPropertyNames, -} from '../../../types'; -import { - ERROR_MESSAGES, - getNormalizedCSSTransitionConfigUpdates, - normalizeCSSTransitionProperties, -} from '../config'; +import { ERROR_MESSAGES, normalizeCSSTransitionProperties } from '../config'; describe(normalizeCSSTransitionProperties, () => { describe('when there is a single transition property', () => { @@ -331,231 +322,3 @@ describe(normalizeCSSTransitionProperties, () => { }); }); }); - -describe(getNormalizedCSSTransitionConfigUpdates, () => { - test('returns empty object if nothing changed', () => { - const oldConfig: NormalizedCSSTransitionConfig = { - properties: 'all', - settings: { - all: { - duration: 1500, - timingFunction: cubicBezier(0.4, 0, 0.2, 1).normalize(), - delay: 300, - allowDiscrete: false, - }, - }, - }; - const newConfig: NormalizedCSSTransitionConfig = { - properties: 'all', - settings: { - all: { - duration: 1500, - timingFunction: cubicBezier(0.4, 0, 0.2, 1).normalize(), - delay: 300, - allowDiscrete: false, - }, - }, - }; - - expect( - getNormalizedCSSTransitionConfigUpdates(oldConfig, newConfig) - ).toEqual({}); - }); - - describe('property changes', () => { - test.each([ - ['all', ['opacity'], ['opacity']], - [['opacity'], 'all', 'all'], - [['opacity'], ['transform'], ['transform']], - [['opacity', 'transform'], 'all', 'all'], - ['all', ['opacity', 'transform'], ['opacity', 'transform']], - [['opacity', 'transform'], ['opacity'], ['opacity']], - ] satisfies Repeat[])( - 'returns property update if properties changed from %p to %p', - (oldProperties, newProperties, expected) => { - const oldConfig: NormalizedCSSTransitionConfig = { - properties: oldProperties, - settings: {}, - }; - const newConfig: NormalizedCSSTransitionConfig = { - properties: newProperties, - settings: {}, - }; - - expect( - getNormalizedCSSTransitionConfigUpdates(oldConfig, newConfig) - ).toEqual({ properties: expected }); - } - ); - - test.each([ - 'all', - ['opacity'], - ['opacity', 'transform'], - ] satisfies NormalizedCSSTransitionPropertyNames[])( - 'does not return property update if properties did not change from %p', - (properties) => { - const oldConfig: NormalizedCSSTransitionConfig = { - properties, - settings: {}, - }; - const newConfig: NormalizedCSSTransitionConfig = { - properties, - settings: {}, - }; - - expect( - getNormalizedCSSTransitionConfigUpdates(oldConfig, newConfig) - ).toEqual({}); - } - ); - }); - - describe('settings changes', () => { - describe('single transition settings', () => { - test('returns all new settings if at least one setting changed', () => { - const oldConfig: NormalizedCSSTransitionConfig = { - properties: 'all', - settings: { - all: { - duration: 1500, - timingFunction: cubicBezier(0.4, 0, 0.2, 1).normalize(), - delay: 300, - allowDiscrete: false, - }, - }, - }; - const newConfig: NormalizedCSSTransitionConfig = { - properties: 'all', - settings: { - all: { - duration: 1500, - timingFunction: 'ease-in', // changed - delay: 300, - allowDiscrete: false, - }, - }, - }; - - expect( - getNormalizedCSSTransitionConfigUpdates(oldConfig, newConfig) - ).toEqual({ - settings: { - all: { - duration: 1500, - timingFunction: 'ease-in', - delay: 300, - allowDiscrete: false, - }, - }, - }); - }); - - test('returns empty object if nothing changed', () => { - const oldConfig: NormalizedCSSTransitionConfig = { - properties: 'all', - settings: { - all: { - duration: 1500, - timingFunction: cubicBezier(0.4, 0, 0.2, 1).normalize(), - delay: 300, - allowDiscrete: false, - }, - }, - }; - const newConfig: NormalizedCSSTransitionConfig = { - properties: 'all', - settings: { - all: { - duration: 1500, - timingFunction: cubicBezier(0.4, 0, 0.2, 1).normalize(), - delay: 300, - allowDiscrete: false, - }, - }, - }; - - expect( - getNormalizedCSSTransitionConfigUpdates(oldConfig, newConfig) - ).toEqual({}); - }); - }); - - describe('multiple transition settings', () => { - test('returns all new settings if at least one setting changed', () => { - const oldConfig: NormalizedCSSTransitionConfig = { - properties: ['opacity', 'transform', 'width'], - settings: { - opacity: { - duration: 1500, - timingFunction: cubicBezier(0.4, 0, 0.2, 1).normalize(), - delay: 300, - allowDiscrete: false, - }, - transform: { - duration: 2000, - timingFunction: 'ease-in', - delay: 500, - allowDiscrete: false, - }, - width: { - duration: 1000, - timingFunction: 'ease-out', - delay: 200, - allowDiscrete: false, - }, - }, - }; - const newConfig: NormalizedCSSTransitionConfig = { - properties: ['transform', 'width', 'opacity'], - settings: { - opacity: { - duration: 1500, - timingFunction: cubicBezier(0.4, 0, 0.2, 1).normalize(), - delay: 500, - allowDiscrete: false, - }, - transform: { - duration: 2000, - timingFunction: 'ease-in', - delay: 500, - allowDiscrete: true, - }, - width: { - duration: 500, - timingFunction: 'ease', - delay: 200, - allowDiscrete: false, - }, - }, - }; - - expect( - getNormalizedCSSTransitionConfigUpdates(oldConfig, newConfig) - ).toEqual({ - properties: ['transform', 'width', 'opacity'], - settings: { - opacity: { - duration: 1500, - timingFunction: cubicBezier(0.4, 0, 0.2, 1).normalize(), - delay: 500, - allowDiscrete: false, - }, - transform: { - duration: 2000, - timingFunction: 'ease-in', - delay: 500, - allowDiscrete: true, - }, - width: { - duration: 500, - timingFunction: 'ease', - delay: 200, - allowDiscrete: false, - }, - }, - }); - }); - }); - }); -}); diff --git a/packages/react-native-reanimated/src/css/native/normalization/transition/config.ts b/packages/react-native-reanimated/src/css/native/normalization/transition/config.ts index 7a014a128eb4..6820b7b760f7 100644 --- a/packages/react-native-reanimated/src/css/native/normalization/transition/config.ts +++ b/packages/react-native-reanimated/src/css/native/normalization/transition/config.ts @@ -5,10 +5,8 @@ import type { CSSTransitionProperties, CSSTransitionProperty, } from '../../../types'; -import { areArraysEqual, deepEqual } from '../../../utils'; import type { NormalizedCSSTransitionConfig, - NormalizedCSSTransitionConfigUpdates, NormalizedSingleCSSTransitionSettings, } from '../../types'; import { @@ -134,41 +132,3 @@ export function normalizeCSSTransitionProperties( settings, }; } - -export function getNormalizedCSSTransitionConfigUpdates( - oldConfig: NormalizedCSSTransitionConfig, - newConfig: NormalizedCSSTransitionConfig -): NormalizedCSSTransitionConfigUpdates { - const configUpdates: NormalizedCSSTransitionConfigUpdates = {}; - - if ( - oldConfig.properties !== newConfig.properties && - (!Array.isArray(oldConfig.properties) || - !Array.isArray(newConfig.properties) || - !areArraysEqual(oldConfig.properties, newConfig.properties)) - ) { - configUpdates.properties = newConfig.properties; - } - - const newSettingsKeys = Object.keys(newConfig.settings); - const oldSettingsKeys = Object.keys(oldConfig.settings); - - if (newSettingsKeys.length !== oldSettingsKeys.length) { - configUpdates.settings = newConfig.settings; - } else { - for (const key of newSettingsKeys) { - if ( - !oldConfig.settings[key] || - // TODO - think of a better way to compare settings (necessary for - // timing functions comparison). Maybe add some custom way instead - // of deepEqual - !deepEqual(oldConfig.settings[key], newConfig.settings[key]) - ) { - configUpdates.settings = newConfig.settings; - break; - } - } - } - - return configUpdates; -} diff --git a/packages/react-native-reanimated/src/css/native/normalization/transition/index.ts b/packages/react-native-reanimated/src/css/native/normalization/transition/index.ts index 514cf90c04cf..8d1a0a296016 100644 --- a/packages/react-native-reanimated/src/css/native/normalization/transition/index.ts +++ b/packages/react-native-reanimated/src/css/native/normalization/transition/index.ts @@ -1,5 +1,2 @@ 'use strict'; -export { - getNormalizedCSSTransitionConfigUpdates, - normalizeCSSTransitionProperties, -} from './config'; +export { normalizeCSSTransitionProperties } from './config'; diff --git a/packages/react-native-reanimated/src/css/native/proxy.ts b/packages/react-native-reanimated/src/css/native/proxy.ts index 914f40bfa5df..8b0db9935af0 100644 --- a/packages/react-native-reanimated/src/css/native/proxy.ts +++ b/packages/react-native-reanimated/src/css/native/proxy.ts @@ -3,13 +3,14 @@ import type { ShadowNodeWrapper, StyleProps } from '../../commonTypes'; import { ReanimatedModule } from '../../ReanimatedModule'; import type { CSSAnimationUpdates, + CSSTransitionUpdates, NormalizedCSSAnimationKeyframesConfig, - NormalizedCSSTransitionConfig, + NormalizedNewCSSTransitionConfig, } from './types'; // COMMON -export function setViewStyle(viewTag: number, style: StyleProps) { +export function setViewStyle(viewTag: number, style: StyleProps | null) { ReanimatedModule.setViewStyle(viewTag, style); } @@ -61,16 +62,16 @@ export function unregisterCSSAnimations(viewTag: number) { export function registerCSSTransition( shadowNodeWrapper: ShadowNodeWrapper, - transitionConfig: NormalizedCSSTransitionConfig + config: NormalizedNewCSSTransitionConfig ) { - ReanimatedModule.registerCSSTransition(shadowNodeWrapper, transitionConfig); + ReanimatedModule.registerCSSTransition(shadowNodeWrapper, config); } export function updateCSSTransition( viewTag: number, - configUpdates: Partial + updates: CSSTransitionUpdates ) { - ReanimatedModule.updateCSSTransition(viewTag, configUpdates); + ReanimatedModule.updateCSSTransition(viewTag, updates); } export function unregisterCSSTransition(viewTag: number) { diff --git a/packages/react-native-reanimated/src/css/native/types/transition.ts b/packages/react-native-reanimated/src/css/native/types/transition.ts index cd85232d9632..a2542b8a211a 100644 --- a/packages/react-native-reanimated/src/css/native/types/transition.ts +++ b/packages/react-native-reanimated/src/css/native/types/transition.ts @@ -8,12 +8,19 @@ export type NormalizedSingleCSSTransitionSettings = { allowDiscrete: boolean; }; -export type NormalizedCSSTransitionPropertyNames = 'all' | string[]; - export type NormalizedCSSTransitionConfig = { - properties: NormalizedCSSTransitionPropertyNames; + properties: 'all' | string[]; + settings: Record; +}; + +type CSSTransitionPropertyUpdates = Record; + +export type NormalizedNewCSSTransitionConfig = { + properties: CSSTransitionPropertyUpdates; settings: Record; }; -export type NormalizedCSSTransitionConfigUpdates = - Partial; +export type CSSTransitionUpdates = { + properties?: CSSTransitionPropertyUpdates; + settings?: Record>; +}; diff --git a/packages/react-native-reanimated/src/css/types/interfaces.ts b/packages/react-native-reanimated/src/css/types/interfaces.ts index 96417881bd86..6fbd00f583d6 100644 --- a/packages/react-native-reanimated/src/css/types/interfaces.ts +++ b/packages/react-native-reanimated/src/css/types/interfaces.ts @@ -1,6 +1,6 @@ 'use strict'; +import type { UnknownRecord } from '../../common'; import type { ExistingCSSAnimationProperties } from './animation'; -import type { CSSStyle } from './props'; import type { CSSTransitionProperties } from './transition'; export interface ICSSAnimationsManager { @@ -9,11 +9,14 @@ export interface ICSSAnimationsManager { } export interface ICSSTransitionsManager { - update(transitionProperties: CSSTransitionProperties | null): void; + update( + transitionProperties: CSSTransitionProperties | null, + style: UnknownRecord | null + ): void; unmountCleanup(): void; } export interface ICSSManager { - update(style: CSSStyle | null): void; + update(style: UnknownRecord | null): void; unmountCleanup(): void; } diff --git a/packages/react-native-reanimated/src/css/utils/equality.ts b/packages/react-native-reanimated/src/css/utils/equality.ts index 2a1a187252ac..b502df00760a 100644 --- a/packages/react-native-reanimated/src/css/utils/equality.ts +++ b/packages/react-native-reanimated/src/css/utils/equality.ts @@ -1,17 +1,4 @@ 'use strict'; -export function areArraysEqual(array1: T[], array2: T[]): boolean { - if (array1.length !== array2.length) { - return false; - } - - for (let i = 0; i < array1.length; i++) { - if (array1[i] !== array2[i]) { - return false; - } - } - - return true; -} export function deepEqual(obj1: T, obj2: T): boolean { if (obj1 === obj2) { diff --git a/packages/react-native-reanimated/src/css/utils/props.ts b/packages/react-native-reanimated/src/css/utils/props.ts index 409ee768021f..1cdc1156e7a8 100644 --- a/packages/react-native-reanimated/src/css/utils/props.ts +++ b/packages/react-native-reanimated/src/css/utils/props.ts @@ -1,5 +1,5 @@ 'use strict'; -import type { AnyRecord, PlainStyle } from '../../common'; +import type { AnyRecord, PlainStyle, UnknownRecord } from '../../common'; import { logger } from '../../common'; import { isSharedValue } from '../../isSharedValue'; import type { @@ -8,6 +8,7 @@ import type { CSSTransitionProperties, ExistingCSSAnimationProperties, } from '../types'; +import { deepEqual } from './equality'; import { isAnimationProp, isCSSKeyframesObject, @@ -95,3 +96,43 @@ function validateCSSTransitionProps(props: Partial) { ); } } + +function hasValue(value: unknown): boolean { + return value !== null && value !== undefined; +} + +export function getChangedProps( + previousStyle: UnknownRecord | null, + nextStyle: UnknownRecord | null, + allowedProperties?: string[] +): Record | null { + if (!previousStyle || !nextStyle) { + return null; + } + + const allowedPropertiesArray = + allowedProperties ?? + Array.from( + new Set([...Object.keys(previousStyle), ...Object.keys(nextStyle)]) + ); + + const diff: Record = {}; + + for (const property of allowedPropertiesArray) { + const nextValue = nextStyle[property]; + const prevValue = previousStyle[property]; + + if (!hasValue(prevValue) || !hasValue(nextValue)) { + if (prevValue !== nextValue) { + diff[property] = [prevValue, nextValue]; + } + continue; + } + + if (!deepEqual(prevValue, nextValue)) { + diff[property] = [prevValue, nextValue]; + } + } + + return Object.keys(diff).length > 0 ? diff : null; +}