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

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

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

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

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

23 24 25 26 27
enum AppBarBehavior {
  anchor,
  scroll,
}

28
enum _ScaffoldSlot {
29 30 31 32 33 34 35
  body,
  toolBar,
  bottomSheet,
  snackBar,
  floatingActionButton,
  drawer,
}
Hans Muller's avatar
Hans Muller committed
36

37
class _ScaffoldLayout extends MultiChildLayoutDelegate {
38 39 40 41
  _ScaffoldLayout({ this.padding });

  final EdgeDims padding;

42 43
  void performLayout(Size size) {
    BoxConstraints looseConstraints = new BoxConstraints.loose(size);
44

45 46 47 48 49
    // This part of the layout has the same effect as putting the toolbar and
    // body in a column and making the body flexible. What's different is that
    // in this case the toolbar appears -after- the body in the stacking order,
    // so the toolbar's shadow is drawn on top of the body.

50
    final BoxConstraints fullWidthConstraints = looseConstraints.tighten(width: size.width);
51 52
    double contentTop = padding.top;
    double contentBottom = size.height - padding.bottom;
53

54
    if (isChild(_ScaffoldSlot.toolBar)) {
55
      contentTop = layoutChild(_ScaffoldSlot.toolBar, fullWidthConstraints).height;
56
      positionChild(_ScaffoldSlot.toolBar, Offset.zero);
57 58
    }

59
    if (isChild(_ScaffoldSlot.body)) {
60
      final double bodyHeight = contentBottom - contentTop;
61
      final BoxConstraints bodyConstraints = fullWidthConstraints.tighten(height: bodyHeight);
62
      layoutChild(_ScaffoldSlot.body, bodyConstraints);
63
      positionChild(_ScaffoldSlot.body, new Offset(0.0, contentTop));
64
    }
65 66 67 68 69 70 71 72 73 74

    // The BottomSheet and the SnackBar are anchored to the bottom of the parent,
    // they're as wide as the parent and are given their intrinsic height.
    // If all three elements are present then either the center of the FAB straddles
    // the top edge of the BottomSheet or the bottom of the FAB is
    // _kFloatingActionButtonMargin above the SnackBar, whichever puts the FAB
    // the farthest above the bottom of the parent. If only the FAB is has a
    // non-zero height then it's inset from the parent's right and bottom edges
    // by _kFloatingActionButtonMargin.

75 76 77
    Size bottomSheetSize = Size.zero;
    Size snackBarSize = Size.zero;

78 79
    if (isChild(_ScaffoldSlot.bottomSheet)) {
      bottomSheetSize = layoutChild(_ScaffoldSlot.bottomSheet, fullWidthConstraints);
80
      positionChild(_ScaffoldSlot.bottomSheet, new Offset((size.width - bottomSheetSize.width) / 2.0, contentBottom - bottomSheetSize.height));
81 82
    }

83 84
    if (isChild(_ScaffoldSlot.snackBar)) {
      snackBarSize = layoutChild(_ScaffoldSlot.snackBar, fullWidthConstraints);
85
      positionChild(_ScaffoldSlot.snackBar, new Offset(0.0, contentBottom - snackBarSize.height));
86 87
    }

88 89
    if (isChild(_ScaffoldSlot.floatingActionButton)) {
      final Size fabSize = layoutChild(_ScaffoldSlot.floatingActionButton, looseConstraints);
90
      final double fabX = size.width - fabSize.width - _kFloatingActionButtonMargin;
91
      double fabY = contentBottom - fabSize.height - _kFloatingActionButtonMargin;
92
      if (snackBarSize.height > 0.0)
93
        fabY = math.min(fabY, contentBottom - snackBarSize.height - fabSize.height - _kFloatingActionButtonMargin);
94
      if (bottomSheetSize.height > 0.0)
95
        fabY = math.min(fabY, contentBottom - bottomSheetSize.height - fabSize.height / 2.0);
96
      positionChild(_ScaffoldSlot.floatingActionButton, new Offset(fabX, fabY));
97
    }
98

99 100 101
    if (isChild(_ScaffoldSlot.drawer)) {
      layoutChild(_ScaffoldSlot.drawer, new BoxConstraints.tight(size));
      positionChild(_ScaffoldSlot.drawer, Offset.zero);
102
    }
Hans Muller's avatar
Hans Muller committed
103
  }
104

105 106 107
  bool shouldRelayout(_ScaffoldLayout oldDelegate) {
    return padding != oldDelegate.padding;
  }
Hans Muller's avatar
Hans Muller committed
108 109
}

