scaffold.dart 41.6 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.

Hixie's avatar
Hixie committed
5 6
import 'dart:async';
import 'dart:collection';
7
import 'dart:math' as math;
8

9
import 'package:flutter/foundation.dart';
10
import 'package:flutter/widgets.dart';
11

12
import 'app_bar.dart';
13
import 'bottom_sheet.dart';
14
import 'button.dart';
15
import 'button_bar.dart';
16
import 'drawer.dart';
17
import 'flexible_space_bar.dart';
18
import 'material.dart';
Hixie's avatar
Hixie committed
19
import 'snack_bar.dart';
20
import 'theme.dart';
21

22
const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent
23 24
const Duration _kFloatingActionButtonSegue = const Duration(milliseconds: 200);
final Tween<double> _kFloatingActionButtonTurnTween = new Tween<double>(begin: -0.125, end: 0.0);
25

26
enum _ScaffoldSlot {
27
  body,
28
  appBar,
29 30
  bottomSheet,
  snackBar,
31
  persistentFooter,
32
  bottomNavigationBar,
33 34
  floatingActionButton,
  drawer,
35
  endDrawer,
36
  statusBar,
37
}
Hans Muller's avatar
Hans Muller committed
38

39
class _ScaffoldLayout extends MultiChildLayoutDelegate {
40
  _ScaffoldLayout({
41
    @required this.statusBarHeight,
42
    @required this.bottomViewInset,
Ian Hickson's avatar
Ian Hickson committed
43
    @required this.endPadding, // for floating action button
44
    @required this.textDirection,
45
  });
46

47
  final double statusBarHeight;
48
  final double bottomViewInset;
Ian Hickson's avatar
Ian Hickson committed
49
  final double endPadding;
50
  final TextDirection textDirection;
51

52
  @override
53
  void performLayout(Size size) {
54
    final BoxConstraints looseConstraints = new BoxConstraints.loose(size);
55

56
    // This part of the layout has the same effect as putting the app bar and
57
    // body in a column and making the body flexible. What's different is that
58
    // in this case the app bar appears _after_ the body in the stacking order,
59
    // so the app bar's shadow is drawn on top of the body.
60

61
    final BoxConstraints fullWidthConstraints = looseConstraints.tighten(width: size.width);
62
    final double bottom = math.max(0.0, size.height - bottomViewInset);
63
    double contentTop = 0.0;
64
    double contentBottom = bottom;
65

66
    if (hasChild(_ScaffoldSlot.appBar)) {
67
      contentTop = layoutChild(_ScaffoldSlot.appBar, fullWidthConstraints).height;
68
      positionChild(_ScaffoldSlot.appBar, Offset.zero);
69 70
    }

71 72 73 74 75 76
    if (hasChild(_ScaffoldSlot.bottomNavigationBar)) {
      final double bottomNavigationBarHeight = layoutChild(_ScaffoldSlot.bottomNavigationBar, fullWidthConstraints).height;
      contentBottom -= bottomNavigationBarHeight;
      positionChild(_ScaffoldSlot.bottomNavigationBar, new Offset(0.0, contentBottom));
    }

77
    if (hasChild(_ScaffoldSlot.persistentFooter)) {
Ian Hickson's avatar
Ian Hickson committed
78 79 80 81 82
      final BoxConstraints footerConstraints = new BoxConstraints(
        maxWidth: fullWidthConstraints.maxWidth,
        maxHeight: math.max(0.0, contentBottom - contentTop),
      );
      final double persistentFooterHeight = layoutChild(_ScaffoldSlot.persistentFooter, footerConstraints).height;
83 84 85 86
      contentBottom -= persistentFooterHeight;
      positionChild(_ScaffoldSlot.persistentFooter, new Offset(0.0, contentBottom));
    }

87
    if (hasChild(_ScaffoldSlot.body)) {
88 89
      final BoxConstraints bodyConstraints = new BoxConstraints(
        maxWidth: fullWidthConstraints.maxWidth,
90
        maxHeight: math.max(0.0, contentBottom - contentTop),
91
      );
92
      layoutChild(_ScaffoldSlot.body, bodyConstraints);
93
      positionChild(_ScaffoldSlot.body, new Offset(0.0, contentTop));
94
    }
95 96

    // The BottomSheet and the SnackBar are anchored to the bottom of the parent,
97 98 99 100
    // they're as wide as the parent and are given their intrinsic height. The
    // only difference is that SnackBar appears on the top side of the
    // BottomNavigationBar while the BottomSheet is stacked on top of it.
    //
101 102 103 104 105 106 107
    // If all three elements are present then either the center of the FAB straddles
    // the top edge of the BottomSheet or the bottom of the FAB is
    // _kFloatingActionButtonMargin above the SnackBar, whichever puts the FAB
    // the farthest above the bottom of the parent. If only the FAB is has a
    // non-zero height then it's inset from the parent's right and bottom edges
    // by _kFloatingActionButtonMargin.

108 109 110
    Size bottomSheetSize = Size.zero;
    Size snackBarSize = Size.zero;

111
    if (hasChild(_ScaffoldSlot.bottomSheet)) {
Ian Hickson's avatar
Ian Hickson committed
112 113 114 115 116
      final BoxConstraints bottomSheetConstraints = new BoxConstraints(
        maxWidth: fullWidthConstraints.maxWidth,
        maxHeight: math.max(0.0, contentBottom - contentTop),
      );
      bottomSheetSize = layoutChild(_ScaffoldSlot.bottomSheet, bottomSheetConstraints);
117
      positionChild(_ScaffoldSlot.bottomSheet, new Offset((size.width - bottomSheetSize.width) / 2.0, bottom - bottomSheetSize.height));
118 119
    }

120
    if (hasChild(_ScaffoldSlot.snackBar)) {
121
      snackBarSize = layoutChild(_ScaffoldSlot.snackBar, fullWidthConstraints);
122
      positionChild(_ScaffoldSlot.snackBar, new Offset(0.0, contentBottom - snackBarSize.height));
123 124
    }

125
    if (hasChild(_ScaffoldSlot.floatingActionButton)) {
126
      final Size fabSize = layoutChild(_ScaffoldSlot.floatingActionButton, looseConstraints);
127 128 129 130
      double fabX;
      assert(textDirection != null);
      switch (textDirection) {
        case TextDirection.rtl:
Ian Hickson's avatar
Ian Hickson committed
131
          fabX = _kFloatingActionButtonMargin + endPadding;
132 133
          break;
        case TextDirection.ltr:
Ian Hickson's avatar
Ian Hickson committed
134
          fabX = size.width - fabSize.width - _kFloatingActionButtonMargin - endPadding;
135 136
          break;
      }
137
      double fabY = contentBottom - fabSize.height - _kFloatingActionButtonMargin;
138
      if (snackBarSize.height > 0.0)
139
        fabY = math.min(fabY, contentBottom - snackBarSize.height - fabSize.height - _kFloatingActionButtonMargin);
140
      if (bottomSheetSize.height > 0.0)
141
        fabY = math.min(fabY, contentBottom - bottomSheetSize.height - fabSize.height / 2.0);
142
      positionChild(_ScaffoldSlot.floatingActionButton, new Offset(fabX, fabY));
143
    }
144

145 146 147 148 149
    if (hasChild(_ScaffoldSlot.statusBar)) {
      layoutChild(_ScaffoldSlot.statusBar, fullWidthConstraints.tighten(height: statusBarHeight));
      positionChild(_ScaffoldSlot.statusBar, Offset.zero);
    }

150
    if (hasChild(_ScaffoldSlot.drawer)) {
151 152
      layoutChild(_ScaffoldSlot.drawer, new BoxConstraints.tight(size));
      positionChild(_ScaffoldSlot.drawer, Offset.zero);
153
    }
154 155 156 157 158

    if (hasChild(_ScaffoldSlot.endDrawer)) {
      layoutChild(_ScaffoldSlot.endDrawer, new BoxConstraints.tight(size));
      positionChild(_ScaffoldSlot.endDrawer, Offset.zero);
    }
Hans Muller's avatar
Hans Muller committed
159
  }
160

161
  @override
162
  bool shouldRelayout(_ScaffoldLayout oldDelegate) {
Ian Hickson's avatar
Ian Hickson committed
163
    return oldDelegate.statusBarHeight != statusBarHeight
164
        || oldDelegate.bottomViewInset != bottomViewInset
Ian Hickson's avatar
Ian Hickson committed
165
        || oldDelegate.endPadding != endPadding
166
        || oldDelegate.textDirection != textDirection;
167
  }
Hans Muller's avatar
Hans Muller committed
168 169
}

170
class _FloatingActionButtonTransition extends StatefulWidget {
171
  const _FloatingActionButtonTransition({
172
    Key key,
173
    this.child,
174
  }) : super(key: key);
175 176 177

  final Widget child;

178
  @override
179 180 181
  _FloatingActionButtonTransitionState createState() => new _FloatingActionButtonTransitionState();
}

182 183 184
class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTransition> with TickerProviderStateMixin {
  AnimationController _previousController;
  AnimationController _currentController;
185 186 187
  CurvedAnimation _previousAnimation;
  CurvedAnimation _currentAnimation;
  Widget _previousChild;
188

189
  @override
190 191
  void initState() {
    super.initState();
192

193 194 195 196
    _previousController = new AnimationController(
      duration: _kFloatingActionButtonSegue,
      vsync: this,
    )..addStatusListener(_handleAnimationStatusChanged);
197 198 199 200
    _previousAnimation = new CurvedAnimation(
      parent: _previousController,
      curve: Curves.easeIn
    );
201 202 203 204 205

    _currentController = new AnimationController(
      duration: _kFloatingActionButtonSegue,
      vsync: this,
    );
206 207 208 209 210
    _currentAnimation = new CurvedAnimation(
      parent: _currentController,
      curve: Curves.easeIn
    );

211 212
    // If we start out with a child, have the child appear fully visible instead
    // of animating in.
213
    if (widget.child != null)
214
      _currentController.value = 1.0;
215 216
  }

217
  @override
218
  void dispose() {
219 220
    _previousController.dispose();
    _currentController.dispose();
221 222 223
    super.dispose();
  }

224
  @override
225
  void didUpdateWidget(_FloatingActionButtonTransition oldWidget) {
226
    super.didUpdateWidget(oldWidget);
227 228 229
    final bool oldChildIsNull = oldWidget.child == null;
    final bool newChildIsNull = widget.child == null;
    if (oldChildIsNull == newChildIsNull && oldWidget.child?.key == widget.child?.key)
230
      return;
231 232
    if (_previousController.status == AnimationStatus.dismissed) {
      final double currentValue = _currentController.value;
233
      if (currentValue == 0.0 || oldWidget.child == null) {
234 235 236
        // The current child hasn't started its entrance animation yet. We can
        // just skip directly to the new child's entrance.
        _previousChild = null;
237
        if (widget.child != null)
238 239 240 241 242
          _currentController.forward();
      } else {
        // Otherwise, we need to copy the state from the current controller to
        // the previous controller and run an exit animation for the previous
        // widget before running the entrance animation for the new child.
243
        _previousChild = oldWidget.child;
244 245 246 247 248 249 250 251 252 253 254 255
        _previousController
          ..value = currentValue
          ..reverse();
        _currentController.value = 0.0;
      }
    }
  }

  void _handleAnimationStatusChanged(AnimationStatus status) {
    setState(() {
      if (status == AnimationStatus.dismissed) {
        assert(_currentController.status == AnimationStatus.dismissed);
256
        if (widget.child != null)
257 258 259
          _currentController.forward();
      }
    });
260 261
  }

262
  @override
263
  Widget build(BuildContext context) {
264
    final List<Widget> children = <Widget>[];
265
    if (_previousAnimation.status != AnimationStatus.dismissed) {
266
      children.add(new ScaleTransition(
267
        scale: _previousAnimation,
268
        child: _previousChild,
269 270 271 272 273 274 275
      ));
    }
    if (_currentAnimation.status != AnimationStatus.dismissed) {
      children.add(new ScaleTransition(
        scale: _currentAnimation,
        child: new RotationTransition(
          turns: _kFloatingActionButtonTurnTween.animate(_currentAnimation),
276
          child: widget.child,
277
        )
278 279 280 281 282 283
      ));
    }
    return new Stack(children: children);
  }
}

