dialog.dart 14.3 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:async';

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

10
import 'button.dart';
11
import 'button_bar.dart';
12
import 'colors.dart';
13
import 'ink_well.dart';
14
import 'material.dart';
15
import 'theme.dart';
16

17 18 19
// Examples can assume:
// enum Department { treasury, state }

20
/// A material design dialog.
21
///
22 23 24 25
/// 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.
26 27
///
/// See also:
Ian Hickson's avatar
Ian Hickson committed
28
///
29 30 31
///  * [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.
32
///  * <https://material.google.com/components/dialogs.html>
33
class Dialog extends StatelessWidget {
34 35 36
  /// Creates a dialog.
  ///
  /// Typically used in conjunction with [showDialog].
37
  const Dialog({
38 39 40 41
    Key key,
    this.child,
  }) : super(key: key);

42
  /// The widget below this widget in the tree.
43 44 45
  final Widget child;

  Color _getColor(BuildContext context) {
46
    return Theme.of(context).dialogBackgroundColor;
47 48 49 50 51 52
  }

  @override
  Widget build(BuildContext context) {
    return new Center(
      child: new Container(
53
        margin: const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0),
54
        child: new ConstrainedBox(
55
          constraints: const BoxConstraints(minWidth: 280.0),
56
          child: new Material(
57
            elevation: 24.0,
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
            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
77
/// using a scrolling widget, such as [ListView], for [content] to avoid
78
/// overflow.
79 80 81 82 83 84 85
///
/// 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.
///
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
/// ## 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<Null> _neverSatisfied() async {
///   return showDialog<Null>(
///     context: context,
///     barrierDismissible: false, // user must tap button!
///     child: new AlertDialog(
///       title: new Text('Rewind and remember'),
///       content: new SingleChildScrollView(
///         child: new ListBody(
///           children: <Widget>[
///             new Text('You will never be satisfied.'),
///             new Text('You\’re like me. I’m never satisfied.'),
///           ],
///         ),
///       ),
///       actions: <Widget>[
///         new FlatButton(
///           child: new Text('Regret'),
///           onPressed: () {
///             Navigator.of(context).pop();
///           },
///         ),
///       ],
///     ),
///   );
/// }
/// ```
///
119 120
/// See also:
///
121 122 123
///  * [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.
124 125 126 127 128
///  * <https://material.google.com/components/dialogs.html#dialogs-alerts>
class AlertDialog extends StatelessWidget {
  /// Creates an alert dialog.
  ///
  /// Typically used in conjunction with [showDialog].
129
  const AlertDialog({
130
    Key key,
131
    this.title,
132
    this.titlePadding,
133
    this.content,
134
    this.contentPadding,
135
    this.actions
136
  }) : super(key: key);
137 138 139

  /// 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
140 141
  ///
  /// Typically a [Text] widget.
142 143
  final Widget title;

144 145 146 147
  /// Padding around the title.
  ///
  /// Uses material design default if none is supplied. If there is no title, no
  /// padding will be provided.
148
  final EdgeInsetsGeometry titlePadding;
149

150 151
  /// 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
152
  ///
153 154
  /// Typically, this is a [ListView] containing the contents of the dialog.
  /// Using a [ListView] ensures that the contents can scroll if they are too
155
  /// big to fit on the display.
156 157
  final Widget content;

158 159 160
  /// Padding around the content.
  ///
  /// Uses material design default if none is supplied.
161
  final EdgeInsetsGeometry contentPadding;
162

163 164
  /// The (optional) set of actions that are displayed at the bottom of the
  /// dialog.
Ian Hickson's avatar
Ian Hickson committed
165 166 167 168
  ///
  /// Typically this is a list of [FlatButton] widgets.
  ///
  /// These widgets will be wrapped in a [ButtonBar].
169 170
  final List<Widget> actions;

171
  @override
172
  Widget build(BuildContext context) {
173
    final List<Widget> children = <Widget>[];
174 175

    if (title != null) {
176
      children.add(new Padding(
177
        padding: titlePadding ?? new EdgeInsetsDirectional.fromSTEB(24.0, 24.0, 24.0, content == null ? 20.0 : 0.0),
178
        child: new DefaultTextStyle(
179
          style: Theme.of(context).textTheme.title,
180 181
          child: title,
        ),
182 183 184 185
      ));
    }

    if (content != null) {
186
      children.add(new Flexible(
187
        child: new Padding(
188
          padding: contentPadding ?? const EdgeInsetsDirectional.fromSTEB(24.0, 20.0, 24.0, 24.0),
189 190
          child: new DefaultTextStyle(
            style: Theme.of(context).textTheme.subhead,
191 192 193
            child: content,
          ),
        ),
194 195 196
      ));
    }

197
    if (actions != null) {
198
      children.add(new ButtonTheme.bar(
199
        child: new ButtonBar(
200 201
          children: actions,
        ),
202
      ));
203
    }
204

205 206 207 208 209
    return new Dialog(
      child: new IntrinsicWidth(
        child: new Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
210 211 212
          children: children,
        ),
      ),
213 214 215 216
    );
  }
}

217 218 219 220 221 222 223
/// 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.
///
224 225 226 227 228 229 230 231 232
/// ## Sample code
///
/// ```dart
/// new SimpleDialogOption(
///   onPressed: () { Navigator.pop(context, Department.treasury); },
///   child: const Text('Treasury department'),
/// )
/// ```
///
233 234 235 236 237 238 239 240 241
/// 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.
///  * <https://material.google.com/components/dialogs.html#dialogs-simple-dialogs>
class SimpleDialogOption extends StatelessWidget {
  /// Creates an option for a [SimpleDialog].
242
  const SimpleDialogOption({
243 244 245 246 247 248 249 250
    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.
251 252 253
  ///
  /// When used in a [SimpleDialog], this will typically call [Navigator.pop]
  /// with a value for [showDialog] to complete its future with.
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
  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
      ),
    );
  }
}

273 274 275 276 277 278 279 280 281 282 283
/// 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.
///
/// 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.
///
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
/// ## 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<Null> _askedToLead() async {
///   switch (await showDialog<Department>(
///     context: context,
///     child: new SimpleDialog(
///       title: const Text('Select assignment'),
///       children: <Widget>[
///         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;
///   }
/// }
/// ```
///
326 327
/// See also:
///
328
///  * [SimpleDialogOption], which are options used in this type of dialog.
329 330 331
///  * [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.
332 333 334 335 336
///  * <https://material.google.com/components/dialogs.html#dialogs-simple-dialogs>
class SimpleDialog extends StatelessWidget {
  /// Creates a simple dialog.
  ///
  /// Typically used in conjunction with [showDialog].
337
  const SimpleDialog({
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
    Key key,
    this.title,
    this.titlePadding,
    this.children,
    this.contentPadding,
  }) : 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.
  ///
  /// Uses material design default if none is supplied. If there is no title, no
  /// padding will be provided.
355
  final EdgeInsetsGeometry titlePadding;
356

357 358
  /// The (optional) content of the dialog is displayed in a
  /// [SingleChildScrollView] underneath the title.
359
  ///
360
  /// Typically a list of [SimpleDialogOption]s.
361 362 363 364 365
  final List<Widget> children;

  /// Padding around the content.
  ///
  /// Uses material design default if none is supplied.
366
  final EdgeInsetsGeometry contentPadding;
367 368 369 370 371 372 373

  @override
  Widget build(BuildContext context) {
    final List<Widget> body = <Widget>[];

    if (title != null) {
      body.add(new Padding(
374
        padding: titlePadding ?? const EdgeInsetsDirectional.fromSTEB(24.0, 24.0, 24.0, 0.0),
375 376 377 378 379 380 381 382 383
        child: new DefaultTextStyle(
          style: Theme.of(context).textTheme.title,
          child: title
        )
      ));
    }

    if (children != null) {
      body.add(new Flexible(
384
        child: new SingleChildScrollView(
385
          padding: contentPadding ?? const EdgeInsetsDirectional.fromSTEB(0.0, 12.0, 0.0, 16.0),
386
          child: new ListBody(children: children),
387 388 389 390 391 392 393
        )
      ));
    }

    return new Dialog(
      child: new IntrinsicWidth(
        stepWidth: 56.0,
394
        child: new ConstrainedBox(
395 396 397 398
          constraints: const BoxConstraints(minWidth: 280.0),
          child: new Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.stretch,
399
            children: body,
400 401 402
          )
        )
      )
403
    );
404 405
  }
}
406

Hixie's avatar
Hixie committed
407 408
class _DialogRoute<T> extends PopupRoute<T> {
  _DialogRoute({
409
    @required this.theme,
410
    bool barrierDismissible: true,
411
    @required this.child,
412 413
  }) : assert(barrierDismissible != null),
       _barrierDismissible = barrierDismissible;
414

Adam Barth's avatar
Adam Barth committed
415
  final Widget child;
416
  final ThemeData theme;
417

418
  @override
419
  Duration get transitionDuration => const Duration(milliseconds: 150);
420 421

  @override
422 423
  bool get barrierDismissible => _barrierDismissible;
  final bool _barrierDismissible;
424 425

  @override
426 427
  Color get barrierColor => Colors.black54;

428
  @override
429
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
430
    return theme != null ? new Theme(data: theme, child: child) : child;
431
  }
432

433
  @override
434
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
435 436 437 438 439
    return new FadeTransition(
      opacity: new CurvedAnimation(
        parent: animation,
        curve: Curves.easeOut
      ),
440 441
      child: child
    );
442 443 444
  }
}

445 446 447 448 449
/// Displays a dialog above the current contents of the app.
///
/// This function typically receives a [Dialog] widget as its child argument.
/// Content below the dialog is dimmed with a [ModalBarrier].
///
450
/// Returns a [Future] that resolves to the value (if any) that was passed to
451 452 453
/// [Navigator.pop] when the dialog was closed.
///
/// See also:
454 455 456
///  * [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.
457
///  * [Dialog], on which [SimpleDialog] and [AlertDialog] are based.
458
///  * <https://material.google.com/components/dialogs.html>
459
Future<T> showDialog<T>({
460
  @required BuildContext context,
461
  bool barrierDismissible: true,
462
  @required Widget child,
463
}) {
464
  return Navigator.of(context, rootNavigator: true).push(new _DialogRoute<T>(
465 466
    child: child,
    theme: Theme.of(context, shadowThemeOnly: true),
467
    barrierDismissible: barrierDismissible,
468
  ));
469
}