110 111 112 113 114 115 116 117 118 119 120 121 122 123
class _FloatingActionButtonTransition extends StatefulComponent {
  _FloatingActionButtonTransition({
    Key key,
    this.child
  }) : super(key: key) {
    assert(child != null);
  }

  final Widget child;

  _FloatingActionButtonTransitionState createState() => new _FloatingActionButtonTransitionState();
}

class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTransition> {
124
  final AnimationController controller = new AnimationController(duration: _kFloatingActionButtonSegue);
125 126 127 128
  Widget oldChild;

  void initState() {
    super.initState();
129
    controller.forward().then((_) {
130 131 132 133 134
      oldChild = null;
    });
  }

  void dispose() {
135
    controller.stop();
136 137 138 139 140 141 142
    super.dispose();
  }

  void didUpdateConfig(_FloatingActionButtonTransition oldConfig) {
    if (Widget.canUpdate(oldConfig.child, config.child))
      return;
    oldChild = oldConfig.child;
143 144 145
    controller
      ..value = 0.0
      ..forward().then((_) {
146 147 148 149 150 151 152 153
        oldChild = null;
      });
  }

  Widget build(BuildContext context) {
    final List<Widget> children = new List<Widget>();
    if (oldChild != null) {
      children.add(new ScaleTransition(
154 155 156 157
        // TODO(abarth): We should use ReversedAnimation here.
        scale: new Tween<double>(
          begin: 1.0,
          end: 0.0
158
        ).animate(new CurvedAnimation(
159 160 161
          parent: controller,
          curve: const Interval(0.0, 0.5, curve: Curves.easeIn)
        )),
162 163 164 165 166
        child: oldChild
      ));
    }

    children.add(new ScaleTransition(
167 168 169 170
      scale: new CurvedAnimation(
        parent: controller,
        curve: const Interval(0.5, 1.0, curve: Curves.easeIn)
      ),
171 172 173 174 175 176 177
      child: config.child
    ));

    return new Stack(children: children);
  }
}

Hixie's avatar
Hixie committed
178
class Scaffold extends StatefulComponent {
Adam Barth's avatar
Adam Barth committed
179 180 181
  Scaffold({
    Key key,
    this.toolBar,
Hixie's avatar
Hixie committed
182
    this.body,
183
    this.floatingActionButton,
184 185 186 187 188 189 190 191
    this.drawer,
    this.scrollableKey,
    this.appBarBehavior: AppBarBehavior.anchor,
    this.appBarHeight
  }) : super(key: key) {
    assert((appBarBehavior == AppBarBehavior.scroll) ? scrollableKey != null : true);
    assert((appBarBehavior == AppBarBehavior.scroll) ? appBarHeight != null && appBarHeight > kToolBarHeight : true);
  }
Adam Barth's avatar
Adam Barth committed
192

193
  final ToolBar toolBar;
Hixie's avatar
Hixie committed
194
  final Widget body;
Adam Barth's avatar
Adam Barth committed
195
  final Widget floatingActionButton;
196
  final Widget drawer;
197 198 199
  final Key scrollableKey;
  final AppBarBehavior appBarBehavior;
  final double appBarHeight;
Hixie's avatar
Hixie committed
200

201
  /// The state from the closest instance of this class that encloses the given context.
Ian Hickson's avatar
Ian Hickson committed
202
  static ScaffoldState of(BuildContext context) => context.ancestorStateOfType(const TypeMatcher<ScaffoldState>());
Hixie's avatar
Hixie committed
203 204 205 206 207 208

  ScaffoldState createState() => new ScaffoldState();
}

class ScaffoldState extends State<Scaffold> {

209 210 211 212 213 214 215 216
  // APPBAR API

  AnimationController _appBarController;

  Animation<double> get appBarAnimation => _appBarController.view;

  double get appBarHeight => config.appBarHeight;

217 218 219 220 221 222 223 224
  // DRAWER API

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

  void openDrawer() {
    _drawerKey.currentState.open();
  }

225 226
  // SNACKBAR API

227
  Queue<ScaffoldFeatureController<SnackBar>> _snackBars = new Queue<ScaffoldFeatureController<SnackBar>>();
228
  AnimationController _snackBarController;
Hixie's avatar
Hixie committed
229 230
  Timer _snackBarTimer;

231
  ScaffoldFeatureController showSnackBar(SnackBar snackbar) {
232
    _snackBarController ??= SnackBar.createAnimationController()
Hixie's avatar
Hixie committed
233
      ..addStatusListener(_handleSnackBarStatusChange);
234
    if (_snackBars.isEmpty) {
235 236
      assert(_snackBarController.isDismissed);
      _snackBarController.forward();
237
    }
238 239
    ScaffoldFeatureController<SnackBar> controller;
    controller = new ScaffoldFeatureController<SnackBar>._(
240 241 242
      // 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.
243
      snackbar.withAnimation(_snackBarController, fallbackKey: new UniqueKey()),
244 245 246 247 248 249 250
      new Completer(),
      () {
        assert(_snackBars.first == controller);
        _hideSnackBar();
      },
      null // SnackBar doesn't use a builder function so setState() wouldn't rebuild it
    );
Hixie's avatar
Hixie committed
251
    setState(() {
252
      _snackBars.addLast(controller);
Hixie's avatar
Hixie committed
253
    });
254
    return controller;
Hixie's avatar
Hixie committed
255 256
  }

257
  void _handleSnackBarStatusChange(AnimationStatus status) {
Hixie's avatar
Hixie committed
258
    switch (status) {
259
      case AnimationStatus.dismissed:
Hixie's avatar
Hixie committed
260 261 262 263
        assert(_snackBars.isNotEmpty);
        setState(() {
          _snackBars.removeFirst();
        });
264
        if (_snackBars.isNotEmpty)
265
          _snackBarController.forward();
Hixie's avatar
Hixie committed
266
        break;
267
      case AnimationStatus.completed:
Hixie's avatar
Hixie committed
268 269 270 271 272
        setState(() {
          assert(_snackBarTimer == null);
          // build will create a new timer if necessary to dismiss the snack bar
        });
        break;
273 274
      case AnimationStatus.forward:
      case AnimationStatus.reverse:
Hixie's avatar
Hixie committed
275 276 277 278 279
        break;
    }
  }

  void _hideSnackBar() {
280 281
    assert(_snackBarController.status == AnimationStatus.forward ||
           _snackBarController.status == AnimationStatus.completed);
282
    _snackBars.first._completer.complete();
283
    _snackBarController.reverse();
284
    _snackBarTimer?.cancel();
Hixie's avatar
Hixie committed
285 286 287
    _snackBarTimer = null;
  }

288 289 290 291

  // PERSISTENT BOTTOM SHEET API

  List<Widget> _dismissedBottomSheets;
292
  ScaffoldFeatureController _currentBottomSheet;
293

294
  ScaffoldFeatureController showBottomSheet(WidgetBuilder builder) {
295 296 297 298 299 300
    if (_currentBottomSheet != null) {
      _currentBottomSheet.close();
      assert(_currentBottomSheet == null);
    }
    Completer completer = new Completer();
    GlobalKey<_PersistentBottomSheetState> bottomSheetKey = new GlobalKey<_PersistentBottomSheetState>();
301
    AnimationController controller = BottomSheet.createAnimationController()
302 303
      ..forward();
    _PersistentBottomSheet bottomSheet;
Hixie's avatar
Hixie committed
304 305
    LocalHistoryEntry entry = new LocalHistoryEntry(
      onRemove: () {
306 307 308 309 310 311 312 313 314 315 316
        assert(_currentBottomSheet._widget == bottomSheet);
        assert(bottomSheetKey.currentState != null);
        bottomSheetKey.currentState.close();
        _dismissedBottomSheets ??= <Widget>[];
        _dismissedBottomSheets.add(bottomSheet);
        _currentBottomSheet = null;
        completer.complete();
      }
    );
    bottomSheet = new _PersistentBottomSheet(
      key: bottomSheetKey,
317
      animationController: controller,
318 319
      onClosing: () {
        assert(_currentBottomSheet._widget == bottomSheet);
Hixie's avatar
Hixie committed
320
        entry.remove();
321 322 323 324 325 326 327 328 329
      },
      onDismissed: () {
        assert(_dismissedBottomSheets != null);
        setState(() {
          _dismissedBottomSheets.remove(bottomSheet);
        });
      },
      builder: builder
    );
Hixie's avatar
Hixie committed
330
    ModalRoute.of(context).addLocalHistoryEntry(entry);
331
    setState(() {
332
      _currentBottomSheet = new ScaffoldFeatureController._(
333
        bottomSheet,
334
        completer,
Hixie's avatar
Hixie committed
335
        () => entry.remove(),
Hans Muller's avatar
Hans Muller committed
336
        (VoidCallback fn) { bottomSheetKey.currentState?.setState(fn); }
337 338 339 340 341 342 343 344
      );
    });
    return _currentBottomSheet;
  }


