import 'dart:async'; import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart'; // Examples can assume: // enum Department { treasury, state } /// A material design dialog. /// /// 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. /// /// See also: /// /// * [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. /// * class Dialog extends StatelessWidget { /// Creates a dialog. /// /// Typically used in conjunction with [showDialog]. const Dialog({ Key key, this.child, this.insetAnimationDuration: const Duration(milliseconds: 100), this.insetAnimationCurve: Curves.decelerate, }) : super(key: key); /// The widget below this widget in the tree. /// /// {@macro flutter.widgets.child} final Widget child; /// 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. final Duration insetAnimationDuration; /// The curve to use for the animation shown when the system keyboard intrudes /// into the space that the dialog is placed in. /// /// Defaults to [Curves.fastOutSlowIn]. final Curve insetAnimationCurve; Color _getColor(BuildContext context) { return Theme.of(context).dialogBackgroundColor; } @override Widget build(BuildContext context) { return new AnimatedPadding( padding: MediaQuery.of(context).viewInsets + const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0), duration: insetAnimationDuration, curve: insetAnimationCurve, child: new MediaQuery.removeViewInsets( removeLeft: true, removeTop: true, removeRight: true, removeBottom: true, context: context, child: new Center( child: new ConstrainedBox( constraints: const BoxConstraints(minWidth: 280.0), child: new Material( shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10.0)), side: BorderSide(color: Colors.white, width: 1.0, style: BorderStyle.solid)), elevation: 30.0, color: _getColor(context), type: MaterialType.card, child: child, ), ), ), ), ); } } /// 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. /// /// 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. Consider /// using a scrolling widget, such as [ListView], for [content] to avoid /// overflow. /// /// 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. /// /// ## Sample code /// /// 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 /// Future _neverSatisfied() async { /// return showDialog( /// context: context, /// barrierDismissible: false, // user must tap button! /// builder: (BuildContext context) { /// return new AlertDialog( /// title: new Text('Rewind and remember'), /// content: new SingleChildScrollView( /// child: new ListBody( /// children: [ /// new Text('You will never be satisfied.'), /// new Text('You\’re like me. I’m never satisfied.'), /// ], /// ), /// ), /// actions: [ /// new FlatButton( /// child: new Text('Regret'), /// onPressed: () { /// Navigator.of(context).pop(); /// }, /// ), /// ], /// ); /// }, /// ); /// } /// ``` /// /// See also: /// /// * [SimpleDialog], which handles the scrolling of the contents but has no [actions]. /// * [Dialog], on which [AlertDialog] and [SimpleDialog] are based. /// * [showDialog], which actually displays the dialog and returns its result. /// * class CustomAlertDialog extends StatelessWidget { /// Creates an alert dialog. /// /// Typically used in conjunction with [showDialog]. /// /// 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. const CustomAlertDialog({ Key key, this.title, this.titlePadding, this.content, this.contentPadding: const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0), this.actions, this.semanticLabel, }) : assert(contentPadding != null), super(key: key); /// 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. /// /// 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]. final EdgeInsetsGeometry titlePadding; /// The (optional) content of the dialog is displayed in the center of the /// dialog in a lighter font. /// /// Typically, this is a [ListView] containing the contents of the dialog. /// Using a [ListView] ensures that the contents can scroll if they are too /// big to fit on the display. final Widget content; /// Padding around the content. /// /// 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. final EdgeInsetsGeometry contentPadding; /// The (optional) set of actions that are displayed at the bottom of the /// dialog. /// /// Typically this is a list of [FlatButton] widgets. /// /// 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]. final List actions; /// The semantic label of the dialog used by accessibility frameworks to /// announce screen transitions when the dialog is opened and closed. /// /// If this label is not provided, a semantic label will be infered from the /// [title] if it is not null. If there is no title, the label will be taken /// from [MaterialLocalizations.alertDialogLabel]. /// /// See also: /// /// * [SemanticsConfiguration.isRouteName], for a description of how this /// value is used. final String semanticLabel; @override Widget build(BuildContext context) { final List children = []; String label = semanticLabel; if (title != null) { children.add(new Padding( padding: titlePadding ?? new EdgeInsets.fromLTRB( 24.0, 24.0, 24.0, content == null ? 20.0 : 0.0), child: new DefaultTextStyle( style: Theme.of(context).textTheme.title, child: new Semantics(child: title, namesRoute: true), ), )); } else { switch (defaultTargetPlatform) { case TargetPlatform.iOS: label = semanticLabel; break; case TargetPlatform.android: case TargetPlatform.fuchsia: label = semanticLabel ?? MaterialLocalizations.of(context)?.alertDialogLabel; } } if (content != null) { children.add(new Flexible( child: new Padding( padding: contentPadding, child: new DefaultTextStyle( style: Theme.of(context).textTheme.subhead, child: content, ), ), )); } if (actions != null) { children.add(new ButtonTheme.bar( child: new ButtonBar( children: actions, ), )); } Widget dialogChild = new IntrinsicWidth( child: new Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: children, ), ); if (label != null) dialogChild = new Semantics(namesRoute: true, label: label, child: dialogChild); return new Dialog(child: dialogChild); } } /// 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. /// /// 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. /// /// ## Sample code /// /// ```dart /// new SimpleDialogOption( /// onPressed: () { Navigator.pop(context, Department.treasury); }, /// child: const Text('Treasury department'), /// ) /// ``` /// /// See also: /// /// * [SimpleDialog], for a dialog in which to use this widget. /// * [showDialog], which actually displays the dialog and returns its result. /// * [FlatButton], which are commonly used as actions in other kinds of /// dialogs, such as [AlertDialog]s. /// * class SimpleDialogOption extends StatelessWidget { /// Creates an option for a [SimpleDialog]. const SimpleDialogOption({ Key key, this.onPressed, 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. /// /// When used in a [SimpleDialog], this will typically call [Navigator.pop] /// with a value for [showDialog] to complete its future with. final VoidCallback onPressed; /// The widget below this widget in the tree. /// /// Typically a [Text] widget. final Widget child; @override Widget build(BuildContext context) { return new InkWell( onTap: onPressed, child: new Padding( padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 24.0), child: child), ); } } /// 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. /// /// 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. /// /// 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. /// /// ## Sample code /// /// 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 /// Future _askedToLead() async { /// switch (await showDialog( /// context: context, /// builder: (BuildContext context) { /// return new SimpleDialog( /// title: const Text('Select assignment'), /// children: [ /// new SimpleDialogOption( /// onPressed: () { Navigator.pop(context, Department.treasury); }, /// child: const Text('Treasury department'), /// ), /// new SimpleDialogOption( /// onPressed: () { Navigator.pop(context, Department.state); }, /// child: const Text('State department'), /// ), /// ], /// ); /// } /// )) { /// case Department.treasury: /// // Let's go. /// // ... /// break; /// case Department.state: /// // ... /// break; /// } /// } /// ``` /// /// See also: /// /// * [SimpleDialogOption], which are options used in this type of dialog. /// * [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. /// * class SimpleDialog extends StatelessWidget { /// Creates a simple dialog. /// /// Typically used in conjunction with [showDialog]. /// /// The [titlePadding] and [contentPadding] arguments must not be null. const SimpleDialog({ Key key, this.title, this.titlePadding: const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0), this.children, this.contentPadding: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0), this.semanticLabel, }) : assert(titlePadding != null), assert(contentPadding != null), super(key: key); /// 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. /// /// 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]. final EdgeInsetsGeometry titlePadding; /// The (optional) content of the dialog is displayed in a /// [SingleChildScrollView] underneath the title. /// /// Typically a list of [SimpleDialogOption]s. final List children; /// Padding around the content. /// /// 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. final EdgeInsetsGeometry contentPadding; /// The semantic label of the dialog used by accessibility frameworks to /// announce screen transitions when the dialog is opened and closed. /// /// If this label is not provided, a semantic label will be infered from the /// [title] if it is not null. If there is no title, the label will be taken /// from [MaterialLocalizations.dialogLabel]. /// /// See also: /// /// * [SemanticsConfiguration.isRouteName], for a description of how this /// value is used. final String semanticLabel; @override Widget build(BuildContext context) { final List body = []; String label = semanticLabel; if (title != null) { body.add(new Padding( padding: titlePadding, child: new DefaultTextStyle( style: Theme.of(context).textTheme.title, child: new Semantics(namesRoute: true, child: title), ))); } else { switch (defaultTargetPlatform) { case TargetPlatform.iOS: label = semanticLabel; break; case TargetPlatform.android: case TargetPlatform.fuchsia: label = semanticLabel ?? MaterialLocalizations.of(context)?.dialogLabel; } } if (children != null) { body.add(new Flexible( child: new SingleChildScrollView( padding: contentPadding, child: new ListBody(children: children), ))); } Widget dialogChild = new IntrinsicWidth( stepWidth: 56.0, child: new ConstrainedBox( constraints: const BoxConstraints(minWidth: 280.0), child: new Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: body, ), ), ); if (label != null) dialogChild = new Semantics( namesRoute: true, label: label, child: dialogChild, ); return new Dialog(child: dialogChild); } } class _DialogRoute extends PopupRoute { _DialogRoute({ @required this.theme, bool barrierDismissible: true, this.barrierLabel, @required this.child, RouteSettings settings, }) : assert(barrierDismissible != null), _barrierDismissible = barrierDismissible, super(settings: settings); final Widget child; final ThemeData theme; @override Duration get transitionDuration => const Duration(milliseconds: 150); @override bool get barrierDismissible => _barrierDismissible; final bool _barrierDismissible; @override Color get barrierColor => Colors.black54; @override final String barrierLabel; @override Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { return new SafeArea( child: new Builder(builder: (BuildContext context) { final Widget annotatedChild = new Semantics( child: child, scopesRoute: true, explicitChildNodes: true, ); return theme != null ? new Theme(data: theme, child: annotatedChild) : annotatedChild; }), ); } @override Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { return new FadeTransition( opacity: new CurvedAnimation(parent: animation, curve: Curves.easeOut), child: child); } } /// Displays a dialog above the current contents of the app. /// /// This function takes a `builder` which typically builds a [Dialog] widget. /// Content below the dialog is dimmed with a [ModalBarrier]. This widget 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. /// /// 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. /// /// The `child` argument is deprecated, and should be replaced with `builder`. /// /// Returns a [Future] that resolves to the value (if any) that was passed to /// [Navigator.pop] when the dialog was closed. /// /// The dialog route created by this method is pushed to the root navigator. /// If the application has multiple [Navigator] objects, it may be necessary to /// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the /// dialog rather just 'Navigator.pop(context, result)`. /// /// See also: /// * [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. /// * [Dialog], on which [SimpleDialog] and [AlertDialog] are based. /// * Future customShowDialog({ @required BuildContext context, bool barrierDismissible: true, @Deprecated( 'Instead of using the "child" argument, return the child from a closure ' 'provided to the "builder" argument. This will ensure that the BuildContext ' 'is appropriate for widgets built in the dialog.') Widget child, WidgetBuilder builder, }) { assert(child == null || builder == null); return Navigator.of(context, rootNavigator: true).push(new _DialogRoute( child: child ?? new Builder(builder: builder), theme: Theme.of(context, shadowThemeOnly: true), barrierDismissible: barrierDismissible, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, )); }