scaffold.dart 27.5 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/widgets.dart';
10

11
import 'app_bar.dart';
12
import 'bottom_sheet.dart';
13
import 'drawer.dart';
14
import 'icons.dart';
15
import 'icon_button.dart';
16
import 'material.dart';
Hixie's avatar
Hixie committed
17
import 'snack_bar.dart';
18

19
const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent
20
const Duration _kFloatingActionButtonSegue = const Duration(milliseconds: 400);
21

22 23 24
/// The Scaffold's appbar is the toolbar, tabbar, and the "flexible space" that's
/// stacked behind them. The Scaffold's appBarBehavior defines how the appbar
/// responds to scrolling the application.
25
enum AppBarBehavior {
26
  /// The tool bar's layout does not respond to scrolling.
27
  anchor,
28 29 30 31 32 33

  /// The tool bar's appearance and layout depend on the scrollOffset of the
  /// Scrollable identified by the Scaffold's scrollableKey. With the scrollOffset
  /// at 0.0, scrolling downwards causes the toolbar's flexible space to shrink,
  /// and then the entire toolbar fade outs and scrolls off the top of the screen.
  /// Scrolling upwards always causes the toolbar to reappear.
34
  scroll,
35 36 37 38 39 40

  /// The tool bar's appearance and layout depend on the scrollOffset of the
  /// Scrollable identified by the Scaffold's scrollableKey. With the scrollOffset
  /// at 0.0, Scrolling downwards causes the toolbar's flexible space to shrink.
  /// Other than that, the toolbar remains anchored at the top.
  under,
41 42
}

43
enum _ScaffoldSlot {
44
  body,
45
  appBar,
46 47 48 49 50
  bottomSheet,
  snackBar,
  floatingActionButton,
  drawer,
}
Hans Muller's avatar
Hans Muller committed
51

52
class _ScaffoldLayout extends MultiChildLayoutDelegate {
53
  _ScaffoldLayout({ this.padding, this.appBarBehavior: AppBarBehavior.anchor });
54

55
  final EdgeInsets padding;
56
  final AppBarBehavior appBarBehavior;
57

58
  @override
59 60
  void performLayout(Size size) {
    BoxConstraints looseConstraints = new BoxConstraints.loose(size);
61

62
    // This part of the layout has the same effect as putting the app bar and
63
    // body in a column and making the body flexible. What's different is that
64 65
    // in this case the app bar appears -after- the body in the stacking order,
    // so the app bar's shadow is drawn on top of the body.
66

67
    final BoxConstraints fullWidthConstraints = looseConstraints.tighten(width: size.width);
68 69
    double contentTop = padding.top;
    double contentBottom = size.height - padding.bottom;
70

71
    if (hasChild(_ScaffoldSlot.appBar)) {
72 73 74
      final double appBarHeight = layoutChild(_ScaffoldSlot.appBar, fullWidthConstraints).height;
      if (appBarBehavior == AppBarBehavior.anchor)
        contentTop = appBarHeight;
75
      positionChild(_ScaffoldSlot.appBar, Offset.zero);
76 77
    }

78
    if (hasChild(_ScaffoldSlot.body)) {
79
      final double bodyHeight = contentBottom - contentTop;
80
      final BoxConstraints bodyConstraints = fullWidthConstraints.tighten(height: bodyHeight);
81
      layoutChild(_ScaffoldSlot.body, bodyConstraints);
82
      positionChild(_ScaffoldSlot.body, new Offset(0.0, contentTop));
83
    }
84 85 86 87 88 89 90 91 92 93

    // The BottomSheet and the SnackBar are anchored to the bottom of the parent,
    // they're as wide as the parent and are given their intrinsic height.
    // 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.

94 95 96
    Size bottomSheetSize = Size.zero;
    Size snackBarSize = Size.zero;

97
    if (hasChild(_ScaffoldSlot.bottomSheet)) {
98
      bottomSheetSize = layoutChild(_ScaffoldSlot.bottomSheet, fullWidthConstraints);
99
      positionChild(_ScaffoldSlot.bottomSheet, new Offset((size.width - bottomSheetSize.width) / 2.0, contentBottom - bottomSheetSize.height));
100 101
    }

102
    if (hasChild(_ScaffoldSlot.snackBar)) {
103
      snackBarSize = layoutChild(_ScaffoldSlot.snackBar, fullWidthConstraints);
104
      positionChild(_ScaffoldSlot.snackBar, new Offset(0.0, contentBottom - snackBarSize.height));
105 106
    }

107
    if (hasChild(_ScaffoldSlot.floatingActionButton)) {
108
      final Size fabSize = layoutChild(_ScaffoldSlot.floatingActionButton, looseConstraints);
109
      final double fabX = size.width - fabSize.width - _kFloatingActionButtonMargin;
110
      double fabY = contentBottom - fabSize.height - _kFloatingActionButtonMargin;
111
      if (snackBarSize.height > 0.0)
112
        fabY = math.min(fabY, contentBottom - snackBarSize.height - fabSize.height - _kFloatingActionButtonMargin);
113
      if (bottomSheetSize.height > 0.0)
114
        fabY = math.min(fabY, contentBottom - bottomSheetSize.height - fabSize.height / 2.0);
115
      positionChild(_ScaffoldSlot.floatingActionButton, new Offset(fabX, fabY));
116
    }
117

118
    if (hasChild(_ScaffoldSlot.drawer)) {
119 120
      layoutChild(_ScaffoldSlot.drawer, new BoxConstraints.tight(size));
      positionChild(_ScaffoldSlot.drawer, Offset.zero);
121
    }
Hans Muller's avatar
Hans Muller committed
122
  }
123

124
  @override
125 126 127
  bool shouldRelayout(_ScaffoldLayout oldDelegate) {
    return padding != oldDelegate.padding;
  }
Hans Muller's avatar
Hans Muller committed
128 129
}

