diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index d523c5ada1b..564bbf97ed7 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart @@ -20,7 +20,6 @@ class StockHome extends StatefulComponent { class StockHomeState extends State { final GlobalKey scaffoldKey = new GlobalKey(); - final GlobalKey _bottomSheetPlaceholderKey = new GlobalKey(); bool _isSearching = false; String _searchQuery; @@ -202,11 +201,7 @@ class StockHomeState extends State { Navigator.of(context).pushNamed('/stock/${stock.symbol}', mostValuableKeys: mostValuableKeys); }, onShow: (Stock stock, Key arrowKey) { - showBottomSheet( - placeholderKey: _bottomSheetPlaceholderKey, - context: context, - child: new StockSymbolBottomSheet(stock: stock) - ); + scaffoldKey.currentState.showBottomSheet((BuildContext context) => new StockSymbolBottomSheet(stock: stock)); } ); } @@ -256,12 +251,14 @@ class StockHomeState extends State { showModalBottomSheet( // TODO(ianh): Fill this out. context: context, - child: new Column([ - new Input( - key: companyNameKey, - placeholder: 'Company Name' - ), - ]) + builder: (BuildContext context) { + return new Column([ + new Input( + key: companyNameKey, + placeholder: 'Company Name' + ), + ]); + } ); } @@ -278,7 +275,6 @@ class StockHomeState extends State { key: scaffoldKey, toolBar: _isSearching ? buildSearchBar() : buildToolBar(), body: buildTabNavigator(), - bottomSheet: new Placeholder(key: _bottomSheetPlaceholderKey), floatingActionButton: buildFloatingActionButton() ); } diff --git a/examples/stocks/lib/stock_symbol_viewer.dart b/examples/stocks/lib/stock_symbol_viewer.dart index 9d4affd5eef..62cb1c642e0 100644 --- a/examples/stocks/lib/stock_symbol_viewer.dart +++ b/examples/stocks/lib/stock_symbol_viewer.dart @@ -80,11 +80,11 @@ class StockSymbolBottomSheet extends StatelessComponent { Widget build(BuildContext context) { return new Container( - child: new StockSymbolView(stock: stock), padding: new EdgeDims.all(10.0), decoration: new BoxDecoration( border: new Border(top: new BorderSide(color: Colors.black26, width: 1.0)) - ) + ), + child: new StockSymbolView(stock: stock) ); } } diff --git a/packages/flutter/lib/src/material/bottom_sheet.dart b/packages/flutter/lib/src/material/bottom_sheet.dart index f88e627bac2..5e268328237 100644 --- a/packages/flutter/lib/src/material/bottom_sheet.dart +++ b/packages/flutter/lib/src/material/bottom_sheet.dart @@ -18,17 +18,31 @@ const double _kCloseProgressThreshold = 0.5; const Color _kTransparent = const Color(0x00000000); const Color _kBarrierColor = Colors.black54; -class _BottomSheetDragController extends StatelessComponent { - _BottomSheetDragController({ +class BottomSheet extends StatelessComponent { + BottomSheet({ Key key, this.performance, - this.child, - this.childHeight - }) : super(key: key); + this.onClosing, + this.childHeight, + this.builder + }) : super(key: key) { + assert(onClosing != null); + } + /// The performance that controls the bottom sheet's position. The BottomSheet + /// widget will manipulate the position of this performance, it is not just a + /// passive observer. final Performance performance; - final Widget child; + final VoidCallback onClosing; final double childHeight; + final WidgetBuilder builder; + + static Performance createPerformance() { + return new Performance( + duration: _kBottomSheetDuration, + debugLabel: 'BottomSheet' + ); + } bool get _dismissUnderway => performance.direction == AnimationDirection.reverse; @@ -42,13 +56,11 @@ class _BottomSheetDragController extends StatelessComponent { if (_dismissUnderway) return; if (velocity.dy > _kMinFlingVelocity) { - performance.fling(velocity: -velocity.dy / childHeight).then((_) { - Navigator.of(context).pop(); - }); + performance.fling(velocity: -velocity.dy / childHeight); + onClosing(); } else if (performance.progress < _kCloseProgressThreshold) { - performance.fling(velocity: -1.0).then((_) { - Navigator.of(context).pop(); - }); + performance.fling(velocity: -1.0); + onClosing(); } else { performance.forward(); } @@ -58,46 +70,19 @@ class _BottomSheetDragController extends StatelessComponent { return new GestureDetector( onVerticalDragUpdate: _handleDragUpdate, onVerticalDragEnd: (Offset velocity) { _handleDragEnd(velocity, context); }, - child: child + child: new Material( + child: builder(context) + ) ); } } -class _BottomSheetRoute extends OverlayRoute { - _BottomSheetRoute({ this.completer, this.child }); +// PERSISTENT BOTTOM SHEETS - final Completer completer; - final Widget child; - Performance performance; +// See scaffold.dart - void didPush(OverlayState overlay, OverlayEntry insertionPoint) { - performance = new Performance(duration: _kBottomSheetDuration, debugLabel: debugLabel) - ..forward(); - super.didPush(overlay, insertionPoint); - } - void didPop(dynamic result) { - void finish() { - super.didPop(result); // clear the overlay entries - completer.complete(result); - } - if (performance.isDismissed) - finish(); - else - performance.reverse().then((_) { finish(); }); - } - - String get debugLabel => '$runtimeType'; - String toString() => '$runtimeType(performance: $performance)'; -} - -class _ModalBottomSheet extends StatefulComponent { - _ModalBottomSheet({ Key key, this.route }) : super(key: key); - - final _ModalBottomSheetRoute route; - - _ModalBottomSheetState createState() => new _ModalBottomSheetState(); -} +// MODAL BOTTOM SHEETS class _ModalBottomSheetLayout extends OneChildLayoutDelegate { // The distance from the bottom of the parent to the top of the BottomSheet child. @@ -118,6 +103,14 @@ class _ModalBottomSheetLayout extends OneChildLayoutDelegate { } } +class _ModalBottomSheet extends StatefulComponent { + _ModalBottomSheet({ Key key, this.route }) : super(key: key); + + final _ModalBottomSheetRoute route; + + _ModalBottomSheetState createState() => new _ModalBottomSheetState(); +} + class _ModalBottomSheetState extends State<_ModalBottomSheet> { final _ModalBottomSheetLayout _layout = new _ModalBottomSheetLayout(); @@ -133,10 +126,11 @@ class _ModalBottomSheetState extends State<_ModalBottomSheet> { child: new CustomOneChildLayout( delegate: _layout, token: _layout.childTop.value, - child: new _BottomSheetDragController( + child: new BottomSheet( performance: config.route.performance, - child: new Material(child: config.route.child), - childHeight: _layout.childTop.end + onClosing: () { Navigator.of(context).pop(); }, + childHeight: _layout.childTop.end, + builder: config.route.builder ) ) ); @@ -146,9 +140,30 @@ class _ModalBottomSheetState extends State<_ModalBottomSheet> { } } -class _ModalBottomSheetRoute extends _BottomSheetRoute { - _ModalBottomSheetRoute({ Completer completer, Widget child }) - : super(completer: completer, child: child); +class _ModalBottomSheetRoute extends OverlayRoute { + _ModalBottomSheetRoute({ this.completer, this.builder }); + + final Completer completer; + final WidgetBuilder builder; + Performance performance; + + void didPush(OverlayState overlay, OverlayEntry insertionPoint) { + performance = BottomSheet.createPerformance() + ..forward(); + super.didPush(overlay, insertionPoint); + } + + void _finish(dynamic result) { + super.didPop(result); // clear the overlay entries + completer.complete(result); + } + + void didPop(dynamic result) { + if (performance.isDismissed) + _finish(result); + else + performance.reverse().then((_) { _finish(result); }); + } Widget _buildModalBarrier(BuildContext context) { return new AnimatedModalBarrier( @@ -168,61 +183,18 @@ class _ModalBottomSheetRoute extends _BottomSheetRoute { _buildModalBarrier, _buildBottomSheet, ]; + + String get debugLabel => '$runtimeType'; + String toString() => '$runtimeType(performance: $performance)'; } -Future showModalBottomSheet({ BuildContext context, Widget child }) { - assert(child != null); +Future showModalBottomSheet({ BuildContext context, WidgetBuilder builder }) { + assert(context != null); + assert(builder != null); final Completer completer = new Completer(); Navigator.of(context).pushEphemeral(new _ModalBottomSheetRoute( completer: completer, - child: child + builder: builder )); return completer.future; } - -class _PersistentBottomSheet extends StatefulComponent { - _PersistentBottomSheet({ Key key, this.route }) : super(key: key); - - final _BottomSheetRoute route; - - _PersistentBottomSheetState createState() => new _PersistentBottomSheetState(); -} - -class _PersistentBottomSheetState extends State<_PersistentBottomSheet> { - - double _childHeight; - void _updateChildHeight(Size newSize) { - setState(() { - _childHeight = newSize.height; - }); - } - - Widget build(BuildContext context) { - return new AlignTransition( - performance: config.route.performance, - alignment: new AnimatedValue(const FractionalOffset(0.0, 0.0)), - heightFactor: new AnimatedValue(0.0, end: 1.0), - child: new _BottomSheetDragController( - performance: config.route.performance, - childHeight: _childHeight, - child: new Material( - child: new SizeObserver(child: config.route.child, onSizeChanged: _updateChildHeight) - ) - ) - ); - } -} - -Future showBottomSheet({ BuildContext context, GlobalKey placeholderKey, Widget child }) { - assert(child != null); - assert(placeholderKey != null); - final Completer completer = new Completer(); - _BottomSheetRoute route = new _BottomSheetRoute(child: child, completer: completer); - placeholderKey.currentState.child = new _PersistentBottomSheet(route: route); - Navigator.of(context).pushEphemeral(route); - return completer.future.then((_) { - // If our overlay has been obscured by an opaque OverlayEntry then currentState - // will have been cleared already. - placeholderKey.currentState?.child = null; - }); -} diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index af78e38dbef..0139c2e1ce5 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -11,9 +11,10 @@ import 'package:flutter/animation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'bottom_sheet.dart'; import 'material.dart'; -import 'tool_bar.dart'; import 'snack_bar.dart'; +import 'tool_bar.dart'; const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent @@ -57,7 +58,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { if (isChild(_Child.bottomSheet)) { bottomSheetSize = layoutChild(_Child.bottomSheet, fullWidthConstraints); - positionChild(_Child.bottomSheet, new Point(0.0, size.height - bottomSheetSize.height)); + positionChild(_Child.bottomSheet, new Point((size.width - bottomSheetSize.width) / 2.0, size.height - bottomSheetSize.height)); } if (isChild(_Child.snackBar)) { @@ -85,13 +86,11 @@ class Scaffold extends StatefulComponent { Key key, this.toolBar, this.body, - this.bottomSheet, this.floatingActionButton }) : super(key: key); final ToolBar toolBar; final Widget body; - final Widget bottomSheet; // this is for non-modal bottom sheets final Widget floatingActionButton; static ScaffoldState of(BuildContext context) => context.ancestorStateOfType(ScaffoldState); @@ -101,6 +100,8 @@ class Scaffold extends StatefulComponent { class ScaffoldState extends State { + // SNACKBAR API + Queue _snackBars = new Queue(); Performance _snackBarPerformance; Timer _snackBarTimer; @@ -108,6 +109,10 @@ class ScaffoldState extends State { void showSnackBar(SnackBar snackbar) { _snackBarPerformance ??= SnackBar.createPerformance() ..addStatusListener(_handleSnackBarStatusChange); + if (_snackBars.isEmpty) { + assert(_snackBarPerformance.isDismissed); + _snackBarPerformance.forward(); + } setState(() { _snackBars.addLast(snackbar.withPerformance(_snackBarPerformance)); }); @@ -120,6 +125,8 @@ class ScaffoldState extends State { setState(() { _snackBars.removeFirst(); }); + if (_snackBars.isNotEmpty) + _snackBarPerformance.forward(); break; case PerformanceStatus.completed: setState(() { @@ -138,6 +145,63 @@ class ScaffoldState extends State { _snackBarTimer = null; } + + // PERSISTENT BOTTOM SHEET API + + List _dismissedBottomSheets; + BottomSheetController _currentBottomSheet; + + BottomSheetController showBottomSheet(WidgetBuilder builder) { + if (_currentBottomSheet != null) { + _currentBottomSheet.close(); + assert(_currentBottomSheet == null); + } + Completer completer = new Completer(); + GlobalKey<_PersistentBottomSheetState> bottomSheetKey = new GlobalKey<_PersistentBottomSheetState>(); + Performance performance = BottomSheet.createPerformance() + ..forward(); + _PersistentBottomSheet bottomSheet; + Route route = new StateRoute( + onPop: () { + assert(_currentBottomSheet._widget == bottomSheet); + assert(bottomSheetKey.currentState != null); + bottomSheetKey.currentState.close(); + _dismissedBottomSheets ??= []; + _dismissedBottomSheets.add(bottomSheet); + _currentBottomSheet = null; + completer.complete(); + } + ); + bottomSheet = new _PersistentBottomSheet( + key: bottomSheetKey, + performance: performance, + onClosing: () { + assert(_currentBottomSheet._widget == bottomSheet); + Navigator.of(context).remove(route); + }, + onDismissed: () { + assert(_dismissedBottomSheets != null); + setState(() { + _dismissedBottomSheets.remove(bottomSheet); + }); + }, + builder: builder + ); + Navigator.of(context).push(route); + setState(() { + _currentBottomSheet = new BottomSheetController._( + bottomSheet, + completer.future, + () => Navigator.of(context).remove(route), + setState + ); + }); + return _currentBottomSheet; + } + + + // INTERNALS + void dispose() { _snackBarPerformance?.stop(); _snackBarPerformance = null; @@ -156,8 +220,6 @@ class ScaffoldState extends State { final Widget materialBody = config.body != null ? new Material(child: config.body) : null; if (_snackBars.length > 0) { - if (_snackBarPerformance.isDismissed) - _snackBarPerformance.forward(); ModalRoute route = ModalRoute.of(context); if (route == null || route.isCurrent) { if (_snackBarPerformance.isCompleted && _snackBarTimer == null) @@ -171,12 +233,105 @@ class ScaffoldState extends State { final Listchildren = new List(); _addIfNonNull(children, materialBody, _Child.body); _addIfNonNull(children, paddedToolBar, _Child.toolBar); - _addIfNonNull(children, config.bottomSheet, _Child.bottomSheet); + + if (_currentBottomSheet != null || + (_dismissedBottomSheets != null && _dismissedBottomSheets.isNotEmpty)) { + List bottomSheets = []; + if (_dismissedBottomSheets != null && _dismissedBottomSheets.isNotEmpty) + bottomSheets.addAll(_dismissedBottomSheets); + if (_currentBottomSheet != null) + bottomSheets.add(_currentBottomSheet._widget); + Widget stack = new Stack( + bottomSheets, + alignment: const FractionalOffset(0.5, 1.0) // bottom-aligned, centered + ); + _addIfNonNull(children, stack, _Child.bottomSheet); + } + if (_snackBars.isNotEmpty) _addIfNonNull(children, _snackBars.first, _Child.snackBar); + _addIfNonNull(children, config.floatingActionButton, _Child.floatingActionButton); return new CustomMultiChildLayout(children, delegate: _scaffoldLayout); } } + +class BottomSheetController { + const BottomSheetController._(this._widget, this.closed, this.close, this.setState); + final Widget _widget; + final Future closed; + final VoidCallback close; // call this to close the bottom sheet + final StateSetter setState; +} + +class _PersistentBottomSheet extends StatefulComponent { + _PersistentBottomSheet({ + Key key, + this.performance, + this.onClosing, + this.onDismissed, + this.builder + }) : super(key: key); + + final Performance performance; + final VoidCallback onClosing; + final VoidCallback onDismissed; + final WidgetBuilder builder; + + _PersistentBottomSheetState createState() => new _PersistentBottomSheetState(); +} + +class _PersistentBottomSheetState extends State<_PersistentBottomSheet> { + + // We take ownership of the performance given in the first configuration. + // We also share control of that performance with out BottomSheet widget. + + void initState() { + super.initState(); + assert(config.performance.status == PerformanceStatus.forward); + config.performance.addStatusListener(_handleStatusChange); + } + + void didUpdateConfig(_PersistentBottomSheet oldConfig) { + super.didUpdateConfig(oldConfig); + assert(config.performance == oldConfig.performance); + } + + void dispose() { + config.performance.stop(); + super.dispose(); + } + + void close() { + config.performance.reverse(); + } + + void _handleStatusChange(PerformanceStatus status) { + if (status == PerformanceStatus.dismissed && config.onDismissed != null) + config.onDismissed(); + } + + double _childHeight; + void _updateChildHeight(Size newSize) { + setState(() { + _childHeight = newSize.height; + }); + } + + Widget build(BuildContext context) { + return new AlignTransition( + performance: config.performance, + alignment: new AnimatedValue(const FractionalOffset(0.0, 0.0)), + heightFactor: new AnimatedValue(0.0, end: 1.0), + child: new BottomSheet( + performance: config.performance, + onClosing: config.onClosing, + childHeight: _childHeight, + builder: (BuildContext context) => new SizeObserver(child: config.builder(context), onSizeChanged: _updateChildHeight) + ) + ); + } + +} diff --git a/packages/flutter/lib/src/material/snack_bar.dart b/packages/flutter/lib/src/material/snack_bar.dart index 8254874bc09..fb28461f1e1 100644 --- a/packages/flutter/lib/src/material/snack_bar.dart +++ b/packages/flutter/lib/src/material/snack_bar.dart @@ -21,10 +21,10 @@ const Color _kSnackBackground = const Color(0xFF323232); // TODO(ianh): Implement the Tablet version of snackbar if we're "on a tablet". -const Duration kSnackBarTransitionDuration = const Duration(milliseconds: 250); +const Duration _kSnackBarTransitionDuration = const Duration(milliseconds: 250); const Duration kSnackBarShortDisplayDuration = const Duration(milliseconds: 1500); const Duration kSnackBarMediumDisplayDuration = const Duration(milliseconds: 2750); -const Curve snackBarFadeCurve = const Interval(0.72, 1.0, curve: Curves.fastOutSlowIn); +const Curve _snackBarFadeCurve = const Interval(0.72, 1.0, curve: Curves.fastOutSlowIn); class SnackBarAction extends StatelessComponent { SnackBarAction({Key key, this.label, this.onPressed }) : super(key: key) { @@ -91,7 +91,7 @@ class SnackBar extends StatelessComponent { style: new TextStyle(color: Theme.of(context).accentColor), child: new FadeTransition( performance: performance, - opacity: new AnimatedValue(0.0, end: 1.0, curve: snackBarFadeCurve), + opacity: new AnimatedValue(0.0, end: 1.0, curve: _snackBarFadeCurve), child: new Row(children) ) ) @@ -105,7 +105,7 @@ class SnackBar extends StatelessComponent { static Performance createPerformance() { return new Performance( - duration: kSnackBarTransitionDuration, + duration: _kSnackBarTransitionDuration, debugLabel: 'SnackBar' ); } diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 49f53b6a19c..d40d660773d 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -332,6 +332,9 @@ enum _StateLifecycle { defunct, } +/// The signature of setState() methods. +typedef void StateSetter(VoidCallback fn); + /// The logic and internal state for a StatefulComponent. abstract class State { /// The current configuration (an instance of the corresponding @@ -377,7 +380,7 @@ abstract class State { /// If you just change the state directly without calling setState(), then the /// component will not be scheduled for rebuilding, meaning that its rendering /// will not be updated. - void setState(void fn()) { + void setState(VoidCallback fn) { assert(_debugLifecycleState != _StateLifecycle.defunct); fn(); _element.markNeedsBuild(); diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index b4813694b06..c22d45f6786 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -133,6 +133,28 @@ class NavigatorState extends State { assert(_ephemeral.isEmpty); } + /// Pops the given route, if it's the current route. If it's not the current + /// route, removes it from the list of active routes without notifying any + /// observers or adjacent routes. + /// + /// Do not use this for ModalRoutes, or indeed anything other than + /// StateRoutes. Doing so would cause very odd results, e.g. ModalRoutes would + /// get confused about who is current. + void remove(Route route, [dynamic result]) { + assert(_modal.contains(route)); + assert(route.overlayEntries.isEmpty); + if (_modal.last == route) { + pop(result); + } else { + setState(() { + _modal.remove(route); + route.didPop(result); + }); + } + } + + /// Removes the current route, notifying the observer (if any), and the + /// previous routes (using [Route.didPopNext]). void pop([dynamic result]) { setState(() { // We use setState to guarantee that we'll rebuild, since the routes can't diff --git a/packages/unit/test/widget/bottom_sheet_test.dart b/packages/unit/test/widget/bottom_sheet_test.dart index 9448fe6577c..3af6df5974a 100644 --- a/packages/unit/test/widget/bottom_sheet_test.dart +++ b/packages/unit/test/widget/bottom_sheet_test.dart @@ -26,7 +26,10 @@ void main() { tester.pump(); expect(tester.findText('BottomSheet'), isNull); - showModalBottomSheet(context: context, child: new Text('BottomSheet')).then((_) { + showModalBottomSheet( + context: context, + builder: (BuildContext context) => new Text('BottomSheet') + ).then((_) { showBottomSheetThenCalled = true; }); @@ -42,7 +45,7 @@ void main() { expect(showBottomSheetThenCalled, isTrue); expect(tester.findText('BottomSheet'), isNull); - showModalBottomSheet(context: context, child: new Text('BottomSheet')); + showModalBottomSheet(context: context, builder: (BuildContext context) => new Text('BottomSheet')); tester.pump(); // bottom sheet show animation starts tester.pump(new Duration(seconds: 1)); // animation done expect(tester.findText('BottomSheet'), isNotNull); @@ -58,45 +61,60 @@ void main() { test('Verify that a downwards fling dismisses a persistent BottomSheet', () { testWidgets((WidgetTester tester) { - GlobalKey _bottomSheetPlaceholderKey = new GlobalKey(); - BuildContext context; + GlobalKey scaffoldKey = new GlobalKey(); bool showBottomSheetThenCalled = false; tester.pumpWidget(new MaterialApp( routes: { '/': (RouteArguments args) { - context = args.context; return new Scaffold( - bottomSheet: new Placeholder(key: _bottomSheetPlaceholderKey), + key: scaffoldKey, body: new Center(child: new Text('body')) ); } } )); - tester.pump(); + expect(showBottomSheetThenCalled, isFalse); expect(tester.findText('BottomSheet'), isNull); - showBottomSheet( - context: context, - child: new Container(child: new Text('BottomSheet'), margin: new EdgeDims.all(40.0)), - placeholderKey: _bottomSheetPlaceholderKey - ).then((_) { + scaffoldKey.currentState.showBottomSheet((BuildContext context) { + return new Container( + margin: new EdgeDims.all(40.0), + child: new Text('BottomSheet') + ); + }).closed.then((_) { showBottomSheetThenCalled = true; }); - expect(_bottomSheetPlaceholderKey.currentState.child, isNotNull); + expect(showBottomSheetThenCalled, isFalse); + expect(tester.findText('BottomSheet'), isNull); + tester.pump(); // bottom sheet show animation starts + + expect(showBottomSheetThenCalled, isFalse); + expect(tester.findText('BottomSheet'), isNotNull); + tester.pump(new Duration(seconds: 1)); // animation done + + expect(showBottomSheetThenCalled, isFalse); expect(tester.findText('BottomSheet'), isNotNull); tester.fling(tester.findText('BottomSheet'), const Offset(0.0, 20.0), 1000.0); + tester.pump(); // drain the microtask queue (Future completion callback) + + expect(showBottomSheetThenCalled, isTrue); + expect(tester.findText('BottomSheet'), isNotNull); + tester.pump(); // bottom sheet dismiss animation starts + + expect(showBottomSheetThenCalled, isTrue); + expect(tester.findText('BottomSheet'), isNotNull); + tester.pump(new Duration(seconds: 1)); // animation done - tester.pump(new Duration(seconds: 1)); // rebuild frame without the bottom sheet + expect(showBottomSheetThenCalled, isTrue); expect(tester.findText('BottomSheet'), isNull); - expect(_bottomSheetPlaceholderKey.currentState.child, isNull); }); }); diff --git a/packages/unit/test/widget/widget_tester.dart b/packages/unit/test/widget/widget_tester.dart index 4a2ea3ccdef..5d24a8fcd64 100644 --- a/packages/unit/test/widget/widget_tester.dart +++ b/packages/unit/test/widget/widget_tester.dart @@ -162,7 +162,7 @@ class WidgetTester { assert(velocity != 0.0); // velocity is pixels/second final TestPointer p = new TestPointer(pointer); final HitTestResult result = _hitTest(startLocation); - final kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy + const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy final double timeStampDelta = 1000.0 * offset.distance / (kMoveCount * velocity); double timeStamp = 0.0; _dispatchEvent(p.down(startLocation, timeStamp: new Duration(milliseconds: timeStamp.round())), result);