284 285
/// Implements the basic material design visual layout structure.
///
286
/// This class provides APIs for showing drawers, snack bars, and bottom sheets.
287
///
288 289 290 291 292 293
/// To display a snackbar or a persistent bottom sheet, obtain the
/// [ScaffoldState] for the current [BuildContext] via [Scaffold.of] and use the
/// [ScaffoldState.showSnackBar] and [ScaffoldState.showBottomSheet] functions.
///
/// See also:
///
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
///  * [AppBar], which is a horizontal bar typically shown at the top of an app
///    using the [appBar] property.
///  * [FloatingActionButton], which is a circular button typically shown in the
///    bottom right corner of the app using the [floatingActionButton] property.
///  * [Drawer], which is a vertical panel that is typically displayed to the
///    left of the body (and often hidden on phones) using the [drawer]
///    property.
///  * [BottomNavigationBar], which is a horizontal array of buttons typically
///    shown along the bottom of the app using the [bottomNavigationBar]
///    property.
///  * [SnackBar], which is a temporary notification typically shown near the
///    bottom of the app using the [ScaffoldState.showSnackBar] method.
///  * [BottomSheet], which is an overlay typically shown near the bottom of the
///    app. A bottom sheet can either be persistent, in which case it is shown
///    using the [ScaffoldState.showBottomSheet] method, or modal, in which case
///    it is shown using the [showModalBottomSheet] function.
///  * [ScaffoldState], which is the state associated with this widget.
311
///  * <https://material.google.com/layout/structure.html>
312
class Scaffold extends StatefulWidget {
313
  /// Creates a visual scaffold for material design widgets.
314
  const Scaffold({
Adam Barth's avatar
Adam Barth committed
315
    Key key,
316
    this.appBar,
Hixie's avatar
Hixie committed
317
    this.body,
318
    this.floatingActionButton,
319
    this.persistentFooterButtons,
320
    this.drawer,
321
    this.endDrawer,
322
    this.bottomNavigationBar,
323
    this.backgroundColor,
324 325 326
    this.resizeToAvoidBottomPadding: true,
    this.primary: true,
  }) : assert(primary != null), super(key: key);
Adam Barth's avatar
Adam Barth committed
327

328
  /// An app bar to display at the top of the scaffold.
329
  final PreferredSizeWidget appBar;
330 331 332 333 334

