dialog.dart 36.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7
import 'dart:ui';
8

xster's avatar
xster committed
9
import 'package:flutter/foundation.dart';
10
import 'package:flutter/widgets.dart';
11

12
import 'button_bar.dart';
13
import 'colors.dart';
14
import 'debug.dart';
15
import 'dialog_theme.dart';
16
import 'ink_well.dart';
17
import 'material.dart';
18
import 'material_localizations.dart';
19
import 'theme.dart';
20
import 'theme_data.dart';
21

22 23
// Examples can assume:
// enum Department { treasury, state }
24
// BuildContext context;
25

26 27
const EdgeInsets _defaultInsetPadding = EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0);

28
/// A material design dialog.
29
///
30 31 32 33
/// This dialog widget does not have any opinion about the contents of the
/// dialog. Rather than using this widget directly, consider using [AlertDialog]
/// or [SimpleDialog], which implement specific kinds of material design
/// dialogs.
34 35
///
/// See also:
Ian Hickson's avatar
Ian Hickson committed
36
///
37 38 39
///  * [AlertDialog], for dialogs that have a message and some buttons.
///  * [SimpleDialog], for dialogs that offer a variety of options.
///  * [showDialog], which actually displays the dialog and returns its result.
40
///  * <https://material.io/design/components/dialogs.html>
41
class Dialog extends StatelessWidget {
42 43 44
  /// Creates a dialog.
  ///
  /// Typically used in conjunction with [showDialog].
45
  const Dialog({
46
    Key key,
47 48
    this.backgroundColor,
    this.elevation,
49 50
    this.insetAnimationDuration = const Duration(milliseconds: 100),
    this.insetAnimationCurve = Curves.decelerate,
51 52
    this.insetPadding = _defaultInsetPadding,
    this.clipBehavior = Clip.none,
53
    this.shape,
54
    this.child,
55 56
  }) : assert(clipBehavior != null),
       super(key: key);
57

58 59
  /// {@template flutter.material.dialog.backgroundColor}
  /// The background color of the surface of this [Dialog].
60
  ///
61 62 63 64 65 66 67 68 69 70 71 72 73 74
  /// This sets the [Material.color] on this [Dialog]'s [Material].
  ///
  /// If `null`, [ThemeData.cardColor] is used.
  /// {@endtemplate}
  final Color backgroundColor;

  /// {@template flutter.material.dialog.elevation}
  /// The z-coordinate of this [Dialog].
  ///
  /// If null then [DialogTheme.elevation] is used, and if that's null then the
  /// dialog's elevation is 24.0.
  /// {@endtemplate}
  /// {@macro flutter.material.material.elevation}
  final double elevation;
75

76
  /// {@template flutter.material.dialog.insetAnimationDuration}
77 78 79 80
  /// The duration of the animation to show when the system keyboard intrudes
  /// into the space that the dialog is placed in.
  ///
  /// Defaults to 100 milliseconds.
81
  /// {@endtemplate}
82 83
  final Duration insetAnimationDuration;

84
  /// {@template flutter.material.dialog.insetAnimationCurve}
85 86 87
  /// The curve to use for the animation shown when the system keyboard intrudes
  /// into the space that the dialog is placed in.
  ///
88
  /// Defaults to [Curves.decelerate].
89
  /// {@endtemplate}
90 91
  final Curve insetAnimationCurve;

92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
  /// {@template flutter.material.dialog.insetPadding}
  /// The amount of padding added to [MediaQueryData.viewInsets] on the outside
  /// of the dialog. This defines the minimum space between the screen's edges
  /// and the dialog.
  ///
  /// Defaults to `EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0)`.
  /// {@endtemplate}
  final EdgeInsets insetPadding;

  /// {@template flutter.material.dialog.clipBehavior}
  /// Controls how the contents of the dialog are clipped (or not) to the given
  /// [shape].
  ///
  /// See the enum [Clip] for details of all possible options and their common
  /// use cases.
  ///
  /// Defaults to [Clip.none], and must not be null.
  /// {@endtemplate}
  final Clip clipBehavior;

112 113 114 115 116
  /// {@template flutter.material.dialog.shape}
  /// The shape of this dialog's border.
  ///
  /// Defines the dialog's [Material.shape].
  ///
117
  /// The default shape is a [RoundedRectangleBorder] with a radius of 4.0
118 119 120
  /// {@endtemplate}
  final ShapeBorder shape;

121 122 123 124
  /// The widget below this widget in the tree.
  ///
  /// {@macro flutter.widgets.child}
  final Widget child;
125

126
  static const RoundedRectangleBorder _defaultDialogShape =
127
    RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
128
  static const double _defaultElevation = 24.0;
129

130 131
  @override
  Widget build(BuildContext context) {
132
    final DialogTheme dialogTheme = DialogTheme.of(context);
133
    final EdgeInsets effectivePadding = MediaQuery.of(context).viewInsets + (insetPadding ?? const EdgeInsets.all(0.0));
134
    return AnimatedPadding(
135
      padding: effectivePadding,
136 137
      duration: insetAnimationDuration,
      curve: insetAnimationCurve,
138
      child: MediaQuery.removeViewInsets(
139 140 141 142 143
        removeLeft: true,
        removeTop: true,
        removeRight: true,
        removeBottom: true,
        context: context,
144 145
        child: Center(
          child: ConstrainedBox(
146
            constraints: const BoxConstraints(minWidth: 280.0),
147
            child: Material(
148 149
              color: backgroundColor ?? dialogTheme.backgroundColor ?? Theme.of(context).dialogBackgroundColor,
              elevation: elevation ?? dialogTheme.elevation ?? _defaultElevation,
150
              shape: shape ?? dialogTheme.shape ?? _defaultDialogShape,
151
              type: MaterialType.card,
152
              clipBehavior: clipBehavior,
153 154 155 156 157
              child: child,
            ),
          ),
        ),
      ),
158 159 160 161 162 163 164 165 166 167 168
    );
  }
}

