Files
react-native/ReactCommon/fabric/animations/LayoutAnimationKeyFrameManager.cpp
T
Joshua Gross f0c595b57c LayoutAnimations: implement LayoutAnimationStatusDelegate for platform-specific integrations
Summary:
The LayoutAnimationStatusDelegate exists so that platforms can get a signal when animations are starting or have all completed.

This signal is meant to be used ONLY for driving animations at 60fps, or stopping that process, on the platform side.

Changelog: [Internal]

Reviewed By: mdvacca

Differential Revision: D21583109

fbshipit-source-id: 234496841bde226fcd6623c74c1a500e5cd00d99
2020-05-20 14:16:46 -07:00

908 lines
35 KiB
C++

/*
* 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 <algorithm>
#include <chrono>
#include <react/componentregistry/ComponentDescriptorFactory.h>
#include <react/components/root/RootShadowNode.h>
#include <react/components/view/ViewProps.h>
#include <react/core/ComponentDescriptor.h>
#include <react/core/LayoutMetrics.h>
#include <react/core/LayoutableShadowNode.h>
#include <react/core/Props.h>
#include <react/core/RawValue.h>
#include <react/mounting/MountingCoordinator.h>
#include <react/mounting/Differentiator.h>
#include <react/mounting/ShadowTreeRevision.h>
#include <react/mounting/ShadowView.h>
#include <react/mounting/ShadowViewMutation.h>
#include <Glog/logging.h>
namespace facebook {
namespace react {
static better::optional<AnimationType> parseAnimationType(std::string param) {
if (param == "spring") {
return better::optional<AnimationType>(AnimationType::Spring);
}
if (param == "linear") {
return better::optional<AnimationType>(AnimationType::Linear);
}
if (param == "easeInEaseOut") {
return better::optional<AnimationType>(AnimationType::EaseInEaseOut);
}
if (param == "easeIn") {
return better::optional<AnimationType>(AnimationType::EaseIn);
}
if (param == "easeOut") {
return better::optional<AnimationType>(AnimationType::EaseOut);
}
if (param == "keyboard") {
return better::optional<AnimationType>(AnimationType::Keyboard);
}
return {};
}
static better::optional<AnimationProperty> parseAnimationProperty(
std::string param) {
if (param == "opacity") {
return better::optional<AnimationProperty>(AnimationProperty::Opacity);
}
if (param == "scaleX") {
return better::optional<AnimationProperty>(AnimationProperty::ScaleX);
}
if (param == "scaleY") {
return better::optional<AnimationProperty>(AnimationProperty::ScaleY);
}
if (param == "scaleXY") {
return better::optional<AnimationProperty>(AnimationProperty::ScaleXY);
}
return {};
}
static better::optional<AnimationConfig> parseAnimationConfig(
folly::dynamic const &config,
double defaultDuration) {
if (config.empty() || !config.isObject()) {
return better::optional<AnimationConfig>(
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.5;
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>(AnimationConfig{*animationType,
*animationProperty,
duration,
delay,
springDamping,
initialVelocity});
}
// Parse animation config from JS
static better::optional<LayoutAnimationConfig> 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>(LayoutAnimationConfig{
duration, *createConfig, *updateConfig, *deleteConfig});
}
/**
* Globally configure next LayoutAnimation.
*/
void LayoutAnimationKeyFrameManager::uiManagerDidConfigureNextLayoutAnimation(
RawValue const &config,
std::shared_ptr<const EventTarget> successCallback,
std::shared_ptr<const EventTarget> errorCallback) const {
auto layoutAnimationConfig =
parseLayoutAnimationConfig((folly::dynamic)config);
if (layoutAnimationConfig) {
std::lock_guard<std::mutex> lock(currentAnimationMutex_);
currentAnimation_ = better::optional<LayoutAnimation>{
LayoutAnimation{-1,
0,
false,
*layoutAnimationConfig,
successCallback,
errorCallback,
{}}};
} else {
// TODO: call errorCallback
LOG(ERROR) << "Parsing LayoutAnimationConfig failed: "
<< (folly::dynamic)config;
}
}
void LayoutAnimationKeyFrameManager::setLayoutAnimationStatusDelegate(
LayoutAnimationStatusDelegate *delegate) const {
std::lock_guard<std::mutex> lock(layoutAnimationStatusDelegateMutex_);
layoutAnimationStatusDelegate_ = delegate;
}
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<double, double>
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;
static const float PI = 3.14159265358979323846;
if (now >= endTime) {
return {1, 1};
}
if (now < startTime + delay) {
return {0, 0};
}
double linearTimeProgression = 1 -
(double)(endTime - delay - now) / (double)(endTime - animation.startTime);
if (mutationConfig.animationType == AnimationType::Linear) {
return {linearTimeProgression, linearTimeProgression};
} else if (mutationConfig.animationType == AnimationType::EaseIn) {
// This is an accelerator-style interpolator.
// In the future, this parameter (2.0) could be adjusted. This has been the
// default for Classic RN forever.
return {linearTimeProgression, pow(linearTimeProgression, 2.0)};
} else if (mutationConfig.animationType == AnimationType::EaseOut) {
// This is an decelerator-style interpolator.
// In the future, this parameter (2.0) could be adjusted. This has been the
// default for Classic RN forever.
return {linearTimeProgression, 1.0 - pow(1 - linearTimeProgression, 2.0)};
} else if (mutationConfig.animationType == AnimationType::EaseInEaseOut) {
// This is a combination of accelerate+decelerate.
// The animation starts and ends slowly, and speeds up in the middle.
return {linearTimeProgression,
cos((linearTimeProgression + 1.0) * PI) / 2 + 0.5};
} else if (mutationConfig.animationType == AnimationType::Spring) {
// Using mSpringDamping in this equation is not really the exact
// mathematical springDamping, but a good approximation We need to replace
// this equation with the right Factor that accounts for damping and
// friction
double damping = mutationConfig.springDamping;
return {
linearTimeProgression,
(1 +
pow(2, -10 * linearTimeProgression) *
sin((linearTimeProgression - damping / 4) * PI * 2 / damping))};
} else {
return {linearTimeProgression, linearTimeProgression};
}
}
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<MountingTransaction>
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::milliseconds>(
std::chrono::high_resolution_clock::now().time_since_epoch())
.count();
bool inflightAnimationsExistInitially = !inflightAnimations_.empty();
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 <Mutation, AnimationConfig> 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<AnimationKeyFrame, AnimationConfig, LayoutAnimation *>>
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<LayoutAnimation> currentAnimation{};
{
std::lock_guard<std::mutex> 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<Tag> insertedTags;
std::vector<Tag> createdTags;
std::unordered_map<Tag, ShadowViewMutation> movedTags;
std::vector<Tag> 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<AnimationKeyFrame> keyFramesToAnimate;
std::vector<AnimationKeyFrame> 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<const ViewProps *>(props.get());
if (viewProps != nullptr) {
const_cast<ViewProps *>(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<const ViewProps *>(props.get());
if (viewProps != nullptr) {
const_cast<ViewProps *>(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<const ViewProps *>(props.get());
if (viewProps != nullptr) {
const_cast<ViewProps *>(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<const ViewProps *>(props.get());
if (viewProps != nullptr) {
const_cast<ViewProps *>(viewProps)->transform =
Transform::Scale(isScaleX ? 0 : 1, isScaleY ? 0 : 1, 1);
}
viewFinal.props = props;
}
keyFrame = AnimationKeyFrame{
better::optional<ShadowViewMutation>(mutation),
AnimationConfigurationType::Delete,
tag,
parent,
viewStart,
viewFinal,
0};
} else if (mutation.type == ShadowViewMutation::Type::Update) {
viewFinal = ShadowView(mutation.newChildShadowView);
keyFrame = AnimationKeyFrame{
better::optional<ShadowViewMutation>(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<ShadowViewMutation>(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());
// Signal to delegate if all animations are complete, or if we were not
// animating anything and now some animation exists.
if (inflightAnimationsExistInitially && inflightAnimations_.empty()) {
std::lock_guard<std::mutex> lock(layoutAnimationStatusDelegateMutex_);
if (layoutAnimationStatusDelegate_ != nullptr) {
layoutAnimationStatusDelegate_->onAllAnimationsComplete();
}
} else if (
!inflightAnimationsExistInitially && !inflightAnimations_.empty()) {
std::lock_guard<std::mutex> lock(layoutAnimationStatusDelegateMutex_);
if (layoutAnimationStatusDelegate_ != nullptr) {
layoutAnimationStatusDelegate_->onAnimationStarted();
}
}
// 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