mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
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().
This commit is contained in:
parent
6a2bd421a1
commit
03e094aa1b
@ -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> {
|
|||||||
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> {
|
|||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
// TODO(ianh): Fill this out.
|
// TODO(ianh): Fill this out.
|
||||||
context: context,
|
context: context,
|
||||||
child: new Column([
|
builder: (BuildContext context) {
|
||||||
new Input(
|
return new Column([
|
||||||
key: companyNameKey,
|
new Input(
|
||||||
placeholder: 'Company Name'
|
key: companyNameKey,
|
||||||
),
|
placeholder: 'Company Name'
|
||||||
])
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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 {
|
|||||||
|
|
||||||
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;
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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;
|
// See scaffold.dart
|
||||||
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) {
|
// MODAL BOTTOM SHEETS
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
|||||||
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> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
_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';
|
|||||||
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 {
|
|||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
|
|
||||||
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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
_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> {
|
|||||||
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> {
|
|||||||
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);
|
|||||||
|
|
||||||
// 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 {
|
|||||||
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 {
|
|||||||
|
|
||||||
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 {
|
|||||||
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> {
|
|||||||
/// 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> {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
|
|
||||||
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 {
|
|||||||
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);
|
||||||
|
Loading…
Reference in New Issue
Block a user