/// A material design alert dialog.
///
/// An alert dialog informs the user about situations that require
/// acknowledgement. An alert dialog has an optional title and an optional list
/// of actions. The title is displayed above the content and the actions are
/// displayed below the content.
///
169 170
/// {@youtube 560 315 https://www.youtube.com/watch?v=75CsnyRXf5I}
///
171 172 173 174 175 176 177 178 179
/// If the content is too large to fit on the screen vertically, the dialog will
/// display the title and the actions and let the content overflow, which is
/// rarely desired. Consider using a scrolling widget for [content], such as
/// [SingleChildScrollView], to avoid overflow. (However, be aware that since
/// [AlertDialog] tries to size itself using the intrinsic dimensions of its
/// children, widgets such as [ListView], [GridView], and [CustomScrollView],
/// which use lazy viewports, will not work. If this is a problem, consider
/// using [Dialog] directly.)
///
180 181 182 183 184 185
/// For dialogs that offer the user a choice between several options, consider
/// using a [SimpleDialog].
///
/// Typically passed as the child widget to [showDialog], which displays the
/// dialog.
///
186 187
/// {@animation 350 622 https://flutter.github.io/assets-for-api-docs/assets/material/alert_dialog.mp4}
///
188
/// {@tool snippet}
189 190 191 192 193
///
/// This snippet shows a method in a [State] which, when called, displays a dialog box
/// and returns a [Future] that completes when the dialog is dismissed.
///
/// ```dart
194
/// Future<void> _showMyDialog() async {
195
///   return showDialog<void>(
196 197
///     context: context,
///     barrierDismissible: false, // user must tap button!
198
///     builder: (BuildContext context) {
199
///       return AlertDialog(
200
///         title: Text('AlertDialog Title'),
201 202 203
///         content: SingleChildScrollView(
///           child: ListBody(
///             children: <Widget>[
204 205
///               Text('This is a demo alert dialog.'),
///               Text('Would you like to approve of this message?'),
206 207
///             ],
///           ),
208
///         ),
209
///         actions: <Widget>[
210
///           TextButton(
211
///             child: Text('Approve'),
212 213 214 215 216 217 218
///             onPressed: () {
///               Navigator.of(context).pop();
///             },
///           ),
///         ],
///       );
///     },
219 220 221
///   );
/// }
/// ```
222
/// {@end-tool}
223
///
224 225
/// See also:
///
226 227
///  * [SimpleDialog], which handles the scrolling of the contents but has no [actions].
///  * [Dialog], on which [AlertDialog] and [SimpleDialog] are based.
228
///  * [CupertinoAlertDialog], an iOS-styled alert dialog.
229
///  * [showDialog], which actually displays the dialog and returns its result.
230
///  * <https://material.io/design/components/dialogs.html#alert-dialog>
231 232 233 234
class AlertDialog extends StatelessWidget {
  /// Creates an alert dialog.
  ///
  /// Typically used in conjunction with [showDialog].
235 236 237 238
  ///
  /// The [contentPadding] must not be null. The [titlePadding] defaults to
  /// null, which implies a default that depends on the values of the other
  /// properties. See the documentation of [titlePadding] for details.
239
  const AlertDialog({
240
    Key key,
241
    this.title,
242
    this.titlePadding,
243
    this.titleTextStyle,
244
    this.content,
245
    this.contentPadding = const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0),
246
    this.contentTextStyle,
247
    this.actions,
248
    this.actionsPadding = EdgeInsets.zero,
249
    this.actionsOverflowDirection,
250
    this.actionsOverflowButtonSpacing,
251
    this.buttonPadding,
252 253
    this.backgroundColor,
    this.elevation,
254
    this.semanticLabel,
255 256
    this.insetPadding = _defaultInsetPadding,
    this.clipBehavior = Clip.none,
257
    this.shape,
258
    this.scrollable = false,
259
  }) : assert(contentPadding != null),
260
       assert(clipBehavior != null),
261
       super(key: key);
262 263 264

  /// The (optional) title of the dialog is displayed in a large font at the top
  /// of the dialog.
Ian Hickson's avatar
Ian Hickson committed
265 266
  ///
  /// Typically a [Text] widget.
267 268
  final Widget title;

269 270
  /// Padding around the title.
  ///
271 272 273 274 275 276 277 278
  /// If there is no title, no padding will be provided. Otherwise, this padding
  /// is used.
  ///
  /// This property defaults to providing 24 pixels on the top, left, and right
  /// of the title. If the [content] is not null, then no bottom padding is
  /// provided (but see [contentPadding]). If it _is_ null, then an extra 20
  /// pixels of bottom padding is added to separate the [title] from the
  /// [actions].
279
  final EdgeInsetsGeometry titlePadding;
280

281 282
  /// Style for the text in the [title] of this [AlertDialog].
  ///
283 284
  /// If null, [DialogTheme.titleTextStyle] is used. If that's null, defaults to
  /// [TextTheme.headline6] of [ThemeData.textTheme].
285 286
  final TextStyle titleTextStyle;

287 288
  /// The (optional) content of the dialog is displayed in the center of the
  /// dialog in a lighter font.
Ian Hickson's avatar
Ian Hickson committed
289
  ///
290 291 292 293
  /// Typically this is a [SingleChildScrollView] that contains the dialog's
  /// message. As noted in the [AlertDialog] documentation, it's important
  /// to use a [SingleChildScrollView] if there's any risk that the content
  /// will not fit.
294 295
  final Widget content;

296 297
  /// Padding around the content.
  ///
298 299 300 301
  /// If there is no content, no padding will be provided. Otherwise, padding of
  /// 20 pixels is provided above the content to separate the content from the
  /// title, and padding of 24 pixels is provided on the left, right, and bottom
  /// to separate the content from the other edges of the dialog.
302
  final EdgeInsetsGeometry contentPadding;
303

304 305
  /// Style for the text in the [content] of this [AlertDialog].
  ///
306 307
  /// If null, [DialogTheme.contentTextStyle] is used. If that's null, defaults
  /// to [TextTheme.subtitle1] of [ThemeData.textTheme].
308 309
  final TextStyle contentTextStyle;

310 311
  /// The (optional) set of actions that are displayed at the bottom of the
  /// dialog.
Ian Hickson's avatar
Ian Hickson committed
312
  ///
313
  /// Typically this is a list of [TextButton] widgets. It is recommended to
314
  /// set the [Text.textAlign] to [TextAlign.end] for the [Text] within the
315
  /// [TextButton], so that buttons whose labels wrap to an extra line align
316
  /// with the overall [ButtonBar]'s alignment within the dialog.
Ian Hickson's avatar
Ian Hickson committed
317
  ///
318 319 320 321 322 323
  /// These widgets will be wrapped in a [ButtonBar], which introduces 8 pixels
  /// of padding on each side.
  ///
  /// If the [title] is not null but the [content] _is_ null, then an extra 20
  /// pixels of padding is added above the [ButtonBar] to separate the [title]
  /// from the [actions].
324 325
  final List<Widget> actions;

Dan Field's avatar
Dan Field committed
326
  /// Padding around the set of [actions] at the bottom of the dialog.
327 328 329 330 331 332 333 334 335
  ///
  /// Typically used to provide padding to the button bar between the button bar
  /// and the edges of the dialog.
  ///
  /// If are no [actions], then no padding will be included. The padding around
  /// the button bar defaults to zero. It is also important to note that
  /// [buttonPadding] may contribute to the padding on the edges of [actions] as
  /// well.
  ///
336
  /// {@tool snippet}
337 338 339 340 341 342
  /// This is an example of a set of actions aligned with the content widget.
  /// ```dart
  /// AlertDialog(
  ///   title: Text('Title'),
  ///   content: Container(width: 200, height: 200, color: Colors.green),
  ///   actions: <Widget>[
343 344
  ///     ElevatedButton(onPressed: () {}, child: Text('Button 1')),
  ///     ElevatedButton(onPressed: () {}, child: Text('Button 2')),
345 346 347 348 349
  ///   ],
  ///   actionsPadding: EdgeInsets.symmetric(horizontal: 8.0),
  /// )
  /// ```
  /// {@end-tool}
350 351 352 353
  ///
  /// See also:
  ///
  /// * [ButtonBar], which [actions] configures to lay itself out.
354 355
  final EdgeInsetsGeometry actionsPadding;

356 357 358 359 360 361 362 363 364 365 366 367
  /// The vertical direction of [actions] if the children overflow
  /// horizontally.
  ///
  /// If the dialog's [actions] do not fit into a single row, then they
  /// are arranged in a column. The first action is at the top of the
  /// column if this property is set to [VerticalDirection.down], since it
  /// "starts" at the top and "ends" at the bottom. On the other hand,
  /// the first action will be at the bottom of the column if this
  /// property is set to [VerticalDirection.up], since it "starts" at the
  /// bottom and "ends" at the top.
  ///
  /// If null then it will use the surrounding