  // INTERNALS

345 346 347
  void initState() {
    super.initState();
    _appBarController = new AnimationController();
348 349 350 351 352 353
    List<double> scrollValues = PageStorage.of(context)?.readState(context);
    if (scrollValues != null) {
      assert(scrollValues.length == 2);
      _scrollOffset = scrollValues[0];
      _scrollOffsetDelta = scrollValues[1];
    }
354 355
  }

Hixie's avatar
Hixie committed
356
  void dispose() {
357
    _appBarController.stop();
358 359
    _snackBarController?.stop();
    _snackBarController = null;
Hixie's avatar
Hixie committed
360 361
    _snackBarTimer?.cancel();
    _snackBarTimer = null;
362
    PageStorage.of(context)?.writeState(context, <double>[_scrollOffset, _scrollOffsetDelta]);
Hixie's avatar
Hixie committed
363 364 365 366 367 368 369
    super.dispose();
  }

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

371 372
  bool _shouldShowBackArrow;

Hans Muller's avatar
Hans Muller committed
373
  Widget _getModifiedToolBar({ EdgeDims padding, double foregroundOpacity: 1.0, int elevation }) {
374 375 376
    ToolBar toolBar = config.toolBar;
    if (toolBar == null)
      return null;
377
    EdgeDims toolBarPadding = new EdgeDims.only(top: padding.top);
378 379 380 381
    Widget left = toolBar.left;
    if (left == null) {
      if (config.drawer != null) {
        left = new IconButton(
382
          icon: Icons.menu,
Hixie's avatar
Hixie committed
383 384
          onPressed: openDrawer,
          tooltip: 'Open navigation menu' // TODO(ianh): Figure out how to localize this string
385 386 387 388 389
        );
      } else {
        _shouldShowBackArrow ??= Navigator.canPop(context);
        if (_shouldShowBackArrow) {
          left = new IconButton(
390
            icon: Icons.arrow_back,
Hixie's avatar
Hixie committed
391 392
            onPressed: () => Navigator.pop(context),
            tooltip: 'Back' // TODO(ianh): Figure out how to localize this string
393 394 395 396 397
          );
        }
      }
    }
    return toolBar.copyWith(
Hans Muller's avatar
Hans Muller committed
398
      elevation: elevation ?? toolBar.elevation ?? 4,
399
      padding: toolBarPadding,
400
      foregroundOpacity: foregroundOpacity,
401 402 403 404
      left: left
    );
  }

405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464
  double _scrollOffset = 0.0;
  double _scrollOffsetDelta = 0.0;
  double _floatingAppBarHeight = 0.0;

  bool _handleScrollNotification(ScrollNotification notification) {
    final double newScrollOffset = notification.scrollable.scrollOffset;
    if (config.scrollableKey != null && config.scrollableKey == notification.scrollable.config.key)
      setState(() {
        _scrollOffsetDelta = _scrollOffset - newScrollOffset;
        _scrollOffset = newScrollOffset;
      });
    return false;
  }

  double _toolBarOpacity(double progress) {
    // The value of progress is 1.0 if the entire (padded) toolbar is visible, 0.0
    // if the toolbar's height is zero.
    return new Tween<double>(begin: 0.0, end: 1.0).evaluate(new CurvedAnimation(
      parent: new AnimationController()..value = progress.clamp(0.0, 1.0),
      curve: new Interval(0.50, 1.0)
    ));
  }

