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

Hixie's avatar
Hixie committed
9
import 'package:flutter/animation.dart';
Hans Muller's avatar
Hans Muller committed
10
import 'package:flutter/rendering.dart';
11
import 'package:flutter/widgets.dart';
12

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

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

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

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

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

  final EdgeDims padding;

43
  void performLayout(Size size, BoxConstraints constraints) {
44

45 46
    BoxConstraints looseConstraints = constraints.loosen();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return new Stack(children: children);
  }
}

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

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

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

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

class ScaffoldState extends State<Scaffold> {

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

  AnimationController _appBarController;

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

  double get appBarHeight => config.appBarHeight;

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

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

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

227 228
  // SNACKBAR API

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

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

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

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

289 290 291 292

  // PERSISTENT BOTTOM SHEET API

  List<Widget> _dismissedBottomSheets;
293
  ScaffoldFeatureController _currentBottomSheet;
294

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


  // INTERNALS

346 347 348 349 350
  void initState() {
    super.initState();
    _appBarController = new AnimationController();
  }

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

365 366
  bool _shouldShowBackArrow;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

class _PersistentBottomSheetState extends State<_PersistentBottomSheet> {

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

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

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

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

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

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

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

}