368
  /// [ButtonBarThemeData.overflowDirection]. If that is null, it will
369 370 371 372 373 374 375
  /// default to [VerticalDirection.down].
  ///
  /// See also:
  ///
  /// * [ButtonBar], which [actions] configures to lay itself out.
  final VerticalDirection actionsOverflowDirection;

376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
  /// The spacing between [actions] when the button bar overflows.
  ///
  /// If the widgets in [actions] do not fit into a single row, they are
  /// arranged into a column. This parameter provides additional
  /// vertical space in between buttons when it does overflow.
  ///
  /// Note that the button spacing may appear to be more than
  /// the value provided. This is because most buttons adhere to the
  /// [MaterialTapTargetSize] of 48px. So, even though a button
  /// might visually be 36px in height, it might still take up to
  /// 48px vertically.
  ///
  /// If null then no spacing will be added in between buttons in
  /// an overflow state.
  final double actionsOverflowButtonSpacing;

392 393 394 395 396 397
  /// The padding that surrounds each button in [actions].
  ///
  /// This is different from [actionsPadding], which defines the padding
  /// between the entire button bar and the edges of the dialog.
  ///
  /// If this property is null, then it will use the surrounding
398
  /// [ButtonBarThemeData.buttonPadding]. If that is null, it will default to
399
  /// 8.0 logical pixels on the left and right.
400 401 402 403
  ///
  /// See also:
  ///
  /// * [ButtonBar], which [actions] configures to lay itself out.
404 405
  final EdgeInsetsGeometry buttonPadding;

406 407 408 409 410 411 412
  /// {@macro flutter.material.dialog.backgroundColor}
  final Color backgroundColor;

  /// {@macro flutter.material.dialog.elevation}
  /// {@macro flutter.material.material.elevation}
  final double elevation;

413
  /// The semantic label of the dialog used by accessibility frameworks to
414
  /// announce screen transitions when the dialog is opened and closed.
415
  ///
416 417 418 419 420
  /// In iOS, if this label is not provided, a semantic label will be inferred
  /// from the [title] if it is not null.
  ///
  /// In Android, if this label is not provided, the dialog will use the
  /// [MaterialLocalizations.alertDialogLabel] as its label.
421
  ///
422
  /// See also:
423
  ///
424
  ///  * [SemanticsConfiguration.namesRoute], for a description of how this
425 426 427
  ///    value is used.
  final String semanticLabel;

428 429 430 431 432 433
  /// {@macro flutter.material.dialog.insetPadding}
  final EdgeInsets insetPadding;

  /// {@macro flutter.material.dialog.clipBehavior}
  final Clip clipBehavior;

434 435 436
  /// {@macro flutter.material.dialog.shape}
  final ShapeBorder shape;

437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452
  /// Determines whether the [title] and [content] widgets are wrapped in a
  /// scrollable.
  ///
  /// This configuration is used when the [title] and [content] are expected
  /// to overflow. Both [title] and [content] are wrapped in a scroll view,
  /// allowing all overflowed content to be visible while still showing the
  /// button bar.
  @Deprecated(
    'Set scrollable to `true`. This parameter will be removed and '
    'was introduced to migrate AlertDialog to be scrollable by '
    'default. For more information, see '
    'https://flutter.dev/docs/release/breaking-changes/scrollable_alert_dialog. '
    'This feature was deprecated after v1.13.2.'
  )
  final bool scrollable;

453
  @override
