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 { ...@@ -289,6 +289,7 @@ class DraggableScrollableSheet extends StatefulWidget {
this.expand = true, this.expand = true,
this.snap = false, this.snap = false,
this.snapSizes, this.snapSizes,
this.snapAnimationDuration,
this.controller, this.controller,
required this.builder, required this.builder,
}) : assert(initialChildSize != null), }) : assert(initialChildSize != null),
...@@ -298,6 +299,7 @@ class DraggableScrollableSheet extends StatefulWidget { ...@@ -298,6 +299,7 @@ class DraggableScrollableSheet extends StatefulWidget {
assert(maxChildSize <= 1.0), assert(maxChildSize <= 1.0),
assert(minChildSize <= initialChildSize), assert(minChildSize <= initialChildSize),
assert(initialChildSize <= maxChildSize), assert(initialChildSize <= maxChildSize),
assert(snapAnimationDuration == null || snapAnimationDuration > Duration.zero),
assert(expand != null), assert(expand != null),
assert(builder != null); assert(builder != null);
...@@ -365,6 +367,12 @@ class DraggableScrollableSheet extends StatefulWidget { ...@@ -365,6 +367,12 @@ class DraggableScrollableSheet extends StatefulWidget {
/// being built or since the last call to [DraggableScrollableActuator.reset]. /// being built or since the last call to [DraggableScrollableActuator.reset].
final List<double>? snapSizes; 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. /// A controller that can be used to programmatically control this sheet.
final DraggableScrollableController? controller; final DraggableScrollableController? controller;
...@@ -466,6 +474,7 @@ class _DraggableSheetExtent { ...@@ -466,6 +474,7 @@ class _DraggableSheetExtent {
required this.snapSizes, required this.snapSizes,
required this.initialSize, required this.initialSize,
required this.onSizeChanged, required this.onSizeChanged,
this.snapAnimationDuration,
ValueNotifier<double>? currentSize, ValueNotifier<double>? currentSize,
bool? hasDragged, bool? hasDragged,
}) : assert(minSize != null), }) : assert(minSize != null),
...@@ -486,6 +495,7 @@ class _DraggableSheetExtent { ...@@ -486,6 +495,7 @@ class _DraggableSheetExtent {
final double maxSize; final double maxSize;
final bool snap; final bool snap;
final List<double> snapSizes; final List<double> snapSizes;
final Duration? snapAnimationDuration;
final double initialSize; final double initialSize;
final ValueNotifier<double> _currentSize; final ValueNotifier<double> _currentSize;
final VoidCallback onSizeChanged; final VoidCallback onSizeChanged;
...@@ -570,12 +580,14 @@ class _DraggableSheetExtent { ...@@ -570,12 +580,14 @@ class _DraggableSheetExtent {
required List<double> snapSizes, required List<double> snapSizes,
required double initialSize, required double initialSize,
required VoidCallback onSizeChanged, required VoidCallback onSizeChanged,
Duration? snapAnimationDuration,
}) { }) {
return _DraggableSheetExtent( return _DraggableSheetExtent(
minSize: minSize, minSize: minSize,
maxSize: maxSize, maxSize: maxSize,
snap: snap, snap: snap,
snapSizes: snapSizes, snapSizes: snapSizes,
snapAnimationDuration: snapAnimationDuration,
initialSize: initialSize, initialSize: initialSize,
onSizeChanged: onSizeChanged, onSizeChanged: onSizeChanged,
// Use the possibly updated initialSize if the user hasn't dragged yet. // Use the possibly updated initialSize if the user hasn't dragged yet.
...@@ -599,6 +611,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> { ...@@ -599,6 +611,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
maxSize: widget.maxChildSize, maxSize: widget.maxChildSize,
snap: widget.snap, snap: widget.snap,
snapSizes: _impliedSnapSizes(), snapSizes: _impliedSnapSizes(),
snapAnimationDuration: widget.snapAnimationDuration,
initialSize: widget.initialChildSize, initialSize: widget.initialChildSize,
onSizeChanged: _setExtent, onSizeChanged: _setExtent,
); );
...@@ -679,6 +692,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> { ...@@ -679,6 +692,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
maxSize: widget.maxChildSize, maxSize: widget.maxChildSize,
snap: widget.snap, snap: widget.snap,
snapSizes: _impliedSnapSizes(), snapSizes: _impliedSnapSizes(),
snapAnimationDuration: widget.snapAnimationDuration,
initialSize: widget.initialChildSize, initialSize: widget.initialChildSize,
onSizeChanged: _setExtent, onSizeChanged: _setExtent,
); );
...@@ -902,6 +916,7 @@ class _DraggableScrollableSheetScrollPosition extends ScrollPositionWithSingleCo ...@@ -902,6 +916,7 @@ class _DraggableScrollableSheetScrollPosition extends ScrollPositionWithSingleCo
position: extent.currentPixels, position: extent.currentPixels,
initialVelocity: velocity, initialVelocity: velocity,
pixelSnapSize: extent.pixelSnapSizes, pixelSnapSize: extent.pixelSnapSizes,
snapAnimationDuration: extent.snapAnimationDuration,
tolerance: physics.tolerance, tolerance: physics.tolerance,
); );
} else { } else {
...@@ -1065,12 +1080,17 @@ class _SnappingSimulation extends Simulation { ...@@ -1065,12 +1080,17 @@ class _SnappingSimulation extends Simulation {
required this.position, required this.position,
required double initialVelocity, required double initialVelocity,
required List<double> pixelSnapSize, required List<double> pixelSnapSize,
Duration? snapAnimationDuration,
super.tolerance, super.tolerance,
}) { }) {
_pixelSnapSize = _getSnapSize(initialVelocity, pixelSnapSize); _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 // 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. // 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); velocity = math.min(-minimumSpeed, initialVelocity);
} else { } else {
velocity = math.max(minimumSpeed, initialVelocity); velocity = math.max(minimumSpeed, initialVelocity);
......
...@@ -15,6 +15,7 @@ void main() { ...@@ -15,6 +15,7 @@ void main() {
double minChildSize = .25, double minChildSize = .25,
bool snap = false, bool snap = false,
List<double>? snapSizes, List<double>? snapSizes,
Duration? snapAnimationDuration,
double? itemExtent, double? itemExtent,
Key? containerKey, Key? containerKey,
Key? stackKey, Key? stackKey,
...@@ -40,6 +41,7 @@ void main() { ...@@ -40,6 +41,7 @@ void main() {
initialChildSize: initialChildSize, initialChildSize: initialChildSize,
snap: snap, snap: snap,
snapSizes: snapSizes, snapSizes: snapSizes,
snapAnimationDuration: snapAnimationDuration,
builder: (BuildContext context, ScrollController scrollController) { builder: (BuildContext context, ScrollController scrollController) {
return NotificationListener<ScrollNotification>( return NotificationListener<ScrollNotification>(
onNotification: onScrollNotification, onNotification: onScrollNotification,
...@@ -485,58 +487,64 @@ void main() { ...@@ -485,58 +487,64 @@ void main() {
}); });
} }
testWidgets('Zero velocity drag snaps to nearest snap target', (WidgetTester tester) async { for (final Duration? snapAnimationDuration in <Duration?>[null, const Duration(seconds: 2)]) {
const Key stackKey = ValueKey<String>('stack'); testWidgets(
const Key containerKey = ValueKey<String>('container'); 'Zero velocity drag snaps to nearest snap target with '
await tester.pumpWidget(boilerplateWidget(null, 'snapAnimationDuration: $snapAnimationDuration',
snap: true, (WidgetTester tester) async {
stackKey: stackKey, const Key stackKey = ValueKey<String>('stack');
containerKey: containerKey, const Key containerKey = ValueKey<String>('container');
snapSizes: <double>[.25, .5, .75, 1.0], await tester.pumpWidget(boilerplateWidget(null,
)); snap: true,
await tester.pumpAndSettle(); stackKey: stackKey,
final double screenHeight = tester.getSize(find.byKey(stackKey)).height; 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. // 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.drag(find.text('Item 1'), Offset(0, -.35 * screenHeight));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight, tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.75, precisionErrorTolerance), closeTo(.75, precisionErrorTolerance),
); );
// Drag up and snap up. // Drag up and snap up.
await tester.drag(find.text('Item 1'), Offset(0, -.2 * screenHeight)); await tester.drag(find.text('Item 1'), Offset(0, -.2 * screenHeight));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight, tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(1.0, precisionErrorTolerance), closeTo(1.0, precisionErrorTolerance),
); );
// Drag down and snap up. // Drag down and snap up.
await tester.drag(find.text('Item 1'), Offset(0, .1 * screenHeight)); await tester.drag(find.text('Item 1'), Offset(0, .1 * screenHeight));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight, tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(1.0, precisionErrorTolerance), closeTo(1.0, precisionErrorTolerance),
); );
// Drag down and snap down. // Drag down and snap down.
await tester.drag(find.text('Item 1'), Offset(0, .45 * screenHeight)); await tester.drag(find.text('Item 1'), Offset(0, .45 * screenHeight));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight, tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.5, precisionErrorTolerance), closeTo(.5, precisionErrorTolerance),
); );
// Fling up with negligible velocity and snap down. // Fling up with negligible velocity and snap down.
await tester.fling(find.text('Item 1'), Offset(0, .1 * screenHeight), 1); await tester.fling(find.text('Item 1'), Offset(0, .1 * screenHeight), 1);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight, tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.5, precisionErrorTolerance), closeTo(.5, precisionErrorTolerance),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all());
}
for (final List<double>? snapSizes in <List<double>?>[null, <double>[]]) { for (final List<double>? snapSizes in <List<double>?>[null, <double>[]]) {
testWidgets('Setting snapSizes to $snapSizes resolves to min and max', (WidgetTester tester) async { 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