Skip to content

Commit

Permalink
Adaptive alert dialog (flutter#124336)
Browse files Browse the repository at this point in the history
Fixes flutter#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">
  • Loading branch information
MitchellGoodwin authored Apr 18, 2023
1 parent c05bc40 commit bd2617e
Show file tree
Hide file tree
Showing 4 changed files with 391 additions and 1 deletion.
76 changes: 76 additions & 0 deletions examples/api/lib/material/dialog/adaptive_alert_dialog.0.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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'),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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);
});
}
183 changes: 182 additions & 1 deletion packages/flutter/lib/src/material/dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

import 'dart:ui';

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/widgets.dart';

import 'color_scheme.dart';
import 'colors.dart';
Expand Down Expand Up @@ -395,6 +395,69 @@ class AlertDialog extends StatelessWidget {
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.
///
/// Typically, an [Icon] widget. Providing an icon centers the [title]'s text.
Expand Down Expand Up @@ -638,6 +701,7 @@ class AlertDialog extends StatelessWidget {
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
final ThemeData theme = Theme.of(context);

final DialogTheme dialogTheme = DialogTheme.of(context);
final DialogTheme defaults = theme.useMaterial3 ? _DialogDefaultsM3(context) : _DialogDefaultsM2(context);

Expand Down Expand Up @@ -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].
///
/// A simple dialog offers the user a choice between several options. This
Expand Down Expand Up @@ -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) {
if (context is Element && !context.debugIsActive) {
throw FlutterError.fromParts(<DiagnosticsNode>[
Expand Down
Loading

0 comments on commit bd2617e

Please sign in to comment.