Unverified Commit bd2617ec authored by Mitchell Goodwin's avatar Mitchell Goodwin Committed by GitHub

Adaptive alert dialog (#124336)

Fixes #102811. Adds an adaptive constructor to AlertDialog, along with the adaptive function showAdaptiveDialog.

<img width="357" alt="Screenshot 2023-04-06 at 10 40 18 AM" src="https://user-images.githubusercontent.com/58190796/230455412-31100922-cfc5-4252-b8c6-6f076353f29e.png">
<img width="350" alt="Screenshot 2023-04-06 at 10 42 50 AM" src="https://user-images.githubusercontent.com/58190796/230455454-363dd37e-c44e-4aca-b6a0-cfa1d959f606.png">
parent c05bc400
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
/// Flutter code sample for [AlertDialog].
void main() => runApp(const AdaptiveAlertDialogApp());
class AdaptiveAlertDialogApp extends StatelessWidget {
const AdaptiveAlertDialogApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
// Try this: set the platform to TargetPlatform.android and see the difference
theme: ThemeData(platform: TargetPlatform.iOS, useMaterial3: true),
home: Scaffold(
appBar: AppBar(title: const Text('AlertDialog Sample')),
body: const Center(
child: AdaptiveDialogExample(),
),
),
);
}
}
class AdaptiveDialogExample extends StatelessWidget {
const AdaptiveDialogExample({super.key});
Widget adaptiveAction({
required BuildContext context,
required VoidCallback onPressed,
required Widget child
}) {
final ThemeData theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return TextButton(onPressed: onPressed, child: child);
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return CupertinoDialogAction(onPressed: onPressed, child: child);
}
}
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () => showAdaptiveDialog<String>(
context: context,
builder: (BuildContext context) => AlertDialog.adaptive(
title: const Text('AlertDialog Title'),
content: const Text('AlertDialog description'),
actions: <Widget>[
adaptiveAction(
context: context,
onPressed: () => Navigator.pop(context, 'Cancel'),
child: const Text('Cancel'),
),
adaptiveAction(
context: context,
onPressed: () => Navigator.pop(context, 'OK'),
child: const Text('OK'),
),
],
),
),
child: const Text('Show Dialog'),
);
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_api_samples/material/dialog/adaptive_alert_dialog.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Show Adaptive Alert dialog', (WidgetTester tester) async {
const String dialogTitle = 'AlertDialog Title';
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: example.AdaptiveAlertDialogApp(),
),
),
);
expect(find.text(dialogTitle), findsNothing);
await tester.tap(find.widgetWithText(TextButton, 'Show Dialog'));
await tester.pumpAndSettle();
expect(find.text(dialogTitle), findsOneWidget);
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
expect(find.text(dialogTitle), findsNothing);
});
}
...@@ -4,8 +4,8 @@ ...@@ -4,8 +4,8 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/widgets.dart';
import 'color_scheme.dart'; import 'color_scheme.dart';
import 'colors.dart'; import 'colors.dart';
...@@ -395,6 +395,69 @@ class AlertDialog extends StatelessWidget { ...@@ -395,6 +395,69 @@ class AlertDialog extends StatelessWidget {
this.scrollable = false, this.scrollable = false,
}); });
/// Creates an adaptive [AlertDialog] based on whether the target platform is
/// iOS or macOS, following Material design's
/// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
///
/// On iOS and macOS, this constructor creates a [CupertinoAlertDialog]. On
/// other platforms, this creates a Material design [AlertDialog].
///
/// Typically passed as a child of [showAdaptiveDialog], which will display
/// the alert differently based on platform.
///
/// If a [CupertinoAlertDialog] is created only these parameters are used:
/// [title], [content], [actions], [scrollController],
/// [actionScrollController], [insetAnimationDuration], and
/// [insetAnimationCurve]. If a material [AlertDialog] is created,
/// [scrollController], [actionScrollController], [insetAnimationDuration],
/// and [insetAnimationCurve] are ignored.
///
/// The target platform is based on the current [Theme]: [ThemeData.platform].
///
/// {@tool dartpad}
/// This demo shows a [TextButton] which when pressed, calls [showAdaptiveDialog].
/// When called, this method displays an adaptive dialog above the current
/// contents of the app, with different behaviors depending on target platform.
///
/// [CupertinoDialogAction] is conditionally used as the child to show more
/// platform specific design.
///
/// ** See code in examples/api/lib/material/dialog/adaptive_alert_dialog.0.dart **
/// {@end-tool}
const factory AlertDialog.adaptive({
Key? key,
Widget? icon,
EdgeInsetsGeometry? iconPadding,
Color? iconColor,
Widget? title,
EdgeInsetsGeometry? titlePadding,
TextStyle? titleTextStyle,
Widget? content,
EdgeInsetsGeometry? contentPadding,
TextStyle? contentTextStyle,
List<Widget>? actions,
EdgeInsetsGeometry? actionsPadding,
MainAxisAlignment? actionsAlignment,
OverflowBarAlignment? actionsOverflowAlignment,
VerticalDirection? actionsOverflowDirection,
double? actionsOverflowButtonSpacing,
EdgeInsetsGeometry? buttonPadding,
Color? backgroundColor,
double? elevation,
Color? shadowColor,
Color? surfaceTintColor,
String? semanticLabel,
EdgeInsets insetPadding,
Clip clipBehavior,
ShapeBorder? shape,
AlignmentGeometry? alignment,
bool scrollable,
ScrollController? scrollController,
ScrollController? actionScrollController,
Duration insetAnimationDuration,
Curve insetAnimationCurve,
}) = _AdaptiveAlertDialog;
/// An optional icon to display at the top of the dialog. /// An optional icon to display at the top of the dialog.
/// ///
/// Typically, an [Icon] widget. Providing an icon centers the [title]'s text. /// Typically, an [Icon] widget. Providing an icon centers the [title]'s text.
...@@ -638,6 +701,7 @@ class AlertDialog extends StatelessWidget { ...@@ -638,6 +701,7 @@ class AlertDialog extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context)); assert(debugCheckHasMaterialLocalizations(context));
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final DialogTheme dialogTheme = DialogTheme.of(context); final DialogTheme dialogTheme = DialogTheme.of(context);
final DialogTheme defaults = theme.useMaterial3 ? _DialogDefaultsM3(context) : _DialogDefaultsM2(context); final DialogTheme defaults = theme.useMaterial3 ? _DialogDefaultsM3(context) : _DialogDefaultsM2(context);
...@@ -823,6 +887,71 @@ class AlertDialog extends StatelessWidget { ...@@ -823,6 +887,71 @@ class AlertDialog extends StatelessWidget {
} }
} }
class _AdaptiveAlertDialog extends AlertDialog {
const _AdaptiveAlertDialog({
super.key,
super.icon,
super.iconPadding,
super.iconColor,
super.title,
super.titlePadding,
super.titleTextStyle,
super.content,
super.contentPadding,
super.contentTextStyle,
super.actions,
super.actionsPadding,
super.actionsAlignment,
super.actionsOverflowAlignment,
super.actionsOverflowDirection,
super.actionsOverflowButtonSpacing,
super.buttonPadding,
super.backgroundColor,
super.elevation,
super.shadowColor,
super.surfaceTintColor,
super.semanticLabel,
super.insetPadding = _defaultInsetPadding,
super.clipBehavior = Clip.none,
super.shape,
super.alignment,
super.scrollable = false,
this.scrollController,
this.actionScrollController,
this.insetAnimationDuration = const Duration(milliseconds: 100),
this.insetAnimationCurve = Curves.decelerate,
});
final ScrollController? scrollController;
final ScrollController? actionScrollController;
final Duration insetAnimationDuration;
final Curve insetAnimationCurve;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
switch(theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
break;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return CupertinoAlertDialog(
title: title,
content: content,
actions: actions ?? <Widget>[],
scrollController: scrollController,
actionScrollController: actionScrollController,
insetAnimationDuration: insetAnimationDuration,
insetAnimationCurve: insetAnimationCurve,
);
}
return super.build(context);
}
}
/// An option used in a [SimpleDialog]. /// An option used in a [SimpleDialog].
/// ///
/// A simple dialog offers the user a choice between several options. This /// A simple dialog offers the user a choice between several options. This
...@@ -1308,6 +1437,58 @@ Future<T?> showDialog<T>({ ...@@ -1308,6 +1437,58 @@ Future<T?> showDialog<T>({
)); ));
} }
/// Displays either a Material or Cupertino dialog depending on platform.
///
/// On most platforms this function will act the same as [showDialog], except
/// for iOS and macOS, in which case it will act the same as
/// [showCupertinoDialog].
///
/// On Cupertino platforms, [barrierColor], [useSafeArea], and
/// [traversalEdgeBehavior] are ignored.
Future<T?> showAdaptiveDialog<T>({
required BuildContext context,
required WidgetBuilder builder,
bool? barrierDismissible,
Color? barrierColor = Colors.black54,
String? barrierLabel,
bool useSafeArea = true,
bool useRootNavigator = true,
RouteSettings? routeSettings,
Offset? anchorPoint,
TraversalEdgeBehavior? traversalEdgeBehavior,
}) {
final ThemeData theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return showDialog<T>(
context: context,
builder: builder,
barrierDismissible: barrierDismissible ?? true,
barrierColor: barrierColor,
barrierLabel: barrierLabel,
useSafeArea: useSafeArea,
useRootNavigator: useRootNavigator,
routeSettings: routeSettings,
anchorPoint: anchorPoint,
traversalEdgeBehavior: traversalEdgeBehavior,
);
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return showCupertinoDialog<T>(
context: context,
builder: builder,
barrierDismissible: barrierDismissible ?? false,
barrierLabel: barrierLabel,
useRootNavigator: useRootNavigator,
anchorPoint: anchorPoint,
routeSettings: routeSettings,
);
}
}
bool _debugIsActive(BuildContext context) { bool _debugIsActive(BuildContext context) {
if (context is Element && !context.debugIsActive) { if (context is Element && !context.debugIsActive) {
throw FlutterError.fromParts(<DiagnosticsNode>[ throw FlutterError.fromParts(<DiagnosticsNode>[
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
...@@ -2658,6 +2659,108 @@ void main() { ...@@ -2658,6 +2659,108 @@ void main() {
expect(cancelNode.hasFocus, false); expect(cancelNode.hasFocus, false);
}); });
testWidgets('Adaptive AlertDialog shows correct widget on each platform', (WidgetTester tester) async {
final AlertDialog dialog = AlertDialog.adaptive(
content: Container(
height: 5000.0,
width: 300.0,
color: Colors.green[500],
),
actions: <Widget>[
TextButton(
onPressed: () {},
child: const Text('OK'),
),
],
);
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
await tester.pumpWidget(_buildAppWithDialog(dialog, theme: ThemeData(platform: platform)));
await tester.pumpAndSettle();
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
expect(find.byType(CupertinoAlertDialog), findsOneWidget);
await tester.tapAt(const Offset(10.0, 10.0));
await tester.pumpAndSettle();
}
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) {
await tester.pumpWidget(_buildAppWithDialog(dialog, theme: ThemeData(platform: platform)));
await tester.pumpAndSettle();
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
expect(find.byType(CupertinoAlertDialog), findsNothing);
await tester.tapAt(const Offset(10.0, 10.0));
await tester.pumpAndSettle();
}
});
testWidgets('showAdaptiveDialog should not allow dismiss on barrier on iOS by default', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.iOS),
home: const Material(
child: Center(
child: ElevatedButton(
onPressed: null,
child: Text('Go'),
),
),
),
),
);
final BuildContext context = tester.element(find.text('Go'));
showDialog<void>(
context: context,
builder: (BuildContext context) {
return Container(
width: 100.0,
height: 100.0,
alignment: Alignment.center,
child: const Text('Dialog1'),
);
},
);
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.text('Dialog1'), findsOneWidget);
// Tap on the barrier.
await tester.tapAt(const Offset(10.0, 10.0));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.text('Dialog1'), findsNothing);
showAdaptiveDialog<void>(
context: context,
builder: (BuildContext context) {
return Container(
width: 100.0,
height: 100.0,
alignment: Alignment.center,
child: const Text('Dialog2'),
);
},
);
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.text('Dialog2'), findsOneWidget);
// Tap on the barrier, which shouldn't do anything this time.
await tester.tapAt(const Offset(10.0, 10.0));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.text('Dialog2'), findsOneWidget);
});
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();
final FocusNode cancelNode = FocusNode(); final FocusNode cancelNode = FocusNode();
......
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