  /// The primary content of the scaffold.
  ///
  /// Displayed below the app bar and behind the [floatingActionButton] and
  /// [drawer]. To avoid the body being resized to avoid the window padding
335
  /// (e.g., from the onscreen keyboard), see [resizeToAvoidBottomPadding].
336
  ///
337 338 339
  /// The widget in the body of the scaffold is positioned at the top-left of
  /// the available space between the app bar and the bottom of the scaffold. To
  /// center this widget instead, consider putting it in a [Center] widget and
340 341
  /// having that be the body. To expand this widget instead, consider
  /// putting it in a [SizedBox.expand].
342 343 344
  ///
  /// If you have a column of widgets that should normally fit on the screen,
  /// but may overflow and would in such cases need to scroll, consider using a
345
  /// [ListView] as the body of the scaffold. This is also a good choice for
346
  /// the case where your body is a scrollable list.
Hixie's avatar
Hixie committed
347
  final Widget body;
348

349
  /// A button displayed floating above [body], in the bottom right corner.
350 351
  ///
  /// Typically a [FloatingActionButton].
Adam Barth's avatar
Adam Barth committed
352
  final Widget floatingActionButton;
353

354 355 356
  /// A set of buttons that are displayed at the bottom of the scaffold.
  ///
  /// Typically this is a list of [FlatButton] widgets. These buttons are
Ian Hickson's avatar
Ian Hickson committed
357
  /// persistently visible, even if the [body] of the scaffold scrolls.
358 359 360
  ///
  /// These widgets will be wrapped in a [ButtonBar].
  ///
Ian Hickson's avatar
Ian Hickson committed
361 362 363
  /// The [persistentFooterButtons] are rendered above the
  /// [bottomNavigationBar] but below the [body].
  ///
364 365 366 367 368
  /// See also:
  ///
  ///  * <https://material.google.com/components/buttons.html#buttons-persistent-footer-buttons>
  final List<Widget> persistentFooterButtons;

369
  /// A panel displayed to the side of the [body], often hidden on mobile
370 371
  /// devices. Swipes in from either left-to-right ([TextDirection.ltr]) or
  /// right-to-left ([TextDirection.rtl])
372
  ///
373 374
  /// In the uncommon case that you wish to open the drawer manually, use the
  /// [ScaffoldState.openDrawer] function.
375 376
  ///
  /// Typically a [Drawer].
377
  final Widget drawer;
378

379 380 381 382 383 384 385 386 387 388
  /// A panel displayed to the side of the [body], often hidden on mobile
  /// devices. Swipes in from right-to-left ([TextDirection.ltr]) or
  /// left-to-right ([TextDirection.rtl])
  ///
  /// In the uncommon case that you wish to open the drawer manually, use the
  /// [ScaffoldState.openDrawer] function.
  ///
  /// Typically a [Drawer].
  final Widget endDrawer;

389 390 391 392 393
  /// The color of the [Material] widget that underlies the entire Scaffold.
  ///
  /// The theme's [ThemeData.scaffoldBackgroundColor] by default.
  final Color backgroundColor;

394 395
  /// A bottom navigation bar to display at the bottom of the scaffold.
  ///
396 397
  /// Snack bars slide from underneath the bottom navigation bar while bottom
  /// sheets are stacked on top.
Ian Hickson's avatar
Ian Hickson committed
398 399 400
  ///
  /// The [bottomNavigationBar] is rendered below the [persistentFooterButtons]
  /// and the [body].
401 402
  final Widget bottomNavigationBar;

403 404
  /// Whether the [body] (and other floating widgets) should size themselves to
  /// avoid the window's bottom padding.
405 406 407 408 409 410
  ///
  /// For example, if there is an onscreen keyboard displayed above the
  /// scaffold, the body can be resized to avoid overlapping the keyboard, which
  /// prevents widgets inside the body from being obscured by the keyboard.
  ///
  /// Defaults to true.
411
  final bool resizeToAvoidBottomPadding;
412

413 414 415 416 417 418 419 420 421
  /// Whether this scaffold is being displayed at the top of the screen.
  ///
  /// If true then the height of the [appBar] will be extended by the height
  /// of the screen's status bar, i.e. the top padding for [MediaQuery].
  ///
  /// The default value of this property, like the default value of
  /// [AppBar.primary], is true.
  final bool primary;

422
  /// The state from the closest instance of this class that encloses the given context.
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
  /// @override
  /// Widget build(BuildContext context) {
  ///   return new RaisedButton(
  ///     child: new Text('SHOW A SNACKBAR'),
  ///     onPressed: () {
  ///       Scaffold.of(context).showSnackBar(new SnackBar(
  ///         content: new Text('Hello!'),
  ///       ));
  ///     },
  ///   );
  /// }
  /// ```
  ///
  /// When the [Scaffold] is actually created in the same `build` function, the
  /// `context` argument to the `build` function can't be used to find the
  /// [Scaffold] (since it's "above" the widget being returned). In such cases,
  /// the following technique with a [Builder] can be used to provide a new
  /// scope with a [BuildContext] that is "under" the [Scaffold]:
  ///
  /// ```dart
  /// @override
  /// Widget build(BuildContext context) {
  ///   return new Scaffold(
  ///     appBar: new AppBar(
  ///       title: new Text('Demo')
  ///     ),
  ///     body: new Builder(
  ///       // Create an inner BuildContext so that the onPressed methods
  ///       // can refer to the Scaffold with Scaffold.of().
  ///       builder: (BuildContext context) {
  ///         return new Center(
  ///           child: new RaisedButton(
  ///             child: new Text('SHOW A SNACKBAR'),
  ///             onPressed: () {
  ///               Scaffold.of(context).showSnackBar(new SnackBar(
  ///                 content: new Text('Hello!'),
  ///               ));
  ///             },
  ///           ),
  ///         );
  ///       },
  ///     ),
  ///   );
  /// }
  /// ```
472
  ///
473 474 475 476 477 478 479 480 481 482
  /// A more efficient solution is to split your build function into several
  /// widgets. This introduces a new context from which you can obtain the
  /// [Scaffold]. In this solution, you would have an outer widget that creates
  /// the [Scaffold] populated by instances of your new inner widgets, and then
  /// in these inner widgets you would use [Scaffold.of].
  ///
  /// A less elegant but more expedient solution is assign a [GlobalKey] to the
  /// [Scaffold], then use the `key.currentState` property to obtain the
  /// [ScaffoldState] rather than using the [Scaffold.of] function.
  ///
483 484 485 486 487
  /// If there is no [Scaffold] in scope, then this will throw an exception.
  /// To return null if there is no [Scaffold], then pass `nullOk: true`.
  static ScaffoldState of(BuildContext context, { bool nullOk: false }) {
    assert(nullOk != null);
    assert(context != null);
488
    final ScaffoldState result = context.ancestorStateOfType(const TypeMatcher<ScaffoldState>());
489 490 491 492
    if (nullOk || result != null)
      return result;
    throw new FlutterError(
      'Scaffold.of() called with a context that does not contain a Scaffold.\n'
493
      'No Scaffold ancestor could be found starting from the context that was passed to Scaffold.of(). '
494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510
      'This usually happens when the context provided is from the same StatefulWidget as that '
      'whose build function actually creates the Scaffold widget being sought.\n'
      'There are several ways to avoid this problem. The simplest is to use a Builder to get a '
      'context that is "under" the Scaffold. For an example of this, please see the '
      'documentation for Scaffold.of():\n'
      '  https://docs.flutter.io/flutter/material/Scaffold/of.html\n'
      'A more efficient solution is to split your build function into several widgets. This '
      'introduces a new context from which you can obtain the Scaffold. In this solution, '
      'you would have an outer widget that creates the Scaffold populated by instances of '
      'your new inner widgets, and then in these inner widgets you would use Scaffold.of().\n'
      'A less elegant but more expedient solution is assign a GlobalKey to the Scaffold, '
      'then use the key.currentState property to obtain the ScaffoldState rather than '
      'using the Scaffold.of() function.\n'
      'The context used was:\n'
      '  $context'
    );
  }
Hixie's avatar
Hixie committed
511

512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527
  /// Whether the Scaffold that most tightly encloses the given context has a
  /// drawer.
  ///
  /// If this is being used during a build (for example to decide whether to
  /// show an "open drawer" button), set the `registerForUpdates` argument to
  /// true. This will then set up an [InheritedWidget] relationship with the
  /// [Scaffold] so that the client widget gets rebuilt whenever the [hasDrawer]
  /// value changes.
  ///
  /// See also:
  ///  * [Scaffold.of], which provides access to the [ScaffoldState] object as a
  ///    whole, from which you can show snackbars, bottom sheets, and so forth.
  static bool hasDrawer(BuildContext context, { bool registerForUpdates: true }) {
    assert(registerForUpdates != null);
    assert(context != null);
    if (registerForUpdates) {
528
      final _ScaffoldScope scaffold = context.inheritFromWidgetOfExactType(_ScaffoldScope);
529 530
      return scaffold?.hasDrawer ?? false;
    } else {
531
      final ScaffoldState scaffold = context.ancestorStateOfType(const TypeMatcher<ScaffoldState>());
532 533 534 535
      return scaffold?.hasDrawer ?? false;
    }
  }

536
  @override
Hixie's avatar
Hixie committed
537 538 539
  ScaffoldState createState() => new ScaffoldState();
}

540 541 542 543
/// State for a [Scaffold].
///
/// Can display [SnackBar]s and [BottomSheet]s. Retrieve a [ScaffoldState] from
/// the current [BuildContext] using [Scaffold.of].
544
class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
Hixie's avatar
Hixie committed
545

546 547 548
  // DRAWER API

  final GlobalKey<DrawerControllerState> _drawerKey = new GlobalKey<DrawerControllerState>();
549
  final GlobalKey<DrawerControllerState> _endDrawerKey = new GlobalKey<DrawerControllerState>();
550

551
  /// Whether this scaffold has a non-null [Scaffold.drawer].
552
  bool get hasDrawer => widget.drawer != null;
553 554
  /// Whether this scaffold has a non-null [Scaffold.endDrawer].
  bool get hasEndDrawer => widget.endDrawer != null;
555

556 557 558 559
  /// Opens the [Drawer] (if any).
  ///
  /// If the scaffold has a non-null [Scaffold.drawer], this function will cause
  /// the drawer to begin its entrance animation.
