scaffold.dart 94.7 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/rendering.dart';
11
import 'package:flutter/widgets.dart';
12
import 'package:flutter/gestures.dart' show DragStartBehavior;
13

14
import 'app_bar.dart';
15
import 'bottom_sheet.dart';
16
import 'button_bar.dart';
17
import 'colors.dart';
18
import 'divider.dart';
19
import 'drawer.dart';
20
import 'flexible_space_bar.dart';
21
import 'floating_action_button.dart';
22
import 'floating_action_button_location.dart';
23
import 'material.dart';
Hixie's avatar
Hixie committed
24
import 'snack_bar.dart';
25
import 'snack_bar_theme.dart';
26
import 'theme.dart';
27
import 'theme_data.dart';
28

29
// Examples can assume:
30
// TabController tabController;
31
// void setState(VoidCallback fn) { }
32 33 34
// String appBarTitle;
// int tabCount;
// TickerProvider tickerProvider;
35

36 37
const FloatingActionButtonLocation _kDefaultFloatingActionButtonLocation = FloatingActionButtonLocation.endFloat;
const FloatingActionButtonAnimator _kDefaultFloatingActionButtonAnimator = FloatingActionButtonAnimator.scaling;
38

39 40 41 42 43 44
// When the top of the BottomSheet crosses this threshold, it will start to
// shrink the FAB and show a scrim.
const double _kBottomSheetDominatesPercentage = 0.3;
const double _kMinBottomSheetScrimOpacity = 0.1;
const double _kMaxBottomSheetScrimOpacity = 0.6;

45
enum _ScaffoldSlot {
46
  body,
47
  appBar,
48
  bodyScrim,
49 50
  bottomSheet,
  snackBar,
51
  persistentFooter,
52
  bottomNavigationBar,
53 54
  floatingActionButton,
  drawer,
55
  endDrawer,
56
  statusBar,
57
}
Hans Muller's avatar
Hans Muller committed
58

59 60
/// The geometry of the [Scaffold] after all its contents have been laid out
/// except the [FloatingActionButton].
61
///
62
/// The [Scaffold] passes this pre-layout geometry to its
63
/// [FloatingActionButtonLocation], which produces an [Offset] that the
64
/// [Scaffold] uses to position the [FloatingActionButton].
65
///
66 67 68 69 70 71 72
/// For a description of the [Scaffold]'s geometry after it has
/// finished laying out, see the [ScaffoldGeometry].
@immutable
class ScaffoldPrelayoutGeometry {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const ScaffoldPrelayoutGeometry({
73 74 75 76 77 78 79
    @required this.bottomSheetSize,
    @required this.contentBottom,
    @required this.contentTop,
    @required this.floatingActionButtonSize,
    @required this.minInsets,
    @required this.scaffoldSize,
    @required this.snackBarSize,
80 81 82 83
    @required this.textDirection,
  });

  /// The [Size] of [Scaffold.floatingActionButton].
84
  ///
85 86 87 88
  /// If [Scaffold.floatingActionButton] is null, this will be [Size.zero].
  final Size floatingActionButtonSize;

  /// The [Size] of the [Scaffold]'s [BottomSheet].
89
  ///
90 91 92 93 94 95
  /// If the [Scaffold] is not currently showing a [BottomSheet],
  /// this will be [Size.zero].
  final Size bottomSheetSize;

  /// The vertical distance from the Scaffold's origin to the bottom of
  /// [Scaffold.body].
96
  ///
97 98 99 100
  /// This is useful in a [FloatingActionButtonLocation] designed to
  /// place the [FloatingActionButton] at the bottom of the screen, while
  /// keeping it above the [BottomSheet], the [Scaffold.bottomNavigationBar],
  /// or the keyboard.
101
  ///
Ian Hickson's avatar
Ian Hickson committed
102 103 104 105
  /// The [Scaffold.body] is laid out with respect to [minInsets] already. This
  /// means that a [FloatingActionButtonLocation] does not need to factor in
  /// [minInsets.bottom] when aligning a [FloatingActionButton] to
  /// [contentBottom].
106 107 108 109
  final double contentBottom;

  /// The vertical distance from the [Scaffold]'s origin to the top of
  /// [Scaffold.body].
110
  ///
111 112 113
  /// This is useful in a [FloatingActionButtonLocation] designed to
  /// place the [FloatingActionButton] at the top of the screen, while
  /// keeping it below the [Scaffold.appBar].
114
  ///
Ian Hickson's avatar
Ian Hickson committed
115 116 117
  /// The [Scaffold.body] is laid out with respect to [minInsets] already. This
  /// means that a [FloatingActionButtonLocation] does not need to factor in
  /// [minInsets.top] when aligning a [FloatingActionButton] to [contentTop].
118 119 120 121
  final double contentTop;

  /// The minimum padding to inset the [FloatingActionButton] by for it
  /// to remain visible.
122
  ///
123 124 125 126
  /// This value is the result of calling [MediaQuery.padding] in the
  /// [Scaffold]'s [BuildContext],
  /// and is useful for insetting the [FloatingActionButton] to avoid features like
  /// the system status bar or the keyboard.
127
  ///
128 129
  /// If [Scaffold.resizeToAvoidBottomInset] is set to false, [minInsets.bottom]
  /// will be 0.0.
130 131 132
  final EdgeInsets minInsets;

  /// The [Size] of the whole [Scaffold].
133 134
  ///
  /// If the [Size] of the [Scaffold]'s contents is modified by values such as
135
  /// [Scaffold.resizeToAvoidBottomInset] or the keyboard opening, then the
136
  /// [scaffoldSize] will not reflect those changes.
137
  ///
138 139 140 141
  /// This means that [FloatingActionButtonLocation]s designed to reposition
  /// the [FloatingActionButton] based on events such as the keyboard popping
  /// up should use [minInsets] to make sure that the [FloatingActionButton] is
  /// inset by enough to remain visible.
142
  ///
143 144 145 146 147
  /// See [minInsets] and [MediaQuery.padding] for more information on the appropriate
  /// insets to apply.
  final Size scaffoldSize;

  /// The [Size] of the [Scaffold]'s [SnackBar].
148
  ///
149 150 151 152 153 154 155 156 157 158 159 160 161
  /// If the [Scaffold] is not showing a [SnackBar], this will be [Size.zero].
  final Size snackBarSize;

  /// The [TextDirection] of the [Scaffold]'s [BuildContext].
  final TextDirection textDirection;
}

/// A snapshot of a transition between two [FloatingActionButtonLocation]s.
///
/// [ScaffoldState] uses this to seamlessly change transition animations
/// when a running [FloatingActionButtonLocation] transition is interrupted by a new transition.
@immutable
class _TransitionSnapshotFabLocation extends FloatingActionButtonLocation {
162

163 164 165 166 167 168 169 170 171 172
  const _TransitionSnapshotFabLocation(this.begin, this.end, this.animator, this.progress);

  final FloatingActionButtonLocation begin;
  final FloatingActionButtonLocation end;
  final FloatingActionButtonAnimator animator;
  final double progress;

  @override
  Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
    return animator.getOffset(
173 174
      begin: begin.getOffset(scaffoldGeometry),
      end: end.getOffset(scaffoldGeometry),
175 176 177 178 179 180 181 182 183 184 185
      progress: progress,
    );
  }

  @override
  String toString() {
    return '$runtimeType(begin: $begin, end: $end, progress: $progress)';
  }
}

/// Geometry information for [Scaffold] components after layout is finished.
186
///
187 188
/// To get a [ValueNotifier] for the scaffold geometry of a given
/// [BuildContext], use [Scaffold.geometryOf].
189
///
190 191
/// The ScaffoldGeometry is only available during the paint phase, because
/// its value is computed during the animation and layout phases prior to painting.
192
///
193 194 195
/// For an example of using the [ScaffoldGeometry], see the [BottomAppBar],
/// which uses the [ScaffoldGeometry] to paint a notch around the
/// [FloatingActionButton].
196 197
///
/// For information about the [Scaffold]'s geometry that is used while laying
198
/// out the [FloatingActionButton], see [ScaffoldPrelayoutGeometry].
199 200
@immutable
class ScaffoldGeometry {
201
  /// Create an object that describes the geometry of a [Scaffold].
202 203 204 205 206
  const ScaffoldGeometry({
    this.bottomNavigationBarTop,
    this.floatingActionButtonArea,
  });

207 208
  /// The distance from the [Scaffold]'s top edge to the top edge of the
  /// rectangle in which the [Scaffold.bottomNavigationBar] bar is laid out.
209
  ///
210
  /// Null if [Scaffold.bottomNavigationBar] is null.
211 212
  final double bottomNavigationBarTop;

213
  /// The [Scaffold.floatingActionButton]'s bounding rectangle.
214 215 216 217
  ///
  /// This is null when there is no floating action button showing.
  final Rect floatingActionButtonArea;

218
  ScaffoldGeometry _scaleFloatingActionButton(double scaleFactor) {
219 220 221
    if (scaleFactor == 1.0)
      return this;

222
    if (scaleFactor == 0.0) {
223
      return ScaffoldGeometry(
224 225 226
        bottomNavigationBarTop: bottomNavigationBarTop,
      );
    }
227

228
    final Rect scaledButton = Rect.lerp(
229 230
      floatingActionButtonArea.center & Size.zero,
      floatingActionButtonArea,
231
      scaleFactor,
232
    );
233 234 235 236 237 238 239 240 241
    return copyWith(floatingActionButtonArea: scaledButton);
  }

  /// Creates a copy of this [ScaffoldGeometry] but with the given fields replaced with
  /// the new values.
  ScaffoldGeometry copyWith({
    double bottomNavigationBarTop,
    Rect floatingActionButtonArea,
  }) {
242
    return ScaffoldGeometry(
243 244
      bottomNavigationBarTop: bottomNavigationBarTop ?? this.bottomNavigationBarTop,
      floatingActionButtonArea: floatingActionButtonArea ?? this.floatingActionButtonArea,
245 246
    );
  }
247 248
}

249 250 251
class _ScaffoldGeometryNotifier extends ChangeNotifier implements ValueListenable<ScaffoldGeometry> {
  _ScaffoldGeometryNotifier(this.geometry, this.context)
    : assert (context != null);
252 253

  final BuildContext context;
254
  double floatingActionButtonScale;
255
  ScaffoldGeometry geometry;
256 257 258 259 260 261

  @override
  ScaffoldGeometry get value {
    assert(() {
      final RenderObject renderObject = context.findRenderObject();
      if (renderObject == null || !renderObject.owner.debugDoingPaint)
262
        throw FlutterError(
263 264 265 266 267 268
            'Scaffold.geometryOf() must only be accessed during the paint phase.\n'
            'The ScaffoldGeometry is only available during the paint phase, because\n'
            'its value is computed during the animation and layout phases prior to painting.'
        );
      return true;
    }());
269
    return geometry._scaleFloatingActionButton(floatingActionButtonScale);
270 271 272 273 274 275 276
  }

  void _updateWith({
    double bottomNavigationBarTop,
    Rect floatingActionButtonArea,
    double floatingActionButtonScale,
  }) {
277
    this.floatingActionButtonScale = floatingActionButtonScale ?? this.floatingActionButtonScale;
278 279 280
    geometry = geometry.copyWith(
      bottomNavigationBarTop: bottomNavigationBarTop,
      floatingActionButtonArea: floatingActionButtonArea,
281
    );
282
    notifyListeners();
283 284 285
  }
}

286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
// Used to communicate the height of the Scaffold's bottomNavigationBar and
// persistentFooterButtons to the LayoutBuilder which builds the Scaffold's body.
//
// Scaffold expects a _BodyBoxConstraints to be passed to the _BodyBuilder
// widget's LayoutBuilder, see _ScaffoldLayout.performLayout(). The BoxConstraints
// methods that construct new BoxConstraints objects, like copyWith() have not
// been overridden here because we expect the _BodyBoxConstraintsObject to be
// passed along unmodified to the LayoutBuilder. If that changes in the future
// then _BodyBuilder will assert.
class _BodyBoxConstraints extends BoxConstraints {
  const _BodyBoxConstraints({
    double minWidth = 0.0,
    double maxWidth = double.infinity,
    double minHeight = 0.0,
    double maxHeight = double.infinity,
    @required this.bottomWidgetsHeight,
302
    @required this.appBarHeight,
303 304
  }) : assert(bottomWidgetsHeight != null),
       assert(bottomWidgetsHeight >= 0),
305 306
       assert(appBarHeight != null),
       assert(appBarHeight >= 0),
307
       super(minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight);
308 309

  final double bottomWidgetsHeight;
310
  final double appBarHeight;
311 312 313 314 315 316 317 318 319 320

