Skip to content

Commit dd647b0

Browse files
authored
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 | | --- | --- | | <video src="https://github.com/flutter/flutter/assets/82336674/8761f14e-d5a5-4a39-b8e7-9e77433ce2c6" width=250px />| <video src="https://github.com/flutter/flutter/assets/82336674/57b38448-29fb-41ad-a947-d7cf1c160ca3" width=250px /> |
1 parent ff5e2d5 commit dd647b0

File tree

2 files changed

+75
-2
lines changed

2 files changed

+75
-2
lines changed

packages/flutter/lib/src/widgets/overscroll_indicator.dart

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -841,15 +841,23 @@ class _StretchController extends ChangeNotifier {
841841

842842
double get value => _stretchSize.value;
843843

844+
// Constants for absorbImpact.
845+
static const double _kMinVelocity = 1;
846+
static const double _kMaxVelocity = 10000;
847+
static const Duration _kMinStretchDuration = Duration(milliseconds: 50);
848+
844849
/// Handle a fling to the edge of the viewport at a particular velocity.
845850
///
846851
/// The velocity must be positive.
847852
void absorbImpact(double velocity, double totalOverscroll) {
848853
assert(velocity >= 0.0);
849-
velocity = clampDouble(velocity, 1, 10000);
854+
velocity = clampDouble(velocity, _kMinVelocity, _kMaxVelocity);
850855
_stretchSizeTween.begin = _stretchSize.value;
851856
_stretchSizeTween.end = math.min(_stretchIntensity + (_flingFriction / velocity), 1.0);
852-
_stretchController.duration = Duration(milliseconds: (velocity * 0.02).round());
857+
_stretchController.duration = Duration(
858+
milliseconds:
859+
math.max(velocity * 0.02, _kMinStretchDuration.inMilliseconds).round(),
860+
);
853861
_stretchController.forward(from: 0.0);
854862
_state = _StretchState.absorb;
855863
_stretchDirection = totalOverscroll > 0 ? _StretchDirection.trailing : _StretchDirection.leading;

packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,4 +1177,69 @@ void main() {
11771177

11781178
expect(tester.layers, contains(isA<ImageFilterLayer>()));
11791179
});
1180+
1181+
testWidgets('Stretching animation completes after fling under scroll physics with high friction', (WidgetTester tester) async {
1182+
// Regression test for https://github.com/flutter/flutter/issues/146277
1183+
final GlobalKey box1Key = GlobalKey();
1184+
final GlobalKey box2Key = GlobalKey();
1185+
final GlobalKey box3Key = GlobalKey();
1186+
late final OverscrollNotification overscrollNotification;
1187+
final ScrollController controller = ScrollController();
1188+
addTearDown(controller.dispose);
1189+
1190+
await tester.pumpWidget(NotificationListener<OverscrollNotification>(
1191+
child: buildTest(
1192+
box1Key,
1193+
box2Key,
1194+
box3Key,
1195+
controller,
1196+
physics: const _HighFrictionClampingScrollPhysics(),
1197+
),
1198+
onNotification: (OverscrollNotification notification) {
1199+
overscrollNotification = notification;
1200+
return false;
1201+
},
1202+
));
1203+
1204+
expect(find.byType(StretchingOverscrollIndicator), findsOneWidget);
1205+
expect(find.byType(GlowingOverscrollIndicator), findsNothing);
1206+
final RenderBox box1 = tester.renderObject(find.byKey(box1Key));
1207+
final RenderBox box2 = tester.renderObject(find.byKey(box2Key));
1208+
final RenderBox box3 = tester.renderObject(find.byKey(box3Key));
1209+
1210+
expect(controller.offset, 0.0);
1211+
expect(box1.localToGlobal(Offset.zero), Offset.zero);
1212+
expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0));
1213+
expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0));
1214+
1215+
// We fling to the trailing edge and let it settle.
1216+
await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0);
1217+
await tester.pumpAndSettle();
1218+
1219+
// We are now at the trailing edge
1220+
expect(overscrollNotification.velocity, lessThan(25));
1221+
expect(controller.offset, 150.0);
1222+
expect(box1.localToGlobal(Offset.zero).dy, -150.0);
1223+
expect(box2.localToGlobal(Offset.zero).dy, 100.0);
1224+
expect(box3.localToGlobal(Offset.zero).dy, 350.0);
1225+
});
1226+
}
1227+
1228+
final class _HighFrictionClampingScrollPhysics extends ScrollPhysics {
1229+
const _HighFrictionClampingScrollPhysics({super.parent});
1230+
1231+
@override
1232+
ScrollPhysics applyTo(ScrollPhysics? ancestor) {
1233+
return _HighFrictionClampingScrollPhysics(parent: buildParent(ancestor));
1234+
}
1235+
1236+
@override
1237+
Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
1238+
return ClampingScrollSimulation(
1239+
position: position.pixels,
1240+
velocity: velocity,
1241+
friction: 0.94,
1242+
tolerance: tolerance,
1243+
);
1244+
}
11801245
}

0 commit comments

Comments
 (0)