Unverified Commit e7df5ec5 authored by Casey Rogers's avatar Casey Rogers Committed by GitHub

Add Snapping Behavior to DraggableScrollableSheet (#84394)

parent daa051c3
...@@ -12,8 +12,11 @@ void main() { ...@@ -12,8 +12,11 @@ void main() {
double initialChildSize = .5, double initialChildSize = .5,
double maxChildSize = 1.0, double maxChildSize = 1.0,
double minChildSize = .25, double minChildSize = .25,
bool snap = false,
List<double>? snapSizes,
double? itemExtent, double? itemExtent,
Key? containerKey, Key? containerKey,
Key? stackKey,
NotificationListenerCallback<ScrollNotification>? onScrollNotification, NotificationListenerCallback<ScrollNotification>? onScrollNotification,
}) { }) {
return Directionality( return Directionality(
...@@ -21,15 +24,19 @@ void main() { ...@@ -21,15 +24,19 @@ void main() {
child: MediaQuery( child: MediaQuery(
data: const MediaQueryData(), data: const MediaQueryData(),
child: Stack( child: Stack(
key: stackKey,
children: <Widget>[ children: <Widget>[
TextButton( TextButton(
onPressed: onButtonPressed, onPressed: onButtonPressed,
child: const Text('TapHere'), child: const Text('TapHere'),
), ),
DraggableScrollableSheet( DraggableScrollableActuator(
child: DraggableScrollableSheet(
maxChildSize: maxChildSize, maxChildSize: maxChildSize,
minChildSize: minChildSize, minChildSize: minChildSize,
initialChildSize: initialChildSize, initialChildSize: initialChildSize,
snap: snap,
snapSizes: snapSizes,
builder: (BuildContext context, ScrollController scrollController) { builder: (BuildContext context, ScrollController scrollController) {
return NotificationListener<ScrollNotification>( return NotificationListener<ScrollNotification>(
onNotification: onScrollNotification, onNotification: onScrollNotification,
...@@ -46,6 +53,7 @@ void main() { ...@@ -46,6 +53,7 @@ void main() {
); );
}, },
), ),
),
], ],
), ),
), ),
...@@ -84,6 +92,27 @@ void main() { ...@@ -84,6 +92,27 @@ void main() {
expect(tester.getRect(find.byKey(key)), const Rect.fromLTRB(0.0, 325.0, 800.0, 600.0)); expect(tester.getRect(find.byKey(key)), const Rect.fromLTRB(0.0, 325.0, 800.0, 600.0));
}); });
testWidgets('Invalid snap targets throw assertion errors.', (WidgetTester tester) async {
await tester.pumpWidget(_boilerplate(
null,
maxChildSize: .8,
snapSizes: <double>[.9],
));
expect(tester.takeException(), isAssertionError);
await tester.pumpWidget(_boilerplate(
null,
snapSizes: <double>[.1],
));
expect(tester.takeException(), isAssertionError);
await tester.pumpWidget(_boilerplate(
null,
snapSizes: <double>[.6, .6, .9],
));
expect(tester.takeException(), isAssertionError);
});
for (final TargetPlatform platform in TargetPlatform.values) { for (final TargetPlatform platform in TargetPlatform.values) {
group('$platform Scroll Physics', () { group('$platform Scroll Physics', () {
debugDefaultTargetPlatformOverride = platform; debugDefaultTargetPlatformOverride = platform;
...@@ -301,6 +330,190 @@ void main() { ...@@ -301,6 +330,190 @@ void main() {
debugDefaultTargetPlatformOverride = null; debugDefaultTargetPlatformOverride = null;
}); });
testWidgets('Does not snap away from initial child on build', (WidgetTester tester) async {
const Key containerKey = ValueKey<String>('container');
const Key stackKey = ValueKey<String>('stack');
await tester.pumpWidget(_boilerplate(null,
snap: true,
initialChildSize: .7,
containerKey: containerKey,
stackKey: stackKey,
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
// The sheet should not have snapped.
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.7, precisionErrorTolerance,
));
}, variant: TargetPlatformVariant.all());
testWidgets('Does not snap away from initial child on reset', (WidgetTester tester) async {
const Key containerKey = ValueKey<String>('container');
const Key stackKey = ValueKey<String>('stack');
await tester.pumpWidget(_boilerplate(null,
snap: true,
containerKey: containerKey,
stackKey: stackKey,
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
await tester.drag(find.text('Item 1'), Offset(0, -.4 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(1.0, precisionErrorTolerance),
);
DraggableScrollableActuator.reset(tester.element(find.byKey(containerKey)));
await tester.pumpAndSettle();
// The sheet should have reset without snapping away from initial child.
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.5, precisionErrorTolerance),
);
}, variant: TargetPlatformVariant.all());
testWidgets('Zero velocity drag snaps to nearest snap target', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
await tester.pumpWidget(_boilerplate(null,
snap: true,
stackKey: stackKey,
containerKey: containerKey,
snapSizes: <double>[.25, .5, .75, 1.0],
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
// We are dragging up, but we'll snap down because we're closer to .75 than 1.
await tester.drag(find.text('Item 1'), Offset(0, -.35 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.75, precisionErrorTolerance),
);
// Drag up and snap up.
await tester.drag(find.text('Item 1'), Offset(0, -.2 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(1.0, precisionErrorTolerance),
);
// Drag down and snap up.
await tester.drag(find.text('Item 1'), Offset(0, .1 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(1.0, precisionErrorTolerance),
);
// Drag down and snap down.
await tester.drag(find.text('Item 1'), Offset(0, .45 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.5, precisionErrorTolerance),
);
// Fling up with negligible velocity and snap down.
await tester.fling(find.text('Item 1'), Offset(0, .1 * screenHeight), 1);
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.5, precisionErrorTolerance),
);
}, variant: TargetPlatformVariant.all());
for (final List<double>? snapSizes in <List<double>?>[null, <double>[]]) {
testWidgets('Setting snapSizes to $snapSizes resolves to min and max', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
await tester.pumpWidget(_boilerplate(null,
snap: true,
stackKey: stackKey,
containerKey: containerKey,
snapSizes: snapSizes,
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
await tester.drag(find.text('Item 1'), Offset(0, -.4 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(1.0, precisionErrorTolerance,
));
await tester.drag(find.text('Item 1'), Offset(0, .7 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.25, precisionErrorTolerance),
);
}, variant: TargetPlatformVariant.all());
}
testWidgets('Min and max are implicitly added to snapSizes.', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
await tester.pumpWidget(_boilerplate(null,
snap: true,
stackKey: stackKey,
containerKey: containerKey,
snapSizes: <double>[.5],
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
await tester.drag(find.text('Item 1'), Offset(0, -.4 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(1.0, precisionErrorTolerance),
);
await tester.drag(find.text('Item 1'), Offset(0, .7 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.25, precisionErrorTolerance),
);
}, variant: TargetPlatformVariant.all());
testWidgets('Fling snaps in direction of momentum', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
await tester.pumpWidget(_boilerplate(null,
snap: true,
stackKey: stackKey,
containerKey: containerKey,
snapSizes: <double>[.5, .75],
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
await tester.fling(find.text('Item 1'), Offset(0, -.1 * screenHeight), 1000);
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.75, precisionErrorTolerance),
);
await tester.fling(find.text('Item 1'), Offset(0, .3 * screenHeight), 1000);
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.25, precisionErrorTolerance),
);
}, variant: TargetPlatformVariant.all());
testWidgets('ScrollNotification correctly dispatched when flung without covering its container', (WidgetTester tester) async { testWidgets('ScrollNotification correctly dispatched when flung without covering its container', (WidgetTester tester) async {
final List<Type> notificationTypes = <Type>[]; final List<Type> notificationTypes = <Type>[];
await tester.pumpWidget(_boilerplate( await tester.pumpWidget(_boilerplate(
......
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