  // RenderObject.layout() will only short-circuit its call to its performLayout
  // method if the new layout constraints are not == to the current constraints.
  // If the height of the bottom widgets has changed, even though the constraints'
  // min and max values have not, we still want performLayout to happen.
  @override
  bool operator ==(dynamic other) {
    if (super != other)
      return false;
    final _BodyBoxConstraints typedOther = other;
321 322
    return bottomWidgetsHeight == typedOther.bottomWidgetsHeight
        && appBarHeight == typedOther.appBarHeight;
323 324 325 326
  }

  @override
  int get hashCode {
327
    return hashValues(super.hashCode, bottomWidgetsHeight, appBarHeight);
328 329 330 331 332 333 334 335 336 337
  }
}

// Used when Scaffold.extendBody is true to wrap the scaffold's body in a MediaQuery
// whose padding accounts for the height of the bottomNavigationBar and/or the
// persistentFooterButtons.
//
// The bottom widgets' height is passed along via the _BodyBoxConstraints parameter.
// The constraints parameter is constructed in_ScaffoldLayout.performLayout().
class _BodyBuilder extends StatelessWidget {
338 339 340 341
  const _BodyBuilder({
    Key key,
    @required this.extendBody,
    @required this.extendBodyBehindAppBar,
342
    @required this.body,
343 344 345 346
  }) : assert(extendBody != null),
       assert(extendBodyBehindAppBar != null),
       assert(body != null),
       super(key: key);
347 348

  final Widget body;
349 350
  final bool extendBody;
  final bool extendBodyBehindAppBar;
351 352 353

  @override
  Widget build(BuildContext context) {
354 355 356
    if (!extendBody && !extendBodyBehindAppBar)
      return body;

357 358 359 360
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
        final _BodyBoxConstraints bodyConstraints = constraints;
        final MediaQueryData metrics = MediaQuery.of(context);
361 362 363 364 365 366 367 368 369

        final double bottom = extendBody
          ? math.max(metrics.padding.bottom, bodyConstraints.bottomWidgetsHeight)
          : metrics.padding.bottom;

        final double top = extendBodyBehindAppBar
          ? math.max(metrics.padding.top, bodyConstraints.appBarHeight)
          : metrics.padding.top;

370 371 372
        return MediaQuery(
          data: metrics.copyWith(
            padding: metrics.padding.copyWith(
373 374
              top: top,
              bottom: bottom,
375 376 377 378 379 380 381 382 383
            ),
          ),
          child: body,
        );
      },
    );
  }
}

384
class _ScaffoldLayout extends MultiChildLayoutDelegate {
385
  _ScaffoldLayout({
386
    @required this.minInsets,
387
    @required this.textDirection,
388
    @required this.geometryNotifier,
389 390 391 392 393
    // for floating action button
    @required this.previousFloatingActionButtonLocation,
    @required this.currentFloatingActionButtonLocation,
    @required this.floatingActionButtonMoveAnimationProgress,
    @required this.floatingActionButtonMotionAnimator,
394
    @required this.isSnackBarFloating,
395
    @required this.extendBody,
396
    @required this.extendBodyBehindAppBar,
397 398 399 400
  }) : assert(minInsets != null),
       assert(textDirection != null),
       assert(geometryNotifier != null),
       assert(previousFloatingActionButtonLocation != null),
401
       assert(currentFloatingActionButtonLocation != null),
402 403
       assert(extendBody != null),
       assert(extendBodyBehindAppBar != null);
404

405
  final bool extendBody;
406
  final bool extendBodyBehindAppBar;
407
  final EdgeInsets minInsets;
408
  final TextDirection textDirection;
409
  final _ScaffoldGeometryNotifier geometryNotifier;
410

411 412 413 414 415
  final FloatingActionButtonLocation previousFloatingActionButtonLocation;
  final FloatingActionButtonLocation currentFloatingActionButtonLocation;
  final double floatingActionButtonMoveAnimationProgress;
  final FloatingActionButtonAnimator floatingActionButtonMotionAnimator;

416 417
  final bool isSnackBarFloating;

418
  @override
419
  void performLayout(Size size) {
420
    final BoxConstraints looseConstraints = BoxConstraints.loose(size);
421

422
    // This part of the layout has the same effect as putting the app bar and
423
    // body in a column and making the body flexible. What's different is that
424
    // in this case the app bar appears _after_ the body in the stacking order,
425
    // so the app bar's shadow is drawn on top of the body.
426

427
    final BoxConstraints fullWidthConstraints = looseConstraints.tighten(width: size.width);
428
    final double bottom = size.height;
429
    double contentTop = 0.0;
430
    double bottomWidgetsHeight = 0.0;
431
    double appBarHeight = 0.0;
432

433
    if (hasChild(_ScaffoldSlot.appBar)) {
434 435
      appBarHeight = layoutChild(_ScaffoldSlot.appBar, fullWidthConstraints).height;
      contentTop = extendBodyBehindAppBar ? 0.0 : appBarHeight;
436
      positionChild(_ScaffoldSlot.appBar, Offset.zero);
437 438
    }

439
    double bottomNavigationBarTop;
440 441
    if (hasChild(_ScaffoldSlot.bottomNavigationBar)) {
      final double bottomNavigationBarHeight = layoutChild(_ScaffoldSlot.bottomNavigationBar, fullWidthConstraints).height;
442
      bottomWidgetsHeight += bottomNavigationBarHeight;
443
      bottomNavigationBarTop = math.max(0.0, bottom - bottomWidgetsHeight);
444
      positionChild(_ScaffoldSlot.bottomNavigationBar, Offset(0.0, bottomNavigationBarTop));
445 446
    }

447
    if (hasChild(_ScaffoldSlot.persistentFooter)) {
448
      final BoxConstraints footerConstraints = BoxConstraints(
Ian Hickson's avatar
Ian Hickson committed
449
        maxWidth: fullWidthConstraints.maxWidth,
450
        maxHeight: math.max(0.0, bottom - bottomWidgetsHeight - contentTop),
Ian Hickson's avatar
Ian Hickson committed
451 452
      );
      final double persistentFooterHeight = layoutChild(_ScaffoldSlot.persistentFooter, footerConstraints).height;
453
      bottomWidgetsHeight += persistentFooterHeight;
454
      positionChild(_ScaffoldSlot.persistentFooter, Offset(0.0, math.max(0.0, bottom - bottomWidgetsHeight)));
455 456
    }

457 458 459
    // Set the content bottom to account for the greater of the height of any
    // bottom-anchored material widgets or of the keyboard or other
    // bottom-anchored system UI.
460
    final double contentBottom = math.max(0.0, bottom - math.max(minInsets.bottom, bottomWidgetsHeight));
461

462
    if (hasChild(_ScaffoldSlot.body)) {
463 464 465 466
      double bodyMaxHeight = math.max(0.0, contentBottom - contentTop);

      if (extendBody) {
        bodyMaxHeight += bottomWidgetsHeight;
467
        bodyMaxHeight = bodyMaxHeight.clamp(0.0, looseConstraints.maxHeight - contentTop).toDouble();
468 469 470 471
        assert(bodyMaxHeight <= math.max(0.0, looseConstraints.maxHeight - contentTop));
      }

      final BoxConstraints bodyConstraints = _BodyBoxConstraints(
472
        maxWidth: fullWidthConstraints.maxWidth,
473 474
        maxHeight: bodyMaxHeight,
        bottomWidgetsHeight: extendBody ? bottomWidgetsHeight : 0.0,
475
        appBarHeight: appBarHeight,
476
      );
477
      layoutChild(_ScaffoldSlot.body, bodyConstraints);
478
      positionChild(_ScaffoldSlot.body, Offset(0.0, contentTop));
479
    }
480 481

    // The BottomSheet and the SnackBar are anchored to the bottom of the parent,
482 483 484 485
    // 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.
    //
486 487
    // 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
488
    // kFloatingActionButtonMargin above the SnackBar, whichever puts the FAB
489 490
    // 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
491
    // by kFloatingActionButtonMargin.
492

493 494
    Size bottomSheetSize = Size.zero;
    Size snackBarSize = Size.zero;
495 496 497 498 499 500 501 502
    if (hasChild(_ScaffoldSlot.bodyScrim)) {
      final BoxConstraints bottomSheetScrimConstraints = BoxConstraints(
        maxWidth: fullWidthConstraints.maxWidth,
        maxHeight: contentBottom,
      );
      layoutChild(_ScaffoldSlot.bodyScrim, bottomSheetScrimConstraints);
      positionChild(_ScaffoldSlot.bodyScrim, Offset.zero);
    }
503

504 505 506 507 508 509
    // Set the size of the SnackBar early if the behavior is fixed so
    // the FAB can be positioned correctly.
    if (hasChild(_ScaffoldSlot.snackBar) && !isSnackBarFloating) {
      snackBarSize = layoutChild(_ScaffoldSlot.snackBar, fullWidthConstraints);
    }

510
    if (hasChild(_ScaffoldSlot.bottomSheet)) {
511
      final BoxConstraints bottomSheetConstraints = BoxConstraints(
Ian Hickson's avatar
Ian Hickson committed
512 513 514 515
        maxWidth: fullWidthConstraints.maxWidth,
        maxHeight: math.max(0.0, contentBottom - contentTop),
      );
      bottomSheetSize = layoutChild(_ScaffoldSlot.bottomSheet, bottomSheetConstraints);
516
      positionChild(_ScaffoldSlot.bottomSheet, Offset((size.width - bottomSheetSize.width) / 2.0, contentBottom - bottomSheetSize.height));
517 518
    }

519
    Rect floatingActionButtonRect;
520
    if (hasChild(_ScaffoldSlot.floatingActionButton)) {
521
      final Size fabSize = layoutChild(_ScaffoldSlot.floatingActionButton, looseConstraints);
522

523 524
      // To account for the FAB position being changed, we'll animate between
      // the old and new positions.
525
      final ScaffoldPrelayoutGeometry currentGeometry = ScaffoldPrelayoutGeometry(
526 527 528 529 530 531 532 533 534 535 536 537
        bottomSheetSize: bottomSheetSize,
        contentBottom: contentBottom,
        contentTop: contentTop,
        floatingActionButtonSize: fabSize,
        minInsets: minInsets,
        scaffoldSize: size,
        snackBarSize: snackBarSize,
        textDirection: textDirection,
      );
      final Offset currentFabOffset = currentFloatingActionButtonLocation.getOffset(currentGeometry);
      final Offset previousFabOffset = previousFloatingActionButtonLocation.getOffset(currentGeometry);
      final Offset fabOffset = floatingActionButtonMotionAnimator.getOffset(
538 539
        begin: previousFabOffset,
        end: currentFabOffset,
540 541 542 543
        progress: floatingActionButtonMoveAnimationProgress,
      );
      positionChild(_ScaffoldSlot.floatingActionButton, fabOffset);
      floatingActionButtonRect = fabOffset & fabSize;
544
    }
545

546 547 548 549 550 551 552 553 554 555
    if (hasChild(_ScaffoldSlot.snackBar)) {
      if (snackBarSize == Size.zero) {
        snackBarSize = layoutChild(_ScaffoldSlot.snackBar, fullWidthConstraints);
      }
      final double snackBarYOffsetBase = floatingActionButtonRect != null && isSnackBarFloating
        ? floatingActionButtonRect.top
        : contentBottom;
      positionChild(_ScaffoldSlot.snackBar, Offset(0.0, snackBarYOffsetBase - snackBarSize.height));
    }

556
    if (hasChild(_ScaffoldSlot.statusBar)) {
557
      layoutChild(_ScaffoldSlot.statusBar, fullWidthConstraints.tighten(height: minInsets.top));
558 559 560
      positionChild(_ScaffoldSlot.statusBar, Offset.zero);
    }

561
    if (hasChild(_ScaffoldSlot.drawer)) {
562
      layoutChild(_ScaffoldSlot.drawer, BoxConstraints.tight(size));
563
      positionChild(_ScaffoldSlot.drawer, Offset.zero);
564
    }
565 566

    if (hasChild(_ScaffoldSlot.endDrawer)) {
567
      layoutChild(_ScaffoldSlot.endDrawer, BoxConstraints.tight(size));
568 569
      positionChild(_ScaffoldSlot.endDrawer, Offset.zero);
    }
570 571 572 573 574

    geometryNotifier._updateWith(
      bottomNavigationBarTop: bottomNavigationBarTop,
      floatingActionButtonArea: floatingActionButtonRect,
    );
Hans Muller's avatar
Hans Muller committed
575
  }
576

577
  @override
578
  bool shouldRelayout(_ScaffoldLayout oldDelegate) {
579 580 581 582
    return oldDelegate.minInsets != minInsets
        || oldDelegate.textDirection != textDirection
        || oldDelegate.floatingActionButtonMoveAnimationProgress != floatingActionButtonMoveAnimationProgress
        || oldDelegate.previousFloatingActionButtonLocation != previousFloatingActionButtonLocation
583 584 585
        || oldDelegate.currentFloatingActionButtonLocation != currentFloatingActionButtonLocation
        || oldDelegate.extendBody != extendBody
        || oldDelegate.extendBodyBehindAppBar != extendBodyBehindAppBar;
586
  }
Hans Muller's avatar
Hans Muller committed
587 588
}

