mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
6a3c866b6a
Summary: My goal in this diff is to make LayoutAnimations more stable, and more resilient to challenging situations. Namely, LayoutAnimations already works fine with: 1) Reorders 2) Flattening/unflattening 3) Deletion and recreation of the same hierarchy 4) Updates conflicting with an existing animation However, what if /all/ of those things are combined? Handling update conflicts with multiple ongoing animations, repeatedly flattening/unflattening the same layer of hierarchy and reordering both parents and children, etc . This diff does not make LayoutAnimations perfect, but it does make LayoutAnimations much more resilient to situations it was not able to handle before. My primary method of testing was to use two Playground examples: one just repeatedly queues up mutations (some animated, some not) that create, update, and delete in the same hierarchy layer. The second, more complex one, mutates between random view hierarchies that involve a lot of flattening and unflattening as well as reordering, over a depth of 5. It also exercises animations over TextInlineViews, which is more challenging. LayoutAnimations works best with the new "Flattening Differ" for now, because the Flattening Differ produces a much smaller, nearly minimal set of instructions in cases of flattening-unflattening. I would like that to not be a hard requirement for using LayoutAnimations, but it's a good starting-point for now. As part of this work, I also developed a lot of debugging and logging mechanisms that are handy for detecting inconsistencies and debugging crashes. Some are included in this diff behind `#define` statements that are disabled by default, and the rest will be published separately and likely cannot be landed permanently, as they're more invasive changes that are only helpful in debugging. # Followups: - Automate testing: write a suite of C++ tests that mutates between random diffs and guarantees that all mutations in a StubViewTree are sensible - Construct a set of minimal repros that catalogues all remaining crashes and inconsistency issues (these seem to be extremely marginal cases and are very hard to repro - so I think it's fine to run this in prod for now, but I will follow-up as soon as I'm back to catalogue and fix all remaining issues) - This diff focuses on *not crashing*, but it is still possible to construct a sequence of complex mutations that results in (for example) views having some opacity between 0 and 1 if animations are interrupted repeatedly. Although this is easy enough to prevent in product code - the types of scenarios I'm running in tests are very unlikely to ever happen in production - it would be nice to be *sure* that LayoutAnimations will always converge to a sensible View hierarchy with up-to-date props. - In general, the index adjustment logic is complicated. I don't know if there's a great way around it, so I need to at least catalogue and test all edge-cases as mentioned above. Changelog: [Internal] Reviewed By: mdvacca Differential Revision: D23382975 fbshipit-source-id: f379d9aa2a4b9c33fa2ba8fa07870c9e31fad5e7
229 lines
8.1 KiB
C++
229 lines
8.1 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 "LayoutAnimationDriver.h"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
|
|
#include <react/renderer/componentregistry/ComponentDescriptorFactory.h>
|
|
#include <react/renderer/components/root/RootShadowNode.h>
|
|
#include <react/renderer/components/view/ViewProps.h>
|
|
#include <react/renderer/core/ComponentDescriptor.h>
|
|
#include <react/renderer/core/LayoutMetrics.h>
|
|
#include <react/renderer/core/LayoutableShadowNode.h>
|
|
#include <react/renderer/core/Props.h>
|
|
#include <react/renderer/mounting/MountingCoordinator.h>
|
|
|
|
#include <react/renderer/mounting/Differentiator.h>
|
|
#include <react/renderer/mounting/ShadowTreeRevision.h>
|
|
#include <react/renderer/mounting/ShadowView.h>
|
|
#include <react/renderer/mounting/ShadowViewMutation.h>
|
|
|
|
#include <glog/logging.h>
|
|
|
|
namespace facebook {
|
|
namespace react {
|
|
|
|
static double
|
|
getProgressFromValues(double start, double end, double currentValue) {
|
|
auto opacityMinmax = std::minmax({start, end});
|
|
auto min = opacityMinmax.first;
|
|
auto max = opacityMinmax.second;
|
|
return (
|
|
currentValue < min
|
|
? 0
|
|
: (currentValue > max ? 0 : ((max - currentValue) / (max - min))));
|
|
}
|
|
|
|
/**
|
|
* Given an animation and a ShadowView with properties set on it, detect how
|
|
* far through the animation the ShadowView has progressed.
|
|
*
|
|
* @param mutationsList
|
|
* @param now
|
|
*/
|
|
double LayoutAnimationDriver::getProgressThroughAnimation(
|
|
AnimationKeyFrame const &keyFrame,
|
|
LayoutAnimation const *layoutAnimation,
|
|
ShadowView const &animationStateView) const {
|
|
auto layoutAnimationConfig = layoutAnimation->layoutAnimationConfig;
|
|
auto const mutationConfig =
|
|
*(keyFrame.type == AnimationConfigurationType::Delete
|
|
? layoutAnimationConfig.deleteConfig
|
|
: (keyFrame.type == AnimationConfigurationType::Create
|
|
? layoutAnimationConfig.createConfig
|
|
: layoutAnimationConfig.updateConfig));
|
|
|
|
auto initialProps = keyFrame.viewStart.props;
|
|
auto finalProps = keyFrame.viewEnd.props;
|
|
|
|
if (mutationConfig.animationProperty == AnimationProperty::Opacity) {
|
|
// Detect progress through opacity animation.
|
|
const auto &oldViewProps =
|
|
dynamic_cast<const ViewProps *>(initialProps.get());
|
|
const auto &newViewProps =
|
|
dynamic_cast<const ViewProps *>(finalProps.get());
|
|
const auto &animationStateViewProps =
|
|
dynamic_cast<const ViewProps *>(animationStateView.props.get());
|
|
if (oldViewProps != nullptr && newViewProps != nullptr &&
|
|
animationStateViewProps != nullptr) {
|
|
return getProgressFromValues(
|
|
oldViewProps->opacity,
|
|
newViewProps->opacity,
|
|
animationStateViewProps->opacity);
|
|
}
|
|
} else if (
|
|
mutationConfig.animationProperty != AnimationProperty::NotApplicable) {
|
|
// Detect progress through layout animation.
|
|
LayoutMetrics const &finalLayoutMetrics = keyFrame.viewEnd.layoutMetrics;
|
|
LayoutMetrics const &baselineLayoutMetrics =
|
|
keyFrame.viewStart.layoutMetrics;
|
|
LayoutMetrics const &animationStateLayoutMetrics =
|
|
animationStateView.layoutMetrics;
|
|
|
|
if (baselineLayoutMetrics.frame.size.height !=
|
|
finalLayoutMetrics.frame.size.height) {
|
|
return getProgressFromValues(
|
|
baselineLayoutMetrics.frame.size.height,
|
|
finalLayoutMetrics.frame.size.height,
|
|
animationStateLayoutMetrics.frame.size.height);
|
|
}
|
|
if (baselineLayoutMetrics.frame.size.width !=
|
|
finalLayoutMetrics.frame.size.width) {
|
|
return getProgressFromValues(
|
|
baselineLayoutMetrics.frame.size.width,
|
|
finalLayoutMetrics.frame.size.width,
|
|
animationStateLayoutMetrics.frame.size.width);
|
|
}
|
|
if (baselineLayoutMetrics.frame.origin.x !=
|
|
finalLayoutMetrics.frame.origin.x) {
|
|
return getProgressFromValues(
|
|
baselineLayoutMetrics.frame.origin.x,
|
|
finalLayoutMetrics.frame.origin.x,
|
|
animationStateLayoutMetrics.frame.origin.x);
|
|
}
|
|
if (baselineLayoutMetrics.frame.origin.y !=
|
|
finalLayoutMetrics.frame.origin.y) {
|
|
return getProgressFromValues(
|
|
baselineLayoutMetrics.frame.origin.y,
|
|
finalLayoutMetrics.frame.origin.y,
|
|
animationStateLayoutMetrics.frame.origin.y);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void LayoutAnimationDriver::animationMutationsForFrame(
|
|
SurfaceId surfaceId,
|
|
ShadowViewMutation::List &mutationsList,
|
|
uint64_t now) const {
|
|
for (auto &animation : inflightAnimations_) {
|
|
if (animation.surfaceId != surfaceId) {
|
|
continue;
|
|
}
|
|
if (animation.completed) {
|
|
continue;
|
|
}
|
|
|
|
int incompleteAnimations = 0;
|
|
for (const auto &keyframe : animation.keyFrames) {
|
|
if (keyframe.type == AnimationConfigurationType::Noop) {
|
|
continue;
|
|
}
|
|
if (keyframe.invalidated) {
|
|
continue;
|
|
}
|
|
|
|
auto const &baselineShadowView = keyframe.viewStart;
|
|
auto const &finalShadowView = keyframe.viewEnd;
|
|
|
|
// The contract with the "keyframes generation" phase is that any animated
|
|
// node will have a valid configuration.
|
|
auto const layoutAnimationConfig = animation.layoutAnimationConfig;
|
|
auto const mutationConfig =
|
|
(keyframe.type == AnimationConfigurationType::Delete
|
|
? layoutAnimationConfig.deleteConfig
|
|
: (keyframe.type == AnimationConfigurationType::Create
|
|
? layoutAnimationConfig.createConfig
|
|
: layoutAnimationConfig.updateConfig));
|
|
|
|
// Interpolate
|
|
std::pair<double, double> progress =
|
|
calculateAnimationProgress(now, animation, *mutationConfig);
|
|
double animationTimeProgressLinear = progress.first;
|
|
double animationInterpolationFactor = progress.second;
|
|
|
|
auto mutatedShadowView = createInterpolatedShadowView(
|
|
animationInterpolationFactor,
|
|
*mutationConfig,
|
|
baselineShadowView,
|
|
finalShadowView);
|
|
|
|
// Create the mutation instruction
|
|
auto updateMutation = ShadowViewMutation::UpdateMutation(
|
|
keyframe.parentView, baselineShadowView, mutatedShadowView, -1);
|
|
mutationsList.push_back(updateMutation);
|
|
PrintMutationInstruction("Animation Progress:", updateMutation);
|
|
|
|
if (animationTimeProgressLinear < 1) {
|
|
incompleteAnimations++;
|
|
}
|
|
}
|
|
|
|
// Are there no ongoing mutations left in this animation?
|
|
if (incompleteAnimations == 0) {
|
|
animation.completed = true;
|
|
}
|
|
}
|
|
|
|
// Clear out finished animations
|
|
for (auto it = inflightAnimations_.begin();
|
|
it != inflightAnimations_.end();) {
|
|
const auto &animation = *it;
|
|
if (animation.completed) {
|
|
callCallback(animation.successCallback);
|
|
|
|
// Queue up "final" mutations for all keyframes in the completed animation
|
|
for (auto const &keyframe : animation.keyFrames) {
|
|
if (!keyframe.invalidated &&
|
|
keyframe.finalMutationForKeyFrame.hasValue()) {
|
|
auto const &finalMutationForKeyFrame =
|
|
*keyframe.finalMutationForKeyFrame;
|
|
PrintMutationInstruction(
|
|
"Animation Complete: Queuing up Final Mutation:",
|
|
finalMutationForKeyFrame);
|
|
|
|
// Copy so that if something else mutates the inflight animations, it
|
|
// won't change this mutation after this point.
|
|
mutationsList.push_back(
|
|
ShadowViewMutation{finalMutationForKeyFrame.type,
|
|
finalMutationForKeyFrame.parentShadowView,
|
|
finalMutationForKeyFrame.oldChildShadowView,
|
|
finalMutationForKeyFrame.newChildShadowView,
|
|
finalMutationForKeyFrame.index});
|
|
}
|
|
}
|
|
|
|
it = inflightAnimations_.erase(it);
|
|
} else {
|
|
it++;
|
|
}
|
|
}
|
|
|
|
// Final step: make sure that all operations execute in the proper order.
|
|
// REMOVE operations with highest indices must operate first.
|
|
std::stable_sort(
|
|
mutationsList.begin(),
|
|
mutationsList.end(),
|
|
&shouldFirstComeBeforeSecondMutation);
|
|
}
|
|
|
|
} // namespace react
|
|
} // namespace facebook
|