Unverified Commit 0f0cc449 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Scaffold bottomSheet parameter: persistent, not a route (#18379)

parent aecb7d96
......@@ -24,7 +24,8 @@ const double _kCloseProgressThreshold = 0.5;
/// supplements the primary content of the app. A persistent bottom sheet
/// remains visible even when the user interacts with other parts of the app.
/// Persistent bottom sheets can be created and displayed with the
/// [ScaffoldState.showBottomSheet] function.
/// [ScaffoldState.showBottomSheet] function or by specifying the
/// [Scaffold.bottomSheet] constructor parameter.
///
/// * _Modal_. A modal bottom sheet is an alternative to a menu or a dialog and
/// prevents the user from interacting with the rest of the app. Modal bottom
......@@ -32,8 +33,8 @@ const double _kCloseProgressThreshold = 0.5;
/// function.
///
/// The [BottomSheet] widget itself is rarely used directly. Instead, prefer to
/// create a persistent bottom sheet with [ScaffoldState.showBottomSheet] and a modal
/// bottom sheet with [showModalBottomSheet].
/// create a persistent bottom sheet with [ScaffoldState.showBottomSheet] or
/// [Scaffold.bottomSheet], and a modal bottom sheet with [showModalBottomSheet].
///
/// See also:
///
......@@ -49,9 +50,11 @@ class BottomSheet extends StatefulWidget {
const BottomSheet({
Key key,
this.animationController,
this.enableDrag = true,
@required this.onClosing,
@required this.builder
}) : assert(onClosing != null),
}) : assert(enableDrag != null),
assert(onClosing != null),
assert(builder != null),
super(key: key);
......@@ -74,6 +77,12 @@ class BottomSheet extends StatefulWidget {
/// [Material] widget.
final WidgetBuilder builder;
/// If true, the bottom sheet can dragged up and down and dismissed by swiping
/// downards.
///
/// Default is true.
final bool enableDrag;
@override
_BottomSheetState createState() => new _BottomSheetState();
......@@ -124,13 +133,14 @@ class _BottomSheetState extends State<BottomSheet> {
@override
Widget build(BuildContext context) {
return new GestureDetector(
final Widget bottomSheet = new Material(
key: _childKey,
child: widget.builder(context),
);
return !widget.enableDrag ? bottomSheet : new GestureDetector(
onVerticalDragUpdate: _handleDragUpdate,
onVerticalDragEnd: _handleDragEnd,
child: new Material(
key: _childKey,
child: widget.builder(context)
)
child: bottomSheet,
);
}
}
......@@ -289,24 +299,30 @@ Future<T> showModalBottomSheet<T>({
/// Shows a persistent material design bottom sheet in the nearest [Scaffold].
///
/// Returns a controller that can be used to close and otherwise manipulate the
/// bottom sheet.
///
/// To rebuild the bottom sheet (e.g. if it is stateful), call
/// [PersistentBottomSheetController.setState] on the controller returned by
/// this method.
///
/// The new bottom sheet becomes a [LocalHistoryEntry] for the enclosing
/// [ModalRoute] and a back button is added to the appbar of the [Scaffold]
/// that closes the bottom sheet.
///
/// To create a persistent bottom sheet that is not a [LocalHistoryEntry] and
/// does not add a back button to the enclosing Scaffold's appbar, use the
/// [Scaffold.bottomSheet] constructor parameter.
///
/// A persistent bottom sheet shows information that supplements the primary
/// content of the app. A persistent bottom sheet remains visible even when the
/// user interacts with other parts of the app. A [Scaffold] is required in the
/// given `context`; its [ScaffoldState.showBottomSheet] method is used to
/// actually show the bottom sheet.
/// content of the app. A persistent bottom sheet remains visible even when
/// the user interacts with other parts of the app.
///
/// A closely related widget is a modal bottom sheet, which is an alternative
/// to a menu or a dialog and prevents the user from interacting with the rest
/// of the app. Modal bottom sheets can be created and displayed with the
/// [showModalBottomSheet] function.
///
/// Returns a controller that can be used to close and otherwise manipulate the
/// bottom sheet.
///
/// To rebuild the bottom sheet (e.g. if it is stateful), call
/// [PersistentBottomSheetController.setState] on the value returned from this
/// method.
///
/// The `context` argument is used to look up the [Scaffold] for the bottom
/// sheet. It is only used when the method is called. Its corresponding widget
/// can be safely removed from the tree before the bottom sheet is closed.
......
......@@ -763,6 +763,7 @@ class Scaffold extends StatefulWidget {
this.drawer,
this.endDrawer,
this.bottomNavigationBar,
this.bottomSheet,
this.backgroundColor,
this.resizeToAvoidBottomPadding = true,
this.primary = true,
......@@ -853,6 +854,37 @@ class Scaffold extends StatefulWidget {
/// and the [body].
final Widget bottomNavigationBar;
/// The persistent bottom sheet to display.
///
/// A persistent bottom sheet shows information that supplements the primary
/// content of the app. A persistent bottom sheet remains visible even when
/// the user interacts with other parts of the app.
///
/// A closely related widget is a modal bottom sheet, which is an alternative
/// to a menu or a dialog and prevents the user from interacting with the rest
/// of the app. Modal bottom sheets can be created and displayed with the
/// [showModalBottomSheet] function.
///
/// Unlike the persistent bottom sheet displayed by [showBottomSheet]
/// this bottom sheet is not a [LocalHistoryEntry] and cannot be dismissed
/// with the scaffold appbar's back button.
///
/// If a persistent bottom sheet created with [showBottomSheet] is already
/// visible, it must be closed before building the Scaffold with a new
/// [bottomSheet].
///
/// The value of [bottomSheet] can be any widget at all. It's unlikely to
/// actually be a [BottomSheet], which is used by the implementations of
/// [showBottomSheet] and [showModalBottomSheet]. Typically it's a widget
/// that includes [Material].
///
/// See also:
///
/// * [showBottomSheet], which displays a bottom sheet as a route that can
/// be dismissed with the scaffold's back button.
/// * [showModalBottomSheet], which displays a modal bottom sheet.
final Widget bottomSheet;
/// Whether the [body] (and other floating widgets) should size themselves to
/// avoid the window's bottom padding.
///
......@@ -1214,44 +1246,32 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
final List<_PersistentBottomSheet> _dismissedBottomSheets = <_PersistentBottomSheet>[];
PersistentBottomSheetController<dynamic> _currentBottomSheet;
/// Shows a persistent material design bottom sheet.
///
/// A persistent bottom sheet shows information that supplements the primary
/// content of the app. A persistent bottom sheet remains visible even when
/// the user interacts with other parts of the app.
///
/// A closely related widget is a modal bottom sheet, which is an alternative
/// to a menu or a dialog and prevents the user from interacting with the rest
/// of the app. Modal bottom sheets can be created and displayed with the
/// [showModalBottomSheet] function.
///
/// Returns a controller that can be used to close and otherwise manipulate the
/// bottom sheet.
///
/// To rebuild the bottom sheet (e.g. if it is stateful), call
/// [PersistentBottomSheetController.setState] on the value returned from this
/// method.
///
/// See also:
///
/// * [BottomSheet], which is the widget typically returned by the `builder`.
/// * [showBottomSheet], which calls this method given a [BuildContext].
/// * [showModalBottomSheet], which can be used to display a modal bottom
/// sheet.
/// * [Scaffold.of], for information about how to obtain the [ScaffoldState].
/// * <https://material.google.com/components/bottom-sheets.html#bottom-sheets-persistent-bottom-sheets>
PersistentBottomSheetController<T> showBottomSheet<T>(WidgetBuilder builder) {
void _maybeBuildCurrentBottomSheet() {
if (widget.bottomSheet != null) {
// The new _currentBottomSheet is not a local history entry so a "back" button
// will not be added to the Scaffold's appbar and the bottom sheet will not
// support drag or swipe to dismiss.
_currentBottomSheet = _buildBottomSheet<void>(
(BuildContext context) => widget.bottomSheet,
BottomSheet.createAnimationController(this) ..value = 1.0,
false,
);
}
}
void _closeCurrentBottomSheet() {
if (_currentBottomSheet != null) {
_currentBottomSheet.close();
assert(_currentBottomSheet == null);
}
}
PersistentBottomSheetController<T> _buildBottomSheet<T>(WidgetBuilder builder, AnimationController controller, bool isLocalHistoryEntry) {
final Completer<T> completer = new Completer<T>();
final GlobalKey<_PersistentBottomSheetState> bottomSheetKey = new GlobalKey<_PersistentBottomSheetState>();
final AnimationController controller = BottomSheet.createAnimationController(this)
..forward();
_PersistentBottomSheet bottomSheet;
final LocalHistoryEntry entry = new LocalHistoryEntry(
onRemove: () {
void _removeCurrentBottomSheet() {
assert(_currentBottomSheet._widget == bottomSheet);
assert(bottomSheetKey.currentState != null);
bottomSheetKey.currentState.close();
......@@ -1262,13 +1282,21 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
});
completer.complete();
}
);
final LocalHistoryEntry entry = isLocalHistoryEntry
? new LocalHistoryEntry(onRemove: _removeCurrentBottomSheet)
: null;
bottomSheet = new _PersistentBottomSheet(
key: bottomSheetKey,
animationController: controller,
enableDrag: isLocalHistoryEntry,
onClosing: () {
assert(_currentBottomSheet._widget == bottomSheet);
if (isLocalHistoryEntry)
entry.remove();
else
_removeCurrentBottomSheet();
},
onDismissed: () {
if (_dismissedBottomSheets.contains(bottomSheet)) {
......@@ -1280,14 +1308,59 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
},
builder: builder
);
if (isLocalHistoryEntry)
ModalRoute.of(context).addLocalHistoryEntry(entry);
setState(() {
_currentBottomSheet = new PersistentBottomSheetController<T>._(
return new PersistentBottomSheetController<T>._(
bottomSheet,
completer,
entry.remove,
(VoidCallback fn) { bottomSheetKey.currentState?.setState(fn); }
isLocalHistoryEntry ? entry.remove : _removeCurrentBottomSheet,
(VoidCallback fn) { bottomSheetKey.currentState?.setState(fn); },
isLocalHistoryEntry,
);
}
/// Shows a persistent material design bottom sheet in the nearest [Scaffold].
///
/// Returns a controller that can be used to close and otherwise manipulate the
/// bottom sheet.
///
/// To rebuild the bottom sheet (e.g. if it is stateful), call
/// [PersistentBottomSheetController.setState] on the controller returned by
/// this method.
///
/// The new bottom sheet becomes a [LocalHistoryEntry] for the enclosing
/// [ModalRoute] and a back button is added to the appbar of the [Scaffold]
/// that closes the bottom sheet.
///
/// To create a persistent bottom sheet that is not a [LocalHistoryEntry] and
/// does not add a back button to the enclosing Scaffold's appbar, use the
/// [Scaffold.bottomSheet] constructor parameter.
///
/// A persistent bottom sheet shows information that supplements the primary
/// content of the app. A persistent bottom sheet remains visible even when
/// the user interacts with other parts of the app.
///
/// A closely related widget is a modal bottom sheet, which is an alternative
/// to a menu or a dialog and prevents the user from interacting with the rest
/// of the app. Modal bottom sheets can be created and displayed with the
/// [showModalBottomSheet] function.
///
/// See also:
///
/// * [BottomSheet], which is the widget typically returned by the `builder`.
/// * [showBottomSheet], which calls this method given a [BuildContext].
/// * [showModalBottomSheet], which can be used to display a modal bottom
/// sheet.
/// * [Scaffold.of], for information about how to obtain the [ScaffoldState].
/// * <https://material.google.com/components/bottom-sheets.html#bottom-sheets-persistent-bottom-sheets>
PersistentBottomSheetController<T> showBottomSheet<T>(WidgetBuilder builder) {
_closeCurrentBottomSheet();
final AnimationController controller = BottomSheet.createAnimationController(this)
..forward();
setState(() {
_currentBottomSheet = _buildBottomSheet<T>(builder, controller, true);
});
return _currentBottomSheet;
}
......@@ -1355,6 +1428,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
value: 1.0,
duration: kFloatingActionButtonSegue * 2,
);
_maybeBuildCurrentBottomSheet();
}
@override
......@@ -1366,10 +1440,24 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
if (widget.floatingActionButtonLocation != oldWidget.floatingActionButtonLocation) {
_moveFloatingActionButton(widget.floatingActionButtonLocation ?? _kDefaultFloatingActionButtonLocation);
}
if (widget.bottomSheet != oldWidget.bottomSheet) {
assert(() {
if (widget.bottomSheet != null && _currentBottomSheet?._isLocalHistoryEntry == true) {
throw new FlutterError(
'Scaffold.bottomSheet cannot be specified while a bottom sheet displayed '
'with showBottomSheet() is still visible.\n Use the PersistentBottomSheetController '
'returned by showBottomSheet() to close the old bottom sheet before creating '
'a Scaffold with a (non null) bottomSheet.'
);
}
return true;
}());
_closeCurrentBottomSheet();
_maybeBuildCurrentBottomSheet();
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
_snackBarController?.dispose();
......@@ -1659,12 +1747,14 @@ class _PersistentBottomSheet extends StatefulWidget {
const _PersistentBottomSheet({
Key key,
this.animationController,
this.enableDrag = true,
this.onClosing,
this.onDismissed,
this.builder
}) : super(key: key);
final AnimationController animationController; // we control it, but it must be disposed by whoever created it
final bool enableDrag;
final VoidCallback onClosing;
final VoidCallback onDismissed;
final WidgetBuilder builder;
......@@ -1677,7 +1767,8 @@ class _PersistentBottomSheetState extends State<_PersistentBottomSheet> {
@override
void initState() {
super.initState();
assert(widget.animationController.status == AnimationStatus.forward);
assert(widget.animationController.status == AnimationStatus.forward
|| widget.animationController.status == AnimationStatus.completed);
widget.animationController.addStatusListener(_handleStatusChange);
}
......@@ -1711,6 +1802,7 @@ class _PersistentBottomSheetState extends State<_PersistentBottomSheet> {
container: true,
child: new BottomSheet(
animationController: widget.animationController,
enableDrag: widget.enableDrag,
onClosing: widget.onClosing,
builder: widget.builder
)
......@@ -1728,8 +1820,11 @@ class PersistentBottomSheetController<T> extends ScaffoldFeatureController<_Pers
_PersistentBottomSheet widget,
Completer<T> completer,
VoidCallback close,
StateSetter setState
StateSetter setState,
this._isLocalHistoryEntry,
) : super._(widget, completer, close, setState);
final bool _isLocalHistoryEntry;
}
class _ScaffoldScope extends InheritedWidget {
......
......@@ -133,4 +133,62 @@ void main() {
),
);
});
testWidgets('Scaffold.bottomSheet', (WidgetTester tester) async {
final Key bottomSheetKey = new UniqueKey();
await tester.pumpWidget(
new MaterialApp(
home: new Scaffold(
body: const Placeholder(),
bottomSheet: new Container(
key: bottomSheetKey,
alignment: Alignment.center,
height: 200.0,
child: new Builder(
builder: (BuildContext context) {
return new RaisedButton(
child: const Text('showModalBottomSheet'),
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) => const Text('modal bottom sheet'),
);
},
);
},
),
),
),
),
);
expect(find.text('showModalBottomSheet'), findsOneWidget);
expect(tester.getSize(find.byKey(bottomSheetKey)), const Size(800.0, 200.0));
expect(tester.getTopLeft(find.byKey(bottomSheetKey)), const Offset(0.0, 400.0));
// Show the modal bottomSheet
await tester.tap(find.text('showModalBottomSheet'));
await tester.pumpAndSettle();
expect(find.text('modal bottom sheet'), findsOneWidget);
// Dismiss the modal bottomSheet
await tester.tap(find.text('modal bottom sheet'));
await tester.pumpAndSettle();
expect(find.text('modal bottom sheet'), findsNothing);
expect(find.text('showModalBottomSheet'), findsOneWidget);
// Remove the persistent bottomSheet
await tester.pumpWidget(
new MaterialApp(
home: const Scaffold(
bottomSheet: null,
body: const Placeholder(),
),
),
);
await tester.pumpAndSettle();
expect(find.text('showModalBottomSheet'), findsNothing);
expect(find.byKey(bottomSheetKey), findsNothing);
});
}
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