589 590 591
/// Handler for scale and rotation animations in the [FloatingActionButton].
///
/// Currently, there are two types of [FloatingActionButton] animations:
592
///
593 594 595 596
/// * Entrance/Exit animations, which this widget triggers
///   when the [FloatingActionButton] is added, updated, or removed.
/// * Motion animations, which are triggered by the [Scaffold]
///   when its [FloatingActionButtonLocation] is updated.
597
class _FloatingActionButtonTransition extends StatefulWidget {
598
  const _FloatingActionButtonTransition({
599
    Key key,
600 601 602 603
    @required this.child,
    @required this.fabMoveAnimation,
    @required this.fabMotionAnimator,
    @required this.geometryNotifier,
604
    @required this.currentController,
605
  }) : assert(fabMoveAnimation != null),
606
       assert(fabMotionAnimator != null),
607
       assert(currentController != null),
608
       super(key: key);
609 610

  final Widget child;
611 612
  final Animation<double> fabMoveAnimation;
  final FloatingActionButtonAnimator fabMotionAnimator;
613
  final _ScaffoldGeometryNotifier geometryNotifier;
614

615 616 617
  /// Controls the current child widget.child as it exits.
  final AnimationController currentController;

618
  @override
619
  _FloatingActionButtonTransitionState createState() => _FloatingActionButtonTransitionState();
620 621
}

622
class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTransition> with TickerProviderStateMixin {
623
  // The animations applied to the Floating Action Button when it is entering or exiting.
624
  // Controls the previous widget.child as it exits.
625
  AnimationController _previousController;
626 627 628 629
  Animation<double> _previousScaleAnimation;
  Animation<double> _previousRotationAnimation;
  // The animations to run, considering the widget's fabMoveAnimation and the current/previous entrance/exit animations.
  Animation<double> _currentScaleAnimation;
630
  Animation<double> _extendedCurrentScaleAnimation;
631
  Animation<double> _currentRotationAnimation;
632
  Widget _previousChild;
633

634
  @override
635 636
  void initState() {
    super.initState();
637

638
    _previousController = AnimationController(
639
      duration: kFloatingActionButtonSegue,
640
      vsync: this,
641 642
    )..addStatusListener(_handlePreviousAnimationStatusChanged);
    _updateAnimations();
643

644 645 646
    if (widget.child != null) {
      // If we start out with a child, have the child appear fully visible instead
      // of animating in.
647
      widget.currentController.value = 1.0;
648
    } else {
649 650 651 652
      // If we start without a child we update the geometry object with a
      // floating action button scale of 0, as it is not showing on the screen.
      _updateGeometryScale(0.0);
    }
653 654
  }

655
  @override
656
  void dispose() {
657
    _previousController.dispose();
658 659 660
    super.dispose();
  }

661
  @override
662
  void didUpdateWidget(_FloatingActionButtonTransition oldWidget) {
663
    super.didUpdateWidget(oldWidget);
664 665 666
    final bool oldChildIsNull = oldWidget.child == null;
    final bool newChildIsNull = widget.child == null;
    if (oldChildIsNull == newChildIsNull && oldWidget.child?.key == widget.child?.key)
667
      return;
668
    if (oldWidget.fabMotionAnimator != widget.fabMotionAnimator || oldWidget.fabMoveAnimation != widget.fabMoveAnimation) {
669 670 671
      // Get the right scale and rotation animations to use for this widget.
      _updateAnimations();
    }
672
    if (_previousController.status == AnimationStatus.dismissed) {
673
      final double currentValue = widget.currentController.value;
674
      if (currentValue == 0.0 || oldWidget.child == null) {
675 676 677
        // The current child hasn't started its entrance animation yet. We can
        // just skip directly to the new child's entrance.
        _previousChild = null;
678
        if (widget.child != null)
679
          widget.currentController.forward();
680 681 682 683
      } 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.
684
        _previousChild = oldWidget.child;
685 686 687
        _previousController
          ..value = currentValue
          ..reverse();
688
        widget.currentController.value = 0.0;
689 690 691 692
      }
    }
  }

693 694 695 696 697
  static final Animatable<double> _entranceTurnTween = Tween<double>(
    begin: 1.0 - kFloatingActionButtonTurnInterval,
    end: 1.0,
  ).chain(CurveTween(curve: Curves.easeIn));

698 699
  void _updateAnimations() {
    // Get the animations for exit and entrance.
700
    final CurvedAnimation previousExitScaleAnimation = CurvedAnimation(
701 702 703
      parent: _previousController,
      curve: Curves.easeIn,
    );
704 705
    final Animation<double> previousExitRotationAnimation = Tween<double>(begin: 1.0, end: 1.0).animate(
      CurvedAnimation(
706 707 708
        parent: _previousController,
        curve: Curves.easeIn,
      ),
709 710
    );

711
    final CurvedAnimation currentEntranceScaleAnimation = CurvedAnimation(
712
      parent: widget.currentController,
713 714
      curve: Curves.easeIn,
    );
715
    final Animation<double> currentEntranceRotationAnimation = widget.currentController.drive(_entranceTurnTween);
716 717 718 719

    // Get the animations for when the FAB is moving.
    final Animation<double> moveScaleAnimation = widget.fabMotionAnimator.getScaleAnimation(parent: widget.fabMoveAnimation);
    final Animation<double> moveRotationAnimation = widget.fabMotionAnimator.getRotationAnimation(parent: widget.fabMoveAnimation);
720

721
    // Aggregate the animations.
722 723
    _previousScaleAnimation = AnimationMin<double>(moveScaleAnimation, previousExitScaleAnimation);
    _currentScaleAnimation = AnimationMin<double>(moveScaleAnimation, currentEntranceScaleAnimation);
724
    _extendedCurrentScaleAnimation = _currentScaleAnimation.drive(CurveTween(curve: const Interval(0.0, 0.1)));
725

726 727
    _previousRotationAnimation = TrainHoppingAnimation(previousExitRotationAnimation, moveRotationAnimation);
    _currentRotationAnimation = TrainHoppingAnimation(currentEntranceRotationAnimation, moveRotationAnimation);
728 729 730 731 732 733

    _currentScaleAnimation.addListener(_onProgressChanged);
    _previousScaleAnimation.addListener(_onProgressChanged);
  }

  void _handlePreviousAnimationStatusChanged(AnimationStatus status) {
734 735
    setState(() {
      if (status == AnimationStatus.dismissed) {
736
        assert(widget.currentController.status == AnimationStatus.dismissed);
737
        if (widget.child != null)
738
          widget.currentController.forward();
739 740
      }
    });
741 742
  }

743 744 745 746 747 748 749
  bool _isExtendedFloatingActionButton(Widget widget) {
    if (widget is! FloatingActionButton)
      return false;
    final FloatingActionButton fab = widget;
    return fab.isExtended;
  }

750
  @override
751
  Widget build(BuildContext context) {
752
    return Stack(
753
      alignment: Alignment.centerRight,
754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785
      children: <Widget>[
        if (_previousController.status != AnimationStatus.dismissed)
          if (_isExtendedFloatingActionButton(_previousChild))
            FadeTransition(
              opacity: _previousScaleAnimation,
              child: _previousChild,
            )
          else
            ScaleTransition(
              scale: _previousScaleAnimation,
              child: RotationTransition(
                turns: _previousRotationAnimation,
                child: _previousChild,
              ),
            ),
        if (_isExtendedFloatingActionButton(widget.child))
          ScaleTransition(
            scale: _extendedCurrentScaleAnimation,
            child: FadeTransition(
              opacity: _currentScaleAnimation,
              child: widget.child,
            ),
          )
        else
          ScaleTransition(
            scale: _currentScaleAnimation,
            child: RotationTransition(
              turns: _currentRotationAnimation,
              child: widget.child,
            ),
          ),
      ],
786
    );
787
  }
788 789

  void _onProgressChanged() {
790
    _updateGeometryScale(math.max(_previousScaleAnimation.value, _currentScaleAnimation.value));
791 792 793 794 795 796 797
  }

  void _updateGeometryScale(double scale) {
    widget.geometryNotifier._updateWith(
      floatingActionButtonScale: scale,
    );
  }
798 799
}

