Unverified Commit b7534760 authored by hangyu's avatar hangyu Committed by GitHub

Add a snapAnimationDuration param in DraggableScrollableSheet (#107396)

* Add a snapAnimationDuration param in DraggableScrollableSheet

* snapAnimationDuration.inMilliseconds > 0

* Update draggable_scrollable_sheet.dart
parent d092601d
......@@ -289,6 +289,7 @@ class DraggableScrollableSheet extends StatefulWidget {
this.expand = true,
this.snap = false,
this.snapSizes,
this.snapAnimationDuration,
this.controller,
required this.builder,
}) : assert(initialChildSize != null),
......@@ -298,6 +299,7 @@ class DraggableScrollableSheet extends StatefulWidget {
assert(maxChildSize <= 1.0),
assert(minChildSize <= initialChildSize),
assert(initialChildSize <= maxChildSize),
assert(snapAnimationDuration == null || snapAnimationDuration > Duration.zero),
assert(expand != null),
assert(builder != null);
......@@ -365,6 +367,12 @@ class DraggableScrollableSheet extends StatefulWidget {
/// being built or since the last call to [DraggableScrollableActuator.reset].
final List<double>? snapSizes;
/// Defines a duration for the snap animations.
///
/// If it's not set, then the animation duration is the distance to the snap
/// target divided by the velocity of the widget.
final Duration? snapAnimationDuration;
/// A controller that can be used to programmatically control this sheet.
final DraggableScrollableController? controller;
......@@ -466,6 +474,7 @@ class _DraggableSheetExtent {
required this.snapSizes,
required this.initialSize,
required this.onSizeChanged,
this.snapAnimationDuration,
ValueNotifier<double>? currentSize,
bool? hasDragged,
}) : assert(minSize != null),
......@@ -486,6 +495,7 @@ class _DraggableSheetExtent {
final double maxSize;
final bool snap;
final List<double> snapSizes;
final Duration? snapAnimationDuration;
final double initialSize;
final ValueNotifier<double> _currentSize;
final VoidCallback onSizeChanged;
......@@ -570,12 +580,14 @@ class _DraggableSheetExtent {
required List<double> snapSizes,
required double initialSize,
required VoidCallback onSizeChanged,
Duration? snapAnimationDuration,
}) {
return _DraggableSheetExtent(
minSize: minSize,
maxSize: maxSize,
snap: snap,
snapSizes: snapSizes,
snapAnimationDuration: snapAnimationDuration,
initialSize: initialSize,
onSizeChanged: onSizeChanged,
// Use the possibly updated initialSize if the user hasn't dragged yet.
......@@ -599,6 +611,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
maxSize: widget.maxChildSize,
snap: widget.snap,
snapSizes: _impliedSnapSizes(),
snapAnimationDuration: widget.snapAnimationDuration,
initialSize: widget.initialChildSize,
onSizeChanged: _setExtent,
);
......@@ -679,6 +692,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
maxSize: widget.maxChildSize,
snap: widget.snap,
snapSizes: _impliedSnapSizes(),
snapAnimationDuration: widget.snapAnimationDuration,
initialSize: widget.initialChildSize,
onSizeChanged: _setExtent,
);
......@@ -902,6 +916,7 @@ class _DraggableScrollableSheetScrollPosition extends ScrollPositionWithSingleCo
position: extent.currentPixels,
initialVelocity: velocity,
pixelSnapSize: extent.pixelSnapSizes,
snapAnimationDuration: extent.snapAnimationDuration,
tolerance: physics.tolerance,
);
} else {
......@@ -1065,12 +1080,17 @@ class _SnappingSimulation extends Simulation {
required this.position,
required double initialVelocity,
required List<double> pixelSnapSize,
Duration? snapAnimationDuration,
super.tolerance,
}) {
_pixelSnapSize = _getSnapSize(initialVelocity, pixelSnapSize);
if (snapAnimationDuration != null && snapAnimationDuration.inMilliseconds > 0) {
velocity = (_pixelSnapSize - position) * 1000 / snapAnimationDuration.inMilliseconds;
}
// Check the direction of the target instead of the sign of the velocity because
// we may snap in the opposite direction of velocity if velocity is very low.
if (_pixelSnapSize < position) {
else if (_pixelSnapSize < position) {
velocity = math.min(-minimumSpeed, initialVelocity);
} else {
velocity = math.max(minimumSpeed, initialVelocity);
......
......@@ -15,6 +15,7 @@ void main() {
double minChildSize = .25,
bool snap = false,
List<double>? snapSizes,
Duration? snapAnimationDuration,
double? itemExtent,
Key? containerKey,
Key? stackKey,
......@@ -40,6 +41,7 @@ void main() {
initialChildSize: initialChildSize,
snap: snap,
snapSizes: snapSizes,
snapAnimationDuration: snapAnimationDuration,
builder: (BuildContext context, ScrollController scrollController) {
return NotificationListener<ScrollNotification>(
onNotification: onScrollNotification,
......@@ -485,58 +487,64 @@ void main() {
});
}
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(boilerplateWidget(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;
for (final Duration? snapAnimationDuration in <Duration?>[null, const Duration(seconds: 2)]) {
testWidgets(
'Zero velocity drag snaps to nearest snap target with '
'snapAnimationDuration: $snapAnimationDuration',
(WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
await tester.pumpWidget(boilerplateWidget(null,
snap: true,
stackKey: stackKey,
containerKey: containerKey,
snapSizes: <double>[.25, .5, .75, 1.0],
snapAnimationDuration: snapAnimationDuration
));
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),
);
// 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 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 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),
);
// 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());
// 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 {
......
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