From dd647b0909662c8346e4336c99349ee3cbf8bc14 Mon Sep 17 00:00:00 2001
From: Gil Nobrega <82336674+gilnobrega@users.noreply.github.com>
Date: Tue, 23 Apr 2024 20:14:30 +0100
Subject: [PATCH] Fix frozen `StretchingOverscrollIndicator` animation
(#147195)
`StretchingOverscrollIndicator`'s controller does not have a minimum value for its animation duration.
When the `OverscrollNotification`'s `velocity` is small enough (< `25`) the controller's `absorbImpact` method sets this animation duration to 0ms, making the animation appear frozen to the user.
This PR sets a minimum animation duration of 50ms.
Fixes #146277
| Before | After |
| --- | --- |
| | |
---
.../lib/src/widgets/overscroll_indicator.dart | 12 +++-
.../overscroll_stretch_indicator_test.dart | 65 +++++++++++++++++++
2 files changed, 75 insertions(+), 2 deletions(-)
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,
+ );
+ }
}