Support custom transition duration for DialogRoute, CupertinoDialogRoute and show dialog methods. (#154048)

Currently we don't support custom transition duration for `DialogRoute`, `CupertinoDialogRoute` and show dialog methods , This PR will to support that.
This commit is contained in:
Nguyen Phuc Loi 2024-09-07 01:42:36 +07:00 committed by GitHub
parent d9321159bf
commit 0eaeb0d1c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 415 additions and 9 deletions

View File

@ -57,6 +57,9 @@ const Color kCupertinoModalBarrierColor = CupertinoDynamicColor.withBrightness(
// The duration of the transition used when a modal popup is shown. // The duration of the transition used when a modal popup is shown.
const Duration _kModalPopupTransitionDuration = Duration(milliseconds: 335); const Duration _kModalPopupTransitionDuration = Duration(milliseconds: 335);
// The transition duration used for [CupertinoDialogRoute] transitions.
const Duration _kCupertinoDialogRouteTransitionDuration = Duration(milliseconds: 250);
// Offset from offscreen to the right to fully on screen. // Offset from offscreen to the right to fully on screen.
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>( final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
begin: const Offset(1.0, 0.0), begin: const Offset(1.0, 0.0),
@ -1269,6 +1272,10 @@ Widget _buildCupertinoDialogTransitions(BuildContext context, Animation<double>
/// By default, `useRootNavigator` is `true` and the dialog route created by /// By default, `useRootNavigator` is `true` and the dialog route created by
/// this method is pushed to the root navigator. /// this method is pushed to the root navigator.
/// ///
/// the `transitionDuration` argument is used to specify the duration of
/// the dialog's entrance and exit animations. If it's not provided or `null`,
/// then it uses the default value as set by [CupertinoDialogRoute].
///
/// {@macro flutter.widgets.RawDialogRoute} /// {@macro flutter.widgets.RawDialogRoute}
/// ///
/// If the application has multiple [Navigator] objects, it may be necessary to /// If the application has multiple [Navigator] objects, it may be necessary to
@ -1313,6 +1320,7 @@ Future<T?> showCupertinoDialog<T>({
bool barrierDismissible = false, bool barrierDismissible = false,
RouteSettings? routeSettings, RouteSettings? routeSettings,
Offset? anchorPoint, Offset? anchorPoint,
Duration? transitionDuration,
}) { }) {
return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(CupertinoDialogRoute<T>( return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(CupertinoDialogRoute<T>(
@ -1323,6 +1331,7 @@ Future<T?> showCupertinoDialog<T>({
barrierColor: CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context), barrierColor: CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context),
settings: routeSettings, settings: routeSettings,
anchorPoint: anchorPoint, anchorPoint: anchorPoint,
transitionDuration: transitionDuration,
)); ));
} }
@ -1350,6 +1359,10 @@ Future<T?> showCupertinoDialog<T>({
/// barrier that darkens everything below the dialog. If `null`, then /// barrier that darkens everything below the dialog. If `null`, then
/// [CupertinoDynamicColor.resolve] is used to compute the modal color. /// [CupertinoDynamicColor.resolve] is used to compute the modal color.
/// ///
/// The `transitionDuration` argument is used to specify the duration of
/// the dialog's entrance and exit animations. If it's not provided or `null`,
/// then the default duration `Duration(milliseconds: 250)` is used.
///
/// The `settings` argument define the settings for this route. See /// The `settings` argument define the settings for this route. See
/// [RouteSettings] for details. /// [RouteSettings] for details.
/// ///
@ -1372,7 +1385,7 @@ class CupertinoDialogRoute<T> extends RawDialogRoute<T> {
Color? barrierColor, Color? barrierColor,
String? barrierLabel, String? barrierLabel,
// This transition duration was eyeballed comparing with iOS // This transition duration was eyeballed comparing with iOS
super.transitionDuration = const Duration(milliseconds: 250), Duration? transitionDuration,
this.transitionBuilder, this.transitionBuilder,
super.settings, super.settings,
super.requestFocus, super.requestFocus,
@ -1384,6 +1397,7 @@ class CupertinoDialogRoute<T> extends RawDialogRoute<T> {
transitionBuilder: transitionBuilder ?? _buildCupertinoDialogTransitions, transitionBuilder: transitionBuilder ?? _buildCupertinoDialogTransitions,
barrierLabel: barrierLabel ?? CupertinoLocalizations.of(context).modalBarrierDismissLabel, barrierLabel: barrierLabel ?? CupertinoLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: barrierColor ?? CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context), barrierColor: barrierColor ?? CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context),
transitionDuration: transitionDuration ?? _kCupertinoDialogRouteTransitionDuration,
); );
/// Custom transition builder /// Custom transition builder

View File

@ -31,6 +31,9 @@ import 'theme_data.dart';
const EdgeInsets _defaultInsetPadding = EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0); const EdgeInsets _defaultInsetPadding = EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0);
// The transition duration used for [DialogRoute] transitions.
const Duration _kDialogRouteTransitionDuration = Duration(milliseconds: 150);
/// A Material Design dialog. /// A Material Design dialog.
/// ///
/// This dialog widget does not have any opinion about the contents of the /// This dialog widget does not have any opinion about the contents of the
@ -1345,6 +1348,10 @@ Widget _buildMaterialDialogTransitions(BuildContext context, Animation<double> a
/// field from `DialogThemeData` is used. If that is `null` the default color /// field from `DialogThemeData` is used. If that is `null` the default color
/// `Colors.black54` is used. /// `Colors.black54` is used.
/// ///
/// the `transitionDuration` argument is used to specify the duration of
/// the dialog's entrance and exit animations. If it's not provided or `null`,
/// then it uses the default value as set by [DialogRoute].
///
/// The `useSafeArea` argument is used to indicate if the dialog should only /// The `useSafeArea` argument is used to indicate if the dialog should only
/// display in 'safe' areas of the screen not used by the operating system /// display in 'safe' areas of the screen not used by the operating system
/// (see [SafeArea] for more details). It is `true` by default, which means /// (see [SafeArea] for more details). It is `true` by default, which means
@ -1428,6 +1435,7 @@ Future<T?> showDialog<T>({
RouteSettings? routeSettings, RouteSettings? routeSettings,
Offset? anchorPoint, Offset? anchorPoint,
TraversalEdgeBehavior? traversalEdgeBehavior, TraversalEdgeBehavior? traversalEdgeBehavior,
Duration? transitionDuration,
}) { }) {
assert(_debugIsActive(context)); assert(_debugIsActive(context));
assert(debugCheckHasMaterialLocalizations(context)); assert(debugCheckHasMaterialLocalizations(context));
@ -1454,6 +1462,7 @@ Future<T?> showDialog<T>({
themes: themes, themes: themes,
anchorPoint: anchorPoint, anchorPoint: anchorPoint,
traversalEdgeBehavior: traversalEdgeBehavior ?? TraversalEdgeBehavior.closedLoop, traversalEdgeBehavior: traversalEdgeBehavior ?? TraversalEdgeBehavior.closedLoop,
transitionDuration: transitionDuration,
)); ));
} }
@ -1465,6 +1474,10 @@ Future<T?> showDialog<T>({
/// ///
/// On Cupertino platforms, [barrierColor], [useSafeArea], and /// On Cupertino platforms, [barrierColor], [useSafeArea], and
/// [traversalEdgeBehavior] are ignored. /// [traversalEdgeBehavior] are ignored.
///
/// The `transitionDuration` argument is used to specify the duration of
/// the dialog's entrance and exit animations. If it's not provided or `null`,
/// then it uses the default value as set by [DialogRoute] or [CupertinoDialogRoute].
Future<T?> showAdaptiveDialog<T>({ Future<T?> showAdaptiveDialog<T>({
required BuildContext context, required BuildContext context,
required WidgetBuilder builder, required WidgetBuilder builder,
@ -1476,6 +1489,7 @@ Future<T?> showAdaptiveDialog<T>({
RouteSettings? routeSettings, RouteSettings? routeSettings,
Offset? anchorPoint, Offset? anchorPoint,
TraversalEdgeBehavior? traversalEdgeBehavior, TraversalEdgeBehavior? traversalEdgeBehavior,
Duration? transitionDuration,
}) { }) {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
switch (theme.platform) { switch (theme.platform) {
@ -1494,6 +1508,7 @@ Future<T?> showAdaptiveDialog<T>({
routeSettings: routeSettings, routeSettings: routeSettings,
anchorPoint: anchorPoint, anchorPoint: anchorPoint,
traversalEdgeBehavior: traversalEdgeBehavior, traversalEdgeBehavior: traversalEdgeBehavior,
transitionDuration: transitionDuration,
); );
case TargetPlatform.iOS: case TargetPlatform.iOS:
case TargetPlatform.macOS: case TargetPlatform.macOS:
@ -1505,6 +1520,7 @@ Future<T?> showAdaptiveDialog<T>({
useRootNavigator: useRootNavigator, useRootNavigator: useRootNavigator,
anchorPoint: anchorPoint, anchorPoint: anchorPoint,
routeSettings: routeSettings, routeSettings: routeSettings,
transitionDuration: transitionDuration,
); );
} }
} }
@ -1552,6 +1568,10 @@ bool _debugIsActive(BuildContext context) {
/// barrier that darkens everything below the dialog. If `null`, the default /// barrier that darkens everything below the dialog. If `null`, the default
/// color `Colors.black54` is used. /// color `Colors.black54` is used.
/// ///
/// the `transitionDuration` argument is used to specify the duration of
/// the dialog's entrance and exit animations. If it's not provided or `null`,
/// then the default duration `Duration(milliseconds: 150)` is used.
///
/// The `useSafeArea` argument is used to indicate if the dialog should only /// The `useSafeArea` argument is used to indicate if the dialog should only
/// display in 'safe' areas of the screen not used by the operating system /// display in 'safe' areas of the screen not used by the operating system
/// (see [SafeArea] for more details). It is `true` by default, which means /// (see [SafeArea] for more details). It is `true` by default, which means
@ -1582,6 +1602,7 @@ class DialogRoute<T> extends RawDialogRoute<T> {
super.barrierDismissible, super.barrierDismissible,
String? barrierLabel, String? barrierLabel,
bool useSafeArea = true, bool useSafeArea = true,
Duration? transitionDuration,
super.settings, super.settings,
super.requestFocus, super.requestFocus,
super.anchorPoint, super.anchorPoint,
@ -1596,8 +1617,8 @@ class DialogRoute<T> extends RawDialogRoute<T> {
return dialog; return dialog;
}, },
barrierLabel: barrierLabel ?? MaterialLocalizations.of(context).modalBarrierDismissLabel, barrierLabel: barrierLabel ?? MaterialLocalizations.of(context).modalBarrierDismissLabel,
transitionDuration: const Duration(milliseconds: 150),
transitionBuilder: _buildMaterialDialogTransitions, transitionBuilder: _buildMaterialDialogTransitions,
transitionDuration: transitionDuration ?? _kDialogRouteTransitionDuration,
); );
CurvedAnimation? _curvedAnimation; CurvedAnimation? _curvedAnimation;

View File

@ -1883,6 +1883,45 @@ void main() {
expect(nestedObserver.dialogCount, 0); expect(nestedObserver.dialogCount, 0);
}); });
testWidgets('showCupertinoDialog - custom transitionDuration', (WidgetTester tester) async {
final DialogObserver rootObserver = DialogObserver();
final DialogObserver nestedObserver = DialogObserver();
await tester.pumpWidget(CupertinoApp(
navigatorObservers: <NavigatorObserver>[rootObserver],
home: Navigator(
observers: <NavigatorObserver>[nestedObserver],
onGenerateRoute: (RouteSettings settings) {
return PageRouteBuilder<dynamic>(
pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) {
return GestureDetector(
onTap: () async {
await showCupertinoDialog<void>(
context: context,
transitionDuration: const Duration(milliseconds: 50),
builder: (BuildContext context) => const SizedBox(),
);
},
child: const Text('tap'),
);
},
);
},
),
));
// Open the dialog.
await tester.tap(find.text('tap'));
await tester.pump();
expect(rootObserver.dialogCount, 1);
expect(nestedObserver.dialogCount, 0);
expect(rootObserver.dialogRoutes.length, equals(1));
final ModalRoute<dynamic> route = rootObserver.dialogRoutes.last;
expect(route is CupertinoDialogRoute, true);
expect(route.transitionDuration.inMilliseconds, 50);
});
testWidgets('showCupertinoDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { testWidgets('showCupertinoDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async {
final DialogObserver rootObserver = DialogObserver(); final DialogObserver rootObserver = DialogObserver();
final DialogObserver nestedObserver = DialogObserver(); final DialogObserver nestedObserver = DialogObserver();
@ -2932,15 +2971,24 @@ class PopupObserver extends NavigatorObserver {
} }
class DialogObserver extends NavigatorObserver { class DialogObserver extends NavigatorObserver {
int dialogCount = 0; final List<ModalRoute<dynamic>> dialogRoutes = <ModalRoute<dynamic>>[];
int get dialogCount => dialogRoutes.length;
@override @override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) { void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route is CupertinoDialogRoute) { if (route is CupertinoDialogRoute) {
dialogCount++; dialogRoutes.add(route);
} }
super.didPush(route, previousRoute); super.didPush(route, previousRoute);
} }
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route is CupertinoDialogRoute) {
dialogRoutes.removeLast();
}
super.didPop(route, previousRoute);
}
} }
class RouteSettingsObserver extends NavigatorObserver { class RouteSettingsObserver extends NavigatorObserver {

View File

@ -2533,6 +2533,50 @@ void main() {
expect(currentRouteSetting.name, '/'); expect(currentRouteSetting.name, '/');
}); });
testWidgets('showDialog - custom transitionDuration', (WidgetTester tester) async {
final DialogObserver rootObserver = DialogObserver();
await tester.pumpWidget(
MaterialApp(
navigatorObservers: <NavigatorObserver>[rootObserver],
home: Material(
child: Builder(
builder: (BuildContext context) {
return Center(
child: ElevatedButton(
child: const Text('X'),
onPressed: () {
showDialog<void>(
context: context,
transitionDuration: const Duration(milliseconds: 50),
builder: (BuildContext context) {
return const AlertDialog(
title: Text('Title'),
content: Text('Y'),
actions: <Widget>[],
);
},
);
},
),
);
},
),
),
),
);
await tester.tap(find.text('X'));
await tester.pump();
expect(rootObserver.dialogRoutes.length, equals(1));
final ModalRoute<dynamic> route = rootObserver.dialogRoutes.last;
expect(route is DialogRoute, true);
expect(route.barrierDismissible, isNotNull);
expect(route.barrierColor, isNotNull);
expect(route.transitionDuration, isNotNull);
expect(route.transitionDuration.inMilliseconds, 50);
});
testWidgets('showDialog - custom barrierLabel', (WidgetTester tester) async { testWidgets('showDialog - custom barrierLabel', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
@ -2829,6 +2873,50 @@ void main() {
expect(find.text('Dialog2'), findsOneWidget); expect(find.text('Dialog2'), findsOneWidget);
}); });
testWidgets('showAdaptiveDialog - custom transitionDuration', (WidgetTester tester) async {
final DialogObserver rootObserver = DialogObserver();
await tester.pumpWidget(
MaterialApp(
navigatorObservers: <NavigatorObserver>[rootObserver],
home: Material(
child: Builder(
builder: (BuildContext context) {
return Center(
child: ElevatedButton(
child: const Text('X'),
onPressed: () {
showAdaptiveDialog<void>(
context: context,
transitionDuration: const Duration(milliseconds: 50),
builder: (BuildContext context) {
return const AlertDialog(
title: Text('Title'),
content: Text('Y'),
actions: <Widget>[],
);
},
);
},
),
);
},
),
),
),
);
await tester.tap(find.text('X'));
await tester.pump();
expect(rootObserver.dialogRoutes.length, equals(1));
final ModalRoute<dynamic> route = rootObserver.dialogRoutes.last;
expect(route is DialogRoute, true);
expect(route.barrierDismissible, isNotNull);
expect(route.barrierColor, isNotNull);
expect(route.transitionDuration, isNotNull);
expect(route.transitionDuration.inMilliseconds, 50);
});
testWidgets('Uses open focus traversal when overridden', (WidgetTester tester) async { testWidgets('Uses open focus traversal when overridden', (WidgetTester tester) async {
final FocusNode okNode = FocusNode(); final FocusNode okNode = FocusNode();
addTearDown(okNode.dispose); addTearDown(okNode.dispose);
@ -3002,15 +3090,24 @@ class _RestorableDialogTestWidget extends StatelessWidget {
} }
class DialogObserver extends NavigatorObserver { class DialogObserver extends NavigatorObserver {
int dialogCount = 0; final List<ModalRoute<dynamic>> dialogRoutes = <ModalRoute<dynamic>>[];
int get dialogCount => dialogRoutes.length;
@override @override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) { void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route is DialogRoute) { if (route is DialogRoute) {
dialogCount++; dialogRoutes.add(route);
} }
super.didPush(route, previousRoute); super.didPush(route, previousRoute);
} }
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route is DialogRoute) {
dialogRoutes.removeLast();
}
super.didPop(route, previousRoute);
}
} }
class _ClosureNavigatorObserver extends NavigatorObserver { class _ClosureNavigatorObserver extends NavigatorObserver {

View File

@ -5,6 +5,7 @@
import 'dart:collection'; import 'dart:collection';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -1920,6 +1921,233 @@ void main() {
}); });
}); });
group('DialogRoute', () {
testWidgets('DialogRoute - default transitionDuration', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
final DialogObserver rootObserver = DialogObserver();
await tester.pumpWidget(
MaterialApp(
navigatorObservers: <NavigatorObserver>[rootObserver],
home: Builder(
builder: (BuildContext context) {
return ElevatedButton(
onPressed: () {
showDialog<void>(
context: context,
builder: (BuildContext innerContext) {
return Container(
key: containerKey,
color: Colors.green,
);
},
);
},
child: const Text('Open dialog'),
);
},
),
),
);
// Open the dialog.
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expect(find.byKey(containerKey), findsOneWidget);
expect(rootObserver.dialogCount, 1);
final ModalRoute<dynamic> route = rootObserver.dialogRoutes.last;
expect(route is RawDialogRoute, true);
expect(route.transitionDuration.inMilliseconds, 150);
// 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('DialogRoute - custom transitionDuration', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
final DialogObserver rootObserver = DialogObserver();
await tester.pumpWidget(
MaterialApp(
navigatorObservers: <NavigatorObserver>[rootObserver],
home: Builder(
builder: (BuildContext context) {
return ElevatedButton(
onPressed: () {
showDialog<void>(
context: context,
transitionDuration: const Duration(milliseconds: 300),
builder: (BuildContext innerContext) {
return Container(
key: containerKey,
color: Colors.green,
);
},
);
},
child: const Text('Open dialog'),
);
},
),
),
);
// Open the dialog.
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expect(find.byKey(containerKey), findsOneWidget);
expect(rootObserver.dialogCount, 1);
final ModalRoute<dynamic> route = rootObserver.dialogRoutes.last;
expect(route is RawDialogRoute, true);
expect(route.transitionDuration.inMilliseconds, 300);
// 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);
});
});
group('CupertinoDialogRoute', () {
testWidgets('CupertinoDialogRoute - default transitionDuration', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
final DialogObserver rootObserver = DialogObserver();
await tester.pumpWidget(
MaterialApp(
navigatorObservers: <NavigatorObserver>[rootObserver],
theme: ThemeData(platform: TargetPlatform.iOS),
home: Builder(
builder: (BuildContext context) {
return ElevatedButton(
onPressed: () {
showCupertinoDialog<void>(
context: context,
builder: (BuildContext innerContext) {
return Container(
key: containerKey,
color: Colors.green,
);
},
);
},
child: const Text('Open dialog'),
);
},
),
),
);
// Open the dialog.
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expect(find.byKey(containerKey), findsOneWidget);
expect(rootObserver.dialogCount, 1);
final ModalRoute<dynamic> route = rootObserver.dialogRoutes.last;
expect(route is RawDialogRoute, true);
expect(route.transitionDuration.inMilliseconds, 250);
// 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: 125));
expect(find.byKey(containerKey), findsOneWidget);
// Container should be present at the very end of the transition.
await tester.pump(const Duration(milliseconds: 125));
expect(find.byKey(containerKey), findsOneWidget);
// Container have transitioned out after 250ms.
await tester.pump(const Duration(milliseconds: 1));
expect(find.byKey(containerKey), findsNothing);
});
testWidgets('CupertinoDialogRoute - custom transitionDuration', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
final DialogObserver rootObserver = DialogObserver();
await tester.pumpWidget(
MaterialApp(
navigatorObservers: <NavigatorObserver>[rootObserver],
home: Builder(
builder: (BuildContext context) {
return ElevatedButton(
onPressed: () {
showDialog<void>(
context: context,
transitionDuration: const Duration(milliseconds: 100),
builder: (BuildContext innerContext) {
return Container(
key: containerKey,
color: Colors.green,
);
},
);
},
child: const Text('Open dialog'),
);
},
),
),
);
// Open the dialog.
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expect(find.byKey(containerKey), findsOneWidget);
expect(rootObserver.dialogCount, 1);
final ModalRoute<dynamic> route = rootObserver.dialogRoutes.last;
expect(route is RawDialogRoute, true);
expect(route.transitionDuration.inMilliseconds, 100);
// 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: 50));
expect(find.byKey(containerKey), findsOneWidget);
// Container should be present at the very end of the transition.
await tester.pump(const Duration(milliseconds: 50));
expect(find.byKey(containerKey), findsOneWidget);
// Container have transitioned out after 100ms.
await tester.pump(const Duration(milliseconds: 1));
expect(find.byKey(containerKey), findsNothing);
});
});
testWidgets('can be dismissed with escape keyboard shortcut', (WidgetTester tester) async { testWidgets('can be dismissed with escape keyboard shortcut', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
@ -2155,13 +2383,12 @@ class TestPageRouteBuilder extends PageRouteBuilder<void> {
class DialogObserver extends NavigatorObserver { class DialogObserver extends NavigatorObserver {
final List<ModalRoute<dynamic>> dialogRoutes = <ModalRoute<dynamic>>[]; final List<ModalRoute<dynamic>> dialogRoutes = <ModalRoute<dynamic>>[];
int dialogCount = 0; int get dialogCount => dialogRoutes.length;
@override @override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) { void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route is RawDialogRoute) { if (route is RawDialogRoute) {
dialogRoutes.add(route); dialogRoutes.add(route);
dialogCount++;
} }
super.didPush(route, previousRoute); super.didPush(route, previousRoute);
} }
@ -2170,7 +2397,6 @@ class DialogObserver extends NavigatorObserver {
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) { void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route is RawDialogRoute) { if (route is RawDialogRoute) {
dialogRoutes.removeLast(); dialogRoutes.removeLast();
dialogCount--;
} }
super.didPop(route, previousRoute); super.didPop(route, previousRoute);
} }