Support keeping a bottom sheet with a DraggableScrollableSheet from closing on drag/fling to min extent (#127339)
This commit is contained in:
parent
758ea6c096
commit
a6d62ca8de
@ -315,7 +315,7 @@ class _BottomSheetState extends State<BottomSheet> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool extentChanged(DraggableScrollableNotification notification) {
|
bool extentChanged(DraggableScrollableNotification notification) {
|
||||||
if (notification.extent == notification.minExtent) {
|
if (notification.extent == notification.minExtent && notification.shouldCloseOnMinExtent) {
|
||||||
widget.onClosing();
|
widget.onClosing();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -3190,7 +3190,9 @@ class _StandardBottomSheetState extends State<_StandardBottomSheet> {
|
|||||||
scaffold.showBodyScrim(false, 0.0);
|
scaffold.showBodyScrim(false, 0.0);
|
||||||
}
|
}
|
||||||
// If the Scaffold.bottomSheet != null, we're a persistent bottom sheet.
|
// If the Scaffold.bottomSheet != null, we're a persistent bottom sheet.
|
||||||
if (notification.extent == notification.minExtent && scaffold.widget.bottomSheet == null) {
|
if (notification.extent == notification.minExtent &&
|
||||||
|
scaffold.widget.bottomSheet == null &&
|
||||||
|
notification.shouldCloseOnMinExtent) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -308,6 +308,7 @@ class DraggableScrollableSheet extends StatefulWidget {
|
|||||||
this.snapSizes,
|
this.snapSizes,
|
||||||
this.snapAnimationDuration,
|
this.snapAnimationDuration,
|
||||||
this.controller,
|
this.controller,
|
||||||
|
this.shouldCloseOnMinExtent = true,
|
||||||
required this.builder,
|
required this.builder,
|
||||||
}) : assert(minChildSize >= 0.0),
|
}) : assert(minChildSize >= 0.0),
|
||||||
assert(maxChildSize <= 1.0),
|
assert(maxChildSize <= 1.0),
|
||||||
@ -391,6 +392,13 @@ class DraggableScrollableSheet extends StatefulWidget {
|
|||||||
/// A controller that can be used to programmatically control this sheet.
|
/// A controller that can be used to programmatically control this sheet.
|
||||||
final DraggableScrollableController? controller;
|
final DraggableScrollableController? controller;
|
||||||
|
|
||||||
|
/// Whether the sheet, when dragged (or flung) to its minimum size, should
|
||||||
|
/// cause its parent sheet to close.
|
||||||
|
///
|
||||||
|
/// Set on emitted [DraggableScrollableNotification]s. It is up to parent
|
||||||
|
/// classes to properly read and handle this value.
|
||||||
|
final bool shouldCloseOnMinExtent;
|
||||||
|
|
||||||
/// The builder that creates a child to display in this widget, which will
|
/// The builder that creates a child to display in this widget, which will
|
||||||
/// use the provided [ScrollController] to enable dragging and scrolling
|
/// use the provided [ScrollController] to enable dragging and scrolling
|
||||||
/// of the contents.
|
/// of the contents.
|
||||||
@ -432,6 +440,7 @@ class DraggableScrollableNotification extends Notification with ViewportNotifica
|
|||||||
required this.maxExtent,
|
required this.maxExtent,
|
||||||
required this.initialExtent,
|
required this.initialExtent,
|
||||||
required this.context,
|
required this.context,
|
||||||
|
this.shouldCloseOnMinExtent = true,
|
||||||
}) : assert(0.0 <= minExtent),
|
}) : assert(0.0 <= minExtent),
|
||||||
assert(maxExtent <= 1.0),
|
assert(maxExtent <= 1.0),
|
||||||
assert(minExtent <= extent),
|
assert(minExtent <= extent),
|
||||||
@ -458,6 +467,12 @@ class DraggableScrollableNotification extends Notification with ViewportNotifica
|
|||||||
/// is live when it first gets the notification.
|
/// is live when it first gets the notification.
|
||||||
final BuildContext context;
|
final BuildContext context;
|
||||||
|
|
||||||
|
/// Whether the widget that fired this notification, when dragged (or flung)
|
||||||
|
/// to minExtent, should cause its parent sheet to close.
|
||||||
|
///
|
||||||
|
/// It is up to parent classes to properly read and handle this value.
|
||||||
|
final bool shouldCloseOnMinExtent;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void debugFillDescription(List<String> description) {
|
void debugFillDescription(List<String> description) {
|
||||||
super.debugFillDescription(description);
|
super.debugFillDescription(description);
|
||||||
@ -487,6 +502,7 @@ class _DraggableSheetExtent {
|
|||||||
ValueNotifier<double>? currentSize,
|
ValueNotifier<double>? currentSize,
|
||||||
bool? hasDragged,
|
bool? hasDragged,
|
||||||
bool? hasChanged,
|
bool? hasChanged,
|
||||||
|
this.shouldCloseOnMinExtent = true,
|
||||||
}) : assert(minSize >= 0),
|
}) : assert(minSize >= 0),
|
||||||
assert(maxSize <= 1),
|
assert(maxSize <= 1),
|
||||||
assert(minSize <= initialSize),
|
assert(minSize <= initialSize),
|
||||||
@ -504,6 +520,7 @@ class _DraggableSheetExtent {
|
|||||||
final List<double> snapSizes;
|
final List<double> snapSizes;
|
||||||
final Duration? snapAnimationDuration;
|
final Duration? snapAnimationDuration;
|
||||||
final double initialSize;
|
final double initialSize;
|
||||||
|
final bool shouldCloseOnMinExtent;
|
||||||
final ValueNotifier<double> _currentSize;
|
final ValueNotifier<double> _currentSize;
|
||||||
double availablePixels;
|
double availablePixels;
|
||||||
|
|
||||||
@ -576,6 +593,7 @@ class _DraggableSheetExtent {
|
|||||||
extent: currentSize,
|
extent: currentSize,
|
||||||
initialExtent: initialSize,
|
initialExtent: initialSize,
|
||||||
context: context,
|
context: context,
|
||||||
|
shouldCloseOnMinExtent: shouldCloseOnMinExtent,
|
||||||
).dispatch(context);
|
).dispatch(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -598,6 +616,7 @@ class _DraggableSheetExtent {
|
|||||||
required List<double> snapSizes,
|
required List<double> snapSizes,
|
||||||
required double initialSize,
|
required double initialSize,
|
||||||
Duration? snapAnimationDuration,
|
Duration? snapAnimationDuration,
|
||||||
|
bool shouldCloseOnMinExtent = true,
|
||||||
}) {
|
}) {
|
||||||
return _DraggableSheetExtent(
|
return _DraggableSheetExtent(
|
||||||
minSize: minSize,
|
minSize: minSize,
|
||||||
@ -613,6 +632,7 @@ class _DraggableSheetExtent {
|
|||||||
: initialSize),
|
: initialSize),
|
||||||
hasDragged: hasDragged,
|
hasDragged: hasDragged,
|
||||||
hasChanged: hasChanged,
|
hasChanged: hasChanged,
|
||||||
|
shouldCloseOnMinExtent: shouldCloseOnMinExtent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -631,6 +651,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
|
|||||||
snapSizes: _impliedSnapSizes(),
|
snapSizes: _impliedSnapSizes(),
|
||||||
snapAnimationDuration: widget.snapAnimationDuration,
|
snapAnimationDuration: widget.snapAnimationDuration,
|
||||||
initialSize: widget.initialChildSize,
|
initialSize: widget.initialChildSize,
|
||||||
|
shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent,
|
||||||
);
|
);
|
||||||
_scrollController = _DraggableScrollableSheetScrollController(extent: _extent);
|
_scrollController = _DraggableScrollableSheetScrollController(extent: _extent);
|
||||||
widget.controller?._attach(_scrollController);
|
widget.controller?._attach(_scrollController);
|
||||||
|
@ -185,6 +185,44 @@ void main() {
|
|||||||
expect(find.text('Two'), findsNothing);
|
expect(find.text('Two'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Verify DraggableScrollableSheet.shouldCloseOnMinExtent == false prevents dismissal', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
key: scaffoldKey,
|
||||||
|
body: const Center(child: Text('body')),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
scaffoldKey.currentState!.showBottomSheet<void>((BuildContext context) {
|
||||||
|
return DraggableScrollableSheet(
|
||||||
|
expand: false,
|
||||||
|
shouldCloseOnMinExtent: false,
|
||||||
|
builder: (_, ScrollController controller) {
|
||||||
|
return ListView(
|
||||||
|
controller: controller,
|
||||||
|
shrinkWrap: true,
|
||||||
|
children: const <Widget>[
|
||||||
|
SizedBox(height: 100.0, child: Text('One')),
|
||||||
|
SizedBox(height: 100.0, child: Text('Two')),
|
||||||
|
SizedBox(height: 100.0, child: Text('Three')),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Two'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.drag(find.text('Two'), const Offset(0.0, 400.0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Two'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Verify that a BottomSheet animates non-linearly', (WidgetTester tester) async {
|
testWidgets('Verify that a BottomSheet animates non-linearly', (WidgetTester tester) async {
|
||||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
|
@ -20,7 +20,9 @@ void main() {
|
|||||||
Key? containerKey,
|
Key? containerKey,
|
||||||
Key? stackKey,
|
Key? stackKey,
|
||||||
NotificationListenerCallback<ScrollNotification>? onScrollNotification,
|
NotificationListenerCallback<ScrollNotification>? onScrollNotification,
|
||||||
|
NotificationListenerCallback<DraggableScrollableNotification>? onDraggableScrollableNotification,
|
||||||
bool ignoreController = false,
|
bool ignoreController = false,
|
||||||
|
bool shouldCloseOnMinExtent = true,
|
||||||
}) {
|
}) {
|
||||||
return Directionality(
|
return Directionality(
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
@ -42,19 +44,23 @@ void main() {
|
|||||||
snap: snap,
|
snap: snap,
|
||||||
snapSizes: snapSizes,
|
snapSizes: snapSizes,
|
||||||
snapAnimationDuration: snapAnimationDuration,
|
snapAnimationDuration: snapAnimationDuration,
|
||||||
|
shouldCloseOnMinExtent: shouldCloseOnMinExtent,
|
||||||
builder: (BuildContext context, ScrollController scrollController) {
|
builder: (BuildContext context, ScrollController scrollController) {
|
||||||
return NotificationListener<ScrollNotification>(
|
return NotificationListener<ScrollNotification>(
|
||||||
onNotification: onScrollNotification,
|
onNotification: onScrollNotification,
|
||||||
child: ColoredBox(
|
child: NotificationListener<DraggableScrollableNotification>(
|
||||||
key: containerKey,
|
onNotification: onDraggableScrollableNotification,
|
||||||
color: const Color(0xFFABCDEF),
|
child:ColoredBox(
|
||||||
child: ListView.builder(
|
key: containerKey,
|
||||||
controller: ignoreController ? null : scrollController,
|
color: const Color(0xFFABCDEF),
|
||||||
itemExtent: itemExtent,
|
child: ListView.builder(
|
||||||
itemCount: itemCount,
|
controller: ignoreController ? null : scrollController,
|
||||||
itemBuilder: (BuildContext context, int index) => Text('Item $index'),
|
itemExtent: itemExtent,
|
||||||
|
itemCount: itemCount,
|
||||||
|
itemBuilder: (BuildContext context, int index) => Text('Item $index'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -864,6 +870,22 @@ void main() {
|
|||||||
expect(notificationTypes, types);
|
expect(notificationTypes, types);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Emits DraggableScrollableNotification with shouldCloseOnMinExtent set to non-default value', (WidgetTester tester) async {
|
||||||
|
DraggableScrollableNotification? receivedNotification;
|
||||||
|
await tester.pumpWidget(boilerplateWidget(
|
||||||
|
null,
|
||||||
|
shouldCloseOnMinExtent: false,
|
||||||
|
onDraggableScrollableNotification: (DraggableScrollableNotification notification) {
|
||||||
|
receivedNotification = notification;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.flingFrom(const Offset(0, 325), const Offset(0, -325), 200);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(receivedNotification!.shouldCloseOnMinExtent, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Do not crash when remove the tree during animation.', (WidgetTester tester) async {
|
testWidgets('Do not crash when remove the tree during animation.', (WidgetTester tester) async {
|
||||||
// Regression test for https://github.com/flutter/flutter/issues/89214
|
// Regression test for https://github.com/flutter/flutter/issues/89214
|
||||||
await tester.pumpWidget(boilerplateWidget(
|
await tester.pumpWidget(boilerplateWidget(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user