454
  Widget build(BuildContext context) {
455
    assert(debugCheckHasMaterialLocalizations(context));
456 457
    final ThemeData theme = Theme.of(context);
    final DialogTheme dialogTheme = DialogTheme.of(context);
458

459
    String label = semanticLabel;
460 461 462 463 464 465 466 467 468
    switch (theme.platform) {
      case TargetPlatform.iOS:
      case TargetPlatform.macOS:
        break;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        label ??= MaterialLocalizations.of(context)?.alertDialogLabel;
469 470
    }

471 472 473 474 475
    // The paddingScaleFactor is used to adjust the padding of Dialog's
    // children.
    final double paddingScaleFactor = _paddingScaleFactor(MediaQuery.of(context).textScaleFactor);
    final TextDirection textDirection = Directionality.of(context);

476 477
    Widget titleWidget;
    Widget contentWidget;
478
    Widget actionsWidget;
479 480 481 482 483 484 485 486 487 488
    if (title != null) {
      final EdgeInsets defaultTitlePadding = EdgeInsets.fromLTRB(24.0, 24.0, 24.0, content == null ? 20.0 : 0.0);
      final EdgeInsets effectiveTitlePadding = titlePadding?.resolve(textDirection) ?? defaultTitlePadding;
      titleWidget = Padding(
        padding: EdgeInsets.only(
          left: effectiveTitlePadding.left * paddingScaleFactor,
          right: effectiveTitlePadding.right * paddingScaleFactor,
          top: effectiveTitlePadding.top * paddingScaleFactor,
          bottom: effectiveTitlePadding.bottom,
        ),
489
        child: DefaultTextStyle(
490
          style: titleTextStyle ?? dialogTheme.titleTextStyle ?? theme.textTheme.headline6,
491 492
          child: Semantics(
            child: title,
493
            namesRoute: label == null,
494 495 496 497
            container: true,
          ),
        ),
      );
498
    }
499

500 501
    if (content != null) {
      final EdgeInsets effectiveContentPadding = contentPadding.resolve(textDirection);
502
      contentWidget = Padding(
503 504 505 506 507 508
        padding: EdgeInsets.only(
          left: effectiveContentPadding.left * paddingScaleFactor,
          right: effectiveContentPadding.right * paddingScaleFactor,
          top: title == null ? effectiveContentPadding.top * paddingScaleFactor : effectiveContentPadding.top,
          bottom: effectiveContentPadding.bottom,
        ),
509
        child: DefaultTextStyle(
510
          style: contentTextStyle ?? dialogTheme.contentTextStyle ?? theme.textTheme.subtitle1,
511 512 513
          child: content,
        ),
      );
514 515
    }

516

517
    if (actions != null) {
518 519 520 521
      actionsWidget = Padding(
        padding: actionsPadding,
        child: ButtonBar(
          buttonPadding: buttonPadding,
522
          overflowDirection: actionsOverflowDirection,
523
          overflowButtonSpacing: actionsOverflowButtonSpacing,
524 525 526
          children: actions,
        ),
      );
527
    }
528

529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547
    List<Widget> columnChildren;
    if (scrollable) {
      columnChildren = <Widget>[
        if (title != null || content != null)
          Flexible(
            child: SingleChildScrollView(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: <Widget>[
                  if (title != null)
                    titleWidget,
                  if (content != null)
                    contentWidget,
                ],
              ),
            ),
          ),
        if (actions != null)
548
          actionsWidget,
549 550 551 552 553 554 555 556
      ];
    } else {
      columnChildren = <Widget>[
        if (title != null)
          titleWidget,
        if (content != null)
          Flexible(child: contentWidget),
        if (actions != null)
557
          actionsWidget,
558 559 560
      ];
    }

561 562
    Widget dialogChild = IntrinsicWidth(
      child: Column(
563 564
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
565
        children: columnChildren,
566
      ),
567
    );
568 569

    if (label != null)
570
      dialogChild = Semantics(
571 572
        scopesRoute: true,
        explicitChildNodes: true,
573 574
        namesRoute: true,
        label: label,
575
        child: dialogChild,
576 577
      );

578 579 580
    return Dialog(
      backgroundColor: backgroundColor,
      elevation: elevation,
581 582
      insetPadding: insetPadding,
      clipBehavior: clipBehavior,
583 584 585
      shape: shape,
      child: dialogChild,
    );
586 587 588
  }
}

589 590 591 592 593 594 595
/// An option used in a [SimpleDialog].
///
/// A simple dialog offers the user a choice between several options. This
/// widget is commonly used to represent each of the options. If the user
/// selects this option, the widget will call the [onPressed] callback, which
/// typically uses [Navigator.pop] to close the dialog.
///
596 597 598 599 600 601
/// The padding on a [SimpleDialogOption] is configured to combine with the
/// default [SimpleDialog.contentPadding] so that each option ends up 8 pixels
/// from the other vertically, with 20 pixels of spacing between the dialog's
/// title and the first option, and 24 pixels of spacing between the last option
/// and the bottom of the dialog.
///
602
/// {@tool snippet}
603 604
///
/// ```dart
605
/// SimpleDialogOption(
606 607 608 609
///   onPressed: () { Navigator.pop(context, Department.treasury); },
///   child: const Text('Treasury department'),
/// )
/// ```
610
/// {@end-tool}
611
///
612 613 614 615
/// See also:
///
///  * [SimpleDialog], for a dialog in which to use this widget.
///  * [showDialog], which actually displays the dialog and returns its result.
616
///  * [TextButton], which are commonly used as actions in other kinds of
617
///    dialogs, such as [AlertDialog]s.
618
///  * <https://material.io/design/components/dialogs.html#simple-dialog>
619 620
class SimpleDialogOption extends StatelessWidget {
  /// Creates an option for a [SimpleDialog].
621
  const SimpleDialogOption({
622 623
    Key key,
    this.onPressed,
624
    this.padding,
625 626 627 628 629 630
    this.child,
  }) : super(key: key);

  /// The callback that is called when this option is selected.
  ///
  /// If this is set to null, the option cannot be selected.
631 632 633
  ///
  /// When used in a [SimpleDialog], this will typically call [Navigator.pop]
  /// with a value for [showDialog] to complete its future with.
634 635 636 637 638 639 640
  final VoidCallback onPressed;