130
class _FloatingActionButtonTransition extends StatefulWidget {
131 132 133 134 135 136 137 138 139
  _FloatingActionButtonTransition({
    Key key,
    this.child
  }) : super(key: key) {
    assert(child != null);
  }

  final Widget child;

140
  @override
141 142 143 144
  _FloatingActionButtonTransitionState createState() => new _FloatingActionButtonTransitionState();
}

class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTransition> {
145
  final AnimationController controller = new AnimationController(duration: _kFloatingActionButtonSegue);
146 147
  Widget oldChild;

148
  @override
149 150
  void initState() {
    super.initState();
151
    controller.forward().then((_) {
152 153 154 155
      oldChild = null;
    });
  }

156
  @override
157
  void dispose() {
158
    controller.stop();
159 160 161
    super.dispose();
  }

162
  @override
163 164 165 166
  void didUpdateConfig(_FloatingActionButtonTransition oldConfig) {
    if (Widget.canUpdate(oldConfig.child, config.child))
      return;
    oldChild = oldConfig.child;
167 168 169
    controller
      ..value = 0.0
      ..forward().then((_) {
170 171 172 173
        oldChild = null;
      });
  }

174
  @override
175 176 177 178
  Widget build(BuildContext context) {
    final List<Widget> children = new List<Widget>();
    if (oldChild != null) {
      children.add(new ScaleTransition(
179 180 181 182
        // TODO(abarth): We should use ReversedAnimation here.
        scale: new Tween<double>(
          begin: 1.0,
          end: 0.0
183
        ).animate(new CurvedAnimation(
184 185 186
          parent: controller,
          curve: const Interval(0.0, 0.5, curve: Curves.easeIn)
        )),
187 188 189 190 191
        child: oldChild
      ));
    }

    children.add(new ScaleTransition(
192 193 194 195
      scale: new CurvedAnimation(
        parent: controller,
        curve: const Interval(0.5, 1.0, curve: Curves.easeIn)
      ),
196 197 198 199 200 201 202
      child: config.child
    ));

    return new Stack(children: children);
  }
}

