// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // This file is run as part of a reduced test set in CI on Mac and Windows // machines. @Tags(<String>['reduced-test-set']) library; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { Widget buildTest( GlobalKey box1Key, GlobalKey box2Key, GlobalKey box3Key, ScrollController controller, { Axis axis = Axis.vertical, bool reverse = false, TextDirection textDirection = TextDirection.ltr, double boxHeight = 250.0, double boxWidth = 300.0, ScrollPhysics? physics, }) { final AxisDirection axisDirection; switch (axis) { case Axis.horizontal: if (textDirection == TextDirection.rtl) { axisDirection = reverse ? AxisDirection.right : AxisDirection.left; } else { axisDirection = reverse ? AxisDirection.left : AxisDirection.right; } case Axis.vertical: axisDirection = reverse ? AxisDirection.up : AxisDirection.down; } return Directionality( textDirection: textDirection, child: MediaQuery( data: const MediaQueryData(size: Size(800.0, 600.0)), child: ScrollConfiguration( behavior: const ScrollBehavior().copyWith(overscroll: false), child: StretchingOverscrollIndicator( axisDirection: axisDirection, child: CustomScrollView( physics: physics, reverse: reverse, scrollDirection: axis, controller: controller, slivers: <Widget>[ SliverToBoxAdapter(child: Container( color: const Color(0xD0FF0000), key: box1Key, height: boxHeight, width: boxWidth, )), SliverToBoxAdapter(child: Container( color: const Color(0xFFFFFF00), key: box2Key, height: boxHeight, width: boxWidth, )), SliverToBoxAdapter(child: Container( color: const Color(0xFF6200EA), key: box3Key, height: boxHeight, width: boxWidth, )), ], ), ), ), ), ); } testWidgets('Stretch overscroll will do nothing when axes do not match', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: const MediaQueryData(size: Size(800.0, 600.0)), child: ScrollConfiguration( behavior: const ScrollBehavior().copyWith(overscroll: false), child: StretchingOverscrollIndicator( axisDirection: AxisDirection.right, child: CustomScrollView( controller: controller, slivers: <Widget>[ SliverToBoxAdapter(child: Container( color: const Color(0xD0FF0000), key: box1Key, height: 250.0, )), SliverToBoxAdapter(child: Container( color: const Color(0xFFFFFF00), key: box2Key, height: 250.0, width: 300.0, )), ], ), ), ), ), ) ); 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)); expect(controller.offset, 0.0); expect(box1.localToGlobal(Offset.zero), Offset.zero); expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); // Overscroll the start, no stretching occurs. await gesture.moveBy(const Offset(0.0, 200.0)); await tester.pumpAndSettle(); expect(box1.localToGlobal(Offset.zero), Offset.zero); expect(box2.localToGlobal(Offset.zero).dy, 250.0); await gesture.up(); await tester.pumpAndSettle(); // Overscroll released expect(box1.localToGlobal(Offset.zero), Offset.zero); expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); }); testWidgets('Stretch overscroll vertically', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget( buildTest(box1Key, box2Key, box3Key, controller), ); 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)); await expectLater( find.byType(CustomScrollView), matchesGoldenFile('overscroll_stretch.vertical.start.png'), ); TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); // Overscroll the start await gesture.moveBy(const Offset(0.0, 200.0)); await tester.pumpAndSettle(); expect(box1.localToGlobal(Offset.zero), Offset.zero); expect(box2.localToGlobal(Offset.zero).dy, greaterThan(255.0)); expect(box3.localToGlobal(Offset.zero).dy, greaterThan(510.0)); await expectLater( find.byType(CustomScrollView), matchesGoldenFile('overscroll_stretch.vertical.start.stretched.png'), ); await gesture.up(); await tester.pumpAndSettle(); // Stretch released back to the start 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)); // Jump to end of the list controller.jumpTo(controller.position.maxScrollExtent); await tester.pumpAndSettle(); 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); await expectLater( find.byType(CustomScrollView), matchesGoldenFile('overscroll_stretch.vertical.end.png'), ); gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); // Overscroll the end await gesture.moveBy(const Offset(0.0, -200.0)); await tester.pumpAndSettle(); expect(box1.localToGlobal(Offset.zero).dy, lessThan(-165)); expect(box2.localToGlobal(Offset.zero).dy, lessThan(90.0)); expect(box3.localToGlobal(Offset.zero).dy, lessThan(350.0)); await expectLater( find.byType(CustomScrollView), matchesGoldenFile('overscroll_stretch.vertical.end.stretched.png'), ); await gesture.up(); await tester.pumpAndSettle(); // Stretch released back expect(box1.localToGlobal(Offset.zero).dy, -150.0); expect(box2.localToGlobal(Offset.zero).dy, 100.0); expect(box3.localToGlobal(Offset.zero).dy, 350.0); }); testWidgets('Stretch overscroll works in reverse - vertical', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget( buildTest(box1Key, box2Key, box3Key, controller, reverse: true), ); 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), const Offset(0.0, 350.0)); expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); expect(box3.localToGlobal(Offset.zero), const Offset(0.0, -150.0)); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); // Overscroll await gesture.moveBy(const Offset(0.0, -200.0)); await tester.pumpAndSettle(); expect(box1.localToGlobal(Offset.zero).dy, lessThan(350.0)); expect(box2.localToGlobal(Offset.zero).dy, lessThan(100.0)); expect(box3.localToGlobal(Offset.zero).dy, lessThan(-150.0)); await expectLater( find.byType(CustomScrollView), matchesGoldenFile('overscroll_stretch.vertical.reverse.png'), ); }); testWidgets('Stretch overscroll works in reverse - horizontal', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget( buildTest( box1Key, box2Key, box3Key, controller, axis: Axis.horizontal, reverse: true, ), ); 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), const Offset(500.0, 0.0)); expect(box2.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); expect(box3.localToGlobal(Offset.zero), const Offset(-100.0, 0.0)); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); // Overscroll await gesture.moveBy(const Offset(-200.0, 0.0)); await tester.pumpAndSettle(); expect(box1.localToGlobal(Offset.zero).dx, lessThan(500.0)); expect(box2.localToGlobal(Offset.zero).dx, lessThan(200.0)); expect(box3.localToGlobal(Offset.zero).dx, lessThan(-100.0)); await expectLater( find.byType(CustomScrollView), matchesGoldenFile('overscroll_stretch.horizontal.reverse.png'), ); }); testWidgets('Stretch overscroll works in reverse - horizontal - RTL', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget( buildTest( box1Key, box2Key, box3Key, controller, axis: Axis.horizontal, reverse: true, textDirection: TextDirection.rtl, ) ); 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(300.0, 0.0)); expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); await expectLater( find.byType(CustomScrollView), matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.start.png'), ); TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); // Overscroll the start await gesture.moveBy(const Offset(200.0, 0.0)); await tester.pumpAndSettle(); expect(box1.localToGlobal(Offset.zero), Offset.zero); expect(box2.localToGlobal(Offset.zero).dx, greaterThan(305.0)); expect(box3.localToGlobal(Offset.zero).dx, greaterThan(610.0)); await expectLater( find.byType(CustomScrollView), matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.start.stretched.png'), ); await gesture.up(); await tester.pumpAndSettle(); // Stretch released back to the start expect(box1.localToGlobal(Offset.zero), Offset.zero); expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); // Jump to end of the list controller.jumpTo(controller.position.maxScrollExtent); await tester.pumpAndSettle(); expect(controller.offset, 100.0); expect(box1.localToGlobal(Offset.zero).dx, -100.0); expect(box2.localToGlobal(Offset.zero).dx, 200.0); expect(box3.localToGlobal(Offset.zero).dx, 500.0); await expectLater( find.byType(CustomScrollView), matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.end.png'), ); gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); // Overscroll the end await gesture.moveBy(const Offset(-200.0, 0.0)); await tester.pumpAndSettle(); expect(box1.localToGlobal(Offset.zero).dx, lessThan(-116.0)); expect(box2.localToGlobal(Offset.zero).dx, lessThan(190.0)); expect(box3.localToGlobal(Offset.zero).dx, lessThan(500.0)); await expectLater( find.byType(CustomScrollView), matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.end.stretched.png'), ); await gesture.up(); await tester.pumpAndSettle(); // Stretch released back expect(box1.localToGlobal(Offset.zero).dx, -100.0); expect(box2.localToGlobal(Offset.zero).dx, 200.0); expect(box3.localToGlobal(Offset.zero).dx, 500.0); }); testWidgets('Stretch overscroll horizontally', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget( buildTest(box1Key, box2Key, box3Key, controller, axis: Axis.horizontal) ); 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(300.0, 0.0)); expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); await expectLater( find.byType(CustomScrollView), matchesGoldenFile('overscroll_stretch.horizontal.start.png'), ); TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); // Overscroll the start await gesture.moveBy(const Offset(200.0, 0.0)); await tester.pumpAndSettle(); expect(box1.localToGlobal(Offset.zero), Offset.zero); expect(box2.localToGlobal(Offset.zero).dx, greaterThan(305.0)); expect(box3.localToGlobal(Offset.zero).dx, greaterThan(610.0)); await expectLater( find.byType(CustomScrollView), matchesGoldenFile('overscroll_stretch.horizontal.start.stretched.png'), ); await gesture.up(); await tester.pumpAndSettle(); // Stretch released back to the start expect(box1.localToGlobal(Offset.zero), Offset.zero); expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); // Jump to end of the list controller.jumpTo(controller.position.maxScrollExtent); await tester.pumpAndSettle(); expect(controller.offset, 100.0); expect(box1.localToGlobal(Offset.zero).dx, -100.0); expect(box2.localToGlobal(Offset.zero).dx, 200.0); expect(box3.localToGlobal(Offset.zero).dx, 500.0); await expectLater( find.byType(CustomScrollView), matchesGoldenFile('overscroll_stretch.horizontal.end.png'), ); gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); // Overscroll the end await gesture.moveBy(const Offset(-200.0, 0.0)); await tester.pumpAndSettle(); expect(box1.localToGlobal(Offset.zero).dx, lessThan(-116.0)); expect(box2.localToGlobal(Offset.zero).dx, lessThan(190.0)); expect(box3.localToGlobal(Offset.zero).dx, lessThan(500.0)); await expectLater( find.byType(CustomScrollView), matchesGoldenFile('overscroll_stretch.horizontal.end.stretched.png'), ); await gesture.up(); await tester.pumpAndSettle(); // Stretch released back expect(box1.localToGlobal(Offset.zero).dx, -100.0); expect(box2.localToGlobal(Offset.zero).dx, 200.0); expect(box3.localToGlobal(Offset.zero).dx, 500.0); }); testWidgets('Stretch overscroll horizontally RTL', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget( buildTest( box1Key, box2Key, box3Key, controller, axis: Axis.horizontal, textDirection: TextDirection.rtl, ) ); 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), const Offset(500.0, 0.0)); expect(box2.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); expect(box3.localToGlobal(Offset.zero), const Offset(-100.0, 0.0)); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); // Overscroll await gesture.moveBy(const Offset(-200.0, 0.0)); await tester.pumpAndSettle(); expect(box1.localToGlobal(Offset.zero).dx, lessThan(500.0)); expect(box2.localToGlobal(Offset.zero).dx, lessThan(200.0)); expect(box3.localToGlobal(Offset.zero).dx, lessThan(-100.0)); await expectLater( find.byType(CustomScrollView), matchesGoldenFile('overscroll_stretch.horizontal.rtl.png'), ); }); testWidgets('Disallow stretching overscroll', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); addTearDown(controller.dispose); double indicatorNotification = 0; await tester.pumpWidget( NotificationListener<OverscrollIndicatorNotification>( onNotification: (OverscrollIndicatorNotification notification) { notification.disallowIndicator(); indicatorNotification += 1; return false; }, child: buildTest(box1Key, box2Key, box3Key, controller), ) ); 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(indicatorNotification, 0.0); 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)); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); // Overscroll the start, should not stretch await gesture.moveBy(const Offset(0.0, 200.0)); await tester.pumpAndSettle(); expect(indicatorNotification, 1.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)); await gesture.up(); await tester.pumpAndSettle(); }); testWidgets('Stretch does not overflow bounds of container', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/90197 await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: const MediaQueryData(size: Size(800.0, 600.0)), child: ScrollConfiguration( behavior: const ScrollBehavior().copyWith(overscroll: false), child: Column( children: <Widget>[ StretchingOverscrollIndicator( axisDirection: AxisDirection.down, child: SizedBox( height: 300, child: ListView.builder( itemCount: 20, itemBuilder: (BuildContext context, int index){ return Padding( padding: const EdgeInsets.all(10.0), child: Text('Index $index'), ); }, ), ), ), Opacity( opacity: 0.5, child: Container( color: const Color(0xD0FF0000), height: 100, ), ), ], ), ), ), )); expect(find.text('Index 1'), findsOneWidget); expect(tester.getCenter(find.text('Index 1')).dy, 51.0); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Index 1'))); // Overscroll the start. await gesture.moveBy(const Offset(0.0, 200.0)); await tester.pumpAndSettle(); expect(find.text('Index 1'), findsOneWidget); expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(0)); // Image should not show the text overlapping the red area below the list. await expectLater( find.byType(Column), matchesGoldenFile('overscroll_stretch.no_overflow.png'), ); await gesture.up(); await tester.pumpAndSettle(); }); testWidgets('Clip behavior is updated as needed', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/97867 await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: const MediaQueryData(size: Size(800.0, 600.0)), child: ScrollConfiguration( behavior: const ScrollBehavior().copyWith(overscroll: false), child: Column( children: <Widget>[ StretchingOverscrollIndicator( axisDirection: AxisDirection.down, child: SizedBox( height: 300, child: ListView.builder( itemCount: 20, itemBuilder: (BuildContext context, int index){ return Padding( padding: const EdgeInsets.all(10.0), child: Text('Index $index'), ); }, ), ), ), Opacity( opacity: 0.5, child: Container( color: const Color(0xD0FF0000), height: 100, ), ), ], ), ), ), ), ); expect(find.text('Index 1'), findsOneWidget); expect(tester.getCenter(find.text('Index 1')).dy, 51.0); RenderClipRect renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first; // Currently not clipping expect(renderClip.clipBehavior, equals(Clip.none)); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Index 1'))); // Overscroll the start. await gesture.moveBy(const Offset(0.0, 200.0)); await tester.pumpAndSettle(); expect(find.text('Index 1'), findsOneWidget); expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(0)); renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first; // Now clipping expect(renderClip.clipBehavior, equals(Clip.hardEdge)); await gesture.up(); await tester.pumpAndSettle(); }); testWidgets('clipBehavior parameter updates overscroll clipping behavior', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/103491 Widget buildFrame(Clip clipBehavior) { return Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: const MediaQueryData(size: Size(800.0, 600.0)), child: ScrollConfiguration( behavior: const ScrollBehavior().copyWith(overscroll: false), child: Column( children: <Widget>[ StretchingOverscrollIndicator( axisDirection: AxisDirection.down, clipBehavior: clipBehavior, child: SizedBox( height: 300, child: ListView.builder( itemCount: 20, itemBuilder: (BuildContext context, int index){ return Padding( padding: const EdgeInsets.all(10.0), child: Text('Index $index'), ); }, ), ), ), Opacity( opacity: 0.5, child: Container( color: const Color(0xD0FF0000), height: 100, ), ), ], ), ), ), ); } await tester.pumpWidget(buildFrame(Clip.none)); expect(find.text('Index 1'), findsOneWidget); expect(tester.getCenter(find.text('Index 1')).dy, 51.0); RenderClipRect renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first; // Currently not clipping expect(renderClip.clipBehavior, equals(Clip.none)); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Index 1'))); // Overscroll the start. await gesture.moveBy(const Offset(0.0, 200.0)); await tester.pumpAndSettle(); expect(find.text('Index 1'), findsOneWidget); expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(0)); renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first; // Now clipping expect(renderClip.clipBehavior, equals(Clip.none)); await gesture.up(); await tester.pumpAndSettle(); }); testWidgets('Stretch limit', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/99264 await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: const MediaQueryData(), child: ScrollConfiguration( behavior: const ScrollBehavior().copyWith(overscroll: false), child: StretchingOverscrollIndicator( axisDirection: AxisDirection.down, child: SizedBox( height: 300, child: ListView.builder( itemCount: 20, itemBuilder: (BuildContext context, int index){ return Padding( padding: const EdgeInsets.all(10.0), child: Text('Index $index'), ); }, ), ), ), ), ) ) ); const double maxStretchLocation = 52.63178407049861; expect(find.text('Index 1'), findsOneWidget); expect(tester.getCenter(find.text('Index 1')).dy, 51.0); TestGesture pointer = await tester.startGesture(tester.getCenter(find.text('Index 1'))); // Overscroll beyond the limit (the viewport is 600.0). await pointer.moveBy(const Offset(0.0, 610.0)); await tester.pumpAndSettle(); expect(find.text('Index 1'), findsOneWidget); expect(tester.getCenter(find.text('Index 1')).dy, maxStretchLocation); pointer = await tester.startGesture(tester.getCenter(find.text('Index 1'))); // Overscroll way way beyond the limit await pointer.moveBy(const Offset(0.0, 1000.0)); await tester.pumpAndSettle(); expect(find.text('Index 1'), findsOneWidget); expect(tester.getCenter(find.text('Index 1')).dy, maxStretchLocation); await pointer.up(); await tester.pumpAndSettle(); }); testWidgets('Multiple pointers will not exceed stretch limit', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/99264 await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: const MediaQueryData(), child: ScrollConfiguration( behavior: const ScrollBehavior().copyWith(overscroll: false), child: StretchingOverscrollIndicator( axisDirection: AxisDirection.down, child: SizedBox( height: 300, child: ListView.builder( itemCount: 20, itemBuilder: (BuildContext context, int index){ return Padding( padding: const EdgeInsets.all(10.0), child: Text('Index $index'), ); }, ), ), ), ), ) ) ); expect(find.text('Index 1'), findsOneWidget); expect(tester.getCenter(find.text('Index 1')).dy, 51.0); final TestGesture pointer1 = await tester.startGesture(tester.getCenter(find.text('Index 1'))); // Overscroll the start. await pointer1.moveBy(const Offset(0.0, 210.0)); await tester.pumpAndSettle(); expect(find.text('Index 1'), findsOneWidget); double lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; expect(lastStretchedLocation, greaterThan(51.0)); final TestGesture pointer2 = await tester.startGesture(tester.getCenter(find.text('Index 1'))); // Add overscroll from an additional pointer await pointer2.moveBy(const Offset(0.0, 210.0)); await tester.pumpAndSettle(); expect(find.text('Index 1'), findsOneWidget); expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(lastStretchedLocation)); lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; final TestGesture pointer3 = await tester.startGesture(tester.getCenter(find.text('Index 1'))); // Add overscroll from an additional pointer, exceeding the max stretch (600) await pointer3.moveBy(const Offset(0.0, 210.0)); await tester.pumpAndSettle(); expect(find.text('Index 1'), findsOneWidget); expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(lastStretchedLocation)); lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; final TestGesture pointer4 = await tester.startGesture(tester.getCenter(find.text('Index 1'))); // Since we have maxed out the overscroll, it should not have stretched // further, regardless of the number of pointers. await pointer4.moveBy(const Offset(0.0, 210.0)); await tester.pumpAndSettle(); expect(find.text('Index 1'), findsOneWidget); expect(tester.getCenter(find.text('Index 1')).dy, lastStretchedLocation); await pointer1.up(); await pointer2.up(); await pointer3.up(); await pointer4.up(); await tester.pumpAndSettle(); }); testWidgets('Stretch overscroll vertically, change direction mid scroll', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget( buildTest( box1Key, box2Key, box3Key, controller, // Setting the `boxHeight` to 100.0 will make the boxes fit in the // scrollable viewport. boxHeight: 100, // To make the scroll view in the test still scrollable, we need to add // the `AlwaysScrollableScrollPhysics`. physics: const AlwaysScrollableScrollPhysics(), ), ); 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, 100.0)); expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 200.0)); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); // Overscroll the start await gesture.moveBy(const Offset(0.0, 600.0)); await tester.pumpAndSettle(); // The boxes should now be at different locations because of the scaling. expect(box1.localToGlobal(Offset.zero), Offset.zero); expect(box2.localToGlobal(Offset.zero).dy, greaterThan(103.0)); expect(box3.localToGlobal(Offset.zero).dy, greaterThan(206.0)); // Move the pointer up a miniscule amount to trigger a directional change. await gesture.moveBy(const Offset(0.0, -20.0)); await tester.pumpAndSettle(); // The boxes should remain roughly at the same locations, since the pointer // didn't move far. expect(box1.localToGlobal(Offset.zero), Offset.zero); expect(box2.localToGlobal(Offset.zero).dy, greaterThan(103.0)); expect(box3.localToGlobal(Offset.zero).dy, greaterThan(206.0)); // Now make the pointer overscroll to the end await gesture.moveBy(const Offset(0.0, -1200.0)); await tester.pumpAndSettle(); expect(box1.localToGlobal(Offset.zero).dy, lessThan(-19.0)); expect(box2.localToGlobal(Offset.zero).dy, lessThan(85.0)); expect(box3.localToGlobal(Offset.zero).dy, lessThan(188.0)); // Release the pointer await gesture.up(); await tester.pumpAndSettle(); // Now the boxes should be back to their original locations. expect(box1.localToGlobal(Offset.zero), Offset.zero); expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 200.0)); }); testWidgets('Stretch overscroll horizontally, change direction mid scroll', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget( buildTest( box1Key, box2Key, box3Key, controller, // Setting the `boxWidth` to 100.0 will make the boxes fit in the // scrollable viewport. boxWidth: 100, // To make the scroll view in the test still scrollable, we need to add // the `AlwaysScrollableScrollPhysics`. physics: const AlwaysScrollableScrollPhysics(), axis: Axis.horizontal, ), ); 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(100.0, 0.0)); expect(box3.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); // Overscroll the start await gesture.moveBy(const Offset(600.0, 0.0)); await tester.pumpAndSettle(); // The boxes should now be at different locations because of the scaling. expect(box1.localToGlobal(Offset.zero), Offset.zero); expect(box2.localToGlobal(Offset.zero).dx, greaterThan(102.0)); expect(box3.localToGlobal(Offset.zero).dx, greaterThan(205.0)); // Move the pointer up a miniscule amount to trigger a directional change. await gesture.moveBy(const Offset(-20.0, 0.0)); await tester.pumpAndSettle(); // The boxes should remain roughly at the same locations, since the pointer // didn't move far. expect(box1.localToGlobal(Offset.zero), Offset.zero); expect(box2.localToGlobal(Offset.zero).dx, greaterThan(102.0)); expect(box3.localToGlobal(Offset.zero).dx, greaterThan(205.0)); // Now make the pointer overscroll to the end await gesture.moveBy(const Offset(-1200.0, 0.0)); await tester.pumpAndSettle(); expect(box1.localToGlobal(Offset.zero).dx, lessThan(-19.0)); expect(box2.localToGlobal(Offset.zero).dx, lessThan(85.0)); expect(box3.localToGlobal(Offset.zero).dx, lessThan(188.0)); // Release the pointer await gesture.up(); await tester.pumpAndSettle(); // Now the boxes should be back to their original locations. expect(box1.localToGlobal(Offset.zero), Offset.zero); expect(box2.localToGlobal(Offset.zero), const Offset(100.0, 0.0)); expect(box3.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); }); testWidgets('Fling toward the trailing edge causes stretch toward the leading edge', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget( buildTest(box1Key, box2Key, box3Key, controller), ); 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)); await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); await tester.pump(const Duration(milliseconds: 100)); await tester.pump(const Duration(milliseconds: 100)); await tester.pump(const Duration(milliseconds: 100)); // The boxes should now be at different locations because of the scaling. expect(controller.offset, 150.0); expect(box1.localToGlobal(Offset.zero).dy, lessThan(-160.0)); expect(box2.localToGlobal(Offset.zero).dy, lessThan(93.0)); expect(box3.localToGlobal(Offset.zero).dy, lessThan(347.0)); await tester.pumpAndSettle(); // The boxes should now be at their final position. 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); }); testWidgets('Fling toward the leading edge causes stretch toward the trailing edge', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget( buildTest(box1Key, box2Key, box3Key, controller), ); 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(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); // Now fling to the leading edge await tester.fling(find.byType(CustomScrollView), const Offset(0.0, 50.0), 10000.0); await tester.pump(const Duration(milliseconds: 100)); await tester.pump(const Duration(milliseconds: 100)); await tester.pump(const Duration(milliseconds: 100)); await tester.pump(const Duration(milliseconds: 100)); // The boxes should now be at different locations because of the scaling. expect(controller.offset, 0.0); expect(box1.localToGlobal(Offset.zero).dy, 0.0); expect(box2.localToGlobal(Offset.zero).dy, greaterThan(254.0)); expect(box3.localToGlobal(Offset.zero).dy, greaterThan(508.0)); await tester.pumpAndSettle(); // The boxes should now be at their final position. expect(controller.offset, 0.0); expect(box1.localToGlobal(Offset.zero).dy, 0.0); expect(box2.localToGlobal(Offset.zero).dy, 250.0); expect(box3.localToGlobal(Offset.zero).dy, 500.0); }); testWidgets('changing scroll direction during recede animation will not change the stretch direction', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget( buildTest(box1Key, box2Key, box3Key, controller, boxHeight: 205.0), ); 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)); // Fling to the trailing edge await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); await tester.pumpAndSettle(); expect(box1.localToGlobal(Offset.zero).dy, -15.0); expect(box2.localToGlobal(Offset.zero).dy, 190.0); expect(box3.localToGlobal(Offset.zero).dy, 395.0); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); // Overscroll to the trailing edge await gesture.moveBy(const Offset(0.0, -200.0)); await tester.pumpAndSettle(); expect(box1.localToGlobal(Offset.zero).dy, lessThan(-25.0)); expect(box2.localToGlobal(Offset.zero).dy, lessThan(185.0)); expect(box3.localToGlobal(Offset.zero).dy, lessThan(392.0)); // This will trigger the recede animation // The y offset of the boxes should be increasing, since the boxes were stretched // toward the leading edge. await gesture.moveBy(const Offset(0.0, 150.0)); await tester.pump(const Duration(milliseconds: 100)); // Explicitly check that the box1 offset is not 0.0, since this would probably mean that // the stretch direction is wrong. expect(box1.localToGlobal(Offset.zero).dy, isNot(0.0)); expect(box1.localToGlobal(Offset.zero).dy, lessThan(-12.0)); expect(box2.localToGlobal(Offset.zero).dy, lessThan(197.0)); expect(box3.localToGlobal(Offset.zero).dy, lessThan(407.0)); await tester.pump(const Duration(milliseconds: 100)); expect(box1.localToGlobal(Offset.zero).dy, lessThan(-6.0)); expect(box2.localToGlobal(Offset.zero).dy, lessThan(201.0)); expect(box3.localToGlobal(Offset.zero).dy, lessThan(408.0)); await tester.pumpAndSettle(); // The recede animation is done now, we should now be at the leading edge. expect(box1.localToGlobal(Offset.zero).dy, 0.0); expect(box2.localToGlobal(Offset.zero).dy, 205.0); expect(box3.localToGlobal(Offset.zero).dy, 410.0); await gesture.up(); }); testWidgets('Stretch overscroll only uses image filter during stretch effect', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget( buildTest( box1Key, box2Key, box3Key, controller, axis: Axis.horizontal, ) ); expect(tester.layers, isNot(contains(isA<ImageFilterLayer>()))); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); // Overscroll await gesture.moveBy(const Offset(200.0, 0.0)); await tester.pumpAndSettle(); expect(tester.layers, contains(isA<ImageFilterLayer>())); }); }