  /// The widget below this widget in the tree.
  ///
  /// Typically a [Text] widget.
  final Widget child;

641 642 643 644 645
  /// The amount of space to surround the [child] with.
  ///
  /// Defaults to EdgeInsets.symmetric(vertical: 8.0, horizontal: 24.0).
  final EdgeInsets padding;

646 647
  @override
  Widget build(BuildContext context) {
648
    return InkWell(
649
      onTap: onPressed,
650
      child: Padding(
651
        padding: padding ?? const EdgeInsets.symmetric(vertical: 8.0, horizontal: 24.0),
652
        child: child,
653 654 655 656 657
      ),
    );
  }
}

658 659 660 661 662
/// A simple material design dialog.
///
/// A simple dialog offers the user a choice between several options. A simple
/// dialog has an optional title that is displayed above the choices.
///
663 664 665 666
/// Choices are normally represented using [SimpleDialogOption] widgets. If
/// other widgets are used, see [contentPadding] for notes regarding the
/// conventions for obtaining the spacing expected by Material Design.
///
667 668 669 670 671 672
/// For dialogs that inform the user about a situation, consider using an
/// [AlertDialog].
///
/// Typically passed as the child widget to [showDialog], which displays the
/// dialog.
///
673 674
/// {@animation 350 622 https://flutter.github.io/assets-for-api-docs/assets/material/simple_dialog.mp4}
///
675
/// {@tool snippet}
676 677 678 679 680 681 682 683 684 685 686 687 688
///
/// In this example, the user is asked to select between two options. These
/// options are represented as an enum. The [showDialog] method here returns
/// a [Future] that completes to a value of that enum. If the user cancels
/// the dialog (e.g. by hitting the back button on Android, or tapping on the
/// mask behind the dialog) then the future completes with the null value.
///
/// The return value in this example is used as the index for a switch statement.
/// One advantage of using an enum as the return value and then using that to
/// drive a switch statement is that the analyzer will flag any switch statement
/// that doesn't mention every value in the enum.
///
/// ```dart
689
/// Future<void> _askedToLead() async {
690 691
///   switch (await showDialog<Department>(
///     context: context,
692
///     builder: (BuildContext context) {
693
///       return SimpleDialog(
694 695
///         title: const Text('Select assignment'),
///         children: <Widget>[
696
///           SimpleDialogOption(
697 698 699
///             onPressed: () { Navigator.pop(context, Department.treasury); },
///             child: const Text('Treasury department'),
///           ),
700
///           SimpleDialogOption(
701 702 703 704 705 706
///             onPressed: () { Navigator.pop(context, Department.state); },
///             child: const Text('State department'),
///           ),
///         ],
///       );
///     }
707 708 709 710 711 712 713 714 715 716 717
///   )) {
///     case Department.treasury:
///       // Let's go.
///       // ...
///     break;
///     case Department.state:
///       // ...
///     break;
///   }
/// }
/// ```
718
/// {@end-tool}
719
///
720 721
/// See also:
///
722
///  * [SimpleDialogOption], which are options used in this type of dialog.
723 724 725
///  * [AlertDialog], for dialogs that have a row of buttons below the body.
///  * [Dialog], on which [SimpleDialog] and [AlertDialog] are based.
///  * [showDialog], which actually displays the dialog and returns its result.
726
///  * <https://material.io/design/components/dialogs.html#simple-dialog>
727 728 729 730
class SimpleDialog extends StatelessWidget {
  /// Creates a simple dialog.
  ///
  /// Typically used in conjunction with [showDialog].
731 732
  ///
  /// The [titlePadding] and [contentPadding] arguments must not be null.
733
  const SimpleDialog({
734 735
    Key key,
    this.title,
736
    this.titlePadding = const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0),
737
    this.titleTextStyle,
738
    this.children,
739
    this.contentPadding = const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0),
740 741
    this.backgroundColor,
    this.elevation,
742
    this.semanticLabel,
743
    this.shape,
744 745 746
  }) : assert(titlePadding != null),
       assert(contentPadding != null),
       super(key: key);
747 748 749 750 751 752 753 754 755

  /// The (optional) title of the dialog is displayed in a large font at the top
  /// of the dialog.
  ///
  /// Typically a [Text] widget.
  final Widget title;

  /// Padding around the title.
  ///
756 757 758 759 760 761 762
  /// If there is no title, no padding will be provided.
  ///
  /// By default, this provides the recommend Material Design padding of 24
  /// pixels around the left, top, and right edges of the title.
  ///
  /// See [contentPadding] for the conventions regarding padding between the
  /// [title] and the [children].
763
  final EdgeInsetsGeometry titlePadding;
764

765 766
  /// Style for the text in the [title] of this [SimpleDialog].
  ///
767
  /// If null, [DialogTheme.titleTextStyle] is used. If that's null, defaults to
768
  /// [TextTheme.headline6] of [ThemeData.textTheme].
769 770
  final TextStyle titleTextStyle;

771 772
  /// The (optional) content of the dialog is displayed in a
  /// [SingleChildScrollView] underneath the title.
773
  ///
774
  /// Typically a list of [SimpleDialogOption]s.
775 776 777 778
  final List<Widget> children;

  /// Padding around the content.
  ///
