Unverified Commit 05eae425 authored by chunhtai's avatar chunhtai Committed by GitHub

fix initial routes do not run secondary animation when pops (#47476)

parent 374b55cc
......@@ -256,42 +256,99 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
super.didChangeNext(nextRoute);
}
// A callback method that disposes existing train hopping animation and
// removes its listener.
//
// This property is non-null if there is a train hopping in progress, and the
// caller must reset this property to null after it is called.
VoidCallback _trainHoppingListenerRemover;
void _updateSecondaryAnimation(Route<dynamic> nextRoute) {
// There is an existing train hopping in progress. Unfortunately, we cannot
// dispose current train hopping animation until we replace it with a new
// animation.
final VoidCallback previousTrainHoppingListenerRemover = _trainHoppingListenerRemover;
_trainHoppingListenerRemover = null;
if (nextRoute is TransitionRoute<dynamic> && canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) {
final Animation<double> current = _secondaryAnimation.parent;
if (current != null) {
final Animation<double> currentTrain = current is TrainHoppingAnimation ? current.currentTrain : current;
final Animation<double> nextTrain = nextRoute._animation;
if (currentTrain.value == nextTrain.value) {
if (
currentTrain.value == nextTrain.value ||
nextTrain.status == AnimationStatus.completed ||
nextTrain.status == AnimationStatus.dismissed
) {
_setSecondaryAnimation(nextTrain, nextRoute.completed);
} else {
// Two trains animate at different values. We have to do train hopping.
// There are three possibilities of train hopping:
// 1. We hop on the nextTrain when two trains meet in the middle using
// TrainHoppingAnimation.
// 2. There is no chance to hop on nextTrain because two trains never
// cross each other. We have to directly set the animation to
// nextTrain once the nextTrain stops animating.
// 3. A new _updateSecondaryAnimation is called before train hopping
// finishes. We leave a listener remover for the next call to
// properly clean up the existing train hopping.
TrainHoppingAnimation newAnimation;
void _jumpOnAnimationEnd(AnimationStatus status) {
switch (status) {
case AnimationStatus.completed:
case AnimationStatus.dismissed:
// The nextTrain has stopped animating without train hopping.
// Directly sets the secondary animation and disposes the
// TrainHoppingAnimation.
_setSecondaryAnimation(nextTrain, nextRoute.completed);
if (_trainHoppingListenerRemover != null) {
_trainHoppingListenerRemover();
_trainHoppingListenerRemover = null;
}
break;
case AnimationStatus.forward:
case AnimationStatus.reverse:
break;
}
}
_trainHoppingListenerRemover = () {
nextTrain.removeStatusListener(_jumpOnAnimationEnd);
newAnimation?.dispose();
};
nextTrain.addStatusListener(_jumpOnAnimationEnd);
newAnimation = TrainHoppingAnimation(
currentTrain,
nextTrain,
onSwitchedTrain: () {
assert(_secondaryAnimation.parent == newAnimation);
assert(newAnimation.currentTrain == nextRoute._animation);
// We can hop on the nextTrain, so we don't need to listen to
// whether the nextTrain has stopped.
_setSecondaryAnimation(newAnimation.currentTrain, nextRoute.completed);
newAnimation.dispose();
if (_trainHoppingListenerRemover != null) {
_trainHoppingListenerRemover();
_trainHoppingListenerRemover = null;
}
},
);
_setSecondaryAnimation(newAnimation, nextRoute.completed);
}
if (current is TrainHoppingAnimation) {
current.dispose();
}
} else {
_setSecondaryAnimation(nextRoute._animation, nextRoute.completed);
}
} else {
_setSecondaryAnimation(kAlwaysDismissedAnimation);
}
// Finally, we dispose any previous train hopping animation because it
// has been successfully updated at this point.
if (previousTrainHoppingListenerRemover != null) {
previousTrainHoppingListenerRemover();
}
}
void _setSecondaryAnimation(Animation<double> animation, [Future<dynamic> disposed]) {
_secondaryAnimation.parent = animation;
// Release the reference to the next route's animation when that route
// Releases the reference to the next route's animation when that route
// is disposed.
disposed?.then((dynamic _) {
if (_secondaryAnimation.parent == animation) {
......
......@@ -788,6 +788,43 @@ void main() {
expect(trainHopper2.currentTrain, isNull); // Has been disposed.
});
testWidgets('secondary animation is triggered when pop initial route', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
Animation<double> secondaryAnimationOfRouteOne;
Animation<double> primaryAnimationOfRouteTwo;
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigator,
onGenerateRoute: (RouteSettings settings) {
return PageRouteBuilder<void>(
settings: settings,
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
if (settings.name == '/')
secondaryAnimationOfRouteOne = secondaryAnimation;
else
primaryAnimationOfRouteTwo = animation;
return const Text('Page');
},
);
},
initialRoute: '/a',
)
);
// The secondary animation of the bottom route should be chained with the
// primary animation of top most route.
expect(secondaryAnimationOfRouteOne.value, 1.0);
expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value);
// Pops the top most route and verifies two routes are still chained.
navigator.currentState.pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 30));
expect(secondaryAnimationOfRouteOne.value, 0.9);
expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value);
await tester.pumpAndSettle();
expect(secondaryAnimationOfRouteOne.value, 0.0);
expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value);
});
testWidgets('showGeneralDialog uses root navigator by default', (WidgetTester tester) async {
final DialogObserver rootObserver = DialogObserver();
final DialogObserver nestedObserver = DialogObserver();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment