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 /> |
This commit is contained in:
Gil Nobrega 2024-04-23 20:14:30 +01:00 committed by GitHub
parent ff5e2d5922
commit dd647b0909
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 75 additions and 2 deletions

View File

@ -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;

View File

@ -1177,4 +1177,69 @@ void main() {
expect(tester.layers, contains(isA<ImageFilterLayer>()));
});
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<OverscrollNotification>(
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,
);
}
}