  Widget _buildScrollableAppBar(BuildContext context) {
    final EdgeDims toolBarPadding = MediaQuery.of(context)?.padding ?? EdgeDims.zero;
    final double toolBarHeight = kToolBarHeight + toolBarPadding.top;
    Widget appBar;

    if (_scrollOffset <= appBarHeight && _scrollOffset >= appBarHeight - toolBarHeight) {
      // scrolled to the top, only the toolbar is (partially) visible
      final double height = math.max(_floatingAppBarHeight, appBarHeight - _scrollOffset);
      final double opacity = _toolBarOpacity(1.0 - ((toolBarHeight - height) / toolBarHeight));
      _appBarController.value = (appBarHeight - height) / appBarHeight;
      appBar = new SizedBox(
        height: height,
        child: _getModifiedToolBar(padding: toolBarPadding, foregroundOpacity: opacity)
      );
    } else if (_scrollOffset > appBarHeight) {
      // scrolled down, show the "floating" toolbar
      _floatingAppBarHeight = (_floatingAppBarHeight + _scrollOffsetDelta).clamp(0.0, toolBarHeight);
      final toolBarOpacity = _toolBarOpacity(_floatingAppBarHeight / toolBarHeight);
      _appBarController.value = (appBarHeight - _floatingAppBarHeight) / appBarHeight;
      appBar = new SizedBox(
        height: _floatingAppBarHeight,
        child: _getModifiedToolBar(padding: toolBarPadding, foregroundOpacity: toolBarOpacity)
      );
    } else {
      // _scrollOffset < appBarHeight - toolBarHeight, scrolled to the top, flexible space is visible
      final double height = appBarHeight - _scrollOffset.clamp(0.0, appBarHeight);
      _appBarController.value = (appBarHeight - height) / appBarHeight;
      appBar = new SizedBox(
        height: height,
        child: _getModifiedToolBar(padding: toolBarPadding, elevation: 0)
      );
      _floatingAppBarHeight = 0.0;
    }

    return appBar;
  }

Adam Barth's avatar
Adam Barth committed
465
  Widget build(BuildContext context) {
466
    EdgeDims padding = MediaQuery.of(context)?.padding ?? EdgeDims.zero;
Hixie's avatar
Hixie committed
467 468 469 470

    if (_snackBars.length > 0) {
      ModalRoute route = ModalRoute.of(context);
      if (route == null || route.isCurrent) {
471
        if (_snackBarController.isCompleted && _snackBarTimer == null)
472
          _snackBarTimer = new Timer(_snackBars.first._widget.duration, _hideSnackBar);
Hixie's avatar
Hixie committed
473 474 475 476
      } else {
        _snackBarTimer?.cancel();
        _snackBarTimer = null;
      }
477
    }
478

479 480
    final List<LayoutId> children = new List<LayoutId>();
    _addIfNonNull(children, config.body, _ScaffoldSlot.body);
481 482 483 484 485 486 487 488
    if (config.appBarBehavior == AppBarBehavior.anchor) {
      Widget toolBar = new ConstrainedBox(
        child: _getModifiedToolBar(padding: padding),
        constraints: new BoxConstraints(maxHeight: config.appBarHeight ?? kExtendedToolBarHeight + padding.top)
      );
      _addIfNonNull(children, toolBar, _ScaffoldSlot.toolBar);
    }
    // Otherwise the ToolBar will be part of a [toolbar, body] Stack. See AppBarBehavior.scroll below.
489 490 491 492 493 494 495 496 497

    if (_currentBottomSheet != null ||
        (_dismissedBottomSheets != null && _dismissedBottomSheets.isNotEmpty)) {
      List<Widget> bottomSheets = <Widget>[];
      if (_dismissedBottomSheets != null && _dismissedBottomSheets.isNotEmpty)
        bottomSheets.addAll(_dismissedBottomSheets);
      if (_currentBottomSheet != null)
        bottomSheets.add(_currentBottomSheet._widget);
      Widget stack = new Stack(
498
        children: bottomSheets,
499 500
        alignment: const FractionalOffset(0.5, 1.0) // bottom-aligned, centered
      );
501
      _addIfNonNull(children, stack, _ScaffoldSlot.bottomSheet);
502 503
    }

Hixie's avatar
Hixie committed
504
    if (_snackBars.isNotEmpty)
505
      _addIfNonNull(children, _snackBars.first._widget, _ScaffoldSlot.snackBar);
506

507 508 509 510 511
    if (config.floatingActionButton != null) {
      Widget fab = new _FloatingActionButtonTransition(
        key: new ValueKey<Key>(config.floatingActionButton.key),
        child: config.floatingActionButton
      );
512
      children.add(new LayoutId(child: fab, id: _ScaffoldSlot.floatingActionButton));
513
    }
514

515 516
    if (config.drawer != null) {
      children.add(new LayoutId(
517
        id: _ScaffoldSlot.drawer,
518 519 520 521 522 523 524
        child: new DrawerController(
          key: _drawerKey,
          child: config.drawer
        )
      ));
    }

525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549
    Widget application;

    if (config.appBarBehavior == AppBarBehavior.scroll) {
      double overScroll = _scrollOffset.clamp(double.NEGATIVE_INFINITY, 0.0);
      application = new NotificationListener<ScrollNotification>(
        onNotification: _handleScrollNotification,
        child: new Stack(
          children: <Widget> [
            new CustomMultiChildLayout(
              children: children,
              delegate: new _ScaffoldLayout(
                padding: EdgeDims.zero
              )
            ),
            new Positioned(
              top: -overScroll,
              left: 0.0,
              right: 0.0,
              child: _buildScrollableAppBar(context)
            )
          ]
        )
      );
    } else {
      application = new CustomMultiChildLayout(
550 551 552 553
        children: children,
        delegate: new _ScaffoldLayout(
          padding: padding
        )
554 555 556 557
      );
    }

    return new Material(child: application);
558
  }
559
}
560