800 801
/// Implements the basic material design visual layout structure.
///
802
/// This class provides APIs for showing drawers, snack bars, and bottom sheets.
803
///
804 805 806 807
/// 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.
///
808
/// {@tool snippet --template=stateful_widget_material}
809 810 811 812 813
/// This example shows a [Scaffold] with a [body] and [FloatingActionButton].
/// The [body] is a [Text] placed in a [Center] in order to center the text
/// within the [Scaffold]. The [FloatingActionButton] is connected to a
/// callback that increments a counter.
///
814
/// ![The Scaffold has a white background with a blue AppBar at the top. A blue FloatingActionButton is positioned at the bottom right corner of the Scaffold.](https://flutter.github.io/assets-for-api-docs/assets/material/scaffold.png)
815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837
///
/// ```dart
/// int _count = 0;
///
/// Widget build(BuildContext context) {
///   return Scaffold(
///     appBar: AppBar(
///       title: const Text('Sample Code'),
///     ),
///     body: Center(
///       child: Text('You have pressed the button $_count times.')
///     ),
///     floatingActionButton: FloatingActionButton(
///       onPressed: () => setState(() => _count++),
///       tooltip: 'Increment Counter',
///       child: const Icon(Icons.add),
///     ),
///   );
/// }
/// ```
/// {@end-tool}
///
/// {@tool snippet --template=stateful_widget_material}
838 839 840 841
/// This example shows a [Scaffold] with a blueGrey [backgroundColor], [body]
/// and [FloatingActionButton]. The [body] is a [Text] placed in a [Center] in
/// order to center the text within the [Scaffold]. The [FloatingActionButton]
/// is connected to a callback that increments a counter.
842
///
843
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/scaffold_background_color.png)
844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867
///
/// ```dart
/// int _count = 0;
///
/// Widget build(BuildContext context) {
///   return Scaffold(
///     appBar: AppBar(
///       title: const Text('Sample Code'),
///     ),
///     body: Center(
///       child: Text('You have pressed the button $_count times.')
///     ),
///     backgroundColor: Colors.blueGrey.shade200,
///     floatingActionButton: FloatingActionButton(
///       onPressed: () => setState(() => _count++),
///       tooltip: 'Increment Counter',
///       child: const Icon(Icons.add),
///     ),
///   );
/// }
/// ```
/// {@end-tool}
///
/// {@tool snippet --template=stateful_widget_material}
868 869
/// This example shows a [Scaffold] with an [AppBar], a [BottomAppBar] and a
/// [FloatingActionButton]. The [body] is a [Text] placed in a [Center] in order
870
/// to center the text within the [Scaffold]. The [FloatingActionButton] is
871 872 873
/// centered and docked within the [BottomAppBar] using
/// [FloatingActionButtonLocation.centerDocked]. The [FloatingActionButton] is
/// connected to a callback that increments a counter.
874
///
875
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/scaffold_bottom_app_bar.png)
876
///
877
/// ```dart
878 879 880 881 882 883 884 885 886 887 888
/// int _count = 0;
///
/// Widget build(BuildContext context) {
///   return Scaffold(
///     appBar: AppBar(
///       title: Text('Sample Code'),
///     ),
///     body: Center(
///       child: Text('You have pressed the button $_count times.'),
///     ),
///     bottomNavigationBar: BottomAppBar(
889
///       shape: const CircularNotchedRectangle(),
890 891 892 893 894 895 896 897 898 899 900 901
///       child: Container(height: 50.0,),
///     ),
///     floatingActionButton: FloatingActionButton(
///       onPressed: () => setState(() {
///         _count++;
///       }),
///       tooltip: 'Increment Counter',
///       child: Icon(Icons.add),
///     ),
///     floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
///   );
/// }
902
/// ```
903
/// {@end-tool}
904
///
905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936
/// ## Scaffold layout, the keyboard, and display "notches"
///
/// The scaffold will expand to fill the available space. That usually
/// means that it will occupy its entire window or device screen. When
/// the device's keyboard appears the Scaffold's ancestor [MediaQuery]
/// widget's [MediaQueryData.viewInsets] changes and the Scaffold will
/// be rebuilt. By default the scaffold's [body] is resized to make
/// room for the keyboard. To prevent the resize set
/// [resizeToAvoidBottomInset] to false. In either case the focused
/// widget will be scrolled into view if it's within a scrollable
/// container.
///
/// The [MediaQueryData.padding] value defines areas that might
/// not be completely visible, like the display "notch" on the iPhone
/// X. The scaffold's [body] is not inset by this padding value
/// although an [appBar] or [bottomNavigationBar] will typically
/// cause the body to avoid the padding. The [SafeArea]
/// widget can be used within the scaffold's body to avoid areas
/// like display notches.
///
/// ## Troubleshooting
///
/// ### Nested Scaffolds
///
/// The Scaffold was designed to be the single top level container for
/// a [MaterialApp] and it's typically not necessary to nest
/// scaffolds. For example in a tabbed UI, where the
/// [bottomNavigationBar] is a [TabBar] and the body is a
/// [TabBarView], you might be tempted to make each tab bar view a
/// scaffold with a differently titled AppBar. It would be better to add a
/// listener to the [TabController] that updates the AppBar.
///
937
/// {@tool sample}
938 939 940 941
/// Add a listener to the app's tab controller so that the [AppBar] title of the
/// app's one and only scaffold is reset each time a new tab is selected.
///
/// ```dart
942
/// TabController(vsync: tickerProvider, length: tabCount)..addListener(() {
943 944 945 946 947 948
///   if (!tabController.indexIsChanging) {
///     setState(() {
///       // Rebuild the enclosing scaffold with a new AppBar title
///       appBarTitle = 'Tab ${tabController.index}';
///     });
///   }
949
/// })
950
/// ```
951
/// {@end-tool}
952 953 954 955 956
///
/// Although there are some use cases, like a presentation app that
/// shows embedded flutter content, where nested scaffolds are
/// appropriate, it's best to avoid nesting scaffolds.
///
957 958
/// See also:
///
959 960
///  * [AppBar], which is a horizontal bar typically shown at the top of an app
///    using the [appBar] property.
961 962
///  * [BottomAppBar], which is a horizontal bar typically shown at the bottom
///    of an app using the [bottomNavigationBar] property.
963 964 965 966 967 968 969 970 971 972 973 974 975 976 977
///  * [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.
978
///  * <https://material.io/design/layout/responsive-layout-grid.html>
979
class Scaffold extends StatefulWidget {
980
  /// Creates a visual scaffold for material design widgets.
981
  const Scaffold({
Adam Barth's avatar
Adam Barth committed
982
    Key key,
983
    this.appBar,
Hixie's avatar
Hixie committed
984
    this.body,
985
    this.floatingActionButton,
986 987
    this.floatingActionButtonLocation,
    this.floatingActionButtonAnimator,
988
    this.persistentFooterButtons,
989
    this.drawer,
990
    this.endDrawer,
991
    this.bottomNavigationBar,
992
    this.bottomSheet,
993
    this.backgroundColor,
994 995
    this.resizeToAvoidBottomPadding,
    this.resizeToAvoidBottomInset,
996
    this.primary = true,
997
    this.drawerDragStartBehavior = DragStartBehavior.start,
998
    this.extendBody = false,
999
    this.extendBodyBehindAppBar = false,
1000
    this.drawerScrimColor,
1001
    this.drawerEdgeDragWidth,
1002
  }) : assert(primary != null),
1003
       assert(extendBody != null),
1004
       assert(extendBodyBehindAppBar != null),
1005 1006
       assert(drawerDragStartBehavior != null),
       super(key: key);
Adam Barth's avatar
Adam Barth committed
1007

1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021
  /// If true, and [bottomNavigationBar] or [persistentFooterButtons]
  /// is specified, then the [body] extends to the bottom of the Scaffold,
  /// instead of only extending to the top of the [bottomNavigationBar]
  /// or the [persistentFooterButtons].
  ///
  /// If true, a [MediaQuery] widget whose bottom padding matches the
  /// the height of the [bottomNavigationBar] will be added above the
  /// scaffold's [body].
  ///
  /// This property is often useful when the [bottomNavigationBar] has
  /// a non-rectangular shape, like [CircularNotchedRectangle], which
  /// adds a [FloatingActionButton] sized notch to the top edge of the bar.
  /// In this case specifying `extendBody: true` ensures that that scaffold's
  /// body will be visible through the bottom navigation bar's notch.
1022 1023 1024 1025 1026
  ///
  /// See also:
  ///
  ///  * [extendBodyBehindAppBar], which extends the height of the body
  ///    to the top of the scaffold.
1027 1028
  final bool extendBody;

1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043
  /// If true, and an [appBar] is specified, then the height of the [body] is
  /// extended to include the height of the app bar and the top of the body
  /// is aligned with the top of the app bar.
  ///
  /// This is useful if the app bar's [AppBar.backgroundColor] is not
  /// completely opaque.
  ///
  /// This property is false by default. It must not be null.
  ///
  /// See also:
  ///
  ///  * [extendBody], which extends the height of the body to the bottom
  ///    of the scaffold.
  final bool extendBodyBehindAppBar;

1044
  /// An app bar to display at the top of the scaffold.
1045
  final PreferredSizeWidget appBar;
1046 1047 1048

  /// The primary content of the scaffold.
  ///
1049 1050 1051 1052 1053
  /// Displayed below the [appBar], above the bottom of the ambient
  /// [MediaQuery]'s [MediaQueryData.viewInsets], and behind the
  /// [floatingActionButton] and [drawer]. If [resizeToAvoidBottomInset] is
  /// false then the body is not resized when the onscreen keyboard appears,
  /// i.e. it is not inset by `viewInsets.bottom`.
1054
  ///
1055 1056 1057
  /// 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
1058 1059
  /// having that be the body. To expand this widget instead, consider
  /// putting it in a [SizedBox.expand].
1060 1061 1062
  ///
  /// 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
1063
  /// [ListView] as the body of the scaffold. This is also a good choice for
1064
  /// the case where your body is a scrollable list.
Hixie's avatar
Hixie committed
1065
  final Widget body;
1066

1067
  /// A button displayed floating above [body], in the bottom right corner.
1068 1069
  ///
  /// Typically a [FloatingActionButton].
Adam Barth's avatar
Adam Barth committed
1070
  final Widget floatingActionButton;
1071

1072
  /// Responsible for determining where the [floatingActionButton] should go.
1073
  ///
1074 1075 1076 1077
  /// If null, the [ScaffoldState] will use the default location, [FloatingActionButtonLocation.endFloat].
  final FloatingActionButtonLocation floatingActionButtonLocation;

  /// Animator to move the [floatingActionButton] to a new [floatingActionButtonLocation].
1078
  ///
1079 1080 1081
  /// If null, the [ScaffoldState] will use the default animator, [FloatingActionButtonAnimator.scaling].
  final FloatingActionButtonAnimator floatingActionButtonAnimator;

1082 1083 1084
  /// 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
1085
  /// persistently visible, even if the [body] of the scaffold scrolls.
1086 1087 1088
  ///
  /// These widgets will be wrapped in a [ButtonBar].
  ///
Ian Hickson's avatar
Ian Hickson committed
1089 1090
  /// The [persistentFooterButtons] are rendered above the
  /// [bottomNavigationBar] but below the [body].
1091 1092
  final List<Widget> persistentFooterButtons;

1093
  /// A panel displayed to the side of the [body], often hidden on mobile
1094 1095
  /// devices. Swipes in from either left-to-right ([TextDirection.ltr]) or
  /// right-to-left ([TextDirection.rtl])
1096
  ///
1097 1098
  /// In the uncommon case that you wish to open the drawer manually, use the
  /// [ScaffoldState.openDrawer] function.
1099 1100
  ///
  /// Typically a [Drawer].
1101
  final Widget drawer;
1102

1103 1104 1105 1106 1107
  /// 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
1108
  /// [ScaffoldState.openEndDrawer] function.
1109 1110 1111 1112
  ///
  /// Typically a [Drawer].
  final Widget endDrawer;

1113 1114 1115 1116 1117
  /// The color to use for the scrim that obscures primary content while a drawer is open.
  ///
  /// By default, the color is [Colors.black54]
  final Color drawerScrimColor;

1118 1119 1120 1121 1122
  /// The color of the [Material] widget that underlies the entire Scaffold.
  ///
  /// The theme's [ThemeData.scaffoldBackgroundColor] by default.
  final Color backgroundColor;

1123 1124
  /// A bottom navigation bar to display at the bottom of the scaffold.
  ///
1125 1126
  /// Snack bars slide from underneath the bottom navigation bar while bottom
  /// sheets are stacked on top.
Ian Hickson's avatar
Ian Hickson committed
1127 1128 1129
  ///
  /// The [bottomNavigationBar] is rendered below the [persistentFooterButtons]
  /// and the [body].
1130 1131
  final Widget bottomNavigationBar;

1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162
  /// The persistent bottom sheet to display.
  ///
  /// 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.
  ///
  /// Unlike the persistent bottom sheet displayed by [showBottomSheet]
  /// this bottom sheet is not a [LocalHistoryEntry] and cannot be dismissed
  /// with the scaffold appbar's back button.
  ///
  /// If a persistent bottom sheet created with [showBottomSheet] is already
  /// visible, it must be closed before building the Scaffold with a new
  /// [bottomSheet].
  ///
  /// The value of [bottomSheet] can be any widget at all. It's unlikely to
  /// actually be a [BottomSheet], which is used by the implementations of
  /// [showBottomSheet] and [showModalBottomSheet]. Typically it's a widget
  /// that includes [Material].
  ///
  /// See also:
  ///
  ///  * [showBottomSheet], which displays a bottom sheet as a route that can
  ///    be dismissed with the scaffold's back button.
  ///  * [showModalBottomSheet], which displays a modal bottom sheet.
  final Widget bottomSheet;

1163 1164 1165 1166 1167 1168
  /// This flag is deprecated, please use [resizeToAvoidBottomInset]
  /// instead.
  ///
  /// Originally the name referred [MediaQueryData.padding]. Now it refers
  /// [MediaQueryData.viewInsets], so using [resizeToAvoidBottomInset]
  /// should be clearer to readers.
1169 1170 1171 1172
  @Deprecated(
    'Use resizeToAvoidBottomInset to specify if the body should resize when the keyboard appears. '
    'This feature was deprecated after v1.1.9.'
  )
1173 1174 1175 1176 1177
  final bool resizeToAvoidBottomPadding;