203 204
/// Implements the basic material design visual layout structure.
///
205
/// This class provides APIs for showing drawers, snack bars, and bottom sheets.
206
///
207 208 209 210 211 212 213 214 215 216 217 218 219
/// 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:
///
///  * [AppBar]
///  * [FloatingActionButton]
///  * [Drawer]
///  * [SnackBar]
///  * [BottomSheet]
///  * [ScaffoldState]
///  * <https://www.google.com/design/spec/layout/structure.html>
220
class Scaffold extends StatefulWidget {
221 222 223 224 225
  /// Creates a visual scaffold for material design widgets.
  ///
  /// By default, the [appBarBehavior] causes the [appBar] not to respond to
  /// scrolling and the [body] is resized to avoid the window padding (e.g., to
  /// to avoid being obscured by an onscreen keyboard).
Adam Barth's avatar
Adam Barth committed
226 227
  Scaffold({
    Key key,
228
    this.appBar,
Hixie's avatar
Hixie committed
229
    this.body,
230
    this.floatingActionButton,
231 232
    this.drawer,
    this.scrollableKey,
233
    this.appBarBehavior: AppBarBehavior.anchor,
234
    this.resizeToAvoidBottomPadding: true
235
  }) : super(key: key) {
236
    assert(scrollableKey != null ? (appBarBehavior != AppBarBehavior.anchor) : true);
237
  }
Adam Barth's avatar
Adam Barth committed
238

239
  /// An app bar to display at the top of the scaffold.
240
  final AppBar appBar;
241 242 243 244 245

  /// 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
246
  /// (e.g., from the onscreen keyboard), see [resizeToAvoidBottomPadding].
Hixie's avatar
Hixie committed
247
  final Widget body;
248 249 250 251

  /// A button displayed on top of the body.
  ///
  /// Typically a [FloatingActionButton].
Adam Barth's avatar
Adam Barth committed
252
  final Widget floatingActionButton;
253 254 255 256

  /// A panel displayed to the side of the body, often hidden on mobile devices.
  ///
  /// Typically a [Drawer].
257
  final Widget drawer;
258 259 260 261 262

  /// The key of the primary [Scrollable] widget in the [body].
  ///
  /// Used to control scroll-linked effects, such as the collapse of the
  /// [appBar].
263
  final Key scrollableKey;
264 265 266 267

  /// How the [appBar] should respond to scrolling.
  ///
  /// By default, the [appBar] does not respond to scrolling.
268
  final AppBarBehavior appBarBehavior;
Hixie's avatar
Hixie committed
269

270 271
  /// Whether the [body] (and other floating widgets) should size themselves to
  /// avoid the window's bottom padding.
272 273 274 275 276 277
  ///
  /// 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.
278
  final bool resizeToAvoidBottomPadding;
279

280
  /// The state from the closest instance of this class that encloses the given context.
Ian Hickson's avatar
Ian Hickson committed
281
  static ScaffoldState of(BuildContext context) => context.ancestorStateOfType(const TypeMatcher<ScaffoldState>());
Hixie's avatar
Hixie committed
282

283
  @override
Hixie's avatar
Hixie committed
284 285 286
  ScaffoldState createState() => new ScaffoldState();
}

287 288 289 290
/// State for a [Scaffold].
///
/// Can display [SnackBar]s and [BottomSheet]s. Retrieve a [ScaffoldState] from
/// the current [BuildContext] using [Scaffold.of].
Hixie's avatar
Hixie committed
291 292
class ScaffoldState extends State<Scaffold> {

293 294 295 296
  // APPBAR API

  AnimationController _appBarController;

297 298 299 300
  /// The animation controlling the size of the app bar.
  ///
  /// Useful for linking animation effects to the expansion and collapse of the
  /// app bar.
301 302
  Animation<double> get appBarAnimation => _appBarController.view;

303 304 305
  /// The height of the app bar when fully expanded.
  ///
  /// See [AppBar.expandedHeight].
306
  double get appBarHeight => config.appBar?.expandedHeight ?? 0.0;
307

308 309 310 311
  // DRAWER API