560 561 562 563 564 565
  ///
  /// Normally this is not needed since the [Scaffold] automatically shows an
  /// appropriate [IconButton], and handles the edge-swipe gesture, to show the
  /// drawer.
  ///
  /// To close the drawer once it is open, use [Navigator.pop].
566 567
  ///
  /// See [Scaffold.of] for information about how to obtain the [ScaffoldState].
568
  void openDrawer() {
569
    _drawerKey.currentState?.open();
570 571
  }

572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587
  /// Opens the end side [Drawer] (if any).
  ///
  /// If the scaffold has a non-null [Scaffold.endDrawer], this function will cause
  /// the end side drawer to begin its entrance animation.
  ///
  /// Normally this is not needed since the [Scaffold] automatically shows an
  /// appropriate [IconButton], and handles the edge-swipe gesture, to show the
  /// drawer.
  ///
  /// To close the end side drawer once it is open, use [Navigator.pop].
  ///
  /// See [Scaffold.of] for information about how to obtain the [ScaffoldState].
  void openEndDrawer() {
    _endDrawerKey.currentState?.open();
  }

588 589
  // SNACKBAR API

590
  final Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> _snackBars = new Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>();
591
  AnimationController _snackBarController;
Hixie's avatar
Hixie committed
592 593
  Timer _snackBarTimer;

594
  /// Shows a [SnackBar] at the bottom of the scaffold.
595 596 597 598 599
  ///
  /// A scaffold can show at most one snack bar at a time. If this function is
  /// called while another snack bar is already visible, the given snack bar
  /// will be added to a queue and displayed after the earlier snack bars have
  /// closed.
600 601 602
  ///
  /// To control how long a [SnackBar] remains visible, use [SnackBar.duration].
  ///
603 604 605 606
  /// To remove the [SnackBar] with an exit animation, use [hideCurrentSnackBar]
  /// or call [ScaffoldFeatureController.close] on the returned
  /// [ScaffoldFeatureController]. To remove a [SnackBar] suddenly (without an
  /// animation), use [removeCurrentSnackBar].
607 608
  ///
  /// See [Scaffold.of] for information about how to obtain the [ScaffoldState].
609
  ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(SnackBar snackbar) {
610
    _snackBarController ??= SnackBar.createAnimationController(vsync: this)
Hixie's avatar
Hixie committed
611
      ..addStatusListener(_handleSnackBarStatusChange);
612
    if (_snackBars.isEmpty) {
613 614
      assert(_snackBarController.isDismissed);
      _snackBarController.forward();
615
    }
616 617
    ScaffoldFeatureController<SnackBar, SnackBarClosedReason> controller;
    controller = new ScaffoldFeatureController<SnackBar, SnackBarClosedReason>._(
618 619 620
      // We provide a fallback key so that if back-to-back snackbars happen to
      // match in structure, material ink splashes and highlights don't survive
      // from one to the next.
621
      snackbar.withAnimation(_snackBarController, fallbackKey: new UniqueKey()),
622
      new Completer<SnackBarClosedReason>(),
623 624
      () {
        assert(_snackBars.first == controller);
625
        hideCurrentSnackBar(reason: SnackBarClosedReason.hide);
626 627 628
      },
      null // SnackBar doesn't use a builder function so setState() wouldn't rebuild it
    );
Hixie's avatar
Hixie committed
629
    setState(() {
630
      _snackBars.addLast(controller);
Hixie's avatar
Hixie committed
631
    });
632
    return controller;
Hixie's avatar
Hixie committed
633 634
  }

635
  void _handleSnackBarStatusChange(AnimationStatus status) {
Hixie's avatar
Hixie committed
636
    switch (status) {
637
      case AnimationStatus.dismissed:
Hixie's avatar
Hixie committed
638 639 640 641
        assert(_snackBars.isNotEmpty);
        setState(() {
          _snackBars.removeFirst();
        });
642
        if (_snackBars.isNotEmpty)
643
          _snackBarController.forward();
Hixie's avatar
Hixie committed
644
        break;
645
      case AnimationStatus.completed:
Hixie's avatar
Hixie committed
646 647 648 649 650
        setState(() {
          assert(_snackBarTimer == null);
          // build will create a new timer if necessary to dismiss the snack bar
        });
        break;
651 652
      case AnimationStatus.forward:
      case AnimationStatus.reverse:
Hixie's avatar
Hixie committed
653 654 655 656
        break;
    }
  }

657 658 659 660
  /// Removes the current [SnackBar] (if any) immediately.
  ///
  /// The removed snack bar does not run its normal exit animation. If there are
  /// any queued snack bars, they begin their entrance animation immediately.
661 662
  void removeCurrentSnackBar({ SnackBarClosedReason reason: SnackBarClosedReason.remove }) {
    assert(reason != null);
663 664
    if (_snackBars.isEmpty)
      return;
665
    final Completer<SnackBarClosedReason> completer = _snackBars.first._completer;
666
    if (!completer.isCompleted)
667
      completer.complete(reason);
668 669 670 671 672
    _snackBarTimer?.cancel();
    _snackBarTimer = null;
    _snackBarController.value = 0.0;
  }

673
  /// Removes the current [SnackBar] by running its normal exit animation.
674 675
  ///
  /// The closed completer is called after the animation is complete.
676 677 678 679 680
  void hideCurrentSnackBar({ SnackBarClosedReason reason: SnackBarClosedReason.hide }) {
    assert(reason != null);
    if (_snackBars.isEmpty || _snackBarController.status == AnimationStatus.dismissed)
      return;
    final Completer<SnackBarClosedReason> completer = _snackBars.first._completer;
681
    _snackBarController.reverse().then<void>((Null _) {
682 683 684 685
      assert(mounted);
      if (!completer.isCompleted)
        completer.complete(reason);
    });
686
    _snackBarTimer?.cancel();
Hixie's avatar
Hixie committed
687 688 689
    _snackBarTimer = null;
  }

690

691 692
  // PERSISTENT BOTTOM SHEET API

693
  final List<_PersistentBottomSheet> _dismissedBottomSheets = <_PersistentBottomSheet>[];
694
  PersistentBottomSheetController<dynamic> _currentBottomSheet;
695

696 697 698 699 700 701
  /// Shows a persistent material design bottom sheet.
  ///
  /// A persistent bottom sheet shows information that supplements the primary
  /// content of the app. A persistent bottom sheet remains visible even when
  /// the user interacts with other parts of the app.
  ///
