/* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include "LayoutAnimationKeyFrameManager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace facebook { namespace react { static better::optional parseAnimationType(std::string param) { if (param == "spring") { return better::optional(AnimationType::Spring); } if (param == "linear") { return better::optional(AnimationType::Linear); } if (param == "easeInEaseOut") { return better::optional(AnimationType::EaseInEaseOut); } if (param == "easeIn") { return better::optional(AnimationType::EaseIn); } if (param == "easeOut") { return better::optional(AnimationType::EaseOut); } if (param == "keyboard") { return better::optional(AnimationType::Keyboard); } return {}; } static better::optional parseAnimationProperty( std::string param) { if (param == "opacity") { return better::optional(AnimationProperty::Opacity); } if (param == "scaleX") { return better::optional(AnimationProperty::ScaleX); } if (param == "scaleY") { return better::optional(AnimationProperty::ScaleY); } if (param == "scaleXY") { return better::optional(AnimationProperty::ScaleXY); } return {}; } static better::optional parseAnimationConfig( folly::dynamic const &config, double defaultDuration) { if (config.empty() || !config.isObject()) { return better::optional( AnimationConfig{AnimationType::Linear, AnimationProperty::NotApplicable, defaultDuration, 0, 0, 0}); } folly::dynamic const &animationTypeParam = config["type"]; if (animationTypeParam.empty() || !animationTypeParam.isString()) { return {}; } const auto animationType = parseAnimationType(animationTypeParam.asString()); if (!animationType) { return {}; } folly::dynamic const &animationPropertyParam = config["property"]; if (animationPropertyParam.empty() || !animationPropertyParam.isString()) { return {}; } const auto animationProperty = parseAnimationProperty(animationPropertyParam.asString()); if (!animationProperty) { return {}; } double duration = defaultDuration; double delay = 0; double springDamping = 0; double initialVelocity = 0; auto const durationIt = config.find("duration"); if (durationIt != config.items().end()) { if (durationIt->second.isDouble()) { duration = durationIt->second.asDouble(); } else { return {}; } } auto const delayIt = config.find("delay"); if (delayIt != config.items().end()) { if (delayIt->second.isDouble()) { delay = delayIt->second.asDouble(); } else { return {}; } } auto const springDampingIt = config.find("springDamping"); if (springDampingIt != config.items().end() && springDampingIt->second.isDouble()) { if (springDampingIt->second.isDouble()) { springDamping = springDampingIt->second.asDouble(); } else { return {}; } } auto const initialVelocityIt = config.find("initialVelocity"); if (initialVelocityIt != config.items().end()) { if (initialVelocityIt->second.isDouble()) { initialVelocity = initialVelocityIt->second.asDouble(); } else { return {}; } } return better::optional(AnimationConfig{*animationType, *animationProperty, duration, delay, springDamping, initialVelocity}); } // Parse animation config from JS static better::optional parseLayoutAnimationConfig( folly::dynamic const &config) { if (config.empty() || !config.isObject()) { return {}; } auto const durationIt = config.find("duration"); if (durationIt == config.items().end() || !durationIt->second.isDouble()) { return {}; } const double duration = durationIt->second.asDouble(); const auto createConfig = parseAnimationConfig(config["create"], duration); if (!createConfig) { return {}; } const auto updateConfig = parseAnimationConfig(config["update"], duration); if (!updateConfig) { return {}; } const auto deleteConfig = parseAnimationConfig(config["delete"], duration); if (!deleteConfig) { return {}; } return better::optional(LayoutAnimationConfig{ duration, *createConfig, *updateConfig, *deleteConfig}); } /** * Globally configure next LayoutAnimation. */ void LayoutAnimationKeyFrameManager::uiManagerDidConfigureNextLayoutAnimation( RawValue const &config, std::shared_ptr successCallback, std::shared_ptr errorCallback) const { auto layoutAnimationConfig = parseLayoutAnimationConfig((folly::dynamic)config); if (layoutAnimationConfig) { std::lock_guard lock(currentAnimationMutex_); currentAnimation_ = better::optional{ LayoutAnimation{-1, 0, false, *layoutAnimationConfig, successCallback, errorCallback, {}}}; } else { // TODO: call errorCallback LOG(ERROR) << "Parsing LayoutAnimationConfig failed: " << (folly::dynamic)config; } } bool LayoutAnimationKeyFrameManager::shouldOverridePullTransaction() const { return shouldAnimateFrame(); } bool LayoutAnimationKeyFrameManager::shouldAnimateFrame() const { // There is potentially a race here between getting and setting // `currentMutation_`. We don't want to lock around this because then we're // creating contention between pullTransaction and the JS thread. return currentAnimation_ || !inflightAnimations_.empty(); } static inline const float interpolateFloats(float coefficient, float oldValue, float newValue) { return oldValue + (newValue - oldValue) * coefficient; } std::pair LayoutAnimationKeyFrameManager::calculateAnimationProgress( uint64_t now, const LayoutAnimation &animation, const AnimationConfig &mutationConfig) const { uint64_t startTime = animation.startTime; uint64_t delay = mutationConfig.delay; uint64_t endTime = startTime + delay + mutationConfig.duration; double progress = (now >= endTime) ? 1 : ((now < startTime + delay) ? 0 : 1 - (double)(endTime - delay - now) / (double)(endTime - animation.startTime)); return {progress, progress}; } void LayoutAnimationKeyFrameManager::adjustDelayedMutationIndicesForMutation( SurfaceId surfaceId, ShadowViewMutation const &mutation) const { bool isRemoveMutation = mutation.type == ShadowViewMutation::Type::Remove; bool isInsertMutation = mutation.type == ShadowViewMutation::Type::Insert; assert(isRemoveMutation || isInsertMutation); for (auto &inflightAnimation : inflightAnimations_) { if (inflightAnimation.surfaceId != surfaceId) { continue; } for (auto it = inflightAnimation.keyFrames.begin(); it != inflightAnimation.keyFrames.end(); it++) { auto &animatedKeyFrame = *it; // Detect if they're in the same view hierarchy, but not equivalent // (We've already detected direct conflicts and handled them above) if (animatedKeyFrame.parentView.tag != mutation.parentShadowView.tag) { continue; } if (animatedKeyFrame.type != AnimationConfigurationType::Noop) { continue; } if (!animatedKeyFrame.finalMutationForKeyFrame.has_value()) { continue; } ShadowViewMutation &finalAnimationMutation = *animatedKeyFrame.finalMutationForKeyFrame; if (finalAnimationMutation.type != ShadowViewMutation::Type::Remove) { continue; } // Do we need to adjust the index of this operation? if (isRemoveMutation && mutation.index <= finalAnimationMutation.index) { finalAnimationMutation.index--; } else if ( isInsertMutation && mutation.index <= finalAnimationMutation.index) { finalAnimationMutation.index++; } } } } better::optional LayoutAnimationKeyFrameManager::pullTransaction( SurfaceId surfaceId, MountingTransaction::Number transactionNumber, MountingTelemetry const &telemetry, ShadowViewMutationList mutations) const { // Current time in milliseconds uint64_t now = std::chrono::duration_cast( std::chrono::high_resolution_clock::now().time_since_epoch()) .count(); if (!mutations.empty()) { #ifdef RN_SHADOW_TREE_INTROSPECTION { std::stringstream ss(getDebugDescription(mutations, {})); std::string to; while (std::getline(ss, to, '\n')) { LOG(ERROR) << "LayoutAnimationKeyFrameManager.cpp: got mutation list: Line: " << to; } }; #endif // What to do if we detect a conflict? Get current value and make // that the baseline of the next animation. Scale the remaining time // in the animation // Types of conflicts and how we handle them: // Update -> update: remove the previous update, make it the baseline of the // next update (with current progress) Update -> remove: same, with final // mutation being a remove Insert -> update: treat as update->update Insert // -> remove: same, as update->remove Remove -> update/insert: not possible // We just collect pairs here of and delete them // from active animations. If another animation is queued up from the // current mutations then these deleted mutations will serve as the baseline // for the next animation. If not, the current mutations are executed // immediately without issues. std::vector< std::tuple> conflictingAnimations{}; for (auto &mutation : mutations) { auto const &baselineShadowView = (mutation.type == ShadowViewMutation::Type::Insert) ? mutation.newChildShadowView : mutation.oldChildShadowView; for (auto &inflightAnimation : inflightAnimations_) { if (inflightAnimation.surfaceId != surfaceId) { continue; } for (auto it = inflightAnimation.keyFrames.begin(); it != inflightAnimation.keyFrames.end();) { auto &animatedKeyFrame = *it; // Conflicting animation detected if (animatedKeyFrame.tag == baselineShadowView.tag) { auto const layoutAnimationConfig = inflightAnimation.layoutAnimationConfig; auto const mutationConfig = (animatedKeyFrame.type == AnimationConfigurationType::Delete ? layoutAnimationConfig.deleteConfig : (animatedKeyFrame.type == AnimationConfigurationType::Create ? layoutAnimationConfig.createConfig : layoutAnimationConfig.updateConfig)); conflictingAnimations.push_back(std::make_tuple( animatedKeyFrame, *mutationConfig, &inflightAnimation)); // Delete from existing animation it = inflightAnimation.keyFrames.erase(it); } else { it++; } } } } // Are we animating this list of mutations? better::optional currentAnimation{}; { std::lock_guard lock(currentAnimationMutex_); if (currentAnimation_) { currentAnimation = currentAnimation_; currentAnimation_ = {}; } } if (currentAnimation) { LayoutAnimation animation = currentAnimation.value(); animation.surfaceId = surfaceId; animation.startTime = now; // Pre-process list to: // Catch remove+reinsert (reorders) // Catch delete+create (reparenting) (this should be optimized away at // the diffing level eventually?) // TODO: to prevent this step we could tag Remove/Insert mutations as // being moves on the Differ level, since we know that there? We could use // TinyMap here, but it's not exposed by Differentiator (yet). std::vector insertedTags; std::vector createdTags; std::unordered_map movedTags; std::vector reparentedTags; for (const auto &mutation : mutations) { if (mutation.type == ShadowViewMutation::Type::Insert) { insertedTags.push_back(mutation.newChildShadowView.tag); } if (mutation.type == ShadowViewMutation::Type::Create) { createdTags.push_back(mutation.newChildShadowView.tag); } } // Process mutations list into operations that can be sent to platform // immediately, and those that need to be animated Deletions, removals, // updates are delayed and animated. Creations and insertions are sent to // platform and then "animated in" with opacity updates. Upon completion, // removals and deletions are sent to platform ShadowViewMutation::List immediateMutations; // Remove operations that are actually moves should be copied to // "immediate mutations". The corresponding "insert" will also be executed // immediately and animated as an update. std::vector keyFramesToAnimate; std::vector movesToAnimate; auto const layoutAnimationConfig = animation.layoutAnimationConfig; for (auto &mutation : mutations) { ShadowView baselineShadowView = (mutation.type == ShadowViewMutation::Type::Delete || mutation.type == ShadowViewMutation::Type::Remove ? mutation.oldChildShadowView : mutation.newChildShadowView); auto const &componentDescriptor = getComponentDescriptorForShadowView(baselineShadowView); auto mutationConfig = (mutation.type == ShadowViewMutation::Type::Delete ? layoutAnimationConfig.deleteConfig : (mutation.type == ShadowViewMutation::Type::Insert ? layoutAnimationConfig.createConfig : layoutAnimationConfig.updateConfig)); bool isRemoveReinserted = mutation.type == ShadowViewMutation::Type::Remove && std::find( insertedTags.begin(), insertedTags.end(), mutation.oldChildShadowView.tag) != insertedTags.end(); // Reparenting can result in a node being removed, inserted (moved) and // also deleted and created in the same frame, with the same props etc. // This should eventually be optimized out of the diffing algorithm, but // for now we detect reparenting and prevent the corresponding // Delete/Create instructions from being animated. bool isReparented = (mutation.type == ShadowViewMutation::Delete && std::find( createdTags.begin(), createdTags.end(), mutation.oldChildShadowView.tag) != createdTags.end()) || (mutation.type == ShadowViewMutation::Create && std::find( reparentedTags.begin(), reparentedTags.end(), mutation.newChildShadowView.tag) != reparentedTags.end()); if (isRemoveReinserted) { movedTags.insert({mutation.oldChildShadowView.tag, mutation}); } if (isReparented && mutation.type == ShadowViewMutation::Delete) { reparentedTags.push_back(mutation.oldChildShadowView.tag); } // Inserts that follow a "remove" of the same tag should be treated as // an update (move) animation. bool wasInsertedTagRemoved = false; bool haveConfiguration = mutationConfig.has_value(); if (mutation.type == ShadowViewMutation::Type::Insert) { // If this is a move, we actually don't want to copy this insert // instruction to animated instructions - we want to // generate an Update mutation for Remove+Insert pairs to animate // the layout. // The corresponding Remove and Insert instructions will instead // be treated as "immediate" instructions. auto movedIt = movedTags.find(mutation.newChildShadowView.tag); wasInsertedTagRemoved = movedIt != movedTags.end(); if (wasInsertedTagRemoved) { mutationConfig = layoutAnimationConfig.updateConfig; } haveConfiguration = mutationConfig.has_value(); if (wasInsertedTagRemoved && haveConfiguration) { movesToAnimate.push_back( AnimationKeyFrame{{}, AnimationConfigurationType::Update, mutation.newChildShadowView.tag, mutation.parentShadowView, movedIt->second.oldChildShadowView, mutation.newChildShadowView}); } } // Creates and inserts should also be executed immediately. // Mutations that would otherwise be animated, but have no // configuration, are also executed immediately. if (isRemoveReinserted || !haveConfiguration || isReparented || mutation.type == ShadowViewMutation::Type::Create || mutation.type == ShadowViewMutation::Type::Insert) { immediateMutations.push_back(mutation); // Adjust indices for any non-directly-conflicting animations that // affect the same parent view by inserting or removing anything // from the hierarchy. if (mutation.type == ShadowViewMutation::Type::Insert || mutation.type == ShadowViewMutation::Type::Remove) { adjustDelayedMutationIndicesForMutation(surfaceId, mutation); } } // Deletes, non-move inserts, updates get animated if (!wasInsertedTagRemoved && !isRemoveReinserted && !isReparented && haveConfiguration && mutation.type != ShadowViewMutation::Type::Create) { ShadowView viewStart = ShadowView( mutation.type == ShadowViewMutation::Type::Insert ? mutation.newChildShadowView : mutation.oldChildShadowView); ShadowView viewFinal = ShadowView( mutation.type == ShadowViewMutation::Type::Update ? mutation.newChildShadowView : viewStart); ShadowView parent = mutation.parentShadowView; Tag tag = viewStart.tag; Tag parentTag = mutation.parentShadowView.tag; AnimationKeyFrame keyFrame{}; if (mutation.type == ShadowViewMutation::Type::Insert) { if (mutationConfig->animationProperty == AnimationProperty::Opacity) { auto props = componentDescriptor.cloneProps(viewStart.props, {}); const auto viewProps = dynamic_cast(props.get()); if (viewProps != nullptr) { const_cast(viewProps)->opacity = 0; } viewStart.props = props; } bool isScaleX = mutationConfig->animationProperty == AnimationProperty::ScaleX || mutationConfig->animationProperty == AnimationProperty::ScaleXY; bool isScaleY = mutationConfig->animationProperty == AnimationProperty::ScaleY || mutationConfig->animationProperty == AnimationProperty::ScaleXY; if (isScaleX || isScaleY) { auto props = componentDescriptor.cloneProps(viewStart.props, {}); const auto viewProps = dynamic_cast(props.get()); if (viewProps != nullptr) { const_cast(viewProps)->transform = Transform::Scale(isScaleX ? 0 : 1, isScaleY ? 0 : 1, 1); } viewStart.props = props; } keyFrame = AnimationKeyFrame{{}, AnimationConfigurationType::Create, tag, parent, viewStart, viewFinal, 0}; } else if (mutation.type == ShadowViewMutation::Type::Delete) { if (mutationConfig->animationProperty == AnimationProperty::Opacity) { auto props = componentDescriptor.cloneProps(viewFinal.props, {}); const auto viewProps = dynamic_cast(props.get()); if (viewProps != nullptr) { const_cast(viewProps)->opacity = 0; } viewFinal.props = props; } bool isScaleX = mutationConfig->animationProperty == AnimationProperty::ScaleX || mutationConfig->animationProperty == AnimationProperty::ScaleXY; bool isScaleY = mutationConfig->animationProperty == AnimationProperty::ScaleY || mutationConfig->animationProperty == AnimationProperty::ScaleXY; if (isScaleX || isScaleY) { auto props = componentDescriptor.cloneProps(viewFinal.props, {}); const auto viewProps = dynamic_cast(props.get()); if (viewProps != nullptr) { const_cast(viewProps)->transform = Transform::Scale(isScaleX ? 0 : 1, isScaleY ? 0 : 1, 1); } viewFinal.props = props; } keyFrame = AnimationKeyFrame{ better::optional(mutation), AnimationConfigurationType::Delete, tag, parent, viewStart, viewFinal, 0}; } else if (mutation.type == ShadowViewMutation::Type::Update) { viewFinal = ShadowView(mutation.newChildShadowView); keyFrame = AnimationKeyFrame{ better::optional(mutation), AnimationConfigurationType::Update, tag, parent, viewStart, viewFinal, 0}; } else { // This should just be "Remove" instructions that are not animated // (either this is a "move", or there's a corresponding "Delete" // that is animated). We configure it as a Noop animation so it is // executed when all the other animations are completed. assert(mutation.type == ShadowViewMutation::Type::Remove); // For remove instructions: since the execution of the Remove // instruction will be delayed and therefore may execute outside of // otherwise-expected order, other views may be inserted before the // Remove is executed, requiring index adjustment. { int adjustedIndex = mutation.index; for (const auto &otherMutation : mutations) { if (otherMutation.type == ShadowViewMutation::Type::Insert && otherMutation.parentShadowView.tag == parentTag) { if (otherMutation.index <= adjustedIndex) { adjustedIndex++; } } } mutation = ShadowViewMutation::RemoveMutation( mutation.parentShadowView, mutation.oldChildShadowView, adjustedIndex); } keyFrame = AnimationKeyFrame{ better::optional(mutation), AnimationConfigurationType::Noop, tag, parent, {}, {}, 0}; } // Handle conflicting animations for (auto &conflictingKeyframeTuple : conflictingAnimations) { auto &conflictingKeyFrame = std::get<0>(conflictingKeyframeTuple); auto const &conflictingMutationBaselineShadowView = conflictingKeyFrame.viewStart; // We've found a conflict. if (conflictingMutationBaselineShadowView.tag == tag) { // What's the progress of this ongoing animation? double conflictingAnimationProgress = calculateAnimationProgress( now, *std::get<2>(conflictingKeyframeTuple), std::get<1>(conflictingKeyframeTuple)) .first; // Get a baseline ShadowView at the current progress of the // inflight animation. TODO: handle multiple properties being // animated separately? auto interpolatedInflightShadowView = createInterpolatedShadowView( conflictingAnimationProgress, std::get<1>(conflictingKeyframeTuple), conflictingKeyFrame.viewStart, conflictingKeyFrame.viewEnd); // Pick a Prop or layout property, depending on the current // animation configuration. Figure out how much progress we've // already made in the current animation, and start the animation // from this point. keyFrame.viewStart = interpolatedInflightShadowView; keyFrame.initialProgress = getProgressThroughAnimation( keyFrame, &animation, interpolatedInflightShadowView); // We're guaranteed that a tag only has one animation associated // with it, so we can break here. If we support multiple // animations and animation curves over the same tag in the // future, this will need to be modified to support that. break; } } keyFramesToAnimate.push_back(keyFrame); } } #ifdef RN_SHADOW_TREE_INTROSPECTION { std::stringstream ss(getDebugDescription(immediateMutations, {})); std::string to; while (std::getline(ss, to, '\n')) { LOG(ERROR) << "LayoutAnimationKeyFrameManager.cpp: got IMMEDIATE list: Line: " << to; } } { std::stringstream ss(getDebugDescription(mutationsToAnimate, {})); std::string to; while (std::getline(ss, to, '\n')) { LOG(ERROR) << "LayoutAnimationKeyFrameManager.cpp: got FINAL list: Line: " << to; } } #endif animation.keyFrames = keyFramesToAnimate; inflightAnimations_.push_back(animation); // These will be executed immediately. mutations = immediateMutations; } /* if (currentAnimation) */ else { // If there's no "next" animation, make sure we queue up "final" // operations from all ongoing animations. ShadowViewMutationList finalMutationsForConflictingAnimations{}; for (auto &conflictingKeyframeTuple : conflictingAnimations) { auto &keyFrame = std::get<0>(conflictingKeyframeTuple); if (keyFrame.finalMutationForKeyFrame.hasValue()) { finalMutationsForConflictingAnimations.push_back( *keyFrame.finalMutationForKeyFrame); } } // Append mutations to this list and swap - so that the final // conflicting mutations happen before any other mutations finalMutationsForConflictingAnimations.insert( finalMutationsForConflictingAnimations.end(), mutations.begin(), mutations.end()); mutations = finalMutationsForConflictingAnimations; // Adjust pending mutation indices base on these operations for (auto &mutation : mutations) { if (mutation.type == ShadowViewMutation::Type::Insert || mutation.type == ShadowViewMutation::Type::Remove) { adjustDelayedMutationIndicesForMutation(surfaceId, mutation); } } } } // if (mutations) // We never commit a different root or modify anything - // we just send additional mutations to the mounting layer until the // animations are finished and the mounting layer (view) represents exactly // what is in the most recent shadow tree // Add animation mutations to the end of our existing mutations list in this // function. ShadowViewMutationList mutationsForAnimation{}; animationMutationsForFrame(surfaceId, mutationsForAnimation, now); // Adjust pending mutation indices base on these operations // For example: if a final "remove" mutation has been performed, and there is // another that has not yet been executed because it is a part of an ongoing // animation, its index may need to be adjusted. for (auto const &animatedMutation : mutationsForAnimation) { if (animatedMutation.type == ShadowViewMutation::Type::Insert || animatedMutation.type == ShadowViewMutation::Type::Remove) { adjustDelayedMutationIndicesForMutation(surfaceId, animatedMutation); } } mutations.insert( mutations.end(), mutationsForAnimation.begin(), mutationsForAnimation.end()); // TODO: fill in telemetry return MountingTransaction{ surfaceId, transactionNumber, std::move(mutations), {}}; } ComponentDescriptor const & LayoutAnimationKeyFrameManager::getComponentDescriptorForShadowView( ShadowView const &shadowView) const { return componentDescriptorRegistry_->at(shadowView.componentHandle); } void LayoutAnimationKeyFrameManager::setComponentDescriptorRegistry( const SharedComponentDescriptorRegistry &componentDescriptorRegistry) { componentDescriptorRegistry_ = componentDescriptorRegistry; } /** * Given a `progress` between 0 and 1, a mutation and LayoutAnimation config, * return a ShadowView with mutated props and/or LayoutMetrics. * * @param progress * @param layoutAnimation * @param animatedMutation * @return */ ShadowView LayoutAnimationKeyFrameManager::createInterpolatedShadowView( double progress, AnimationConfig const &animationConfig, ShadowView startingView, ShadowView finalView) const { ComponentDescriptor const &componentDescriptor = getComponentDescriptorForShadowView(startingView); auto mutatedShadowView = ShadowView(startingView); // Animate opacity or scale/transform mutatedShadowView.props = componentDescriptor.interpolateProps( progress, startingView.props, finalView.props); // Interpolate LayoutMetrics LayoutMetrics const &finalLayoutMetrics = finalView.layoutMetrics; LayoutMetrics const &baselineLayoutMetrics = startingView.layoutMetrics; LayoutMetrics interpolatedLayoutMetrics = finalLayoutMetrics; interpolatedLayoutMetrics.frame.origin.x = interpolateFloats( progress, baselineLayoutMetrics.frame.origin.x, finalLayoutMetrics.frame.origin.x); interpolatedLayoutMetrics.frame.origin.y = interpolateFloats( progress, baselineLayoutMetrics.frame.origin.y, finalLayoutMetrics.frame.origin.y); interpolatedLayoutMetrics.frame.size.width = interpolateFloats( progress, baselineLayoutMetrics.frame.size.width, finalLayoutMetrics.frame.size.width); interpolatedLayoutMetrics.frame.size.height = interpolateFloats( progress, baselineLayoutMetrics.frame.size.height, finalLayoutMetrics.frame.size.height); mutatedShadowView.layoutMetrics = interpolatedLayoutMetrics; return mutatedShadowView; } } // namespace react } // namespace facebook