  final GlobalKey<DrawerControllerState> _drawerKey = new GlobalKey<DrawerControllerState>();

312 313 314 315
  /// 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.
316
  void openDrawer() {
317
    _drawerKey.currentState?.open();
318 319
  }

320 321
  // SNACKBAR API

322
  Queue<ScaffoldFeatureController<SnackBar, Null>> _snackBars = new Queue<ScaffoldFeatureController<SnackBar, Null>>();
323
  AnimationController _snackBarController;
Hixie's avatar
Hixie committed
324 325
  Timer _snackBarTimer;

326 327 328 329 330 331
  /// Shows a [SnackBar] at the bottom fo the scaffold.
  ///
  /// 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.
332
  ScaffoldFeatureController<SnackBar, Null> showSnackBar(SnackBar snackbar) {
333
    _snackBarController ??= SnackBar.createAnimationController()
Hixie's avatar
Hixie committed
334
      ..addStatusListener(_handleSnackBarStatusChange);
335
    if (_snackBars.isEmpty) {
336 337
      assert(_snackBarController.isDismissed);
      _snackBarController.forward();
338
    }
339 340
    ScaffoldFeatureController<SnackBar, Null> controller;
    controller = new ScaffoldFeatureController<SnackBar, Null>._(
341 342 343
      // 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.
344
      snackbar.withAnimation(_snackBarController, fallbackKey: new UniqueKey()),
345
      new Completer<Null>(),
346 347 348 349 350 351
      () {
        assert(_snackBars.first == controller);
        _hideSnackBar();
      },
      null // SnackBar doesn't use a builder function so setState() wouldn't rebuild it
    );
Hixie's avatar
Hixie committed
352
    setState(() {
353
      _snackBars.addLast(controller);
Hixie's avatar
Hixie committed
354
    });
355
    return controller;
Hixie's avatar
Hixie committed
356 357
  }

358
  void _handleSnackBarStatusChange(AnimationStatus status) {
Hixie's avatar
Hixie committed
359
    switch (status) {
360
      case AnimationStatus.dismissed:
Hixie's avatar
Hixie committed
361 362 363 364
        assert(_snackBars.isNotEmpty);
        setState(() {
          _snackBars.removeFirst();
        });
365
        if (_snackBars.isNotEmpty)
366
          _snackBarController.forward();
Hixie's avatar
Hixie committed
367
        break;
368
      case AnimationStatus.completed:
Hixie's avatar
Hixie committed
369 370 371 372 373
        setState(() {
          assert(_snackBarTimer == null);
          // build will create a new timer if necessary to dismiss the snack bar
        });
        break;
374 375
      case AnimationStatus.forward:
      case AnimationStatus.reverse:
Hixie's avatar
Hixie committed
376 377 378 379
        break;
    }
  }

380 381 382 383
  /// 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.
384 385 386 387 388 389 390 391 392 393 394
  void removeCurrentSnackBar() {
    if (_snackBars.isEmpty)
      return;
    Completer<Null> completer = _snackBars.first._completer;
    if (!completer.isCompleted)
      completer.complete();
    _snackBarTimer?.cancel();
    _snackBarTimer = null;
    _snackBarController.value = 0.0;
  }

Hixie's avatar
Hixie committed
395
  void _hideSnackBar() {
396 397
    assert(_snackBarController.status == AnimationStatus.forward ||
           _snackBarController.status == AnimationStatus.completed);
398
    _snackBars.first._completer.complete();
399
    _snackBarController.reverse();
400
    _snackBarTimer?.cancel();
Hixie's avatar
Hixie committed
401 402 403
    _snackBarTimer = null;
  }

404 405 406 407

  // PERSISTENT BOTTOM SHEET API