  /// If true the [body] and the scaffold's floating widgets should size
  /// themselves to avoid the onscreen keyboard whose height is defined by the
  /// ambient [MediaQuery]'s [MediaQueryData.viewInsets] `bottom` property.
1178 1179 1180 1181 1182 1183
  ///
  /// 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.
1184
  final bool resizeToAvoidBottomInset;
1185

1186 1187 1188 1189 1190 1191 1192 1193 1194
  /// 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;

1195 1196 1197
  /// {@macro flutter.material.drawer.dragStartBehavior}
  final DragStartBehavior drawerDragStartBehavior;

1198 1199 1200 1201 1202 1203 1204 1205 1206 1207
  /// The width of the area within which a horizontal swipe will open the
  /// drawer.
  ///
  /// By default, the value used is 20.0 added to the padding edge of
  /// `MediaQuery.of(context).padding` that corresponds to [alignment].
  /// This ensures that the drag area for notched devices is not obscured. For
  /// example, if `TextDirection.of(context)` is set to [TextDirection.ltr],
  /// 20.0 will be added to `MediaQuery.of(context).padding.left`.
  final double drawerEdgeDragWidth;

1208
  /// The state from the closest instance of this class that encloses the given context.
1209
  ///
1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240
  /// {@tool snippet --template=freeform}
  /// Typical usage of the [Scaffold.of] function is to call it from within the
  /// `build` method of a child of a [Scaffold].
  ///
  /// ```dart imports
  /// import 'package:flutter/material.dart';
  /// ```
  ///
  /// ```dart main
  /// void main() => runApp(MyApp());
  /// ```
  ///
  /// ```dart preamble
  /// class MyApp extends StatelessWidget {
  ///   // This widget is the root of your application.
  ///   @override
  ///   Widget build(BuildContext context) {
  ///     return MaterialApp(
  ///       title: 'Flutter Code Sample for Scaffold.of.',
  ///       theme: ThemeData(
  ///         primarySwatch: Colors.blue,
  ///       ),
  ///       home: Scaffold(
  ///         body: MyScaffoldBody(),
  ///         appBar: AppBar(title: Text('Scaffold.of Example')),
  ///       ),
  ///       color: Colors.white,
  ///     );
  ///   }
  /// }
  /// ```
1241 1242
  ///
  /// ```dart
1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258
  /// class MyScaffoldBody extends StatelessWidget {
  ///   @override
  ///   Widget build(BuildContext context) {
  ///     return Center(
  ///       child: RaisedButton(
  ///         child: Text('SHOW A SNACKBAR'),
  ///         onPressed: () {
  ///           Scaffold.of(context).showSnackBar(
  ///             SnackBar(
  ///               content: Text('Have a snack!'),
  ///             ),
  ///           );
  ///         },
  ///       ),
  ///     );
  ///   }
1259 1260
  /// }
  /// ```
1261
  /// {@end-tool}
1262
  ///
1263
  /// {@tool snippet --template=stateless_widget_material}
1264 1265
  /// 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
1266 1267 1268 1269
  /// [Scaffold] (since it's "above" the widget being returned in the widget
  /// tree). 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]:
1270 1271 1272
  ///
  /// ```dart
  /// Widget build(BuildContext context) {
1273 1274 1275
  ///   return Scaffold(
  ///     appBar: AppBar(
  ///       title: Text('Demo')
1276
  ///     ),
1277
  ///     body: Builder(
1278 1279 1280
  ///       // Create an inner BuildContext so that the onPressed methods
  ///       // can refer to the Scaffold with Scaffold.of().
  ///       builder: (BuildContext context) {
1281 1282 1283
  ///         return Center(
  ///           child: RaisedButton(
  ///             child: Text('SHOW A SNACKBAR'),
1284
  ///             onPressed: () {
1285
  ///               Scaffold.of(context).showSnackBar(SnackBar(
1286
  ///                 content: Text('Have a snack!'),
1287 1288 1289 1290 1291 1292 1293 1294 1295
  ///               ));
  ///             },
  ///           ),
  ///         );
  ///       },
  ///     ),
  ///   );
  /// }
  /// ```
1296
  /// {@end-tool}
1297
  ///
1298 1299 1300 1301 1302 1303 1304 1305 1306 1307
  /// 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.
  ///
1308 1309
  /// 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`.
1310
  static ScaffoldState of(BuildContext context, { bool nullOk = false }) {
1311 1312
    assert(nullOk != null);
    assert(context != null);
1313
    final ScaffoldState result = context.ancestorStateOfType(const TypeMatcher<ScaffoldState>());
1314 1315
    if (nullOk || result != null)
      return result;
1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341
    throw FlutterError.fromParts(<DiagnosticsNode>[
      ErrorSummary(
        'Scaffold.of() called with a context that does not contain a Scaffold.'
      ),
      ErrorDescription(
        'No Scaffold ancestor could be found starting from the context that was passed to Scaffold.of(). '
        'This usually happens when the context provided is from the same StatefulWidget as that '
        'whose build function actually creates the Scaffold widget being sought.'
      ),
      ErrorHint(
        '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://api.flutter.dev/flutter/material/Scaffold/of.html'
      ),
      ErrorHint(
        '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.'
      ),
      context.describeElement('The context used was')
    ]);
1342
  }
Hixie's avatar
Hixie committed
1343

1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366
  /// Returns a [ValueListenable] for the [ScaffoldGeometry] for the closest
  /// [Scaffold] ancestor of the given context.
  ///
  /// The [ValueListenable.value] is only available at paint time.
  ///
  /// Notifications are guaranteed to be sent before the first paint pass
  /// with the new geometry, but there is no guarantee whether a build or
  /// layout passes are going to happen between the notification and the next
  /// paint pass.
  ///
  /// The closest [Scaffold] ancestor for the context might change, e.g when
  /// an element is moved from one scaffold to another. For [StatefulWidget]s
  /// using this listenable, a change of the [Scaffold] ancestor will
  /// trigger a [State.didChangeDependencies].
  ///
  /// A typical pattern for listening to the scaffold geometry would be to
  /// call [Scaffold.geometryOf] in [State.didChangeDependencies], compare the
  /// return value with the previous listenable, if it has changed, unregister
  /// the listener, and register a listener to the new [ScaffoldGeometry]
  /// listenable.
  static ValueListenable<ScaffoldGeometry> geometryOf(BuildContext context) {
    final _ScaffoldScope scaffoldScope = context.inheritFromWidgetOfExactType(_ScaffoldScope);
    if (scaffoldScope == null)
1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary(
          'Scaffold.geometryOf() called with a context that does not contain a Scaffold.'
        ),
        ErrorDescription(
          'This usually happens when the context provided is from the same StatefulWidget as that '
          'whose build function actually creates the Scaffold widget being sought.'
        ),
        ErrorHint(
          '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://api.flutter.dev/flutter/material/Scaffold/of.html'
        ),
        ErrorHint(
          '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.geometryOf().',
        ),
        context.describeElement('The context used was')
      ]);
1389 1390 1391
    return scaffoldScope.geometryNotifier;
  }

1392 1393 1394 1395 1396 1397 1398 1399 1400 1401
  /// 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:
1402
  ///
1403 1404
  ///  * [Scaffold.of], which provides access to the [ScaffoldState] object as a
  ///    whole, from which you can show snackbars, bottom sheets, and so forth.
1405
  static bool hasDrawer(BuildContext context, { bool registerForUpdates = true }) {
1406 1407 1408
    assert(registerForUpdates != null);
    assert(context != null);
    if (registerForUpdates) {
1409
      final _ScaffoldScope scaffold = context.inheritFromWidgetOfExactType(_ScaffoldScope);
1410 1411
      return scaffold?.hasDrawer ?? false;
    } else {
1412
      final ScaffoldState scaffold = context.ancestorStateOfType(const TypeMatcher<ScaffoldState>());
1413 1414 1415 1416
      return scaffold?.hasDrawer ?? false;
    }
  }

1417
  @override
1418
  ScaffoldState createState() => ScaffoldState();
Hixie's avatar
Hixie committed
1419 1420
}

1421 1422 1423 1424
/// State for a [Scaffold].
///
/// Can display [SnackBar]s and [BottomSheet]s. Retrieve a [ScaffoldState] from
/// the current [BuildContext] using [Scaffold.of].
1425
class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
Hixie's avatar
Hixie committed
1426

1427 1428
  // DRAWER API

1429 1430
  final GlobalKey<DrawerControllerState> _drawerKey = GlobalKey<DrawerControllerState>();
  final GlobalKey<DrawerControllerState> _endDrawerKey = GlobalKey<DrawerControllerState>();
1431

1432 1433
  /// Whether this scaffold has a non-null [Scaffold.appBar].
  bool get hasAppBar => widget.appBar != null;
1434
  /// Whether this scaffold has a non-null [Scaffold.drawer].
1435
  bool get hasDrawer => widget.drawer != null;
1436 1437
  /// Whether this scaffold has a non-null [Scaffold.endDrawer].
  bool get hasEndDrawer => widget.endDrawer != null;
1438 1439
  /// Whether this scaffold has a non-null [Scaffold.floatingActionButton].
  bool get hasFloatingActionButton => widget.floatingActionButton != null;
1440

1441 1442 1443 1444 1445
  double _appBarMaxHeight;
  /// The max height the [Scaffold.appBar] uses.
  ///
  /// This is based on the appBar preferred height plus the top padding.
  double get appBarMaxHeight => _appBarMaxHeight;
jslavitz's avatar
jslavitz committed
1446 1447 1448
  bool _drawerOpened = false;
  bool _endDrawerOpened = false;

1449 1450 1451 1452
  /// Whether the [Scaffold.drawer] is opened.
  ///
  /// See also:
  ///
1453 1454
  ///  * [ScaffoldState.openDrawer], which opens the [Scaffold.drawer] of a
  ///    [Scaffold].
1455 1456 1457 1458 1459 1460
  bool get isDrawerOpen => _drawerOpened;

  /// Whether the [Scaffold.endDrawer] is opened.
  ///
  /// See also:
  ///
1461 1462
  ///  * [ScaffoldState.openEndDrawer], which opens the [Scaffold.endDrawer] of
  ///    a [Scaffold].
1463 1464
  bool get isEndDrawerOpen => _endDrawerOpened;

jslavitz's avatar
jslavitz committed
1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476
  void _drawerOpenedCallback(bool isOpened) {
    setState(() {
      _drawerOpened = isOpened;
    });
  }

  void _endDrawerOpenedCallback(bool isOpened) {
    setState(() {
      _endDrawerOpened = isOpened;
    });
  }

1477 1478 1479 1480
  /// 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.
1481 1482 1483 1484 1485 1486
  ///
  /// 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].
1487 1488
  ///
  /// See [Scaffold.of] for information about how to obtain the [ScaffoldState].
1489
  void openDrawer() {
jslavitz's avatar
jslavitz committed
1490 1491
    if (_endDrawerKey.currentState != null && _endDrawerOpened)
      _endDrawerKey.currentState.close();
1492
    _drawerKey.currentState?.open();
1493 1494
  }

1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507
  /// 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() {
jslavitz's avatar
jslavitz committed
1508 1509
    if (_drawerKey.currentState != null && _drawerOpened)
      _drawerKey.currentState.close();
1510 1511 1512
    _endDrawerKey.currentState?.open();
  }

1513 1514
  // SNACKBAR API

1515
  final Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> _snackBars = Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>();
1516
  AnimationController _snackBarController;
Hixie's avatar
Hixie committed
1517
  Timer _snackBarTimer;
1518
  bool _accessibleNavigation;
Hixie's avatar
Hixie committed
1519

1520
  /// Shows a [SnackBar] at the bottom of the scaffold.
1521 1522 1523 1524 1525
  ///
  /// 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.
1526 1527 1528
  ///
  /// To control how long a [SnackBar] remains visible, use [SnackBar.duration].
  ///
1529 1530 1531 1532
  /// 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].
1533 1534
  ///
  /// See [Scaffold.of] for information about how to obtain the [ScaffoldState].
1535
  ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(SnackBar snackbar) {
1536
    _snackBarController ??= SnackBar.createAnimationController(vsync: this)
Hixie's avatar
Hixie committed
1537
      ..addStatusListener(_handleSnackBarStatusChange);
1538
    if (_snackBars.isEmpty) {
1539 1540
      assert(_snackBarController.isDismissed);
      _snackBarController.forward();
1541
    }
1542
    ScaffoldFeatureController<SnackBar, SnackBarClosedReason> controller;
1543
    controller = ScaffoldFeatureController<SnackBar, SnackBarClosedReason>._(
1544 1545 1546
      // 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.
1547 1548
      snackbar.withAnimation(_snackBarController, fallbackKey: UniqueKey()),
      Completer<SnackBarClosedReason>(),
1549 1550
      () {
        assert(_snackBars.first == controller);
1551
        hideCurrentSnackBar(reason: SnackBarClosedReason.hide);
1552
      },
1553
      null, // SnackBar doesn't use a builder function so setState() wouldn't rebuild it
1554
    );
Hixie's avatar
Hixie committed
1555
    setState(() {
1556
      _snackBars.addLast(controller);
Hixie's avatar
Hixie committed
1557
    });
1558
    return controller;
Hixie's avatar
Hixie committed
1559 1560
  }

1561
  void _handleSnackBarStatusChange(AnimationStatus status) {
Hixie's avatar
Hixie committed
1562
    switch (status) {
1563
      case AnimationStatus.dismissed:
Hixie's avatar
Hixie committed
1564 1565 1566 1567
        assert(_snackBars.isNotEmpty);
        setState(() {
          _snackBars.removeFirst();
        });
1568
        if (_snackBars.isNotEmpty)
1569
          _snackBarController.forward();
Hixie's avatar
Hixie committed
1570
        break;
1571
      case AnimationStatus.completed:
Hixie's avatar
Hixie committed
1572 1573 1574 1575 1576
        setState(() {
          assert(_snackBarTimer == null);
          // build will create a new timer if necessary to dismiss the snack bar
        });
        break;
1577 1578
      case AnimationStatus.forward:
      case AnimationStatus.reverse:
Hixie's avatar
Hixie committed
1579 1580 1581 1582
        break;
    }
  }

1583 1584 1585 1586
  /// 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.
1587
  void removeCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.remove }) {
1588
    assert(reason != null);
1589 1590
    if (_snackBars.isEmpty)
      return;
1591
    final Completer<SnackBarClosedReason> completer = _snackBars.first._completer;
1592
    if (!completer.isCompleted)
1593
      completer.complete(reason);
1594 1595 1596 1597 1598
    _snackBarTimer?.cancel();
    _snackBarTimer = null;
    _snackBarController.value = 0.0;
  }

1599
  /// Removes the current [SnackBar] by running its normal exit animation.
1600 1601
  ///
  /// The closed completer is called after the animation is complete.
