Skip to content

Commit d211f3f

Browse files
authored
Limit overscroll stretching (#99364)
1 parent 4df1c0c commit d211f3f

File tree

2 files changed

+133
-2
lines changed

2 files changed

+133
-2
lines changed

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,15 @@ class _StretchingOverscrollIndicatorState extends State<StretchingOverscrollIndi
707707
} else {
708708
assert(notification.overscroll != 0.0);
709709
if (notification.dragDetails != null) {
710-
_stretchController.pull(notification.overscroll.abs() / notification.metrics.viewportDimension);
710+
// We clamp the overscroll amount relative to the length of the viewport,
711+
// which is the furthest distance a single pointer could pull on the
712+
// screen. This is because more than one pointer will multiply the
713+
// amount of overscroll - https://github.com/flutter/flutter/issues/11884
714+
final double viewportDimension = notification.metrics.viewportDimension;
715+
final double distanceForPull =
716+
(notification.overscroll.abs() / viewportDimension) + _stretchController.pullDistance;
717+
final double clampedOverscroll = distanceForPull.clamp(0, 1.0);
718+
_stretchController.pull(clampedOverscroll);
711719
}
712720
}
713721
}
@@ -818,6 +826,8 @@ class _StretchController extends ChangeNotifier {
818826
late final Animation<double> _stretchSize;
819827
final Tween<double> _stretchSizeTween = Tween<double>(begin: 0.0, end: 0.0);
820828
_StretchState _state = _StretchState.idle;
829+
830+
double get pullDistance => _pullDistance;
821831
double _pullDistance = 0.0;
822832

823833
// Constants from Android.
@@ -848,7 +858,7 @@ class _StretchController extends ChangeNotifier {
848858
/// in the main axis.
849859
void pull(double normalizedOverscroll) {
850860
assert(normalizedOverscroll >= 0.0);
851-
_pullDistance = normalizedOverscroll + _pullDistance;
861+
_pullDistance = normalizedOverscroll;
852862
_stretchSizeTween.begin = _stretchSize.value;
853863
final double linearIntensity =_stretchIntensity * _pullDistance;
854864
final double exponentialIntensity = _stretchIntensity * (1 - math.exp(-_pullDistance * _exponentialScalar));

packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,4 +451,125 @@ void main() {
451451
await gesture.up();
452452
await tester.pumpAndSettle();
453453
});
454+
455+
testWidgets('Stretch limit', (WidgetTester tester) async {
456+
// Regression test for https://github.com/flutter/flutter/issues/99264
457+
await tester.pumpWidget(
458+
Directionality(
459+
textDirection: TextDirection.ltr,
460+
child: MediaQuery(
461+
data: const MediaQueryData(),
462+
child: ScrollConfiguration(
463+
behavior: const ScrollBehavior().copyWith(overscroll: false),
464+
child: StretchingOverscrollIndicator(
465+
axisDirection: AxisDirection.down,
466+
child: SizedBox(
467+
height: 300,
468+
child: ListView.builder(
469+
itemCount: 20,
470+
itemBuilder: (BuildContext context, int index){
471+
return Padding(
472+
padding: const EdgeInsets.all(10.0),
473+
child: Text('Index $index'),
474+
);
475+
},
476+
),
477+
),
478+
),
479+
),
480+
)
481+
)
482+
);
483+
const double maxStretchLocation = 52.63178407049861;
484+
485+
expect(find.text('Index 1'), findsOneWidget);
486+
expect(tester.getCenter(find.text('Index 1')).dy, 51.0);
487+
488+
TestGesture pointer = await tester.startGesture(tester.getCenter(find.text('Index 1')));
489+
// Overscroll beyond the limit (the viewport is 600.0).
490+
await pointer.moveBy(const Offset(0.0, 610.0));
491+
await tester.pumpAndSettle();
492+
expect(find.text('Index 1'), findsOneWidget);
493+
expect(tester.getCenter(find.text('Index 1')).dy, maxStretchLocation);
494+
495+
pointer = await tester.startGesture(tester.getCenter(find.text('Index 1')));
496+
// Overscroll way way beyond the limit
497+
await pointer.moveBy(const Offset(0.0, 1000.0));
498+
await tester.pumpAndSettle();
499+
expect(find.text('Index 1'), findsOneWidget);
500+
expect(tester.getCenter(find.text('Index 1')).dy, maxStretchLocation);
501+
502+
await pointer.up();
503+
await tester.pumpAndSettle();
504+
});
505+
506+
testWidgets('Multiple pointers wll not exceed stretch limit', (WidgetTester tester) async {
507+
// Regression test for https://github.com/flutter/flutter/issues/99264
508+
await tester.pumpWidget(
509+
Directionality(
510+
textDirection: TextDirection.ltr,
511+
child: MediaQuery(
512+
data: const MediaQueryData(),
513+
child: ScrollConfiguration(
514+
behavior: const ScrollBehavior().copyWith(overscroll: false),
515+
child: StretchingOverscrollIndicator(
516+
axisDirection: AxisDirection.down,
517+
child: SizedBox(
518+
height: 300,
519+
child: ListView.builder(
520+
itemCount: 20,
521+
itemBuilder: (BuildContext context, int index){
522+
return Padding(
523+
padding: const EdgeInsets.all(10.0),
524+
child: Text('Index $index'),
525+
);
526+
},
527+
),
528+
),
529+
),
530+
),
531+
)
532+
)
533+
);
534+
expect(find.text('Index 1'), findsOneWidget);
535+
expect(tester.getCenter(find.text('Index 1')).dy, 51.0);
536+
537+
final TestGesture pointer1 = await tester.startGesture(tester.getCenter(find.text('Index 1')));
538+
// Overscroll the start.
539+
await pointer1.moveBy(const Offset(0.0, 210.0));
540+
await tester.pumpAndSettle();
541+
expect(find.text('Index 1'), findsOneWidget);
542+
double lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy;
543+
expect(lastStretchedLocation, greaterThan(51.0));
544+
545+
final TestGesture pointer2 = await tester.startGesture(tester.getCenter(find.text('Index 1')));
546+
// Add overscroll from an additional pointer
547+
await pointer2.moveBy(const Offset(0.0, 210.0));
548+
await tester.pumpAndSettle();
549+
expect(find.text('Index 1'), findsOneWidget);
550+
expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(lastStretchedLocation));
551+
lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy;
552+
553+
final TestGesture pointer3 = await tester.startGesture(tester.getCenter(find.text('Index 1')));
554+
// Add overscroll from an additional pointer, exceeding the max stretch (600)
555+
await pointer3.moveBy(const Offset(0.0, 210.0));
556+
await tester.pumpAndSettle();
557+
expect(find.text('Index 1'), findsOneWidget);
558+
expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(lastStretchedLocation));
559+
lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy;
560+
561+
final TestGesture pointer4 = await tester.startGesture(tester.getCenter(find.text('Index 1')));
562+
// Since we have maxed out the overscroll, it should not have stretched
563+
// further, regardless of the number of pointers.
564+
await pointer4.moveBy(const Offset(0.0, 210.0));
565+
await tester.pumpAndSettle();
566+
expect(find.text('Index 1'), findsOneWidget);
567+
expect(tester.getCenter(find.text('Index 1')).dy, lastStretchedLocation);
568+
569+
await pointer1.up();
570+
await pointer2.up();
571+
await pointer3.up();
572+
await pointer4.up();
573+
await tester.pumpAndSettle();
574+
});
454575
}

0 commit comments

Comments
 (0)