Commit 03e094aa authored by Hixie's avatar Hixie

Convert Persistent Bottom Sheets to a Scaffold API

- `Scaffold.of(context).showBottomSheet(widget);`
- Returns an object with .closed Future and .close() method.
- Uses a StateRoute to handle back button.
- Take the Navigator logic out of the BottomSheet widget.
- Support showing a sheet while an old one is going away.
- Add Navigator.remove().
parent 6a2bd421
...@@ -20,7 +20,6 @@ class StockHome extends StatefulComponent { ...@@ -20,7 +20,6 @@ class StockHome extends StatefulComponent {
class StockHomeState extends State<StockHome> { class StockHomeState extends State<StockHome> {
final GlobalKey scaffoldKey = new GlobalKey(); final GlobalKey scaffoldKey = new GlobalKey();
final GlobalKey<PlaceholderState> _bottomSheetPlaceholderKey = new GlobalKey<PlaceholderState>();
bool _isSearching = false; bool _isSearching = false;
String _searchQuery; String _searchQuery;
...@@ -202,11 +201,7 @@ class StockHomeState extends State<StockHome> { ...@@ -202,11 +201,7 @@ class StockHomeState extends State<StockHome> {
Navigator.of(context).pushNamed('/stock/${stock.symbol}', mostValuableKeys: mostValuableKeys); Navigator.of(context).pushNamed('/stock/${stock.symbol}', mostValuableKeys: mostValuableKeys);
}, },
onShow: (Stock stock, Key arrowKey) { onShow: (Stock stock, Key arrowKey) {
showBottomSheet( scaffoldKey.currentState.showBottomSheet((BuildContext context) => new StockSymbolBottomSheet(stock: stock));
placeholderKey: _bottomSheetPlaceholderKey,
context: context,
child: new StockSymbolBottomSheet(stock: stock)
);
} }
); );
} }
...@@ -256,12 +251,14 @@ class StockHomeState extends State<StockHome> { ...@@ -256,12 +251,14 @@ class StockHomeState extends State<StockHome> {
showModalBottomSheet( showModalBottomSheet(
// TODO(ianh): Fill this out. // TODO(ianh): Fill this out.
context: context, context: context,
child: new Column([ builder: (BuildContext context) {
return new Column([
new Input( new Input(
key: companyNameKey, key: companyNameKey,
placeholder: 'Company Name' placeholder: 'Company Name'
), ),
]) ]);
}
); );
} }
...@@ -278,7 +275,6 @@ class StockHomeState extends State<StockHome> { ...@@ -278,7 +275,6 @@ class StockHomeState extends State<StockHome> {
key: scaffoldKey, key: scaffoldKey,
toolBar: _isSearching ? buildSearchBar() : buildToolBar(), toolBar: _isSearching ? buildSearchBar() : buildToolBar(),
body: buildTabNavigator(), body: buildTabNavigator(),
bottomSheet: new Placeholder(key: _bottomSheetPlaceholderKey),
floatingActionButton: buildFloatingActionButton() floatingActionButton: buildFloatingActionButton()
); );
} }
......
...@@ -80,11 +80,11 @@ class StockSymbolBottomSheet extends StatelessComponent { ...@@ -80,11 +80,11 @@ class StockSymbolBottomSheet extends StatelessComponent {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Container( return new Container(
child: new StockSymbolView(stock: stock),
padding: new EdgeDims.all(10.0), padding: new EdgeDims.all(10.0),
decoration: new BoxDecoration( decoration: new BoxDecoration(
border: new Border(top: new BorderSide(color: Colors.black26, width: 1.0)) border: new Border(top: new BorderSide(color: Colors.black26, width: 1.0))
) ),
child: new StockSymbolView(stock: stock)
); );
} }
} }
...@@ -18,17 +18,31 @@ const double _kCloseProgressThreshold = 0.5; ...@@ -18,17 +18,31 @@ const double _kCloseProgressThreshold = 0.5;
const Color _kTransparent = const Color(0x00000000); const Color _kTransparent = const Color(0x00000000);
const Color _kBarrierColor = Colors.black54; const Color _kBarrierColor = Colors.black54;
class _BottomSheetDragController extends StatelessComponent { class BottomSheet extends StatelessComponent {
_BottomSheetDragController({ BottomSheet({
Key key, Key key,
this.performance, this.performance,
this.child, this.onClosing,
this.childHeight this.childHeight,
}) : super(key: key); 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 Performance performance;
final Widget child; final VoidCallback onClosing;
final double childHeight; final double childHeight;
final WidgetBuilder builder;
static Performance createPerformance() {
return new Performance(
duration: _kBottomSheetDuration,
debugLabel: 'BottomSheet'
);
}
bool get _dismissUnderway => performance.direction == AnimationDirection.reverse; bool get _dismissUnderway => performance.direction == AnimationDirection.reverse;
...@@ -42,13 +56,11 @@ class _BottomSheetDragController extends StatelessComponent { ...@@ -42,13 +56,11 @@ class _BottomSheetDragController extends StatelessComponent {
if (_dismissUnderway) if (_dismissUnderway)
return; return;
if (velocity.dy > _kMinFlingVelocity) { if (velocity.dy > _kMinFlingVelocity) {
performance.fling(velocity: -velocity.dy / childHeight).then((_) { performance.fling(velocity: -velocity.dy / childHeight);
Navigator.of(context).pop(); onClosing();
});
} else if (performance.progress < _kCloseProgressThreshold) { } else if (performance.progress < _kCloseProgressThreshold) {
performance.fling(velocity: -1.0).then((_) { performance.fling(velocity: -1.0);
Navigator.of(context).pop(); onClosing();
});
} else { } else {
performance.forward(); performance.forward();
} }
...@@ -58,46 +70,19 @@ class _BottomSheetDragController extends StatelessComponent { ...@@ -58,46 +70,19 @@ class _BottomSheetDragController extends StatelessComponent {
return new GestureDetector( return new GestureDetector(
onVerticalDragUpdate: _handleDragUpdate, onVerticalDragUpdate: _handleDragUpdate,
onVerticalDragEnd: (Offset velocity) { _handleDragEnd(velocity, context); }, onVerticalDragEnd: (Offset velocity) { _handleDragEnd(velocity, context); },
child: child child: new Material(
child: builder(context)
)
); );
} }
} }
class _BottomSheetRoute extends OverlayRoute { // PERSISTENT BOTTOM SHEETS
_BottomSheetRoute({ this.completer, this.child });
final Completer completer;
final Widget child;
Performance performance;
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 { // See scaffold.dart
_ModalBottomSheet({ Key key, this.route }) : super(key: key);
final _ModalBottomSheetRoute route;
_ModalBottomSheetState createState() => new _ModalBottomSheetState(); // MODAL BOTTOM SHEETS
}
class _ModalBottomSheetLayout extends OneChildLayoutDelegate { class _ModalBottomSheetLayout extends OneChildLayoutDelegate {
// The distance from the bottom of the parent to the top of the BottomSheet child. // The distance from the bottom of the parent to the top of the BottomSheet child.
...@@ -118,6 +103,14 @@ class _ModalBottomSheetLayout extends OneChildLayoutDelegate { ...@@ -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> { class _ModalBottomSheetState extends State<_ModalBottomSheet> {
final _ModalBottomSheetLayout _layout = new _ModalBottomSheetLayout(); final _ModalBottomSheetLayout _layout = new _ModalBottomSheetLayout();
...@@ -133,10 +126,11 @@ class _ModalBottomSheetState extends State<_ModalBottomSheet> { ...@@ -133,10 +126,11 @@ class _ModalBottomSheetState extends State<_ModalBottomSheet> {
child: new CustomOneChildLayout( child: new CustomOneChildLayout(
delegate: _layout, delegate: _layout,
token: _layout.childTop.value, token: _layout.childTop.value,
child: new _BottomSheetDragController( child: new BottomSheet(
performance: config.route.performance, performance: config.route.performance,
child: new Material(child: config.route.child), onClosing: () { Navigator.of(context).pop(); },
childHeight: _layout.childTop.end childHeight: _layout.childTop.end,
builder: config.route.builder
) )
) )
); );
...@@ -146,9 +140,30 @@ class _ModalBottomSheetState extends State<_ModalBottomSheet> { ...@@ -146,9 +140,30 @@ class _ModalBottomSheetState extends State<_ModalBottomSheet> {
} }
} }
class _ModalBottomSheetRoute extends _BottomSheetRoute { class _ModalBottomSheetRoute extends OverlayRoute {
_ModalBottomSheetRoute({ Completer completer, Widget child }) _ModalBottomSheetRoute({ this.completer, this.builder });
: super(completer: completer, child: child);
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) { Widget _buildModalBarrier(BuildContext context) {
return new AnimatedModalBarrier( return new AnimatedModalBarrier(
...@@ -168,61 +183,18 @@ class _ModalBottomSheetRoute extends _BottomSheetRoute { ...@@ -168,61 +183,18 @@ class _ModalBottomSheetRoute extends _BottomSheetRoute {
_buildModalBarrier, _buildModalBarrier,
_buildBottomSheet, _buildBottomSheet,
]; ];
String get debugLabel => '$runtimeType';
String toString() => '$runtimeType(performance: $performance)';
} }
Future showModalBottomSheet({ BuildContext context, Widget child }) { Future showModalBottomSheet({ BuildContext context, WidgetBuilder builder }) {
assert(child != null); assert(context != null);
assert(builder != null);
final Completer completer = new Completer(); final Completer completer = new Completer();
Navigator.of(context).pushEphemeral(new _ModalBottomSheetRoute( Navigator.of(context).pushEphemeral(new _ModalBottomSheetRoute(
completer: completer, completer: completer,
child: child builder: builder
)); ));
return completer.future; 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<FractionalOffset>(const FractionalOffset(0.0, 0.0)),
heightFactor: new AnimatedValue<double>(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<PlaceholderState> 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;
});
}
...@@ -11,9 +11,10 @@ import 'package:flutter/animation.dart'; ...@@ -11,9 +11,10 @@ import 'package:flutter/animation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'bottom_sheet.dart';
import 'material.dart'; import 'material.dart';
import 'tool_bar.dart';
import 'snack_bar.dart'; import 'snack_bar.dart';
import 'tool_bar.dart';
const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent
...@@ -57,7 +58,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { ...@@ -57,7 +58,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
if (isChild(_Child.bottomSheet)) { if (isChild(_Child.bottomSheet)) {
bottomSheetSize = layoutChild(_Child.bottomSheet, fullWidthConstraints); 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)) { if (isChild(_Child.snackBar)) {
...@@ -85,13 +86,11 @@ class Scaffold extends StatefulComponent { ...@@ -85,13 +86,11 @@ class Scaffold extends StatefulComponent {
Key key, Key key,
this.toolBar, this.toolBar,
this.body, this.body,
this.bottomSheet,
this.floatingActionButton this.floatingActionButton
}) : super(key: key); }) : super(key: key);
final ToolBar toolBar; final ToolBar toolBar;
final Widget body; final Widget body;
final Widget bottomSheet; // this is for non-modal bottom sheets
final Widget floatingActionButton; final Widget floatingActionButton;
static ScaffoldState of(BuildContext context) => context.ancestorStateOfType(ScaffoldState); static ScaffoldState of(BuildContext context) => context.ancestorStateOfType(ScaffoldState);
...@@ -101,6 +100,8 @@ class Scaffold extends StatefulComponent { ...@@ -101,6 +100,8 @@ class Scaffold extends StatefulComponent {
class ScaffoldState extends State<Scaffold> { class ScaffoldState extends State<Scaffold> {
// SNACKBAR API
Queue<SnackBar> _snackBars = new Queue<SnackBar>(); Queue<SnackBar> _snackBars = new Queue<SnackBar>();
Performance _snackBarPerformance; Performance _snackBarPerformance;
Timer _snackBarTimer; Timer _snackBarTimer;
...@@ -108,6 +109,10 @@ class ScaffoldState extends State<Scaffold> { ...@@ -108,6 +109,10 @@ class ScaffoldState extends State<Scaffold> {
void showSnackBar(SnackBar snackbar) { void showSnackBar(SnackBar snackbar) {
_snackBarPerformance ??= SnackBar.createPerformance() _snackBarPerformance ??= SnackBar.createPerformance()
..addStatusListener(_handleSnackBarStatusChange); ..addStatusListener(_handleSnackBarStatusChange);
if (_snackBars.isEmpty) {
assert(_snackBarPerformance.isDismissed);
_snackBarPerformance.forward();
}
setState(() { setState(() {
_snackBars.addLast(snackbar.withPerformance(_snackBarPerformance)); _snackBars.addLast(snackbar.withPerformance(_snackBarPerformance));
}); });
...@@ -120,6 +125,8 @@ class ScaffoldState extends State<Scaffold> { ...@@ -120,6 +125,8 @@ class ScaffoldState extends State<Scaffold> {
setState(() { setState(() {
_snackBars.removeFirst(); _snackBars.removeFirst();
}); });
if (_snackBars.isNotEmpty)
_snackBarPerformance.forward();
break; break;
case PerformanceStatus.completed: case PerformanceStatus.completed:
setState(() { setState(() {
...@@ -138,6 +145,63 @@ class ScaffoldState extends State<Scaffold> { ...@@ -138,6 +145,63 @@ class ScaffoldState extends State<Scaffold> {
_snackBarTimer = null; _snackBarTimer = null;
} }
// PERSISTENT BOTTOM SHEET API
List<Widget> _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 ??= <Widget>[];
_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() { void dispose() {
_snackBarPerformance?.stop(); _snackBarPerformance?.stop();
_snackBarPerformance = null; _snackBarPerformance = null;
...@@ -156,8 +220,6 @@ class ScaffoldState extends State<Scaffold> { ...@@ -156,8 +220,6 @@ class ScaffoldState extends State<Scaffold> {
final Widget materialBody = config.body != null ? new Material(child: config.body) : null; final Widget materialBody = config.body != null ? new Material(child: config.body) : null;
if (_snackBars.length > 0) { if (_snackBars.length > 0) {
if (_snackBarPerformance.isDismissed)
_snackBarPerformance.forward();
ModalRoute route = ModalRoute.of(context); ModalRoute route = ModalRoute.of(context);
if (route == null || route.isCurrent) { if (route == null || route.isCurrent) {
if (_snackBarPerformance.isCompleted && _snackBarTimer == null) if (_snackBarPerformance.isCompleted && _snackBarTimer == null)
...@@ -171,12 +233,105 @@ class ScaffoldState extends State<Scaffold> { ...@@ -171,12 +233,105 @@ class ScaffoldState extends State<Scaffold> {
final List<LayoutId>children = new List<LayoutId>(); final List<LayoutId>children = new List<LayoutId>();
_addIfNonNull(children, materialBody, _Child.body); _addIfNonNull(children, materialBody, _Child.body);
_addIfNonNull(children, paddedToolBar, _Child.toolBar); _addIfNonNull(children, paddedToolBar, _Child.toolBar);
_addIfNonNull(children, config.bottomSheet, _Child.bottomSheet);
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(
bottomSheets,
alignment: const FractionalOffset(0.5, 1.0) // bottom-aligned, centered
);
_addIfNonNull(children, stack, _Child.bottomSheet);
}
if (_snackBars.isNotEmpty) if (_snackBars.isNotEmpty)
_addIfNonNull(children, _snackBars.first, _Child.snackBar); _addIfNonNull(children, _snackBars.first, _Child.snackBar);
_addIfNonNull(children, config.floatingActionButton, _Child.floatingActionButton); _addIfNonNull(children, config.floatingActionButton, _Child.floatingActionButton);
return new CustomMultiChildLayout(children, delegate: _scaffoldLayout); 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<FractionalOffset>(const FractionalOffset(0.0, 0.0)),
heightFactor: new AnimatedValue<double>(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)
)
);
}
}
...@@ -21,10 +21,10 @@ const Color _kSnackBackground = const Color(0xFF323232); ...@@ -21,10 +21,10 @@ const Color _kSnackBackground = const Color(0xFF323232);
// TODO(ianh): Implement the Tablet version of snackbar if we're "on a tablet". // 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 kSnackBarShortDisplayDuration = const Duration(milliseconds: 1500);
const Duration kSnackBarMediumDisplayDuration = const Duration(milliseconds: 2750); 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 { class SnackBarAction extends StatelessComponent {
SnackBarAction({Key key, this.label, this.onPressed }) : super(key: key) { SnackBarAction({Key key, this.label, this.onPressed }) : super(key: key) {
...@@ -91,7 +91,7 @@ class SnackBar extends StatelessComponent { ...@@ -91,7 +91,7 @@ class SnackBar extends StatelessComponent {
style: new TextStyle(color: Theme.of(context).accentColor), style: new TextStyle(color: Theme.of(context).accentColor),
child: new FadeTransition( child: new FadeTransition(
performance: performance, performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: snackBarFadeCurve), opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: _snackBarFadeCurve),
child: new Row(children) child: new Row(children)
) )
) )
...@@ -105,7 +105,7 @@ class SnackBar extends StatelessComponent { ...@@ -105,7 +105,7 @@ class SnackBar extends StatelessComponent {
static Performance createPerformance() { static Performance createPerformance() {
return new Performance( return new Performance(
duration: kSnackBarTransitionDuration, duration: _kSnackBarTransitionDuration,
debugLabel: 'SnackBar' debugLabel: 'SnackBar'
); );
} }
......
...@@ -332,6 +332,9 @@ enum _StateLifecycle { ...@@ -332,6 +332,9 @@ enum _StateLifecycle {
defunct, defunct,
} }
/// The signature of setState() methods.
typedef void StateSetter(VoidCallback fn);
/// The logic and internal state for a StatefulComponent. /// The logic and internal state for a StatefulComponent.
abstract class State<T extends StatefulComponent> { abstract class State<T extends StatefulComponent> {
/// The current configuration (an instance of the corresponding /// The current configuration (an instance of the corresponding
...@@ -377,7 +380,7 @@ abstract class State<T extends StatefulComponent> { ...@@ -377,7 +380,7 @@ abstract class State<T extends StatefulComponent> {
/// If you just change the state directly without calling setState(), then the /// If you just change the state directly without calling setState(), then the
/// component will not be scheduled for rebuilding, meaning that its rendering /// component will not be scheduled for rebuilding, meaning that its rendering
/// will not be updated. /// will not be updated.
void setState(void fn()) { void setState(VoidCallback fn) {
assert(_debugLifecycleState != _StateLifecycle.defunct); assert(_debugLifecycleState != _StateLifecycle.defunct);
fn(); fn();
_element.markNeedsBuild(); _element.markNeedsBuild();
......
...@@ -133,6 +133,28 @@ class NavigatorState extends State<Navigator> { ...@@ -133,6 +133,28 @@ class NavigatorState extends State<Navigator> {
assert(_ephemeral.isEmpty); 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]) { void pop([dynamic result]) {
setState(() { setState(() {
// We use setState to guarantee that we'll rebuild, since the routes can't // We use setState to guarantee that we'll rebuild, since the routes can't
......
...@@ -26,7 +26,10 @@ void main() { ...@@ -26,7 +26,10 @@ void main() {
tester.pump(); tester.pump();
expect(tester.findText('BottomSheet'), isNull); 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; showBottomSheetThenCalled = true;
}); });
...@@ -42,7 +45,7 @@ void main() { ...@@ -42,7 +45,7 @@ void main() {
expect(showBottomSheetThenCalled, isTrue); expect(showBottomSheetThenCalled, isTrue);
expect(tester.findText('BottomSheet'), isNull); 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(); // bottom sheet show animation starts
tester.pump(new Duration(seconds: 1)); // animation done tester.pump(new Duration(seconds: 1)); // animation done
expect(tester.findText('BottomSheet'), isNotNull); expect(tester.findText('BottomSheet'), isNotNull);
...@@ -58,45 +61,60 @@ void main() { ...@@ -58,45 +61,60 @@ void main() {
test('Verify that a downwards fling dismisses a persistent BottomSheet', () { test('Verify that a downwards fling dismisses a persistent BottomSheet', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
GlobalKey<PlaceholderState> _bottomSheetPlaceholderKey = new GlobalKey<PlaceholderState>(); GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
BuildContext context;
bool showBottomSheetThenCalled = false; bool showBottomSheetThenCalled = false;
tester.pumpWidget(new MaterialApp( tester.pumpWidget(new MaterialApp(
routes: <String, RouteBuilder>{ routes: <String, RouteBuilder>{
'/': (RouteArguments args) { '/': (RouteArguments args) {
context = args.context;
return new Scaffold( return new Scaffold(
bottomSheet: new Placeholder(key: _bottomSheetPlaceholderKey), key: scaffoldKey,
body: new Center(child: new Text('body')) body: new Center(child: new Text('body'))
); );
} }
} }
)); ));
tester.pump(); expect(showBottomSheetThenCalled, isFalse);
expect(tester.findText('BottomSheet'), isNull); expect(tester.findText('BottomSheet'), isNull);
showBottomSheet( scaffoldKey.currentState.showBottomSheet((BuildContext context) {
context: context, return new Container(
child: new Container(child: new Text('BottomSheet'), margin: new EdgeDims.all(40.0)), margin: new EdgeDims.all(40.0),
placeholderKey: _bottomSheetPlaceholderKey child: new Text('BottomSheet')
).then((_) { );
}).closed.then((_) {
showBottomSheetThenCalled = true; showBottomSheetThenCalled = true;
}); });
expect(_bottomSheetPlaceholderKey.currentState.child, isNotNull); expect(showBottomSheetThenCalled, isFalse);
expect(tester.findText('BottomSheet'), isNull);
tester.pump(); // bottom sheet show animation starts tester.pump(); // bottom sheet show animation starts
expect(showBottomSheetThenCalled, isFalse);
expect(tester.findText('BottomSheet'), isNotNull);
tester.pump(new Duration(seconds: 1)); // animation done tester.pump(new Duration(seconds: 1)); // animation done
expect(showBottomSheetThenCalled, isFalse);
expect(tester.findText('BottomSheet'), isNotNull); expect(tester.findText('BottomSheet'), isNotNull);
tester.fling(tester.findText('BottomSheet'), const Offset(0.0, 20.0), 1000.0); 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 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)); // animation done
tester.pump(new Duration(seconds: 1)); // rebuild frame without the bottom sheet
expect(showBottomSheetThenCalled, isTrue); expect(showBottomSheetThenCalled, isTrue);
expect(tester.findText('BottomSheet'), isNull); expect(tester.findText('BottomSheet'), isNull);
expect(_bottomSheetPlaceholderKey.currentState.child, isNull);
}); });
}); });
......
...@@ -162,7 +162,7 @@ class WidgetTester { ...@@ -162,7 +162,7 @@ class WidgetTester {
assert(velocity != 0.0); // velocity is pixels/second assert(velocity != 0.0); // velocity is pixels/second
final TestPointer p = new TestPointer(pointer); final TestPointer p = new TestPointer(pointer);
final HitTestResult result = _hitTest(startLocation); 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); final double timeStampDelta = 1000.0 * offset.distance / (kMoveCount * velocity);
double timeStamp = 0.0; double timeStamp = 0.0;
_dispatchEvent(p.down(startLocation, timeStamp: new Duration(milliseconds: timeStamp.round())), result); _dispatchEvent(p.down(startLocation, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment