diff --git a/packages/flutter/lib/src/widgets/overscroll_indicator.dart b/packages/flutter/lib/src/widgets/overscroll_indicator.dart index 6963dd22b1d..00b0767af50 100644 --- a/packages/flutter/lib/src/widgets/overscroll_indicator.dart +++ b/packages/flutter/lib/src/widgets/overscroll_indicator.dart @@ -841,15 +841,23 @@ class _StretchController extends ChangeNotifier { double get value => _stretchSize.value; + // Constants for absorbImpact. + static const double _kMinVelocity = 1; + static const double _kMaxVelocity = 10000; + static const Duration _kMinStretchDuration = Duration(milliseconds: 50); + /// Handle a fling to the edge of the viewport at a particular velocity. /// /// The velocity must be positive. void absorbImpact(double velocity, double totalOverscroll) { assert(velocity >= 0.0); - velocity = clampDouble(velocity, 1, 10000); + velocity = clampDouble(velocity, _kMinVelocity, _kMaxVelocity); _stretchSizeTween.begin = _stretchSize.value; _stretchSizeTween.end = math.min(_stretchIntensity + (_flingFriction / velocity), 1.0); - _stretchController.duration = Duration(milliseconds: (velocity * 0.02).round()); + _stretchController.duration = Duration( + milliseconds: + math.max(velocity * 0.02, _kMinStretchDuration.inMilliseconds).round(), + ); _stretchController.forward(from: 0.0); _state = _StretchState.absorb; _stretchDirection = totalOverscroll > 0 ? _StretchDirection.trailing : _StretchDirection.leading; diff --git a/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart b/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart index af368873925..34a7f3ea459 100644 --- a/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart +++ b/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart @@ -1177,4 +1177,69 @@ void main() { expect(tester.layers, contains(isA())); }); + + testWidgets('Stretching animation completes after fling under scroll physics with high friction', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/146277 + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + late final OverscrollNotification overscrollNotification; + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + + await tester.pumpWidget(NotificationListener( + child: buildTest( + box1Key, + box2Key, + box3Key, + controller, + physics: const _HighFrictionClampingScrollPhysics(), + ), + onNotification: (OverscrollNotification notification) { + overscrollNotification = notification; + return false; + }, + )); + + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); + + // We fling to the trailing edge and let it settle. + await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); + await tester.pumpAndSettle(); + + // We are now at the trailing edge + expect(overscrollNotification.velocity, lessThan(25)); + expect(controller.offset, 150.0); + expect(box1.localToGlobal(Offset.zero).dy, -150.0); + expect(box2.localToGlobal(Offset.zero).dy, 100.0); + expect(box3.localToGlobal(Offset.zero).dy, 350.0); + }); +} + +final class _HighFrictionClampingScrollPhysics extends ScrollPhysics { + const _HighFrictionClampingScrollPhysics({super.parent}); + + @override + ScrollPhysics applyTo(ScrollPhysics? ancestor) { + return _HighFrictionClampingScrollPhysics(parent: buildParent(ancestor)); + } + + @override + Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) { + return ClampingScrollSimulation( + position: position.pixels, + velocity: velocity, + friction: 0.94, + tolerance: tolerance, + ); + } }