702
  /// A closely related widget is a modal bottom sheet, which is an alternative
703 704 705 706
  /// to a menu or a dialog and prevents the user from interacting with the rest
  /// of the app. Modal bottom sheets can be created and displayed with the
  /// [showModalBottomSheet] function.
  ///
707
  /// Returns a controller that can be used to close and otherwise manipulate the
708 709 710 711 712
  /// bottom sheet.
  ///
  /// To rebuild the bottom sheet (e.g. if it is stateful), call
  /// [PersistentBottomSheetController.setState] on the value returned from this
  /// method.
713 714 715
  ///
  /// See also:
  ///
716
  ///  * [BottomSheet], which is the widget typically returned by the `builder`.
717
  ///  * [showBottomSheet], which calls this method given a [BuildContext].
718 719 720
  ///  * [showModalBottomSheet], which can be used to display a modal bottom
  ///    sheet.
  ///  * [Scaffold.of], for information about how to obtain the [ScaffoldState].
721
  ///  * <https://material.google.com/components/bottom-sheets.html#bottom-sheets-persistent-bottom-sheets>
722
  PersistentBottomSheetController<T> showBottomSheet<T>(WidgetBuilder builder) {
723 724 725 726
    if (_currentBottomSheet != null) {
      _currentBottomSheet.close();
      assert(_currentBottomSheet == null);
    }
727 728 729
    final Completer<T> completer = new Completer<T>();
    final GlobalKey<_PersistentBottomSheetState> bottomSheetKey = new GlobalKey<_PersistentBottomSheetState>();
    final AnimationController controller = BottomSheet.createAnimationController(this)
730 731
      ..forward();
    _PersistentBottomSheet bottomSheet;
732
    final LocalHistoryEntry entry = new LocalHistoryEntry(
Hixie's avatar
Hixie committed
733
      onRemove: () {
734 735 736
        assert(_currentBottomSheet._widget == bottomSheet);
        assert(bottomSheetKey.currentState != null);
        bottomSheetKey.currentState.close();
737 738 739 740 741
        if (controller.status != AnimationStatus.dismissed)
          _dismissedBottomSheets.add(bottomSheet);
        setState(() {
          _currentBottomSheet = null;
        });
742 743 744 745 746
        completer.complete();
      }
    );
    bottomSheet = new _PersistentBottomSheet(
      key: bottomSheetKey,
747
      animationController: controller,
748 749
      onClosing: () {
        assert(_currentBottomSheet._widget == bottomSheet);
Hixie's avatar
Hixie committed
750
        entry.remove();
751 752
      },
      onDismissed: () {
753
        if (_dismissedBottomSheets.contains(bottomSheet)) {
754
          bottomSheet.animationController.dispose();
755 756 757 758
          setState(() {
            _dismissedBottomSheets.remove(bottomSheet);
          });
        }
759 760 761
      },
      builder: builder
    );
Hixie's avatar
Hixie committed
762
    ModalRoute.of(context).addLocalHistoryEntry(entry);
763
    setState(() {
764
      _currentBottomSheet = new PersistentBottomSheetController<T>._(
765
        bottomSheet,
766
        completer,
767
        entry.remove,
Hans Muller's avatar
Hans Muller committed
768
        (VoidCallback fn) { bottomSheetKey.currentState?.setState(fn); }
769 770 771 772 773 774
      );
    });
    return _currentBottomSheet;
  }


775
  // iOS FEATURES - status bar tap, back gesture
776

777 778 779
  // On iOS, tapping the status bar scrolls the app's primary scrollable to the
  // top. We implement this by providing a primary scroll controller and
  // scrolling it to the top when tapped.
780

781
  final ScrollController _primaryScrollController = new ScrollController();
Hixie's avatar
Hixie committed
782

783 784 785 786 787 788
  void _handleStatusBarTap() {
    if (_primaryScrollController.hasClients) {
      _primaryScrollController.animateTo(
        0.0,
        duration: const Duration(milliseconds: 300),
        curve: Curves.linear, // TODO(ianh): Use a more appropriate curve.
789 790 791 792
      );
    }
  }

793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808

  // INTERNALS

  @override
  void dispose() {
    _snackBarController?.dispose();
    _snackBarController = null;
    _snackBarTimer?.cancel();
    _snackBarTimer = null;
    for (_PersistentBottomSheet bottomSheet in _dismissedBottomSheets)
      bottomSheet.animationController.dispose();
    if (_currentBottomSheet != null)
      _currentBottomSheet._widget.animationController.dispose();
    super.dispose();
  }

Ian Hickson's avatar
Ian Hickson committed
809 810 811 812
  void _addIfNonNull(List<LayoutId> children, Widget child, Object childId, {
    @required bool removeLeftPadding,
    @required bool removeTopPadding,
    @required bool removeRightPadding,
813
    @required bool removeBottomPadding,
Ian Hickson's avatar
Ian Hickson committed
814 815 816 817 818 819 820 821 822 823
  }) {
    if (child != null) {
      children.add(
        new LayoutId(
          id: childId,
          child: new MediaQuery.removePadding(
            context: context,
            removeLeft: removeLeftPadding,
            removeTop: removeTopPadding,
            removeRight: removeRightPadding,
824
            removeBottom: removeBottomPadding,
Ian Hickson's avatar
Ian Hickson committed
825 826 827 828 829
            child: child,
          ),
        ),
      );
    }
830 831
  }

832
  @override