  List<Widget> _dismissedBottomSheets;
408
  PersistentBottomSheetController<dynamic> _currentBottomSheet;
409

410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
  /// 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.
  ///
  /// A closely related widget is  a modal bottom sheet, which is an alternative
  /// 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.
  ///
  /// Returns a contoller that can be used to close and otherwise manipulate the
  /// button sheet.
  ///
  /// See also:
  ///
  ///  * [BottomSheet]
  ///  * [showModalBottomSheet]
  ///  * <https://www.google.com/design/spec/components/bottom-sheets.html#bottom-sheets-persistent-bottom-sheets>
429
  PersistentBottomSheetController<dynamic/*=T*/> showBottomSheet/*<T>*/(WidgetBuilder builder) {
430 431 432 433
    if (_currentBottomSheet != null) {
      _currentBottomSheet.close();
      assert(_currentBottomSheet == null);
    }
434
    Completer<dynamic/*=T*/> completer = new Completer<dynamic/*=T*/>();
435
    GlobalKey<_PersistentBottomSheetState> bottomSheetKey = new GlobalKey<_PersistentBottomSheetState>();
436
    AnimationController controller = BottomSheet.createAnimationController()
437 438
      ..forward();
    _PersistentBottomSheet bottomSheet;
Hixie's avatar
Hixie committed
439 440
    LocalHistoryEntry entry = new LocalHistoryEntry(
      onRemove: () {
441 442 443 444 445 446 447 448 449 450 451
        assert(_currentBottomSheet._widget == bottomSheet);
        assert(bottomSheetKey.currentState != null);
        bottomSheetKey.currentState.close();
        _dismissedBottomSheets ??= <Widget>[];
        _dismissedBottomSheets.add(bottomSheet);
        _currentBottomSheet = null;
        completer.complete();
      }
    );
    bottomSheet = new _PersistentBottomSheet(
      key: bottomSheetKey,
452
      animationController: controller,
453 454
      onClosing: () {
        assert(_currentBottomSheet._widget == bottomSheet);
Hixie's avatar
Hixie committed
455
        entry.remove();
456 457 458 459 460 461 462 463 464
      },
      onDismissed: () {
        assert(_dismissedBottomSheets != null);
        setState(() {
          _dismissedBottomSheets.remove(bottomSheet);
        });
      },
      builder: builder
    );
Hixie's avatar
Hixie committed
465
    ModalRoute.of(context).addLocalHistoryEntry(entry);
466
    setState(() {
467
      _currentBottomSheet = new PersistentBottomSheetController<dynamic/*=T*/>._(
468
        bottomSheet,
469
        completer,
Hixie's avatar
Hixie committed
470
        () => entry.remove(),
Hans Muller's avatar
Hans Muller committed
471
        (VoidCallback fn) { bottomSheetKey.currentState?.setState(fn); }
472 473 474 475 476 477 478 479
      );
    });
    return _currentBottomSheet;
  }


  // INTERNALS

480
  @override
481 482 483
  void initState() {
    super.initState();
    _appBarController = new AnimationController();
484 485 486 487 488 489
    List<double> scrollValues = PageStorage.of(context)?.readState(context);
    if (scrollValues != null) {
      assert(scrollValues.length == 2);
      _scrollOffset = scrollValues[0];
      _scrollOffsetDelta = scrollValues[1];
    }
490 491
  }

492
  @override
Hixie's avatar
Hixie committed
493
  void dispose() {
494
    _appBarController.stop();
495 496
    _snackBarController?.stop();
    _snackBarController = null;
Hixie's avatar
Hixie committed
497 498
    _snackBarTimer?.cancel();
    _snackBarTimer = null;
499
    PageStorage.of(context)?.writeState(context, <double>[_scrollOffset, _scrollOffsetDelta]);
Hixie's avatar
Hixie committed
500 501 502 503 504 505 506
    super.dispose();
  }

