diff --git a/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart b/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart index 4110266db9..6d0dcc67e9 100644 --- a/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart +++ b/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart @@ -15,6 +15,7 @@ library; import 'dart:math' as math; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -909,14 +910,18 @@ class _DraggableScrollableSheetScrollPosition extends ScrollPositionWithSingleCo } } - bool get _isAtSnapSize { - return extent.snapSizes.any((double snapSize) { + // Checks if the sheet's current size is close to a snap size, returning the + // snap size if so; returns null otherwise. + double? _getCurrentSnapSize() { + return extent.snapSizes.firstWhereOrNull((double snapSize) { return (extent.currentSize - snapSize).abs() <= extent.pixelsToSize(physics.toleranceFor(this).distance); }); } - bool get _shouldSnap => extent.snap && extent.hasDragged && !_isAtSnapSize; + bool _isAtSnapSize() => _getCurrentSnapSize() != null; + + bool _shouldSnap() => extent.snap && extent.hasDragged && !_isAtSnapSize(); @override void dispose() { @@ -929,7 +934,7 @@ class _DraggableScrollableSheetScrollPosition extends ScrollPositionWithSingleCo @override void goBallistic(double velocity) { - if ((velocity == 0.0 && !_shouldSnap) || + if ((velocity == 0.0 && !_shouldSnap()) || (velocity < 0.0 && listShouldScroll) || (velocity > 0.0 && extent.isAtMax)) { super.goBallistic(velocity); @@ -981,6 +986,13 @@ class _DraggableScrollableSheetScrollPosition extends ScrollPositionWithSingleCo super.goBallistic(velocity); ballisticController.stop(); } else if (ballisticController.isCompleted) { + // Update the extent value after the snap animation completes to + // avoid rounding errors that could prevent the sheet from closing when + // it reaches minSize. + final double? snapSize = _getCurrentSnapSize(); + if (snapSize != null) { + extent.updateSize(snapSize, context.notificationContext!); + } super.goBallistic(0); } } diff --git a/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart b/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart index 4f44965fba..47a2c295fb 100644 --- a/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart +++ b/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart @@ -1892,4 +1892,30 @@ void main() { expect(receivedNotification!.shouldCloseOnMinExtent, isFalse); controller.dispose(); }); + + // Regression test for https://github.com/flutter/flutter/issues/140701 + testWidgets('DraggableScrollableSheet snaps exactly to minChildSize', ( + WidgetTester tester, + ) async { + double? lastExtent; + + await tester.pumpWidget( + boilerplateWidget( + null, + snap: true, + onDraggableScrollableNotification: (DraggableScrollableNotification notification) { + lastExtent = notification.extent; + return false; + }, + ), + ); + + // One of the conditions for reproducing the round-off error. + await tester.fling(find.text('Item 1'), const Offset(0, 100), 2000); + await tester.pumpFrames( + tester.widget(find.byType(Directionality)), + const Duration(milliseconds: 500), + ); + expect(lastExtent, .25); + }); }