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

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

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

11
import '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
  void performLayout(Size size, BoxConstraints constraints) {
43

44 45
    BoxConstraints looseConstraints = constraints.loosen();

46 47 48 49 50
    // 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.

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

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

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

    // 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.

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

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

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

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

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

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

111 112 113 114 115 116 117 118 119 120 121 122 123 124
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> {
125
  final AnimationController controller = new AnimationController(duration: _kFloatingActionButtonSegue);
126 127 128 129
  Widget oldChild;

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

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

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

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

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

    return new Stack(children: children);
  }
}

Hixie's avatar
Hixie committed
179
class Scaffold extends StatefulComponent {
Adam Barth's avatar
Adam Barth committed
180 181 182
  Scaffold({
    Key key,
    this.toolBar,
Hixie's avatar
Hixie committed
183
    this.body,
184
    this.floatingActionButton,
185 186 187 188 189 190 191 192
    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
193

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

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

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

class ScaffoldState extends State<Scaffold> {

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

  AnimationController _appBarController;

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

  double get appBarHeight => config.appBarHeight;

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

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

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

226 227
  // SNACKBAR API

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

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

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

  void _hideSnackBar() {
281 282
    assert(_snackBarController.status == AnimationStatus.forward ||
           _snackBarController.status == AnimationStatus.completed);
283
    _snackBars.first._completer.complete();
284
    _snackBarController.reverse();
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 348 349
  void initState() {
    super.initState();
    _appBarController = new AnimationController();
  }

Hixie's avatar
Hixie committed
350
  void dispose() {
351
    _appBarController.stop();
352 353
    _snackBarController?.stop();
    _snackBarController = null;
Hixie's avatar
Hixie committed
354 355 356 357 358 359 360 361 362
    _snackBarTimer?.cancel();
    _snackBarTimer = null;
    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
363

364 365
  bool _shouldShowBackArrow;

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

398 399 400 401 402 403 404 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
  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
458
  Widget build(BuildContext context) {
459
    EdgeDims padding = MediaQuery.of(context)?.padding ?? EdgeDims.zero;
Hixie's avatar
Hixie committed
460 461 462 463

    if (_snackBars.length > 0) {
      ModalRoute route = ModalRoute.of(context);
      if (route == null || route.isCurrent) {
464
        if (_snackBarController.isCompleted && _snackBarTimer == null)
465
          _snackBarTimer = new Timer(_snackBars.first._widget.duration, _hideSnackBar);
Hixie's avatar
Hixie committed
466 467 468 469
      } else {
        _snackBarTimer?.cancel();
        _snackBarTimer = null;
      }
470
    }
471

472 473
    final List<LayoutId> children = new List<LayoutId>();
    _addIfNonNull(children, config.body, _ScaffoldSlot.body);
474 475 476 477 478 479 480 481
    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.
482 483 484 485 486 487 488 489 490

    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(
491
        children: bottomSheets,
492 493
        alignment: const FractionalOffset(0.5, 1.0) // bottom-aligned, centered
      );
494
      _addIfNonNull(children, stack, _ScaffoldSlot.bottomSheet);
495 496
    }

Hixie's avatar
Hixie committed
497
    if (_snackBars.isNotEmpty)
498
      _addIfNonNull(children, _snackBars.first._widget, _ScaffoldSlot.snackBar);
499

500 501 502 503 504
    if (config.floatingActionButton != null) {
      Widget fab = new _FloatingActionButtonTransition(
        key: new ValueKey<Key>(config.floatingActionButton.key),
        child: config.floatingActionButton
      );
505
      children.add(new LayoutId(child: fab, id: _ScaffoldSlot.floatingActionButton));
506
    }
507

508 509
    if (config.drawer != null) {
      children.add(new LayoutId(
510
        id: _ScaffoldSlot.drawer,
511 512 513 514 515 516 517
        child: new DrawerController(
          key: _drawerKey,
          child: config.drawer
        )
      ));
    }

518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542
    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(
543 544 545 546
        children: children,
        delegate: new _ScaffoldLayout(
          padding: padding
        )
547 548 549 550
      );
    }

    return new Material(child: application);
551
  }
552
}
553

554 555 556 557 558 559
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
560 561 562 563 564 565
  final StateSetter setState;
}

class _PersistentBottomSheet extends StatefulComponent {
  _PersistentBottomSheet({
    Key key,
566
    this.animationController,
567 568 569 570 571
    this.onClosing,
    this.onDismissed,
    this.builder
  }) : super(key: key);

572
  final AnimationController animationController;
573 574 575 576 577 578 579 580 581
  final VoidCallback onClosing;
  final VoidCallback onDismissed;
  final WidgetBuilder builder;

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

class _PersistentBottomSheetState extends State<_PersistentBottomSheet> {

582 583
  // We take ownership of the animation controller given in the first configuration.
  // We also share control of that animation with out BottomSheet widget.
584 585 586

  void initState() {
    super.initState();
587
    assert(config.animationController.status == AnimationStatus.forward);
588
    config.animationController.addStatusListener(_handleStatusChange);
589 590 591 592
  }

  void didUpdateConfig(_PersistentBottomSheet oldConfig) {
    super.didUpdateConfig(oldConfig);
593
    assert(config.animationController == oldConfig.animationController);
594 595 596
  }

  void dispose() {
597
    config.animationController.stop();
598 599 600 601
    super.dispose();
  }

  void close() {
602
    config.animationController.reverse();
603 604
  }

605 606
  void _handleStatusChange(AnimationStatus status) {
    if (status == AnimationStatus.dismissed && config.onDismissed != null)
607 608 609 610
      config.onDismissed();
  }

  Widget build(BuildContext context) {
611 612
    return new AnimatedBuilder(
      animation: config.animationController,
613
      builder: (BuildContext context, Widget child) {
614 615 616 617 618
        return new Align(
          alignment: const FractionalOffset(0.0, 0.0),
          heightFactor: config.animationController.value,
          child: child
        );
619
      },
Hixie's avatar
Hixie committed
620 621 622 623 624 625 626
      child: new Semantics(
        container: true,
        child: new BottomSheet(
          animationController: config.animationController,
          onClosing: config.onClosing,
          builder: config.builder
        )
627
      )
628 629 630 631
    );
  }

}