diff --git a/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart b/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart
index 6101f8d110507f1ff53200c966ed333d1e4a31a7..84fe3d988b8a0d4baa8375f4ecb4b297ca7a7329 100644
--- a/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart
+++ b/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:math' as math;
+
 import 'package:flutter/foundation.dart';
 import 'package:flutter/gestures.dart';
 
@@ -51,6 +53,12 @@ typedef ScrollableWidgetBuilder = Widget Function(
 /// [ScrollableWidgetBuilder] does not use the provided [ScrollController], the
 /// sheet will remain at the initialChildSize.
 ///
+/// By default, the widget will stay at whatever size the user drags it to. To
+/// make the widget snap to specific sizes whenever they lift their finger
+/// during a drag, set [snap] to `true`. The sheet will snap between
+/// [minChildSize] and [maxChildSize]. Use [snapSizes] to add more sizes for
+/// the sheet to snap between.
+///
 /// By default, the widget will expand its non-occupied area to fill available
 /// space in the parent. If this is not desired, e.g. because the parent wants
 /// to position sheet based on the space it is taking, the [expand] property
@@ -107,6 +115,8 @@ class DraggableScrollableSheet extends StatefulWidget {
     this.minChildSize = 0.25,
     this.maxChildSize = 1.0,
     this.expand = true,
+    this.snap = false,
+    this.snapSizes,
     required this.builder,
   })  : assert(initialChildSize != null),
         assert(minChildSize != null),