Adam Barth's avatar
Adam Barth committed
833
  Widget build(BuildContext context) {
834
    assert(debugCheckHasMediaQuery(context));
Ian Hickson's avatar
Ian Hickson committed
835
    assert(debugCheckHasDirectionality(context));
836
    final MediaQueryData mediaQuery = MediaQuery.of(context);
837
    final ThemeData themeData = Theme.of(context);
Ian Hickson's avatar
Ian Hickson committed
838
    final TextDirection textDirection = Directionality.of(context);
Hixie's avatar
Hixie committed
839

840
    if (_snackBars.isNotEmpty) {
841
      final ModalRoute<dynamic> route = ModalRoute.of(context);
Hixie's avatar
Hixie committed
842
      if (route == null || route.isCurrent) {
843
        if (_snackBarController.isCompleted && _snackBarTimer == null)
844 845 846 847 848
          _snackBarTimer = new Timer(_snackBars.first._widget.duration, () {
            assert(_snackBarController.status == AnimationStatus.forward ||
                   _snackBarController.status == AnimationStatus.completed);
            hideCurrentSnackBar(reason: SnackBarClosedReason.timeout);
          });
Hixie's avatar
Hixie committed
849 850 851 852
      } else {
        _snackBarTimer?.cancel();
        _snackBarTimer = null;
      }
853
    }
854

855
    final List<LayoutId> children = <LayoutId>[];
856

Ian Hickson's avatar
Ian Hickson committed
857 858 859 860 861 862 863
    _addIfNonNull(
      children,
      widget.body,
      _ScaffoldSlot.body,
      removeLeftPadding: false,
      removeTopPadding: widget.appBar != null,
      removeRightPadding: false,
864
      removeBottomPadding: widget.bottomNavigationBar != null,
Ian Hickson's avatar
Ian Hickson committed
865
    );
866

867
    if (widget.appBar != null) {
868
      final double topPadding = widget.primary ? mediaQuery.padding.top : 0.0;
869 870
      final double extent = widget.appBar.preferredSize.height + topPadding;
      assert(extent >= 0.0 && extent.isFinite);
871 872 873 874
      _addIfNonNull(
        children,
        new ConstrainedBox(
          constraints: new BoxConstraints(maxHeight: extent),
875 876 877 878
          child: FlexibleSpaceBar.createSettings(
            currentExtent: extent,
            child: widget.appBar,
          ),
879 880
        ),
        _ScaffoldSlot.appBar,
Ian Hickson's avatar
Ian Hickson committed
881 882 883 884
        removeLeftPadding: false,
        removeTopPadding: false,
        removeRightPadding: false,
        removeBottomPadding: true,
885 886
      );
    }
887

Ian Hickson's avatar
Ian Hickson committed
888 889 890 891 892 893 894 895
    if (_snackBars.isNotEmpty) {
      _addIfNonNull(
        children,
        _snackBars.first._widget,
        _ScaffoldSlot.snackBar,
        removeLeftPadding: false,
        removeTopPadding: true,
        removeRightPadding: false,
896
        removeBottomPadding: false,
Ian Hickson's avatar
Ian Hickson committed
897 898
      );
    }
899

900
    if (widget.persistentFooterButtons != null) {
Ian Hickson's avatar
Ian Hickson committed
901 902 903
      _addIfNonNull(
        children,
        new Container(
904 905 906 907 908 909 910
          decoration: new BoxDecoration(
            border: new Border(
              top: new BorderSide(
                color: themeData.dividerColor
              ),
            ),
          ),
Ian Hickson's avatar
Ian Hickson committed
911 912 913 914 915
          child: new SafeArea(
            child: new ButtonTheme.bar(
              child: new ButtonBar(
                children: widget.persistentFooterButtons
              ),
916 917 918
            ),
          ),
        ),
Ian Hickson's avatar
Ian Hickson committed
919 920 921 922
        _ScaffoldSlot.persistentFooter,
        removeLeftPadding: false,
        removeTopPadding: true,
        removeRightPadding: false,
923
        removeBottomPadding: widget.resizeToAvoidBottomPadding,
Ian Hickson's avatar
Ian Hickson committed
924
      );
925 926
    }

927
    if (widget.bottomNavigationBar != null) {
Ian Hickson's avatar
Ian Hickson committed
928 929 930 931 932 933 934
      _addIfNonNull(
        children,
        widget.bottomNavigationBar,
        _ScaffoldSlot.bottomNavigationBar,
        removeLeftPadding: false,
        removeTopPadding: true,
        removeRightPadding: false,
935
        removeBottomPadding: false,
Ian Hickson's avatar
Ian Hickson committed
936
      );
937
    }
938

939
    if (_currentBottomSheet != null || _dismissedBottomSheets.isNotEmpty) {
940
      final List<Widget> bottomSheets = <Widget>[];
941
      if (_dismissedBottomSheets.isNotEmpty)
942 943 944
        bottomSheets.addAll(_dismissedBottomSheets);
      if (_currentBottomSheet != null)
        bottomSheets.add(_currentBottomSheet._widget);
945
      final Widget stack = new Stack(
946
        children: bottomSheets,
947
        alignment: Alignment.bottomCenter,
948
      );
Ian Hickson's avatar
Ian Hickson committed
949 950 951 952 953 954 955
      _addIfNonNull(
        children,
        stack,
        _ScaffoldSlot.bottomSheet,
        removeLeftPadding: false,
        removeTopPadding: true,
        removeRightPadding: false,
956
        removeBottomPadding: widget.resizeToAvoidBottomPadding,
Ian Hickson's avatar
Ian Hickson committed
957
      );
958 959
    }

Ian Hickson's avatar
Ian Hickson committed
960 961 962
    _addIfNonNull(
      children,
      new _FloatingActionButtonTransition(
963
        child: widget.floatingActionButton,
Ian Hickson's avatar
Ian Hickson committed
964 965 966 967 968 969 970
      ),
      _ScaffoldSlot.floatingActionButton,
      removeLeftPadding: true,
      removeTopPadding: true,
      removeRightPadding: true,
      removeBottomPadding: true,
    );
971

972
    if (themeData.platform == TargetPlatform.iOS) {
Ian Hickson's avatar
Ian Hickson committed
973 974 975
      _addIfNonNull(
        children,
        new GestureDetector(
976
          behavior: HitTestBehavior.opaque,
977
          onTap: _handleStatusBarTap,
978 979
          // iOS accessibility automatically adds scroll-to-top to the clock in the status bar
          excludeFromSemantics: true,
Ian Hickson's avatar
Ian Hickson committed
980 981 982 983 984 985 986
        ),
        _ScaffoldSlot.statusBar,
        removeLeftPadding: false,
        removeTopPadding: true,
        removeRightPadding: false,
        removeBottomPadding: true,
      );
987 988
    }

989
    if (widget.drawer != null) {
990
      assert(hasDrawer);
Ian Hickson's avatar
Ian Hickson committed
991 992 993
      _addIfNonNull(
        children,
        new DrawerController(
994
          key: _drawerKey,
995
          alignment: DrawerAlignment.start,
996
          child: widget.drawer,
Ian Hickson's avatar
Ian Hickson committed
997 998 999 1000 1001 1002 1003 1004 1005 1006
        ),
        _ScaffoldSlot.drawer,
        // remove the side padding from the side we're not touching
        removeLeftPadding: textDirection == TextDirection.rtl,
        removeTopPadding: false,
        removeRightPadding: textDirection == TextDirection.ltr,
        removeBottomPadding: false,
      );
    }

1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024
    if (widget.endDrawer != null) {
      assert(hasEndDrawer);
      _addIfNonNull(
        children,
        new DrawerController(
          key: _endDrawerKey,
          alignment: DrawerAlignment.end,
          child: widget.endDrawer,
        ),
        _ScaffoldSlot.endDrawer,
        // remove the side padding from the side we're not touching
        removeLeftPadding: textDirection == TextDirection.ltr,
        removeTopPadding: false,
        removeRightPadding: textDirection == TextDirection.rtl,
        removeBottomPadding: false,
      );
    }

Ian Hickson's avatar
Ian Hickson committed
1025 1026 1027
    double endPadding;
    switch (textDirection) {
      case TextDirection.rtl:
1028
        endPadding = mediaQuery.padding.left;
Ian Hickson's avatar
Ian Hickson committed
1029 1030
        break;
      case TextDirection.ltr:
1031
        endPadding = mediaQuery.padding.right;
Ian Hickson's avatar
Ian Hickson committed
1032
        break;
1033
    }
Ian Hickson's avatar
Ian Hickson committed
1034
    assert(endPadding != null);
1035

1036 1037 1038 1039 1040
    return new _ScaffoldScope(
      hasDrawer: hasDrawer,
      child: new PrimaryScrollController(
        controller: _primaryScrollController,
        child: new Material(
1041
          color: widget.backgroundColor ?? themeData.scaffoldBackgroundColor,
1042 1043 1044
          child: new CustomMultiChildLayout(
            children: children,
            delegate: new _ScaffoldLayout(
1045
              statusBarHeight: mediaQuery.padding.top,
1046
              bottomViewInset: widget.resizeToAvoidBottomPadding ? mediaQuery.viewInsets.bottom : 0.0,
Ian Hickson's avatar
Ian Hickson committed
1047 1048
              endPadding: endPadding,
              textDirection: textDirection,
1049 1050 1051 1052
            ),
          ),
        ),
      ),
1053
    );
1054
  }