561 562 563 564 565 566
class ScaffoldFeatureController<T extends Widget> {
  const ScaffoldFeatureController._(this._widget, this._completer, this.close, this.setState);
  final T _widget;
  final Completer _completer;
  Future get closed => _completer.future;
  final VoidCallback close; // call this to close the bottom sheet or snack bar
567 568 569 570 571 572
  final StateSetter setState;
}

class _PersistentBottomSheet extends StatefulComponent {
  _PersistentBottomSheet({
    Key key,
573
    this.animationController,
574 575 576 577 578
    this.onClosing,
    this.onDismissed,
    this.builder
  }) : super(key: key);

579
  final AnimationController animationController;
580 581 582 583 584 585 586 587 588
  final VoidCallback onClosing;
  final VoidCallback onDismissed;
  final WidgetBuilder builder;

  _PersistentBottomSheetState createState() => new _PersistentBottomSheetState();
}

class _PersistentBottomSheetState extends State<_PersistentBottomSheet> {

589 590
  // We take ownership of the animation controller given in the first configuration.
  // We also share control of that animation with out BottomSheet widget.
591 592 593

  void initState() {
    super.initState();
594
    assert(config.animationController.status == AnimationStatus.forward);
595
    config.animationController.addStatusListener(_handleStatusChange);
596 597 598 599
  }

  void didUpdateConfig(_PersistentBottomSheet oldConfig) {
    super.didUpdateConfig(oldConfig);
600
    assert(config.animationController == oldConfig.animationController);
601 602 603
  }

  void dispose() {
604
    config.animationController.stop();
605 606 607 608
    super.dispose();
  }

  void close() {
609
    config.animationController.reverse();
610 611
  }

612 613
  void _handleStatusChange(AnimationStatus status) {
    if (status == AnimationStatus.dismissed && config.onDismissed != null)
614 615 616 617
      config.onDismissed();
  }

  Widget build(BuildContext context) {
618 619
    return new AnimatedBuilder(
      animation: config.animationController,
620
      builder: (BuildContext context, Widget child) {
621 622 623 624 625
        return new Align(
          alignment: const FractionalOffset(0.0, 0.0),
          heightFactor: config.animationController.value,
          child: child
        );
626
      },
Hixie's avatar
Hixie committed
627 628 629 630 631 632 633
      child: new Semantics(
        container: true,
        child: new BottomSheet(
          animationController: config.animationController,
          onClosing: config.onClosing,
          builder: config.builder
        )
634
      )
635 636 637 638
    );
  }

}