  void _addIfNonNull(List<LayoutId> children, Widget child, Object childId) {
    if (child != null)
      children.add(new LayoutId(child: child, id: childId));
  }
Adam Barth's avatar
Adam Barth committed
507

508 509
  bool _shouldShowBackArrow;

510
  Widget _getModifiedAppBar({ EdgeInsets padding, int elevation}) {
511 512
    AppBar appBar = config.appBar;
    if (appBar == null)
513
      return null;
514 515
    Widget leading = appBar.leading;
    if (leading == null) {
516
      if (config.drawer != null) {
517
        leading = new IconButton(
518
          icon: Icons.menu,
519
          alignment: FractionalOffset.centerLeft,
Hixie's avatar
Hixie committed
520 521
          onPressed: openDrawer,
          tooltip: 'Open navigation menu' // TODO(ianh): Figure out how to localize this string
522 523 524 525
        );
      } else {
        _shouldShowBackArrow ??= Navigator.canPop(context);
        if (_shouldShowBackArrow) {
526
          leading = new IconButton(
527
            icon: Icons.arrow_back,
528
            alignment: FractionalOffset.centerLeft,
Hixie's avatar
Hixie committed
529 530
            onPressed: () => Navigator.pop(context),
            tooltip: 'Back' // TODO(ianh): Figure out how to localize this string
531 532 533 534
          );
        }
      }
    }
535 536
    return appBar.copyWith(
      elevation: elevation ?? appBar.elevation ?? 4,
537
      padding: new EdgeInsets.only(top: padding.top),
538
      leading: leading
539 540 541
    );
  }

542 543 544 545 546
  double _scrollOffset = 0.0;
  double _scrollOffsetDelta = 0.0;
  double _floatingAppBarHeight = 0.0;