@@ -147,6 +157,26 @@ class DraggableScrollableSheet extends StatefulWidget {
   /// The default value is true.
   final bool expand;
 
+  /// Whether the widget should snap between [snapSizes] when the user lifts
+  /// their finger during a drag.
+  ///
+  /// If the user's finger was still moving when they lifted it, the widget will
+  /// snap to the next snap size (see [snapSizes]) in the direction of the drag.
+  /// If their finger was still, the widget will snap to the nearest snap size.
+  final bool snap;
+
+  /// A list of target sizes that the widget should snap to.
+  ///
+  /// Snap sizes are fractional values of the parent container's height. They
+  /// must be listed in increasing order and be between [minChildSize] and
+  /// [maxChildSize].
+  ///
+  /// The [minChildSize] and [maxChildSize] are implicitly included in snap
+  /// sizes and do not need to be specified here. For example, `snapSizes = [.5]`
+  /// will result in a sheet that snaps between [minChildSize], `.5`, and
+  /// [maxChildSize].
+  final List<double>? snapSizes;
+
   /// The builder that creates a child to display in this widget, which will
   /// use the provided [ScrollController] to enable dragging and scrolling
   /// of the contents.
@@ -241,6 +271,8 @@ class _DraggableSheetExtent {
   _DraggableSheetExtent({
     required this.minExtent,
     required this.maxExtent,
+    required this.snap,
+    required this.snapSizes,
     required this.initialExtent,
     required VoidCallback listener,
   }) : assert(minExtent != null),
@@ -255,21 +287,30 @@ class _DraggableSheetExtent {
 
   final double minExtent;
   final double maxExtent;
+  final bool snap;
+  final List<double> snapSizes;
   final double initialExtent;
   final ValueNotifier<double> _currentExtent;
   double availablePixels;
 
+  // Used to disable snapping until the extent has changed. We do this because
+  // we don't want to snap away from the initial extent.
+  bool hasChanged = false;
+
   bool get isAtMin => minExtent >= _currentExtent.value;
   bool get isAtMax => maxExtent <= _currentExtent.value;
 
   set currentExtent(double value) {
     assert(value != null);
+    hasChanged = true;
     _currentExtent.value = value.clamp(minExtent, maxExtent);
   }
   double get currentExtent => _currentExtent.value;
+  double get currentPixels => extentToPixels(_currentExtent.value);
 
   double get additionalMinExtent => isAtMin ? 0.0 : 1.0;
   double get additionalMaxExtent => isAtMax ? 0.0 : 1.0;
+  List<double> get pixelSnapSizes => snapSizes.map(extentToPixels).toList();
 
   /// The scroll position gets inputs in terms of pixels, but the extent is
   /// expected to be expressed as a number between 0..1.
@@ -277,7 +318,13 @@ class _DraggableSheetExtent {
     if (availablePixels == 0) {
       return;
     }
-    currentExtent += delta / availablePixels * maxExtent;
+    updateExtent(currentExtent + pixelsToExtent(delta), context);
+  }
+
+  /// Set the extent to the new value. [newExtent] should be a number between
+  /// 0..1.
+  void updateExtent(double newExtent, BuildContext context) {
+    currentExtent = newExtent;
     DraggableScrollableNotification(
       minExtent: minExtent,
       maxExtent: maxExtent,
@@ -286,6 +333,14 @@ class _DraggableSheetExtent {
       context: context,
     ).dispatch(context);
   }
+
+  double pixelsToExtent(double pixels) {
+    return pixels / availablePixels * maxExtent;
+  }
+
+  double extentToPixels(double extent) {
+    return extent / maxExtent * availablePixels;
+  }
 }
 
 class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
@@ -298,12 +353,38 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
     _extent = _DraggableSheetExtent(
       minExtent: widget.minChildSize,
       maxExtent: widget.maxChildSize,
+      snap: widget.snap,
+      snapSizes: _impliedSnapSizes(),
       initialExtent: widget.initialChildSize,
       listener: _setExtent,
     );
     _scrollController = _DraggableScrollableSheetScrollController(extent: _extent);
   }
 
+  List<double> _impliedSnapSizes() {
+    for (int index = 0; index < (widget.snapSizes?.length ?? 0); index += 1) {
+      final double snapSize = widget.snapSizes![index];
+      assert(snapSize >= widget.minChildSize && snapSize <= widget.maxChildSize,
+        '${_snapSizeErrorMessage(index)}\nSnap sizes must be between `minChildSize` and `maxChildSize`. ');
+      assert(index == 0 || snapSize > widget.snapSizes![index - 1],
+        '${_snapSizeErrorMessage(index)}\nSnap sizes must be in ascending order. ');
+    }
+    widget.snapSizes?.asMap().forEach((int index, double snapSize) {
+    });
+    // Ensure the snap sizes start and end with the min and max child sizes.
+    if (widget.snapSizes == null || widget.snapSizes!.isEmpty) {
+      return <double>[
+        widget.minChildSize,
+        widget.maxChildSize,
+      ];
+    }
+    return <double>[
+      if (widget.snapSizes!.first != widget.minChildSize) widget.minChildSize,
+      ...widget.snapSizes!,
+      if (widget.snapSizes!.last != widget.maxChildSize) widget.maxChildSize,
+    ];
+  }
+
   @override
   void didChangeDependencies() {
     super.didChangeDependencies();
@@ -318,6 +399,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
           curve: Curves.linear,
         );
       }
+      _extent.hasChanged = false;
       _extent._currentExtent.value = _extent.initialExtent;
     }
   }
@@ -349,6 +431,20 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
     _scrollController.dispose();
     super.dispose();
   }
+
+  String _snapSizeErrorMessage(int invalidIndex) {
+    final List<String> snapSizesWithIndicator = widget.snapSizes!.asMap().keys.map(
+          (int index) {
+        final String snapSizeString = widget.snapSizes![index].toString();
+        if (index == invalidIndex) {
+          return '>>> $snapSizeString <<<';
+        }
+        return snapSizeString;
+      },
+    ).toList();
+    return "Invalid snapSize '${widget.snapSizes![invalidIndex]}' at index $invalidIndex of:\n"
+        '  $snapSizesWithIndicator';
+  }
 }
 
 /// A [ScrollController] suitable for use in a [ScrollableWidgetBuilder] created
@@ -466,6 +562,15 @@ class _DraggableScrollableSheetScrollPosition
     }
   }
 
