mirror of
https://github.com/flutter/flutter
synced 2024-10-13 19:52:53 +00:00
Implement reverseTransitionDuration for TransitionRoute (#48274)
* Implement reverseTransitionDuration in TransitionRoute
This commit is contained in:
parent
c241f9f6b2
commit
51e24a3561
|
@ -91,9 +91,20 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
|
||||||
Future<T> get completed => _transitionCompleter.future;
|
Future<T> get completed => _transitionCompleter.future;
|
||||||
final Completer<T> _transitionCompleter = Completer<T>();
|
final Completer<T> _transitionCompleter = Completer<T>();
|
||||||
|
|
||||||
/// The duration the transition lasts.
|
/// The duration the transition going forwards.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [reverseTransitionDuration], which controls the duration of the
|
||||||
|
/// transition when it is in reverse.
|
||||||
Duration get transitionDuration;
|
Duration get transitionDuration;
|
||||||
|
|
||||||
|
/// The duration the transition going in reverse.
|
||||||
|
///
|
||||||
|
/// By default, the reverse transition duration is set to the value of
|
||||||
|
/// the forwards [transitionDuration].
|
||||||
|
Duration get reverseTransitionDuration => transitionDuration;
|
||||||
|
|
||||||
/// Whether the route obscures previous routes when the transition is complete.
|
/// Whether the route obscures previous routes when the transition is complete.
|
||||||
///
|
///
|
||||||
/// When an opaque route's entrance transition is complete, the routes behind
|
/// When an opaque route's entrance transition is complete, the routes behind
|
||||||
|
@ -127,9 +138,11 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
|
||||||
AnimationController createAnimationController() {
|
AnimationController createAnimationController() {
|
||||||
assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
|
assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
|
||||||
final Duration duration = transitionDuration;
|
final Duration duration = transitionDuration;
|
||||||
|
final Duration reverseDuration = reverseTransitionDuration;
|
||||||
assert(duration != null && duration >= Duration.zero);
|
assert(duration != null && duration >= Duration.zero);
|
||||||
return AnimationController(
|
return AnimationController(
|
||||||
duration: duration,
|
duration: duration,
|
||||||
|
reverseDuration: reverseDuration,
|
||||||
debugLabel: debugLabel,
|
debugLabel: debugLabel,
|
||||||
vsync: navigator,
|
vsync: navigator,
|
||||||
);
|
);
|
||||||
|
|
|
@ -534,7 +534,7 @@ void main() {
|
||||||
expect(focusNode.hasPrimaryFocus, isTrue);
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
group('TrasitionRoute', () {
|
group('TransitionRoute', () {
|
||||||
testWidgets('secondary animation is kDismissed when next route finishes pop', (WidgetTester tester) async {
|
testWidgets('secondary animation is kDismissed when next route finishes pop', (WidgetTester tester) async {
|
||||||
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
|
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
|
@ -863,9 +863,206 @@ void main() {
|
||||||
expect(rootObserver.dialogCount, 0);
|
expect(rootObserver.dialogCount, 0);
|
||||||
expect(nestedObserver.dialogCount, 1);
|
expect(nestedObserver.dialogCount, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('reverseTransitionDuration defaults to transitionDuration', (WidgetTester tester) async {
|
||||||
|
final GlobalKey containerKey = GlobalKey();
|
||||||
|
|
||||||
|
// Default MaterialPageRoute transition duration should be 300ms.
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
onGenerateRoute: (RouteSettings settings) {
|
||||||
|
return MaterialPageRoute<dynamic>(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return RaisedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute<dynamic>(
|
||||||
|
builder: (BuildContext innerContext) {
|
||||||
|
return Container(
|
||||||
|
key: containerKey,
|
||||||
|
color: Colors.green,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('Open page'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
// Open the new route.
|
||||||
|
await tester.tap(find.byType(RaisedButton));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Open page'), findsNothing);
|
||||||
|
expect(find.byKey(containerKey), findsOneWidget);
|
||||||
|
|
||||||
|
// Pop the new route.
|
||||||
|
tester.state<NavigatorState>(find.byType(Navigator)).pop();
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.byKey(containerKey), findsOneWidget);
|
||||||
|
|
||||||
|
// Container should be present halfway through the transition.
|
||||||
|
await tester.pump(const Duration(milliseconds: 150));
|
||||||
|
expect(find.byKey(containerKey), findsOneWidget);
|
||||||
|
|
||||||
|
// Container should be present at the very end of the transition.
|
||||||
|
await tester.pump(const Duration(milliseconds: 150));
|
||||||
|
expect(find.byKey(containerKey), findsOneWidget);
|
||||||
|
|
||||||
|
// Container have transitioned out after 300ms.
|
||||||
|
await tester.pump(const Duration(milliseconds: 1));
|
||||||
|
expect(find.byKey(containerKey), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('reverseTransitionDuration can be customized', (WidgetTester tester) async {
|
||||||
|
final GlobalKey containerKey = GlobalKey();
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
onGenerateRoute: (RouteSettings settings) {
|
||||||
|
return MaterialPageRoute<dynamic>(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return RaisedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
ModifiedReverseTransitionDurationRoute<dynamic>(
|
||||||
|
builder: (BuildContext innerContext) {
|
||||||
|
return Container(
|
||||||
|
key: containerKey,
|
||||||
|
color: Colors.green,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
// modified value, default MaterialPageRoute transition duration should be 300ms.
|
||||||
|
reverseTransitionDuration: const Duration(milliseconds: 150),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('Open page'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
// Open the new route.
|
||||||
|
await tester.tap(find.byType(RaisedButton));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Open page'), findsNothing);
|
||||||
|
expect(find.byKey(containerKey), findsOneWidget);
|
||||||
|
|
||||||
|
// Pop the new route.
|
||||||
|
tester.state<NavigatorState>(find.byType(Navigator)).pop();
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.byKey(containerKey), findsOneWidget);
|
||||||
|
|
||||||
|
// Container should be present halfway through the transition.
|
||||||
|
await tester.pump(const Duration(milliseconds: 75));
|
||||||
|
expect(find.byKey(containerKey), findsOneWidget);
|
||||||
|
|
||||||
|
// Container should be present at the very end of the transition.
|
||||||
|
await tester.pump(const Duration(milliseconds: 75));
|
||||||
|
expect(find.byKey(containerKey), findsOneWidget);
|
||||||
|
|
||||||
|
// Container have transitioned out after 150ms.
|
||||||
|
await tester.pump(const Duration(milliseconds: 1));
|
||||||
|
expect(find.byKey(containerKey), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('custom reverseTransitionDuration does not result in interrupted animations', (WidgetTester tester) async {
|
||||||
|
final GlobalKey containerKey = GlobalKey();
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
theme: ThemeData(
|
||||||
|
pageTransitionsTheme: const PageTransitionsTheme(
|
||||||
|
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||||
|
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), // use a fade transition
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onGenerateRoute: (RouteSettings settings) {
|
||||||
|
return MaterialPageRoute<dynamic>(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return RaisedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
ModifiedReverseTransitionDurationRoute<dynamic>(
|
||||||
|
builder: (BuildContext innerContext) {
|
||||||
|
return Container(
|
||||||
|
key: containerKey,
|
||||||
|
color: Colors.green,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
// modified value, default MaterialPageRoute transition duration should be 300ms.
|
||||||
|
reverseTransitionDuration: const Duration(milliseconds: 150),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('Open page'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
// Open the new route.
|
||||||
|
await tester.tap(find.byType(RaisedButton));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 200)); // jump partway through the forward transition
|
||||||
|
expect(find.byKey(containerKey), findsOneWidget);
|
||||||
|
|
||||||
|
// Gets the opacity of the fade transition while animating forwards.
|
||||||
|
final double topFadeTransitionOpacity = _getOpacity(containerKey, tester);
|
||||||
|
|
||||||
|
// Pop the new route mid-transition.
|
||||||
|
tester.state<NavigatorState>(find.byType(Navigator)).pop();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Transition should not jump. In other words, the fade transition
|
||||||
|
// opacity before and after animation changes directions should remain
|
||||||
|
// the same.
|
||||||
|
expect(_getOpacity(containerKey, tester), topFadeTransitionOpacity);
|
||||||
|
|
||||||
|
// Reverse transition duration should be:
|
||||||
|
// Forward transition elapsed time: 200ms / 300ms = 2 / 3
|
||||||
|
// Reverse transition remaining time: 150ms * 2 / 3 = 100ms
|
||||||
|
|
||||||
|
// Container should be present at the very end of the transition.
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
expect(find.byKey(containerKey), findsOneWidget);
|
||||||
|
|
||||||
|
// Container have transitioned out after 100ms.
|
||||||
|
await tester.pump(const Duration(milliseconds: 1));
|
||||||
|
expect(find.byKey(containerKey), findsNothing);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double _getOpacity(GlobalKey key, WidgetTester tester) {
|
||||||
|
final Finder finder = find.ancestor(
|
||||||
|
of: find.byKey(key),
|
||||||
|
matching: find.byType(FadeTransition),
|
||||||
|
);
|
||||||
|
return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
|
||||||
|
final FadeTransition transition = widget as FadeTransition;
|
||||||
|
return a * transition.opacity.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModifiedReverseTransitionDurationRoute<T> extends MaterialPageRoute<T> {
|
||||||
|
ModifiedReverseTransitionDurationRoute({
|
||||||
|
@required WidgetBuilder builder,
|
||||||
|
RouteSettings settings,
|
||||||
|
this.reverseTransitionDuration,
|
||||||
|
bool fullscreenDialog = false,
|
||||||
|
}) : super(
|
||||||
|
builder: builder,
|
||||||
|
settings: settings,
|
||||||
|
fullscreenDialog: fullscreenDialog,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final Duration reverseTransitionDuration;
|
||||||
|
}
|
||||||
|
|
||||||
class MockPageRoute extends Mock implements PageRoute<dynamic> { }
|
class MockPageRoute extends Mock implements PageRoute<dynamic> { }
|
||||||
|
|
||||||
class MockRoute extends Mock implements Route<dynamic> { }
|
class MockRoute extends Mock implements Route<dynamic> { }
|
||||||
|
|
Loading…
Reference in a new issue