  bool _handleScrollNotification(ScrollNotification notification) {
547 548 549 550
    final ScrollableState scrollable = notification.scrollable;
    if ((scrollable.config.scrollDirection == Axis.vertical) &&
        (config.scrollableKey == null || config.scrollableKey == scrollable.config.key)) {
      final double newScrollOffset = scrollable.scrollOffset;
551 552 553 554
      setState(() {
        _scrollOffsetDelta = _scrollOffset - newScrollOffset;
        _scrollOffset = newScrollOffset;
      });
555
    }
556 557 558
    return false;
  }

559
  Widget _buildAnchoredAppBar(double expandedHeight, double height, EdgeInsets padding) {
560
    // Drive _appBarController to the point where the flexible space has disappeared.
561
    _appBarController.value = (expandedHeight - height) / expandedHeight;
562
    return new SizedBox(
563
      height: height,
564
      child: _getModifiedAppBar(padding: padding)
565 566 567
    );
  }

568
  Widget _buildScrollableAppBar(BuildContext context, EdgeInsets padding) {
569 570 571
    final double expandedHeight = (config.appBar?.expandedHeight ?? 0.0) + padding.top;
    final double collapsedHeight = (config.appBar?.collapsedHeight ?? 0.0) + padding.top;
    final double minimumHeight = (config.appBar?.minimumHeight ?? 0.0) + padding.top;
572 573
    Widget appBar;

574 575
    if (_scrollOffset <= expandedHeight && _scrollOffset >= expandedHeight - minimumHeight) {
      // scrolled to the top, flexible space collapsed, only the toolbar and tabbar are (partially) visible.
576
      if (config.appBarBehavior == AppBarBehavior.under) {
577
        appBar = _buildAnchoredAppBar(expandedHeight, minimumHeight, padding);
578
      } else {
579 580
        final double height = math.max(_floatingAppBarHeight, expandedHeight - _scrollOffset);
        _appBarController.value = (expandedHeight - height) / expandedHeight;
581 582
        appBar = new SizedBox(
          height: height,
583
          child: _getModifiedAppBar(padding: padding)
584 585
        );
      }
586
    } else if (_scrollOffset > expandedHeight) {
587 588
      // scrolled past the entire app bar, maybe show the "floating" toolbar.
      if (config.appBarBehavior == AppBarBehavior.under) {
589
        appBar = _buildAnchoredAppBar(expandedHeight, minimumHeight, padding);
590
      } else {
591 592
        _floatingAppBarHeight = (_floatingAppBarHeight + _scrollOffsetDelta).clamp(0.0, collapsedHeight);
        _appBarController.value = (expandedHeight - _floatingAppBarHeight) / expandedHeight;
593 594
        appBar = new SizedBox(
          height: _floatingAppBarHeight,
595
          child: _getModifiedAppBar(padding: padding)
596 597
        );
      }
598
    } else {
599 600 601
      // _scrollOffset < expandedHeight - collapsedHeight, scrolled to the top, flexible space is visible]
      final double height = expandedHeight - _scrollOffset.clamp(0.0, expandedHeight);
      _appBarController.value = (expandedHeight - height) / expandedHeight;
602 603
      appBar = new SizedBox(
        height: height,
604
        child: _getModifiedAppBar(padding: padding, elevation: 0)
605 606
      );
      _floatingAppBarHeight = 0.0;
607

608 609 610 611 612
    }

    return appBar;
  }

613
  @override
Adam Barth's avatar
Adam Barth committed
614
  Widget build(BuildContext context) {
615
    EdgeInsets padding = MediaQuery.of(context).padding;
616 617
    if (!config.resizeToAvoidBottomPadding)
      padding = new EdgeInsets.fromLTRB(padding.left, padding.top, padding.right, 0.0);
Hixie's avatar
Hixie committed
618 619

    if (_snackBars.length > 0) {
620
      final ModalRoute<dynamic> route = ModalRoute.of(context);
Hixie's avatar
Hixie committed
621
      if (route == null || route.isCurrent) {
622
        if (_snackBarController.isCompleted && _snackBarTimer == null)
623
          _snackBarTimer = new Timer(_snackBars.first._widget.duration, _hideSnackBar);
Hixie's avatar
Hixie committed
624 625 626 627
      } else {
        _snackBarTimer?.cancel();
        _snackBarTimer = null;
      }
628
    }
629

630 631
    final List<LayoutId> children = new List<LayoutId>();
    _addIfNonNull(children, config.body, _ScaffoldSlot.body);
632
    if (config.appBarBehavior == AppBarBehavior.anchor) {
633
      final double expandedHeight = (config.appBar?.expandedHeight ?? 0.0) + padding.top;
634
      final Widget appBar = new ConstrainedBox(
635 636
        constraints: new BoxConstraints(maxHeight: expandedHeight),
        child: _getModifiedAppBar(padding: padding)
637
      );
638
      _addIfNonNull(children, appBar, _ScaffoldSlot.appBar);
639
    } else {
640
      children.add(new LayoutId(child: _buildScrollableAppBar(context, padding), id: _ScaffoldSlot.appBar));
641
    }
642
    // Otherwise the AppBar will be part of a [app bar, body] Stack. See AppBarBehavior.scroll below.
643 644 645

    if (_currentBottomSheet != null ||
        (_dismissedBottomSheets != null && _dismissedBottomSheets.isNotEmpty)) {
646
      final List<Widget> bottomSheets = <Widget>[];
647 648 649 650 651
      if (_dismissedBottomSheets != null && _dismissedBottomSheets.isNotEmpty)
        bottomSheets.addAll(_dismissedBottomSheets);
      if (_currentBottomSheet != null)
        bottomSheets.add(_currentBottomSheet._widget);
      Widget stack = new Stack(
652
        children: bottomSheets,
653
        alignment: FractionalOffset.bottomCenter
654
      );
655
      _addIfNonNull(children, stack, _ScaffoldSlot.bottomSheet);
656 657
    }

Hixie's avatar
Hixie committed
658
    if (_snackBars.isNotEmpty)
659
      _addIfNonNull(children, _snackBars.first._widget, _ScaffoldSlot.snackBar);
660

661
    if (config.floatingActionButton != null) {
662
      final Widget fab = new _FloatingActionButtonTransition(
663 664 665
        key: new ValueKey<Key>(config.floatingActionButton.key),
        child: config.floatingActionButton
      );
666
      children.add(new LayoutId(child: fab, id: _ScaffoldSlot.floatingActionButton));
667
    }
668

669 670
    if (config.drawer != null) {
      children.add(new LayoutId(
671
        id: _ScaffoldSlot.drawer,
672 673 674 675 676 677 678
        child: new DrawerController(
          key: _drawerKey,
          child: config.drawer
        )
      ));
    }

679 680
    Widget application;

681
    if (config.appBarBehavior != AppBarBehavior.anchor) {
682 683
      application = new NotificationListener<ScrollNotification>(
        onNotification: _handleScrollNotification,
684 685 686 687 688 689
        child: new CustomMultiChildLayout(
          children: children,
          delegate: new _ScaffoldLayout(
            padding: EdgeInsets.zero,
            appBarBehavior: config.appBarBehavior
          )
690 691 692 693
        )
      );
    } else {
      application = new CustomMultiChildLayout(
694 695 696 697
        children: children,
        delegate: new _ScaffoldLayout(
          padding: padding
        )
698 699 700 701
      );
    }

    return new Material(child: application);
702
  }
703
}
704