779 780 781 782 783 784 785 786 787 788
  /// By default, this is 12 pixels on the top and 16 pixels on the bottom. This
  /// is intended to be combined with children that have 24 pixels of padding on
  /// the left and right, and 8 pixels of padding on the top and bottom, so that
  /// the content ends up being indented 20 pixels from the title, 24 pixels
  /// from the bottom, and 24 pixels from the sides.
  ///
  /// The [SimpleDialogOption] widget uses such padding.
  ///
  /// If there is no [title], the [contentPadding] should be adjusted so that
  /// the top padding ends up being 24 pixels.
789
  final EdgeInsetsGeometry contentPadding;
790

791 792 793 794 795 796 797
  /// {@macro flutter.material.dialog.backgroundColor}
  final Color backgroundColor;

  /// {@macro flutter.material.dialog.elevation}
  /// {@macro flutter.material.material.elevation}
  final double elevation;

798
  /// The semantic label of the dialog used by accessibility frameworks to
799
  /// announce screen transitions when the dialog is opened and closed.
800
  ///
801
  /// If this label is not provided, a semantic label will be inferred from the
802 803
  /// [title] if it is not null.  If there is no title, the label will be taken
  /// from [MaterialLocalizations.dialogLabel].
804
  ///
805
  /// See also:
806
  ///
807
  ///  * [SemanticsConfiguration.namesRoute], for a description of how this
808 809 810
  ///    value is used.
  final String semanticLabel;

811 812 813
  /// {@macro flutter.material.dialog.shape}
  final ShapeBorder shape;

814 815
  @override
  Widget build(BuildContext context) {
816
    assert(debugCheckHasMaterialLocalizations(context));
817 818
    final ThemeData theme = Theme.of(context);

819 820
    String label = semanticLabel;
    if (title == null) {
821
      switch (theme.platform) {
822
        case TargetPlatform.macOS:
823 824 825 826 827
        case TargetPlatform.iOS:
          label = semanticLabel;
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
828 829
        case TargetPlatform.linux:
        case TargetPlatform.windows:
830 831
          label = semanticLabel ?? MaterialLocalizations.of(context)?.dialogLabel;
      }
832 833
    }

834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871
    // The paddingScaleFactor is used to adjust the padding of Dialog
    // children.
    final double paddingScaleFactor = _paddingScaleFactor(MediaQuery.of(context).textScaleFactor);
    final TextDirection textDirection = Directionality.of(context);

    Widget titleWidget;
    if (title != null) {
      final EdgeInsets effectiveTitlePadding = titlePadding.resolve(textDirection);
      titleWidget = Padding(
        padding: EdgeInsets.only(
          left: effectiveTitlePadding.left * paddingScaleFactor,
          right: effectiveTitlePadding.right * paddingScaleFactor,
          top: effectiveTitlePadding.top * paddingScaleFactor,
          bottom: children == null ? effectiveTitlePadding.bottom * paddingScaleFactor : effectiveTitlePadding.bottom,
        ),
        child: DefaultTextStyle(
          style: titleTextStyle ?? DialogTheme.of(context).titleTextStyle ?? theme.textTheme.headline6,
          child: Semantics(namesRoute: true, child: title),
        ),
      );
    }

    Widget contentWidget;
    if (children != null) {
      final EdgeInsets effectiveContentPadding = contentPadding.resolve(textDirection);
      contentWidget = Flexible(
        child: SingleChildScrollView(
          padding: EdgeInsets.only(
            left: effectiveContentPadding.left * paddingScaleFactor,
            right: effectiveContentPadding.right * paddingScaleFactor,
            top: title == null ? effectiveContentPadding.top * paddingScaleFactor : effectiveContentPadding.top,
            bottom: effectiveContentPadding.bottom * paddingScaleFactor,
          ),
          child: ListBody(children: children),
        ),
      );
    }

872
    Widget dialogChild = IntrinsicWidth(
873
      stepWidth: 56.0,
874
      child: ConstrainedBox(
875
        constraints: const BoxConstraints(minWidth: 280.0),
876
        child: Column(
877 878
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
879 880
          children: <Widget>[
            if (title != null)
881
              titleWidget,
882
            if (children != null)
883
              contentWidget,
884
          ],
885 886
        ),
      ),
887
    );
888 889

    if (label != null)
890
      dialogChild = Semantics(
891 892 893 894
        namesRoute: true,
        label: label,
        child: dialogChild,
      );
895 896 897 898 899 900
    return Dialog(
      backgroundColor: backgroundColor,
      elevation: elevation,
      shape: shape,
      child: dialogChild,
    );
901 902
  }
}
903

904
Widget _buildMaterialDialogTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
905 906
  return FadeTransition(
    opacity: CurvedAnimation(
907 908 909 910 911
      parent: animation,
      curve: Curves.easeOut,
    ),
    child: child,
  );
912 913
}