1602
  void hideCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.hide }) {
1603 1604 1605
    assert(reason != null);
    if (_snackBars.isEmpty || _snackBarController.status == AnimationStatus.dismissed)
      return;
1606
    final MediaQueryData mediaQuery = MediaQuery.of(context);
1607
    final Completer<SnackBarClosedReason> completer = _snackBars.first._completer;
1608 1609 1610 1611
    if (mediaQuery.accessibleNavigation) {
      _snackBarController.value = 0.0;
      completer.complete(reason);
    } else {
1612
      _snackBarController.reverse().then<void>((void value) {
1613 1614 1615 1616 1617
        assert(mounted);
        if (!completer.isCompleted)
          completer.complete(reason);
      });
    }
1618
    _snackBarTimer?.cancel();
Hixie's avatar
Hixie committed
1619 1620 1621
    _snackBarTimer = null;
  }

1622

1623 1624
  // PERSISTENT BOTTOM SHEET API

1625 1626 1627 1628
  // Contains bottom sheets that may still be animating out of view.
  // Important if the app/user takes an action that could repeatedly show a
  // bottom sheet.
  final List<_StandardBottomSheet> _dismissedBottomSheets = <_StandardBottomSheet>[];
1629
  PersistentBottomSheetController<dynamic> _currentBottomSheet;
1630

1631 1632
  void _maybeBuildPersistentBottomSheet() {
    if (widget.bottomSheet != null && _currentBottomSheet == null) {
1633 1634 1635
      // The new _currentBottomSheet is not a local history entry so a "back" button
      // will not be added to the Scaffold's appbar and the bottom sheet will not
      // support drag or swipe to dismiss.
1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656
      final AnimationController animationController = BottomSheet.createAnimationController(this)..value = 1.0;
      LocalHistoryEntry _persistentSheetHistoryEntry;
      bool _persistentBottomSheetExtentChanged(DraggableScrollableNotification notification) {
        if (notification.extent > notification.initialExtent) {
          if (_persistentSheetHistoryEntry == null) {
            _persistentSheetHistoryEntry = LocalHistoryEntry(onRemove: () {
              if (notification.extent > notification.initialExtent) {
                DraggableScrollableActuator.reset(notification.context);
              }
              showBodyScrim(false, 0.0);
              _floatingActionButtonVisibilityValue = 1.0;
              _persistentSheetHistoryEntry = null;
            });
            ModalRoute.of(context).addLocalHistoryEntry(_persistentSheetHistoryEntry);
          }
        } else if (_persistentSheetHistoryEntry != null) {
          ModalRoute.of(context).removeLocalHistoryEntry(_persistentSheetHistoryEntry);
        }
        return false;
      }

1657
      _currentBottomSheet = _buildBottomSheet<void>(
1658 1659 1660 1661 1662 1663 1664 1665 1666 1667
        (BuildContext context) {
          return NotificationListener<DraggableScrollableNotification>(
            onNotification: _persistentBottomSheetExtentChanged,
            child: DraggableScrollableActuator(
              child: widget.bottomSheet,
            ),
          );
        },
        true,
        animationController: animationController,
1668 1669 1670 1671 1672
      );
    }
  }

  void _closeCurrentBottomSheet() {
1673
    if (_currentBottomSheet != null) {
1674 1675 1676 1677 1678 1679 1680 1681 1682
      if (!_currentBottomSheet._isLocalHistoryEntry) {
        _currentBottomSheet.close();
      }
      assert(() {
        _currentBottomSheet?._completer?.future?.whenComplete(() {
          assert(_currentBottomSheet == null);
        });
        return true;
      }());
1683
    }
1684 1685
  }

1686 1687 1688 1689 1690
  PersistentBottomSheetController<T> _buildBottomSheet<T>(
    WidgetBuilder builder,
    bool isPersistent, {
    AnimationController animationController,
    Color backgroundColor,
1691 1692
    double elevation,
    ShapeBorder shape,
1693
    Clip clipBehavior,
1694 1695 1696 1697
  }) {
    assert(() {
      if (widget.bottomSheet != null && isPersistent && _currentBottomSheet != null) {
        throw FlutterError(
1698 1699 1700
          'Scaffold.bottomSheet cannot be specified while a bottom sheet'
          'displayed with showBottomSheet() is still visible.\n'
          'Rebuild the Scaffold with a null bottomSheet before calling showBottomSheet().'
1701 1702 1703 1704 1705
        );
      }
      return true;
    }());

1706
    final Completer<T> completer = Completer<T>();
1707 1708
    final GlobalKey<_StandardBottomSheetState> bottomSheetKey = GlobalKey<_StandardBottomSheetState>();
    _StandardBottomSheet bottomSheet;
1709

1710
    bool removedEntry = false;
1711
    void _removeCurrentBottomSheet() {
1712 1713 1714 1715
      removedEntry = true;
      if (_currentBottomSheet == null) {
        return;
      }
1716 1717
      assert(_currentBottomSheet._widget == bottomSheet);
      assert(bottomSheetKey.currentState != null);
1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736
      _showFloatingActionButton();

      void _closed(void value) {
        setState(() {
          _currentBottomSheet = null;
        });

        if (animationController.status != AnimationStatus.dismissed) {
          _dismissedBottomSheets.add(bottomSheet);
        }
        completer.complete();
      }

      final Future<void> closing = bottomSheetKey.currentState.close();
      if (closing != null) {
        closing.then(_closed);
      } else {
        _closed(null);
      }
1737 1738
    }

1739 1740 1741
    final LocalHistoryEntry entry = isPersistent
      ? null
      : LocalHistoryEntry(onRemove: () {
1742 1743 1744 1745
          if (!removedEntry) {
            _removeCurrentBottomSheet();
          }
        });
1746

1747
    bottomSheet = _StandardBottomSheet(
1748
      key: bottomSheetKey,
1749 1750
      animationController: animationController,
      enableDrag: !isPersistent,
1751
      onClosing: () {
1752 1753 1754
        if (_currentBottomSheet == null) {
          return;
        }
1755
        assert(_currentBottomSheet._widget == bottomSheet);
1756 1757
        if (!isPersistent && !removedEntry) {
          assert(entry != null);
1758
          entry.remove();
1759 1760
          removedEntry = true;
        }
1761 1762
      },
      onDismissed: () {
1763 1764 1765 1766 1767
        if (_dismissedBottomSheets.contains(bottomSheet)) {
          setState(() {
            _dismissedBottomSheets.remove(bottomSheet);
          });
        }
1768
      },
1769
      builder: builder,
1770 1771
      isPersistent: isPersistent,
      backgroundColor: backgroundColor,
1772 1773
      elevation: elevation,
      shape: shape,
1774
      clipBehavior: clipBehavior,
1775
    );
1776

1777
    if (!isPersistent)
1778 1779
      ModalRoute.of(context).addLocalHistoryEntry(entry);

1780
    return PersistentBottomSheetController<T>._(
1781 1782
      bottomSheet,
      completer,
1783 1784 1785
      entry != null
        ? entry.remove
        : _removeCurrentBottomSheet,
1786
      (VoidCallback fn) { bottomSheetKey.currentState?.setState(fn); },
1787
      !isPersistent,
1788 1789 1790
    );
  }

1791 1792
  /// Shows a material design bottom sheet in the nearest [Scaffold]. To show
  /// a persistent bottom sheet, use the [Scaffold.bottomSheet].
1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819
  ///
  /// Returns a controller that can be used to close and otherwise manipulate the
  /// bottom sheet.
  ///
  /// To rebuild the bottom sheet (e.g. if it is stateful), call
  /// [PersistentBottomSheetController.setState] on the controller returned by
  /// this method.
  ///
  /// The new bottom sheet becomes a [LocalHistoryEntry] for the enclosing
  /// [ModalRoute] and a back button is added to the appbar of the [Scaffold]
  /// that closes the bottom sheet.
  ///
  /// To create a persistent bottom sheet that is not a [LocalHistoryEntry] and
  /// does not add a back button to the enclosing Scaffold's appbar, use the
  /// [Scaffold.bottomSheet] constructor parameter.
  ///
  /// 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.
  ///
  /// See also:
  ///
1820 1821
  ///  * [BottomSheet], which becomes the parent of the widget returned by the
  ///    `builder`.
1822 1823 1824 1825
  ///  * [showBottomSheet], which calls this method given a [BuildContext].
  ///  * [showModalBottomSheet], which can be used to display a modal bottom
  ///    sheet.
  ///  * [Scaffold.of], for information about how to obtain the [ScaffoldState].
1826
  ///  * <https://material.io/design/components/sheets-bottom.html#standard-bottom-sheet>
1827 1828 1829
  PersistentBottomSheetController<T> showBottomSheet<T>(
    WidgetBuilder builder, {
    Color backgroundColor,
1830 1831
    double elevation,
    ShapeBorder shape,
1832
    Clip clipBehavior,
1833 1834 1835 1836
  }) {
    assert(() {
      if (widget.bottomSheet != null) {
        throw FlutterError(
1837 1838 1839
          'Scaffold.bottomSheet cannot be specified while a bottom sheet'
          'displayed with showBottomSheet() is still visible.\n'
          'Rebuild the Scaffold with a null bottomSheet before calling showBottomSheet().'
1840 1841 1842 1843 1844 1845
        );
      }
      return true;
    }());
    assert(debugCheckHasMediaQuery(context));

1846
    _closeCurrentBottomSheet();
1847
    final AnimationController controller = BottomSheet.createAnimationController(this)..forward();
1848
    setState(() {
1849 1850 1851 1852 1853
      _currentBottomSheet = _buildBottomSheet<T>(
        builder,
        false,
        animationController: controller,
        backgroundColor: backgroundColor,
1854 1855
        elevation: elevation,
        shape: shape,
1856
        clipBehavior: clipBehavior,
1857
      );
1858 1859 1860 1861
    });
    return _currentBottomSheet;
  }

1862 1863 1864 1865 1866 1867
  // Floating Action Button API
  AnimationController _floatingActionButtonMoveController;
  FloatingActionButtonAnimator _floatingActionButtonAnimator;
  FloatingActionButtonLocation _previousFloatingActionButtonLocation;
  FloatingActionButtonLocation _floatingActionButtonLocation;

1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888
  AnimationController _floatingActionButtonVisibilityController;

  /// Gets the current value of the visibility animation for the
  /// [Scaffold.floatingActionButton].
  double get _floatingActionButtonVisibilityValue => _floatingActionButtonVisibilityController.value;

  /// Sets the current value of the visibility animation for the
  /// [Scaffold.floatingActionButton].  This value must not be null.
  set _floatingActionButtonVisibilityValue(double newValue) {
    assert(newValue != null);
    _floatingActionButtonVisibilityController.value = newValue.clamp(
      _floatingActionButtonVisibilityController.lowerBound,
      _floatingActionButtonVisibilityController.upperBound,
    );
  }

  /// Shows the [Scaffold.floatingActionButton].
  TickerFuture _showFloatingActionButton() {
    return _floatingActionButtonVisibilityController.forward();
  }

1889 1890 1891 1892 1893 1894
  // Moves the Floating Action Button to the new Floating Action Button Location.
  void _moveFloatingActionButton(final FloatingActionButtonLocation newLocation) {
    FloatingActionButtonLocation previousLocation = _floatingActionButtonLocation;
    double restartAnimationFrom = 0.0;
    // If the Floating Action Button is moving right now, we need to start from a snapshot of the current transition.
    if (_floatingActionButtonMoveController.isAnimating) {
1895
      previousLocation = _TransitionSnapshotFabLocation(_previousFloatingActionButtonLocation, _floatingActionButtonLocation, _floatingActionButtonAnimator, _floatingActionButtonMoveController.value);
1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908
      restartAnimationFrom = _floatingActionButtonAnimator.getAnimationRestart(_floatingActionButtonMoveController.value);
    }

    setState(() {
      _previousFloatingActionButtonLocation = previousLocation;
      _floatingActionButtonLocation = newLocation;
    });

    // Animate the motion even when the fab is null so that if the exit animation is running,
    // the old fab will start the motion transition while it exits instead of jumping to the
    // new position.
    _floatingActionButtonMoveController.forward(from: restartAnimationFrom);
  }
1909

1910
  // iOS FEATURES - status bar tap, back gesture
1911

1912 1913 1914
  // 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.
1915

1916
  final ScrollController _primaryScrollController = ScrollController();
Hixie's avatar
Hixie committed
1917

1918 1919 1920 1921 1922 1923
  void _handleStatusBarTap() {
    if (_primaryScrollController.hasClients) {
      _primaryScrollController.animateTo(
        0.0,
        duration: const Duration(milliseconds: 300),
        curve: Curves.linear, // TODO(ianh): Use a more appropriate curve.
1924 1925 1926 1927
      );
    }
  }