1055
}
1056

1057 1058
/// An interface for controlling a feature of a [Scaffold].
///
1059
/// Commonly obtained from [ScaffoldState.showSnackBar] or [ScaffoldState.showBottomSheet].
1060
class ScaffoldFeatureController<T extends Widget, U> {
1061 1062
  const ScaffoldFeatureController._(this._widget, this._completer, this.close, this.setState);
  final T _widget;
1063
  final Completer<U> _completer;
1064 1065

  /// Completes when the feature controlled by this object is no longer visible.
1066
  Future<U> get closed => _completer.future;
1067 1068 1069 1070 1071

  /// Remove the feature (e.g., bottom sheet or snack bar) from the scaffold.
  final VoidCallback close;

  /// Mark the feature (e.g., bottom sheet or snack bar) as needing to rebuild.
1072 1073 1074
  final StateSetter setState;
}

1075
class _PersistentBottomSheet extends StatefulWidget {
1076
  const _PersistentBottomSheet({
1077
    Key key,
1078
    this.animationController,
1079 1080 1081 1082 1083
    this.onClosing,
    this.onDismissed,
    this.builder
  }) : super(key: key);

1084
  final AnimationController animationController; // we control it, but it must be disposed by whoever created it
1085 1086 1087 1088
  final VoidCallback onClosing;
  final VoidCallback onDismissed;
  final WidgetBuilder builder;

1089
  @override
1090 1091 1092 1093
  _PersistentBottomSheetState createState() => new _PersistentBottomSheetState();
}

class _PersistentBottomSheetState extends State<_PersistentBottomSheet> {
1094
  @override
1095 1096
  void initState() {
    super.initState();
1097 1098
    assert(widget.animationController.status == AnimationStatus.forward);
    widget.animationController.addStatusListener(_handleStatusChange);
1099 1100
  }

1101
  @override
1102 1103 1104
  void didUpdateWidget(_PersistentBottomSheet oldWidget) {
    super.didUpdateWidget(oldWidget);
    assert(widget.animationController == oldWidget.animationController);
1105 1106 1107
  }

  void close() {
1108
    widget.animationController.reverse();
1109 1110
  }

1111
  void _handleStatusChange(AnimationStatus status) {
1112 1113
    if (status == AnimationStatus.dismissed && widget.onDismissed != null)
      widget.onDismissed();
1114 1115
  }

1116
  @override
1117
  Widget build(BuildContext context) {
1118
    return new AnimatedBuilder(
1119
      animation: widget.animationController,
1120
      builder: (BuildContext context, Widget child) {
1121
        return new Align(
1122
          alignment: AlignmentDirectional.topStart,
1123
          heightFactor: widget.animationController.value,
1124 1125
          child: child
        );
1126
      },
Hixie's avatar
Hixie committed
1127 1128 1129
      child: new Semantics(
        container: true,
        child: new BottomSheet(
1130 1131 1132
          animationController: widget.animationController,
          onClosing: widget.onClosing,
          builder: widget.builder
Hixie's avatar
Hixie committed
1133
        )
1134
      )
1135 1136 1137 1138
    );
  }

}
1139 1140 1141

/// A [ScaffoldFeatureController] for persistent bottom sheets.
///
1142
/// This is the type of objects returned by [ScaffoldState.showBottomSheet].
1143 1144 1145 1146 1147 1148 1149 1150
class PersistentBottomSheetController<T> extends ScaffoldFeatureController<_PersistentBottomSheet, T> {
  const PersistentBottomSheetController._(
    _PersistentBottomSheet widget,
    Completer<T> completer,
    VoidCallback close,
    StateSetter setState
  ) : super._(widget, completer, close, setState);
}
1151 1152

class _ScaffoldScope extends InheritedWidget {
1153
  const _ScaffoldScope({
1154 1155
    @required this.hasDrawer,
    @required Widget child,
1156 1157
  }) : assert(hasDrawer != null),
       super(child: child);
1158 1159 1160 1161 1162 1163 1164

  final bool hasDrawer;

  @override
  bool updateShouldNotify(_ScaffoldScope oldWidget) {
    return hasDrawer != oldWidget.hasDrawer;
  }
1165
}