914 915 916
/// Displays a Material dialog above the current contents of the app, with
/// Material entrance and exit animations, modal barrier color, and modal
/// barrier behavior (dialog is dismissible with a tap on the barrier).
917
///
918
/// This function takes a `builder` which typically builds a [Dialog] widget.
919 920 921 922
/// Content below the dialog is dimmed with a [ModalBarrier]. The widget
/// returned by the `builder` does not share a context with the location that
/// `showDialog` is originally called from. Use a [StatefulBuilder] or a
/// custom [StatefulWidget] if the dialog needs to update dynamically.
923
///
924 925
/// The `child` argument is deprecated, and should be replaced with `builder`.
///
Ian Hickson's avatar
Ian Hickson committed
926 927 928 929
/// The `context` argument is used to look up the [Navigator] and [Theme] for
/// the dialog. It is only used when the method is called. Its corresponding
/// widget can be safely removed from the tree before the dialog is closed.
///
930 931 932 933
/// The `barrierDismissible` argument is used to indicate whether tapping on the
/// barrier will dismiss the dialog. It is `true` by default and can not be `null`.
///
/// The `barrierColor` argument is used to specify the color of the modal
934
/// barrier that darkens everything below the dialog. If `null` the default color
935 936 937 938
/// `Colors.black54` 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
939
/// (see [SafeArea] for more details). It is `true` by default, which means
940
/// the dialog will not overlap operating system areas. If it is set to `false`
941 942
/// the dialog will only be constrained by the screen size. It can not be `null`.
///
943 944 945
/// The `useRootNavigator` argument is used to determine whether to push the
/// dialog to the [Navigator] furthest from or nearest to the given `context`.
/// By default, `useRootNavigator` is `true` and the dialog route created by
946
/// this method is pushed to the root navigator. It can not be `null`.
947
///
948 949 950
/// The `routeSettings` argument is passed to [showGeneralDialog],
/// see [RouteSettings] for details.
///
951 952
/// If the application has multiple [Navigator] objects, it may be necessary to
/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the
953
/// dialog rather than just `Navigator.pop(context, result)`.
954
///
955 956 957
/// Returns a [Future] that resolves to the value (if any) that was passed to
/// [Navigator.pop] when the dialog was closed.
///
958
/// See also:
959
///
960 961 962
///  * [AlertDialog], for dialogs that have a row of buttons below a body.
///  * [SimpleDialog], which handles the scrolling of the contents and does
///    not show buttons below its body.
963
///  * [Dialog], on which [SimpleDialog] and [AlertDialog] are based.
964 965
///  * [showCupertinoDialog], which displays an iOS-style dialog.
///  * [showGeneralDialog], which allows for customization of the dialog popup.
966
///  * <https://material.io/design/components/dialogs.html>
967
Future<T> showDialog<T>({
968
  @required BuildContext context,
969
  WidgetBuilder builder,
970
  bool barrierDismissible = true,
971 972 973 974
  Color barrierColor,
  bool useSafeArea = true,
  bool useRootNavigator = true,
  RouteSettings routeSettings,
975 976 977
  @Deprecated(
    'Instead of using the "child" argument, return the child from a closure '
    'provided to the "builder" argument. This will ensure that the BuildContext '
978 979 980 981
    'is appropriate for widgets built in the dialog. '
    'This feature was deprecated after v0.2.3.'
  )
  Widget child,
982
}) {
983
  assert(child == null || builder == null);
984 985
  assert(barrierDismissible != null);
  assert(useSafeArea != null);
986
  assert(useRootNavigator != null);
987
  assert(debugCheckHasMaterialLocalizations(context));
988 989

  final ThemeData theme = Theme.of(context, shadowThemeOnly: true);
990 991 992
  return showGeneralDialog(
    context: context,
    pageBuilder: (BuildContext buildContext, Animation<double> animation, Animation<double> secondaryAnimation) {
993
      final Widget pageChild = child ?? Builder(builder: builder);
994 995 996 997 998 999
      Widget dialog = Builder(
        builder: (BuildContext context) {
          return theme != null
            ? Theme(data: theme, child: pageChild)
            : pageChild;
        }
1000
      );
1001 1002 1003 1004
      if (useSafeArea) {
        dialog = SafeArea(child: dialog);
      }
      return dialog;
1005
    },
1006
    barrierDismissible: barrierDismissible,
1007
    barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
1008
    barrierColor: barrierColor ?? Colors.black54,
1009 1010
    transitionDuration: const Duration(milliseconds: 150),
    transitionBuilder: _buildMaterialDialogTransitions,
1011
    useRootNavigator: useRootNavigator,
1012
    routeSettings: routeSettings,
1013
  );
1014
}
1015 1016 1017 1018 1019 1020 1021

double _paddingScaleFactor(double textScaleFactor) {
  final double clampedTextScaleFactor = textScaleFactor.clamp(1.0, 2.0).toDouble();
  // The final padding scale factor is clamped between 1/3 and 1. For example,
  // a non-scaled padding of 24 will produce a padding between 24 and 8.
  return lerpDouble(1.0, 1.0 / 3.0, clampedTextScaleFactor - 1.0);
}