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.
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.
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
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
/// 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}
///
/// If the application has multiple [Navigator] objects, it may be necessary to
@ -1313,6 +1320,7 @@ Future<T?> showCupertinoDialog<T>({
bool barrierDismissible = false,
RouteSettings? routeSettings,
Offset? anchorPoint,
Duration? transitionDuration,
}) {
return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(CupertinoDialogRoute<T>(
@ -1323,6 +1331,7 @@ Future<T?> showCupertinoDialog<T>({
barrierColor: CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context),
settings: routeSettings,
anchorPoint: anchorPoint,
transitionDuration: transitionDuration,
));
}
@ -1350,6 +1359,10 @@ Future<T?> showCupertinoDialog<T>({
/// barrier that darkens everything below the dialog. If `null`, then
/// [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
/// [RouteSettings] for details.
///
@ -1372,7 +1385,7 @@ class CupertinoDialogRoute<T> extends RawDialogRoute<T> {
Color? barrierColor,
String? barrierLabel,
// This transition duration was eyeballed comparing with iOS
super.transitionDuration = const Duration(milliseconds: 250),
Duration? transitionDuration,
this.transitionBuilder,
super.settings,
super.requestFocus,
@ -1384,6 +1397,7 @@ class CupertinoDialogRoute<T> extends RawDialogRoute<T> {
transitionBuilder: transitionBuilder ?? _buildCupertinoDialogTransitions,
barrierLabel: barrierLabel ?? CupertinoLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: barrierColor ?? CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context),
transitionDuration: transitionDuration ?? _kCupertinoDialogRouteTransitionDuration,
);
/// Custom transition builder

View File

@ -31,6 +31,9 @@ import 'theme_data.dart';
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.
///
/// 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
/// `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
/// 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
@ -1428,6 +1435,7 @@ Future<T?> showDialog<T>({
RouteSettings? routeSettings,
Offset? anchorPoint,
TraversalEdgeBehavior? traversalEdgeBehavior,
Duration? transitionDuration,
}) {
assert(_debugIsActive(context));
assert(debugCheckHasMaterialLocalizations(context));
@ -1454,6 +1462,7 @@ Future<T?> showDialog<T>({
themes: themes,
anchorPoint: anchorPoint,
traversalEdgeBehavior: traversalEdgeBehavior ?? TraversalEdgeBehavior.closedLoop,
transitionDuration: transitionDuration,
));
}
@ -1465,6 +1474,10 @@ Future<T?> showDialog<T>({
///
/// On Cupertino platforms, [barrierColor], [useSafeArea], and
/// [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>({
required BuildContext context,
required WidgetBuilder builder,
@ -1476,6 +1489,7 @@ Future<T?> showAdaptiveDialog<T>({
RouteSettings? routeSettings,
Offset? anchorPoint,
TraversalEdgeBehavior? traversalEdgeBehavior,
Duration? transitionDuration,
}) {
final ThemeData theme = Theme.of(context);
switch (theme.platform) {
@ -1494,6 +1508,7 @@ Future<T?> showAdaptiveDialog<T>({
routeSettings: routeSettings,
anchorPoint: anchorPoint,
traversalEdgeBehavior: traversalEdgeBehavior,
transitionDuration: transitionDuration,
);
case TargetPlatform.iOS:
case TargetPlatform.macOS:
@ -1505,6 +1520,7 @@ Future<T?> showAdaptiveDialog<T>({
useRootNavigator: useRootNavigator,
anchorPoint: anchorPoint,
routeSettings: routeSettings,
transitionDuration: transitionDuration,
);
}
}
@ -1552,6 +1568,10 @@ bool _debugIsActive(BuildContext context) {
/// barrier that darkens everything below the dialog. If `null`, the default
/// 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
/// 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
@ -1582,6 +1602,7 @@ class DialogRoute<T> extends RawDialogRoute<T> {
super.barrierDismissible,
String? barrierLabel,
bool useSafeArea = true,
Duration? transitionDuration,
super.settings,
super.requestFocus,
super.anchorPoint,
@ -1596,8 +1617,8 @@ class DialogRoute<T> extends RawDialogRoute<T> {
return dialog;
},
barrierLabel: barrierLabel ?? MaterialLocalizations.of(context).modalBarrierDismissLabel,
transitionDuration: const Duration(milliseconds: 150),
transitionBuilder: _buildMaterialDialogTransitions,
transitionDuration: transitionDuration ?? _kDialogRouteTransitionDuration,
);
CurvedAnimation? _curvedAnimation;

View File

@ -1883,6 +1883,45 @@ void main() {
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 {
final DialogObserver rootObserver = DialogObserver();
final DialogObserver nestedObserver = DialogObserver();
@ -2932,15 +2971,24 @@ class PopupObserver extends NavigatorObserver {
}
class DialogObserver extends NavigatorObserver {
int dialogCount = 0;
final List<ModalRoute<dynamic>> dialogRoutes = <ModalRoute<dynamic>>[];
int get dialogCount => dialogRoutes.length;
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route is CupertinoDialogRoute) {
dialogCount++;
dialogRoutes.add(route);
}
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 {

View File

@ -2533,6 +2533,50 @@ void main() {
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 {
final SemanticsTester semantics = SemanticsTester(tester);
@ -2829,6 +2873,50 @@ void main() {
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 {
final FocusNode okNode = FocusNode();
addTearDown(okNode.dispose);
@ -3002,15 +3090,24 @@ class _RestorableDialogTestWidget extends StatelessWidget {
}
class DialogObserver extends NavigatorObserver {
int dialogCount = 0;
final List<ModalRoute<dynamic>> dialogRoutes = <ModalRoute<dynamic>>[];
int get dialogCount => dialogRoutes.length;
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route is DialogRoute) {
dialogCount++;
dialogRoutes.add(route);
}
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 {

View File

@ -5,6 +5,7 @@
import 'dart:collection';
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.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 {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(MaterialApp(
@ -2155,13 +2383,12 @@ class TestPageRouteBuilder extends PageRouteBuilder<void> {
class DialogObserver extends NavigatorObserver {
final List<ModalRoute<dynamic>> dialogRoutes = <ModalRoute<dynamic>>[];
int dialogCount = 0;
int get dialogCount => dialogRoutes.length;
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route is RawDialogRoute) {
dialogRoutes.add(route);
dialogCount++;
}
super.didPush(route, previousRoute);
}
@ -2170,7 +2397,6 @@ class DialogObserver extends NavigatorObserver {
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route is RawDialogRoute) {
dialogRoutes.removeLast();
dialogCount--;
}
super.didPop(route, previousRoute);
}