Scaffold bottomSheet parameter: persistent, not a route (#18379)
This commit is contained in:
parent
aecb7d9607
commit
0f0cc4490b
@ -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,61 +1246,57 @@ 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: () {
|
||||
assert(_currentBottomSheet._widget == bottomSheet);
|
||||
assert(bottomSheetKey.currentState != null);
|
||||
bottomSheetKey.currentState.close();
|
||||
if (controller.status != AnimationStatus.dismissed)
|
||||
_dismissedBottomSheets.add(bottomSheet);
|
||||
setState(() {
|
||||
_currentBottomSheet = null;
|
||||
});
|
||||
completer.complete();
|
||||
}
|
||||
);
|
||||
|
||||
void _removeCurrentBottomSheet() {
|
||||
assert(_currentBottomSheet._widget == bottomSheet);
|
||||
assert(bottomSheetKey.currentState != null);
|
||||
bottomSheetKey.currentState.close();
|
||||
if (controller.status != AnimationStatus.dismissed)
|
||||
_dismissedBottomSheets.add(bottomSheet);
|
||||
setState(() {
|
||||
_currentBottomSheet = null;
|
||||
});
|
||||
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);
|
||||
entry.remove();
|
||||
if (isLocalHistoryEntry)
|
||||
entry.remove();
|
||||
else
|
||||
_removeCurrentBottomSheet();
|
||||
},
|
||||
onDismissed: () {
|
||||
if (_dismissedBottomSheets.contains(bottomSheet)) {
|
||||
@ -1280,14 +1308,59 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
|
||||
},
|
||||
builder: builder
|
||||
);
|
||||
ModalRoute.of(context).addLocalHistoryEntry(entry);
|
||||
|
||||
if (isLocalHistoryEntry)
|
||||
ModalRoute.of(context).addLocalHistoryEntry(entry);
|
||||
|
||||
return new PersistentBottomSheetController<T>._(
|
||||
bottomSheet,
|
||||
completer,
|
||||
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 = new PersistentBottomSheetController<T>._(
|
||||
bottomSheet,
|
||||
completer,
|
||||
entry.remove,
|
||||
(VoidCallback fn) { bottomSheetKey.currentState?.setState(fn); }
|
||||
);
|
||||
_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);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user