+  bool get _isAtSnapSize {
+    return extent.snapSizes.any(
+      (double snapSize) {
+        return (extent.currentExtent - snapSize).abs() <= extent.pixelsToExtent(physics.tolerance.distance);
+      },
+    );
+  }
+  bool get _shouldSnap => extent.snap && extent.hasChanged && !_isAtSnapSize;
+
   @override
   void dispose() {
     // Stop the animation before dispose.
@@ -475,9 +580,9 @@ class _DraggableScrollableSheetScrollPosition
 
   @override
   void goBallistic(double velocity) {
-    if (velocity == 0.0 ||
-       (velocity < 0.0 && listShouldScroll) ||
-       (velocity > 0.0 && extent.isAtMax)) {
+    if ((velocity == 0.0 && !_shouldSnap) ||
+        (velocity < 0.0 && listShouldScroll) ||
+        (velocity > 0.0 && extent.isAtMax)) {
       super.goBallistic(velocity);
       return;
     }
@@ -485,13 +590,24 @@ class _DraggableScrollableSheetScrollPosition
     _dragCancelCallback?.call();
     _dragCancelCallback = null;
 
-    // The iOS bouncing simulation just isn't right here - once we delegate
-    // the ballistic back to the ScrollView, it will use the right simulation.
-    final Simulation simulation = ClampingScrollSimulation(
-      position: extent.currentExtent,
-      velocity: velocity,
-      tolerance: physics.tolerance,
-    );
+    late final Simulation simulation;
+    if (extent.snap) {
+      // Snap is enabled, simulate snapping instead of clamping scroll.
+      simulation = _SnappingSimulation(
+          position: extent.currentPixels,
+          initialVelocity: velocity,
+          pixelSnapSize: extent.pixelSnapSizes,
+          tolerance: physics.tolerance);
+    } else {
+      // The iOS bouncing simulation just isn't right here - once we delegate
+      // the ballistic back to the ScrollView, it will use the right simulation.
+      simulation = ClampingScrollSimulation(
+        // Run the simulation in terms of pixels, not extent.
+        position: extent.currentPixels,
+        velocity: velocity,
+        tolerance: physics.tolerance,
+      );
+    }
 
     final AnimationController ballisticController = AnimationController.unbounded(
       debugLabel: objectRuntimeType(this, '_DraggableScrollableSheetPosition'),
@@ -500,10 +616,10 @@ class _DraggableScrollableSheetScrollPosition
     // Stop the ballistic animation if a new activity starts.
     // See: [beginActivity].
     _ballisticCancelCallback = ballisticController.stop;
-    double lastDelta = 0;
+    double lastPosition = extent.currentPixels;
     void _tick() {
-      final double delta = ballisticController.value - lastDelta;
-      lastDelta = ballisticController.value;
+      final double delta = ballisticController.value - lastPosition;
+      lastPosition = ballisticController.value;
       extent.addPixelDelta(delta, context.notificationContext!);
       if ((velocity > 0 && extent.isAtMax) || (velocity < 0 && extent.isAtMin)) {
         // Make sure we pass along enough velocity to keep scrolling - otherwise
@@ -630,3 +746,80 @@ class _InheritedResetNotifier extends InheritedNotifier<_ResetNotifier> {
     return wasCalled;
   }
 }
+
+class _SnappingSimulation extends Simulation {
+  _SnappingSimulation({
+    required this.position,
+    required double initialVelocity,
+    required List<double> pixelSnapSize,
+    Tolerance tolerance = Tolerance.defaultTolerance,
+  }) : super(tolerance: tolerance) {
+    _pixelSnapSize = _getSnapSize(initialVelocity, pixelSnapSize);
+    // 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) {
+      velocity = math.min(-minimumSpeed, initialVelocity);
+    } else {
+      velocity = math.max(minimumSpeed, initialVelocity);
+    }
+  }
+
+  final double position;
+  late final double velocity;
+
+  // A minimum speed to snap at. Used to ensure that the snapping animation
+  // does not play too slowly.
+  static const double minimumSpeed = 1600.0;
+
+  late final double _pixelSnapSize;
+
+  @override
+  double dx(double time) {
+    if (isDone(time)) {
+      return 0;
+    }
+    return velocity;
+  }
+
+  @override
+  bool isDone(double time) {
+    return x(time) == _pixelSnapSize;
+  }
+
+  @override
+  double x(double time) {
+    final double newPosition = position + velocity * time;
+    if ((velocity >= 0 && newPosition > _pixelSnapSize) ||
+        (velocity < 0 && newPosition < _pixelSnapSize)) {
+      // We're passed the snap size, return it instead.
+      return _pixelSnapSize;
+    }
+    return newPosition;
+  }
+
+  // Find the two closest snap sizes to the position. If the velocity is
+  // non-zero, select the size in the velocity's direction. Otherwise,
+  // the nearest snap size.
+  double _getSnapSize(double initialVelocity, List<double> pixelSnapSizes) {
+    final int indexOfNextSize = pixelSnapSizes
+        .indexWhere((double size) => size >= position);
+    if (indexOfNextSize == 0) {
+      return pixelSnapSizes.first;
+    }
+    final double nextSize = pixelSnapSizes[indexOfNextSize];
+    final double previousSize = pixelSnapSizes[indexOfNextSize - 1];
+    if (initialVelocity.abs() <= tolerance.velocity) {
+      // If velocity is zero, snap to the nearest snap size with the minimum velocity.
+      if (position - previousSize < nextSize - position) {
+        return previousSize;
+      } else {
+        return nextSize;
+      }
+    }
+    // Snap forward or backward depending on current velocity.
+    if (initialVelocity < 0.0) {
+      return pixelSnapSizes[indexOfNextSize - 1];
+    }
+    return pixelSnapSizes[indexOfNextSize];
+  }
+}
diff --git a/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart b/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart
index 27ed9a4c610a01f98cf5e99b3fa6f187d0ed5958..8fca9060684b5a7f7e579938edb81f65ba57a5f8 100644
--- a/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart
+++ b/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart
@@ -12,8 +12,11 @@ void main() {
     double initialChildSize = .5,
     double maxChildSize = 1.0,
     double minChildSize = .25,
+    bool snap = false,
+    List<double>? snapSizes,
     double? itemExtent,
     Key? containerKey,
+    Key? stackKey,
     NotificationListenerCallback<ScrollNotification>? onScrollNotification,
   }) {
     return Directionality(
@@ -21,30 +24,35 @@ void main() {
       child: MediaQuery(
         data: const MediaQueryData(),
         child: Stack(
+          key: stackKey,
           children: <Widget>[
             TextButton(
               onPressed: onButtonPressed,
               child: const Text('TapHere'),
             ),
-            DraggableScrollableSheet(
-              maxChildSize: maxChildSize,
-              minChildSize: minChildSize,
-              initialChildSize: initialChildSize,
-              builder: (BuildContext context, ScrollController scrollController) {
-                return NotificationListener<ScrollNotification>(
-                  onNotification: onScrollNotification,
-                  child: Container(
-                    key: containerKey,
-                    color: const Color(0xFFABCDEF),
-                    child: ListView.builder(
-                      controller: scrollController,
-                      itemExtent: itemExtent,
-                      itemCount: itemCount,
-                      itemBuilder: (BuildContext context, int index) => Text('Item $index'),
+            DraggableScrollableActuator(
+              child: DraggableScrollableSheet(
+                maxChildSize: maxChildSize,
+                minChildSize: minChildSize,
+                initialChildSize: initialChildSize,
+                snap: snap,
+                snapSizes: snapSizes,
+                builder: (BuildContext context, ScrollController scrollController) {
+                  return NotificationListener<ScrollNotification>(
+                    onNotification: onScrollNotification,
+                    child: Container(
+                      key: containerKey,
+                      color: const Color(0xFFABCDEF),
+                      child: ListView.builder(
+                        controller: scrollController,
+                        itemExtent: itemExtent,
+                        itemCount: itemCount,
+                        itemBuilder: (BuildContext context, int index) => Text('Item $index'),
+                      ),
                     ),
-                  ),
-                );
-              },
+                  );
+                },
+              ),
             ),
           ],
         ),
@@ -84,6 +92,27 @@ void main() {
     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) {
     group('$platform Scroll Physics', () {
       debugDefaultTargetPlatformOverride = platform;
@@ -301,6 +330,190 @@ void main() {
       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 {
       final List<Type> notificationTypes = <Type>[];
       await tester.pumpWidget(_boilerplate(