705 706 707
/// An interface for controlling a feature of a [Scaffold].
///
/// Commonly obtained from [Scaffold.showSnackBar] or [Scaffold.showBottomSheet].
708
class ScaffoldFeatureController<T extends Widget, U> {
709 710
  const ScaffoldFeatureController._(this._widget, this._completer, this.close, this.setState);
  final T _widget;
711 712
  final Completer<U> _completer;
  Future<U> get closed => _completer.future;
713 714 715 716 717

  /// 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.
718 719 720
  final StateSetter setState;
}

721
class _PersistentBottomSheet extends StatefulWidget {
722 723
  _PersistentBottomSheet({
    Key key,
724
    this.animationController,
725 726 727 728 729
    this.onClosing,
    this.onDismissed,
    this.builder
  }) : super(key: key);

730
  final AnimationController animationController;
731 732 733 734
  final VoidCallback onClosing;
  final VoidCallback onDismissed;
  final WidgetBuilder builder;

735
  @override
736 737 738 739 740
  _PersistentBottomSheetState createState() => new _PersistentBottomSheetState();
}

class _PersistentBottomSheetState extends State<_PersistentBottomSheet> {

741 742
  // We take ownership of the animation controller given in the first configuration.
  // We also share control of that animation with out BottomSheet widget.
743

744
  @override
745 746
  void initState() {
    super.initState();
747
    assert(config.animationController.status == AnimationStatus.forward);
748
    config.animationController.addStatusListener(_handleStatusChange);
749 750
  }

751
  @override
752 753
  void didUpdateConfig(_PersistentBottomSheet oldConfig) {
    super.didUpdateConfig(oldConfig);
754
    assert(config.animationController == oldConfig.animationController);
755 756
  }

757
  @override
758
  void dispose() {
759
    config.animationController.stop();
760 761 762 763
    super.dispose();
  }

  void close() {
764
    config.animationController.reverse();
765 766
  }

767 768
  void _handleStatusChange(AnimationStatus status) {
    if (status == AnimationStatus.dismissed && config.onDismissed != null)
769 770 771
      config.onDismissed();
  }

772
  @override
773
  Widget build(BuildContext context) {
774 775
    return new AnimatedBuilder(
      animation: config.animationController,
776
      builder: (BuildContext context, Widget child) {
777
        return new Align(
778
          alignment: FractionalOffset.topLeft,
779 780 781
          heightFactor: config.animationController.value,
          child: child
        );
782
      },
Hixie's avatar
Hixie committed
783 784 785 786 787 788 789
      child: new Semantics(
        container: true,
        child: new BottomSheet(
          animationController: config.animationController,
          onClosing: config.onClosing,
          builder: config.builder
        )
790
      )
791 792 793 794
    );
  }

}
795 796 797 798 799 800 801 802 803 804 805 806

/// A [ScaffoldFeatureController] for persistent bottom sheets.
///
/// This is the type of objects returned by [Scaffold.showBottomSheet].
class PersistentBottomSheetController<T> extends ScaffoldFeatureController<_PersistentBottomSheet, T> {
  const PersistentBottomSheetController._(
    _PersistentBottomSheet widget,
    Completer<T> completer,
    VoidCallback close,
    StateSetter setState
  ) : super._(widget, completer, close, setState);
}