1928 1929
  // INTERNALS

1930 1931
  _ScaffoldGeometryNotifier _geometryNotifier;

1932 1933
  // Backwards compatibility for deprecated resizeToAvoidBottomPadding property
  bool get _resizeToAvoidBottomInset {
1934
    // ignore: deprecated_member_use_from_same_package
1935 1936 1937
    return widget.resizeToAvoidBottomInset ?? widget.resizeToAvoidBottomPadding ?? true;
  }

1938 1939 1940
  @override
  void initState() {
    super.initState();
1941
    _geometryNotifier = _ScaffoldGeometryNotifier(const ScaffoldGeometry(), context);
1942 1943 1944
    _floatingActionButtonLocation = widget.floatingActionButtonLocation ?? _kDefaultFloatingActionButtonLocation;
    _floatingActionButtonAnimator = widget.floatingActionButtonAnimator ?? _kDefaultFloatingActionButtonAnimator;
    _previousFloatingActionButtonLocation = _floatingActionButtonLocation;
1945
    _floatingActionButtonMoveController = AnimationController(
1946 1947 1948 1949
      vsync: this,
      lowerBound: 0.0,
      upperBound: 1.0,
      value: 1.0,
1950 1951
      duration: kFloatingActionButtonSegue * 2,
    );
1952 1953 1954 1955 1956

    _floatingActionButtonVisibilityController = AnimationController(
      duration: kFloatingActionButtonSegue,
      vsync: this,
    );
1957 1958
  }

1959 1960 1961 1962 1963 1964 1965 1966 1967
  @override
  void didUpdateWidget(Scaffold oldWidget) {
    // Update the Floating Action Button Animator, and then schedule the Floating Action Button for repositioning.
    if (widget.floatingActionButtonAnimator != oldWidget.floatingActionButtonAnimator) {
      _floatingActionButtonAnimator = widget.floatingActionButtonAnimator ?? _kDefaultFloatingActionButtonAnimator;
    }
    if (widget.floatingActionButtonLocation != oldWidget.floatingActionButtonLocation) {
      _moveFloatingActionButton(widget.floatingActionButtonLocation ?? _kDefaultFloatingActionButtonLocation);
    }
1968 1969 1970
    if (widget.bottomSheet != oldWidget.bottomSheet) {
      assert(() {
        if (widget.bottomSheet != null && _currentBottomSheet?._isLocalHistoryEntry == true) {
1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981
          throw FlutterError.fromParts(<DiagnosticsNode>[
            ErrorSummary(
              'Scaffold.bottomSheet cannot be specified while a bottom sheet displayed '
              'with showBottomSheet() is still visible.'
            ),
            ErrorHint(
              'Use the PersistentBottomSheetController '
              'returned by showBottomSheet() to close the old bottom sheet before creating '
              'a Scaffold with a (non null) bottomSheet.'
            ),
          ]);
1982 1983 1984 1985
        }
        return true;
      }());
      _closeCurrentBottomSheet();
1986
      _maybeBuildPersistentBottomSheet();
1987
    }
1988 1989 1990
    super.didUpdateWidget(oldWidget);
  }

1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004
  @override
  void didChangeDependencies() {
    final MediaQueryData mediaQuery = MediaQuery.of(context);
    // If we transition from accessible navigation to non-accessible navigation
    // and there is a SnackBar that would have timed out that has already
    // completed its timer, dismiss that SnackBar. If the timer hasn't finished
    // yet, let it timeout as normal.
    if (_accessibleNavigation == true
      && !mediaQuery.accessibleNavigation
      && _snackBarTimer != null
      && !_snackBarTimer.isActive) {
      hideCurrentSnackBar(reason: SnackBarClosedReason.timeout);
    }
    _accessibleNavigation = mediaQuery.accessibleNavigation;
2005
    _maybeBuildPersistentBottomSheet();
2006 2007 2008
    super.didChangeDependencies();
  }

2009 2010 2011 2012 2013
  @override
  void dispose() {
    _snackBarController?.dispose();
    _snackBarTimer?.cancel();
    _snackBarTimer = null;
2014
    _geometryNotifier.dispose();
2015 2016 2017 2018 2019 2020
    for (_StandardBottomSheet bottomSheet in _dismissedBottomSheets) {
      bottomSheet.animationController?.dispose();
    }
    if (_currentBottomSheet != null) {
      _currentBottomSheet._widget.animationController?.dispose();
    }
2021
    _floatingActionButtonMoveController.dispose();
2022
    _floatingActionButtonVisibilityController.dispose();
2023 2024 2025
    super.dispose();
  }

2026 2027 2028 2029
  void _addIfNonNull(
    List<LayoutId> children,
    Widget child,
    Object childId, {
Ian Hickson's avatar
Ian Hickson committed
2030 2031 2032
    @required bool removeLeftPadding,
    @required bool removeTopPadding,
    @required bool removeRightPadding,
2033
    @required bool removeBottomPadding,
2034
    bool removeBottomInset = false,
2035
    bool maintainBottomViewPadding = false,
Ian Hickson's avatar
Ian Hickson committed
2036
  }) {
2037 2038 2039 2040 2041 2042 2043 2044 2045
    MediaQueryData data = MediaQuery.of(context).removePadding(
      removeLeft: removeLeftPadding,
      removeTop: removeTopPadding,
      removeRight: removeRightPadding,
      removeBottom: removeBottomPadding,
    );
    if (removeBottomInset)
      data = data.removeViewInsets(removeBottom: true);

2046 2047 2048 2049 2050 2051
    if (maintainBottomViewPadding && data.viewInsets.bottom != 0.0) {
      data = data.copyWith(
        padding: data.padding.copyWith(bottom: data.viewPadding.bottom)
      );
    }

Ian Hickson's avatar
Ian Hickson committed
2052 2053
    if (child != null) {
      children.add(
2054
        LayoutId(
Ian Hickson's avatar
Ian Hickson committed
2055
          id: childId,
2056
          child: MediaQuery(data: data, child: child),
Ian Hickson's avatar
Ian Hickson committed
2057 2058 2059
        ),
      );
    }
2060 2061
  }

jslavitz's avatar
jslavitz committed
2062 2063 2064 2065 2066
  void _buildEndDrawer(List<LayoutId> children, TextDirection textDirection) {
    if (widget.endDrawer != null) {
      assert(hasEndDrawer);
      _addIfNonNull(
        children,
2067
        DrawerController(
jslavitz's avatar
jslavitz committed
2068 2069 2070 2071
          key: _endDrawerKey,
          alignment: DrawerAlignment.end,
          child: widget.endDrawer,
          drawerCallback: _endDrawerOpenedCallback,
2072
          dragStartBehavior: widget.drawerDragStartBehavior,
2073
          scrimColor: widget.drawerScrimColor,
2074
          edgeDragWidth: widget.drawerEdgeDragWidth,
jslavitz's avatar
jslavitz committed
2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090
        ),
        _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,
      );
    }
  }

  void _buildDrawer(List<LayoutId> children, TextDirection textDirection) {
    if (widget.drawer != null) {
      assert(hasDrawer);
      _addIfNonNull(
        children,
2091
        DrawerController(
jslavitz's avatar
jslavitz committed
2092 2093 2094 2095
          key: _drawerKey,
          alignment: DrawerAlignment.start,
          child: widget.drawer,
          drawerCallback: _drawerOpenedCallback,
2096
          dragStartBehavior: widget.drawerDragStartBehavior,
2097
          scrimColor: widget.drawerScrimColor,
2098
          edgeDragWidth: widget.drawerEdgeDragWidth,
jslavitz's avatar
jslavitz committed
2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109
        ),
        _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,
      );
    }
  }

2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126
  bool _showBodyScrim = false;
  Color _bodyScrimColor = Colors.black;

  /// Whether to show a [ModalBarrier] over the body of the scaffold.
  ///
  /// The `value` parameter must not be null.
  void showBodyScrim(bool value, double opacity) {
    assert(value != null);
    if (_showBodyScrim == value && _bodyScrimColor.opacity == opacity) {
      return;
    }
    setState(() {
      _showBodyScrim = value;
      _bodyScrimColor = Colors.black.withOpacity(opacity);
    });
  }

2127
  @override
Adam Barth's avatar
Adam Barth committed
2128
  Widget build(BuildContext context) {
2129
    assert(debugCheckHasMediaQuery(context));
Ian Hickson's avatar
Ian Hickson committed
2130
    assert(debugCheckHasDirectionality(context));
2131
    final MediaQueryData mediaQuery = MediaQuery.of(context);
2132
    final ThemeData themeData = Theme.of(context);
Ian Hickson's avatar
Ian Hickson committed
2133
    final TextDirection textDirection = Directionality.of(context);
2134
    _accessibleNavigation = mediaQuery.accessibleNavigation;
Hixie's avatar
Hixie committed
2135

2136
    if (_snackBars.isNotEmpty) {
2137
      final ModalRoute<dynamic> route = ModalRoute.of(context);
Hixie's avatar
Hixie committed
2138
      if (route == null || route.isCurrent) {
2139 2140
        if (_snackBarController.isCompleted && _snackBarTimer == null) {
          final SnackBar snackBar = _snackBars.first._widget;
2141
          _snackBarTimer = Timer(snackBar.duration, () {
2142
            assert(_snackBarController.status == AnimationStatus.forward ||
2143 2144 2145 2146 2147
                   _snackBarController.status == AnimationStatus.completed);
            // Look up MediaQuery again in case the setting changed.
            final MediaQueryData mediaQuery = MediaQuery.of(context);
            if (mediaQuery.accessibleNavigation && snackBar.action != null)
              return;
2148 2149
            hideCurrentSnackBar(reason: SnackBarClosedReason.timeout);
          });
2150
        }
Hixie's avatar
Hixie committed
2151 2152 2153 2154
      } else {
        _snackBarTimer?.cancel();
        _snackBarTimer = null;
      }
2155
    }
2156

2157
    final List<LayoutId> children = <LayoutId>[];
Ian Hickson's avatar
Ian Hickson committed
2158 2159
    _addIfNonNull(
      children,
2160 2161 2162
      widget.body == null ? null : _BodyBuilder(
        extendBody: widget.extendBody,
        extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
2163
        body: widget.body,
2164
      ),
Ian Hickson's avatar
Ian Hickson committed
2165 2166 2167 2168
      _ScaffoldSlot.body,
      removeLeftPadding: false,
      removeTopPadding: widget.appBar != null,
      removeRightPadding: false,
2169 2170
      removeBottomPadding: widget.bottomNavigationBar != null || widget.persistentFooterButtons != null,
      removeBottomInset: _resizeToAvoidBottomInset,
Ian Hickson's avatar
Ian Hickson committed
2171
    );
2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185
    if (_showBodyScrim) {
      _addIfNonNull(
        children,
        ModalBarrier(
          dismissible: false,
          color: _bodyScrimColor,
        ),
        _ScaffoldSlot.bodyScrim,
        removeLeftPadding: true,
        removeTopPadding: true,
        removeRightPadding: true,
        removeBottomPadding: true,
      );
    }
2186

2187
    if (widget.appBar != null) {
2188
      final double topPadding = widget.primary ? mediaQuery.padding.top : 0.0;
2189 2190
      _appBarMaxHeight = widget.appBar.preferredSize.height + topPadding;
      assert(_appBarMaxHeight >= 0.0 && _appBarMaxHeight.isFinite);
2191 2192
      _addIfNonNull(
        children,
2193
        ConstrainedBox(
2194
          constraints: BoxConstraints(maxHeight: _appBarMaxHeight),
2195
          child: FlexibleSpaceBar.createSettings(
2196
            currentExtent: _appBarMaxHeight,
2197 2198
            child: widget.appBar,
          ),
2199 2200
        ),
        _ScaffoldSlot.appBar,
Ian Hickson's avatar
Ian Hickson committed
2201 2202 2203 2204
        removeLeftPadding: false,
        removeTopPadding: false,
        removeRightPadding: false,
        removeBottomPadding: true,
2205 2206
      );
    }
2207

2208
    bool isSnackBarFloating = false;
Ian Hickson's avatar
Ian Hickson committed
2209
    if (_snackBars.isNotEmpty) {
2210 2211 2212 2213 2214
      final SnackBarBehavior snackBarBehavior = _snackBars.first._widget.behavior
        ?? themeData.snackBarTheme.behavior
        ?? SnackBarBehavior.fixed;
      isSnackBarFloating = snackBarBehavior == SnackBarBehavior.floating;

Ian Hickson's avatar
Ian Hickson committed
2215 2216 2217 2218 2219 2220 2221
      _addIfNonNull(
        children,
        _snackBars.first._widget,
        _ScaffoldSlot.snackBar,
        removeLeftPadding: false,
        removeTopPadding: true,
        removeRightPadding: false,
2222
        removeBottomPadding: widget.bottomNavigationBar != null || widget.persistentFooterButtons != null,
2223
        maintainBottomViewPadding: !_resizeToAvoidBottomInset,
Ian Hickson's avatar
Ian Hickson committed
2224 2225
      );
    }
2226

2227
    if (widget.persistentFooterButtons != null) {
Ian Hickson's avatar
Ian Hickson committed
2228 2229
      _addIfNonNull(
        children,
2230 2231 2232
        Container(
          decoration: BoxDecoration(
            border: Border(
2233
              top: Divider.createBorderSide(context, width: 1.0),
2234 2235
            ),
          ),
2236
          child: SafeArea(
2237 2238 2239
            top: false,
            child: ButtonBar(
              children: widget.persistentFooterButtons,
2240 2241 2242
            ),
          ),
        ),
Ian Hickson's avatar
Ian Hickson committed
2243 2244 2245 2246
        _ScaffoldSlot.persistentFooter,
        removeLeftPadding: false,
        removeTopPadding: true,
        removeRightPadding: false,
2247
        removeBottomPadding: false,
2248
        maintainBottomViewPadding: !_resizeToAvoidBottomInset,
Ian Hickson's avatar
Ian Hickson committed
2249
      );
2250 2251
    }

2252
    if (widget.bottomNavigationBar != null) {
Ian Hickson's avatar
Ian Hickson committed
2253 2254 2255 2256 2257 2258 2259
      _addIfNonNull(
        children,
        widget.bottomNavigationBar,
        _ScaffoldSlot.bottomNavigationBar,
        removeLeftPadding: false,
        removeTopPadding: true,
        removeRightPadding: false,
2260
        removeBottomPadding: false,
2261
        maintainBottomViewPadding: !_resizeToAvoidBottomInset,
Ian Hickson's avatar
Ian Hickson committed
2262
      );
2263
    }
2264

2265
    if (_currentBottomSheet != null || _dismissedBottomSheets.isNotEmpty) {
2266
      final Widget stack = Stack(
2267
        alignment: Alignment.bottomCenter,
2268 2269 2270 2271
        children: <Widget>[
          ..._dismissedBottomSheets,
          if (_currentBottomSheet != null) _currentBottomSheet._widget,
        ],
2272
      );
Ian Hickson's avatar
Ian Hickson committed
2273 2274 2275 2276 2277 2278 2279
      _addIfNonNull(
        children,
        stack,
        _ScaffoldSlot.bottomSheet,
        removeLeftPadding: false,
        removeTopPadding: true,
        removeRightPadding: false,
2280
        removeBottomPadding: _resizeToAvoidBottomInset,
Ian Hickson's avatar
Ian Hickson committed
2281
      );
2282 2283
    }

Ian Hickson's avatar
Ian Hickson committed
2284 2285
    _addIfNonNull(
      children,
2286
      _FloatingActionButtonTransition(
2287
        child: widget.floatingActionButton,
2288 2289
        fabMoveAnimation: _floatingActionButtonMoveController,
        fabMotionAnimator: _floatingActionButtonAnimator,
2290
        geometryNotifier: _geometryNotifier,
2291
        currentController: _floatingActionButtonVisibilityController,
Ian Hickson's avatar
Ian Hickson committed
2292 2293 2294 2295 2296 2297 2298
      ),
      _ScaffoldSlot.floatingActionButton,
      removeLeftPadding: true,
      removeTopPadding: true,
      removeRightPadding: true,
      removeBottomPadding: true,
    );
2299

2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319
    switch (themeData.platform) {
      case TargetPlatform.iOS:
        _addIfNonNull(
          children,
          GestureDetector(
            behavior: HitTestBehavior.opaque,
            onTap: _handleStatusBarTap,
            // iOS accessibility automatically adds scroll-to-top to the clock in the status bar
            excludeFromSemantics: true,
          ),
          _ScaffoldSlot.statusBar,
          removeLeftPadding: false,
          removeTopPadding: true,
          removeRightPadding: false,
          removeBottomPadding: true,
        );
        break;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        break;
2320 2321
    }

jslavitz's avatar
jslavitz committed
2322 2323 2324 2325 2326 2327
    if (_endDrawerOpened) {
      _buildDrawer(children, textDirection);
      _buildEndDrawer(children, textDirection);
    } else {
      _buildEndDrawer(children, textDirection);
      _buildDrawer(children, textDirection);
2328 2329
    }

2330 2331
    // The minimum insets for contents of the Scaffold to keep visible.
    final EdgeInsets minInsets = mediaQuery.padding.copyWith(
2332
      bottom: _resizeToAvoidBottomInset ? mediaQuery.viewInsets.bottom : 0.0,
2333
    );
2334

2335
    // extendBody locked when keyboard is open
2336
    final bool _extendBody = minInsets.bottom <= 0 && widget.extendBody;
2337

2338
    return _ScaffoldScope(
2339
      hasDrawer: hasDrawer,
2340
      geometryNotifier: _geometryNotifier,
2341
      child: PrimaryScrollController(
2342
        controller: _primaryScrollController,
2343
        child: Material(
2344
          color: widget.backgroundColor ?? themeData.scaffoldBackgroundColor,
2345 2346
          child: AnimatedBuilder(animation: _floatingActionButtonMoveController, builder: (BuildContext context, Widget child) {
            return CustomMultiChildLayout(
2347
              children: children,
2348
              delegate: _ScaffoldLayout(
2349
                extendBody: _extendBody,
2350
                extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
2351 2352 2353 2354 2355 2356 2357
                minInsets: minInsets,
                currentFloatingActionButtonLocation: _floatingActionButtonLocation,
                floatingActionButtonMoveAnimationProgress: _floatingActionButtonMoveController.value,
                floatingActionButtonMotionAnimator: _floatingActionButtonAnimator,
                geometryNotifier: _geometryNotifier,
                previousFloatingActionButtonLocation: _previousFloatingActionButtonLocation,
                textDirection: textDirection,
2358
                isSnackBarFloating: isSnackBarFloating,
2359 2360 2361
              ),
            );
          }),
2362 2363
        ),
      ),
2364
    );
2365
  }
2366
}
2367

2368 2369
/// An interface for controlling a feature of a [Scaffold].
///
2370
/// Commonly obtained from [ScaffoldState.showSnackBar] or [ScaffoldState.showBottomSheet].
2371
class ScaffoldFeatureController<T extends Widget, U> {
2372 2373
  const ScaffoldFeatureController._(this._widget, this._completer, this.close, this.setState);
  final T _widget;
2374
  final Completer<U> _completer;
2375 2376

  /// Completes when the feature controlled by this object is no longer visible.
2377
  Future<U> get closed => _completer.future;
2378 2379 2380 2381 2382

  /// 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.
2383 2384 2385
  final StateSetter setState;
}

2386 2387
class _StandardBottomSheet extends StatefulWidget {
  const _StandardBottomSheet({
2388
    Key key,
2389
    this.animationController,
2390
    this.enableDrag = true,
2391 2392
    this.onClosing,
    this.onDismissed,
2393
    this.builder,
2394 2395
    this.isPersistent = false,
    this.backgroundColor,
2396 2397
    this.elevation,
    this.shape,
2398
    this.clipBehavior,
2399 2400
  }) : super(key: key);

2401
  final AnimationController animationController; // we control it, but it must be disposed by whoever created it.
2402
  final bool enableDrag;
2403 2404 2405
  final VoidCallback onClosing;
  final VoidCallback onDismissed;
  final WidgetBuilder builder;
2406 2407
  final bool isPersistent;
  final Color backgroundColor;
2408 2409
  final double elevation;
  final ShapeBorder shape;
2410
  final Clip clipBehavior;
2411

2412
  @override
2413
  _StandardBottomSheetState createState() => _StandardBottomSheetState();
2414 2415
}

2416
class _StandardBottomSheetState extends State<_StandardBottomSheet> {
2417
  @override
2418 2419
  void initState() {
    super.initState();
2420
    assert(widget.animationController != null);
2421 2422
    assert(widget.animationController.status == AnimationStatus.forward
        || widget.animationController.status == AnimationStatus.completed);
2423
    widget.animationController.addStatusListener(_handleStatusChange);
2424 2425
  }

2426
  @override
2427
  void didUpdateWidget(_StandardBottomSheet oldWidget) {
2428 2429
    super.didUpdateWidget(oldWidget);
    assert(widget.animationController == oldWidget.animationController);
2430 2431
  }

2432 2433
  Future<void> close() {
    assert(widget.animationController != null);
2434
    widget.animationController.reverse();
2435 2436 2437 2438
    if (widget.onClosing != null) {
      widget.onClosing();
    }
    return null;
2439 2440
  }

2441
  void _handleStatusChange(AnimationStatus status) {
2442
    if (status == AnimationStatus.dismissed && widget.onDismissed != null) {
2443
      widget.onDismissed();
2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473
    }
  }

  bool extentChanged(DraggableScrollableNotification notification) {
    final double extentRemaining = 1.0 - notification.extent;
    final ScaffoldState scaffold = Scaffold.of(context);
    if (extentRemaining < _kBottomSheetDominatesPercentage) {
      scaffold._floatingActionButtonVisibilityValue = extentRemaining * _kBottomSheetDominatesPercentage * 10;
      scaffold.showBodyScrim(true,  math.max(
        _kMinBottomSheetScrimOpacity,
        _kMaxBottomSheetScrimOpacity - scaffold._floatingActionButtonVisibilityValue,
      ));
    } else {
      scaffold._floatingActionButtonVisibilityValue = 1.0;
      scaffold.showBodyScrim(false, 0.0);
    }
    // If the Scaffold.bottomSheet != null, we're a persistent bottom sheet.
    if (notification.extent == notification.minExtent && scaffold.widget.bottomSheet == null) {
      close();
    }
    return false;
  }

  Widget _wrapBottomSheet(Widget bottomSheet) {
    return Semantics(
      container: true,
      onDismiss: close,
      child:  NotificationListener<DraggableScrollableNotification>(
        onNotification: extentChanged,
        child: bottomSheet,
2474
      ),
2475
    );
2476 2477
  }

2478
  @override
2479
  Widget build(BuildContext context) {
2480 2481 2482 2483 2484 2485 2486
    if (widget.animationController != null) {
      return AnimatedBuilder(
        animation: widget.animationController,
        builder: (BuildContext context, Widget child) {
          return Align(
            alignment: AlignmentDirectional.topStart,
            heightFactor: widget.animationController.value,
2487
            child: child,
2488
          );
2489
        },
2490 2491 2492 2493 2494 2495 2496
        child: _wrapBottomSheet(
          BottomSheet(
            animationController: widget.animationController,
            enableDrag: widget.enableDrag,
            onClosing: widget.onClosing,
            builder: widget.builder,
            backgroundColor: widget.backgroundColor,
2497 2498
            elevation: widget.elevation,
            shape: widget.shape,
2499
            clipBehavior: widget.clipBehavior,
2500
          ),
2501
        ),
2502 2503 2504 2505 2506 2507 2508 2509
      );
    }

    return _wrapBottomSheet(
      BottomSheet(
        onClosing: widget.onClosing,
        builder: widget.builder,
        backgroundColor: widget.backgroundColor,
2510
      ),
2511 2512 2513 2514
    );
  }

}
2515

2516
/// A [ScaffoldFeatureController] for standard bottom sheets.
2517
///
2518
/// This is the type of objects returned by [ScaffoldState.showBottomSheet].
2519 2520 2521 2522 2523
///
/// This controller is used to display both standard and persistent bottom
/// sheets. A bottom sheet is only persistent if it is set as the
/// [Scaffold.bottomSheet].
class PersistentBottomSheetController<T> extends ScaffoldFeatureController<_StandardBottomSheet, T> {
2524
  const PersistentBottomSheetController._(
2525
    _StandardBottomSheet widget,
2526 2527
    Completer<T> completer,
    VoidCallback close,
2528 2529
    StateSetter setState,
    this._isLocalHistoryEntry,
2530
  ) : super._(widget, completer, close, setState);
2531 2532

  final bool _isLocalHistoryEntry;
2533
}
2534 2535

class _ScaffoldScope extends InheritedWidget {
2536
  const _ScaffoldScope({
2537
    @required this.hasDrawer,
2538
    @required this.geometryNotifier,
2539
    @required Widget child,
2540 2541
  }) : assert(hasDrawer != null),
       super(child: child);
2542 2543

  final bool hasDrawer;
2544
  final _ScaffoldGeometryNotifier geometryNotifier;
2545 2546 2547 2548 2549

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