| // 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. |
| |
| import 'dart:math' as math; |
| |
| import 'package:flutter/widgets.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../rendering/mock_canvas.dart'; |
| |
| final Matcher doesNotOverscroll = isNot(paints..circle()); |
| |
| Future<void> slowDrag(WidgetTester tester, Offset start, Offset offset) async { |
| final TestGesture gesture = await tester.startGesture(start); |
| for (int index = 0; index < 10; index += 1) { |
| await gesture.moveBy(offset); |
| await tester.pump(const Duration(milliseconds: 20)); |
| } |
| await gesture.up(); |
| } |
| |
| void main() { |
| testWidgets('Overscroll indicator color', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| slivers: <Widget>[ |
| SliverToBoxAdapter(child: SizedBox(height: 2000.0)), |
| ], |
| ), |
| ), |
| ); |
| final RenderObject painter = tester.renderObject(find.byType(CustomPaint)); |
| |
| expect(painter, doesNotOverscroll); |
| |
| // the scroll gesture from tester.scroll happens in zero time, so nothing should appear: |
| await tester.drag(find.byType(Scrollable), const Offset(0.0, 100.0)); |
| expect(painter, doesNotOverscroll); |
| await tester.pump(); // allow the ticker to register itself |
| expect(painter, doesNotOverscroll); |
| await tester.pump(const Duration(milliseconds: 100)); // animate |
| expect(painter, doesNotOverscroll); |
| |
| final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0)); |
| await tester.pump(const Duration(milliseconds: 100)); // animate |
| expect(painter, doesNotOverscroll); |
| await gesture.up(); |
| expect(painter, doesNotOverscroll); |
| |
| await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0)); |
| expect(painter, paints..circle(color: const Color(0x0DFFFFFF))); |
| |
| await tester.pumpAndSettle(const Duration(seconds: 1)); |
| expect(painter, doesNotOverscroll); |
| }); |
| |
| testWidgets('Nested scrollable', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: GlowingOverscrollIndicator( |
| axisDirection: AxisDirection.down, |
| color: const Color(0x0DFFFFFF), |
| notificationPredicate: (ScrollNotification notification) => notification.depth == 1, |
| child: const SingleChildScrollView( |
| scrollDirection: Axis.horizontal, |
| child: SizedBox( |
| width: 600.0, |
| child: CustomScrollView( |
| slivers: <Widget>[ |
| SliverToBoxAdapter(child: SizedBox(height: 2000.0)), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderObject outerPainter = tester.renderObject(find.byType(CustomPaint).first); |
| final RenderObject innerPainter = tester.renderObject(find.byType(CustomPaint).last); |
| |
| await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0)); |
| expect(outerPainter, paints..circle()); |
| expect(innerPainter, paints..circle()); |
| }); |
| |
| testWidgets('Overscroll indicator changes side when you drag on the other side', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| slivers: <Widget>[ |
| SliverToBoxAdapter(child: SizedBox(height: 2000.0)), |
| ], |
| ), |
| ), |
| ); |
| final RenderObject painter = tester.renderObject(find.byType(CustomPaint)); |
| |
| await slowDrag(tester, const Offset(400.0, 200.0), const Offset(0.0, 10.0)); |
| expect(painter, paints..circle(x: 400.0)); |
| await slowDrag(tester, const Offset(100.0, 200.0), const Offset(0.0, 10.0)); |
| expect(painter, paints..something((Symbol method, List<dynamic> arguments) { |
| if (method != #drawCircle) |
| return false; |
| final Offset center = arguments[0] as Offset; |
| if (center.dx < 400.0) |
| return true; |
| throw 'Dragging on left hand side did not overscroll on left hand side.'; |
| })); |
| await slowDrag(tester, const Offset(700.0, 200.0), const Offset(0.0, 10.0)); |
| expect(painter, paints..something((Symbol method, List<dynamic> arguments) { |
| if (method != #drawCircle) |
| return false; |
| final Offset center = arguments[0] as Offset; |
| if (center.dx > 400.0) |
| return true; |
| throw 'Dragging on right hand side did not overscroll on right hand side.'; |
| })); |
| |
| await tester.pumpAndSettle(const Duration(seconds: 1)); |
| expect(painter, doesNotOverscroll); |
| }); |
| |
| testWidgets('Overscroll indicator changes side when you shift sides', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| slivers: <Widget>[ |
| SliverToBoxAdapter(child: SizedBox(height: 2000.0)), |
| ], |
| ), |
| ), |
| ); |
| final RenderObject painter = tester.renderObject(find.byType(CustomPaint)); |
| final TestGesture gesture = await tester.startGesture(const Offset(300.0, 200.0)); |
| await gesture.moveBy(const Offset(0.0, 10.0)); |
| await tester.pump(const Duration(milliseconds: 20)); |
| double oldX = 0.0; |
| for (int index = 0; index < 10; index += 1) { |
| await gesture.moveBy(const Offset(50.0, 50.0)); |
| await tester.pump(const Duration(milliseconds: 20)); |
| expect(painter, paints..something((Symbol method, List<dynamic> arguments) { |
| if (method != #drawCircle) |
| return false; |
| final Offset center = arguments[0] as Offset; |
| if (center.dx <= oldX) |
| throw 'Sliding to the right did not make the center of the radius slide to the right.'; |
| oldX = center.dx; |
| return true; |
| })); |
| } |
| await gesture.up(); |
| |
| await tester.pumpAndSettle(const Duration(seconds: 1)); |
| expect(painter, doesNotOverscroll); |
| }); |
| |
| group("Flipping direction of scrollable doesn't change overscroll behavior", () { |
| testWidgets('down', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| physics: AlwaysScrollableScrollPhysics(), |
| slivers: <Widget>[ |
| SliverToBoxAdapter(child: SizedBox(height: 20.0)), |
| ], |
| ), |
| ), |
| ); |
| final RenderObject painter = tester.renderObject(find.byType(CustomPaint)); |
| await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0)); |
| expect(painter, paints..save()..circle()..restore()..save()..scale(y: -1.0)..restore()..restore()); |
| |
| await tester.pumpAndSettle(const Duration(seconds: 1)); |
| expect(painter, doesNotOverscroll); |
| }); |
| |
| testWidgets('up', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| reverse: true, |
| physics: AlwaysScrollableScrollPhysics(), |
| slivers: <Widget>[ |
| SliverToBoxAdapter(child: SizedBox(height: 20.0)), |
| ], |
| ), |
| ), |
| ); |
| final RenderObject painter = tester.renderObject(find.byType(CustomPaint)); |
| await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0)); |
| expect(painter, paints..save()..scale(y: -1.0)..restore()..save()..circle()..restore()..restore()); |
| |
| await tester.pumpAndSettle(const Duration(seconds: 1)); |
| expect(painter, doesNotOverscroll); |
| }); |
| }); |
| |
| testWidgets('Overscroll in both directions', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| physics: AlwaysScrollableScrollPhysics(), |
| slivers: <Widget>[ |
| SliverToBoxAdapter(child: SizedBox(height: 20.0)), |
| ], |
| ), |
| ), |
| ); |
| final RenderObject painter = tester.renderObject(find.byType(CustomPaint)); |
| await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0)); |
| expect(painter, paints..circle()); |
| expect(painter, isNot(paints..circle()..circle())); |
| await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, -5.0)); |
| expect(painter, paints..circle()..circle()); |
| |
| await tester.pumpAndSettle(const Duration(seconds: 1)); |
| expect(painter, doesNotOverscroll); |
| }); |
| |
| testWidgets('Overscroll horizontally', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| scrollDirection: Axis.horizontal, |
| physics: AlwaysScrollableScrollPhysics(), |
| slivers: <Widget>[ |
| SliverToBoxAdapter(child: SizedBox(height: 20.0)), |
| ], |
| ), |
| ), |
| ); |
| final RenderObject painter = tester.renderObject(find.byType(CustomPaint)); |
| await slowDrag(tester, const Offset(200.0, 200.0), const Offset(5.0, 0.0)); |
| expect(painter, paints..rotate(angle: math.pi / 2.0)..circle()..saveRestore()); |
| expect(painter, isNot(paints..circle()..circle())); |
| await slowDrag(tester, const Offset(200.0, 200.0), const Offset(-5.0, 0.0)); |
| expect( |
| painter, |
| paints |
| ..rotate(angle: math.pi / 2.0) |
| ..circle() |
| ..rotate(angle: math.pi / 2.0) |
| ..circle(), |
| ); |
| |
| await tester.pumpAndSettle(const Duration(seconds: 1)); |
| expect(painter, doesNotOverscroll); |
| }); |
| |
| testWidgets('Nested overscrolls do not throw exceptions', (WidgetTester tester) async { |
| await tester.pumpWidget(Directionality( |
| textDirection: TextDirection.ltr, |
| child: PageView( |
| children: <Widget>[ |
| ListView( |
| children: <Widget>[ |
| Container( |
| width: 2000.0, |
| height: 2000.0, |
| color: const Color(0xFF00FF00), |
| ), |
| ], |
| ), |
| ], |
| ), |
| )); |
| |
| await tester.dragFrom(const Offset(100.0, 100.0), const Offset(0.0, 2000.0)); |
| await tester.pumpAndSettle(); |
| }); |
| |
| testWidgets('Changing settings', (WidgetTester tester) async { |
| RenderObject painter; |
| |
| await tester.pumpWidget( |
| const Directionality( |
| textDirection: TextDirection.ltr, |
| child: ScrollConfiguration( |
| behavior: TestScrollBehavior1(), |
| child: CustomScrollView( |
| scrollDirection: Axis.horizontal, |
| physics: AlwaysScrollableScrollPhysics(), |
| reverse: true, |
| slivers: <Widget>[ |
| SliverToBoxAdapter(child: SizedBox(height: 20.0)), |
| ], |
| ), |
| ), |
| ), |
| ); |
| painter = tester.renderObject(find.byType(CustomPaint)); |
| await slowDrag(tester, const Offset(200.0, 200.0), const Offset(5.0, 0.0)); |
| expect(painter, paints..rotate(angle: math.pi / 2.0)..circle(color: const Color(0x0A00FF00))); |
| expect(painter, isNot(paints..circle()..circle())); |
| |
| await tester.pumpAndSettle(const Duration(seconds: 1)); |
| await tester.pumpWidget( |
| const Directionality( |
| textDirection: TextDirection.ltr, |
| child: ScrollConfiguration( |
| behavior: TestScrollBehavior2(), |
| child: CustomScrollView( |
| scrollDirection: Axis.horizontal, |
| physics: AlwaysScrollableScrollPhysics(), |
| slivers: <Widget>[ |
| SliverToBoxAdapter(child: SizedBox(height: 20.0)), |
| ], |
| ), |
| ), |
| ), |
| ); |
| painter = tester.renderObject(find.byType(CustomPaint)); |
| await slowDrag(tester, const Offset(200.0, 200.0), const Offset(5.0, 0.0)); |
| expect(painter, paints..rotate(angle: math.pi / 2.0)..circle(color: const Color(0x0A0000FF))..saveRestore()); |
| expect(painter, isNot(paints..circle()..circle())); |
| }); |
| |
| testWidgets('CustomScrollView overscroll indicator works if there is sliver before center', (WidgetTester tester) async { |
| final Key centerKey = UniqueKey(); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: ScrollConfiguration( |
| behavior: const TestScrollBehavior2(), |
| child: CustomScrollView( |
| center: centerKey, |
| physics: const AlwaysScrollableScrollPhysics(), |
| slivers: <Widget>[ |
| SliverList( |
| delegate: SliverChildBuilderDelegate( |
| (BuildContext context, int index) => Text('First sliver $index'), |
| childCount: 2, |
| ), |
| ), |
| SliverList( |
| key: centerKey, |
| delegate: SliverChildBuilderDelegate( |
| (BuildContext context, int index) => Text('Second sliver $index'), |
| childCount: 5, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('First sliver 1'), findsNothing); |
| |
| await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 300.0)); |
| expect(find.text('First sliver 1'), findsOneWidget); |
| final RenderObject painter = tester.renderObject(find.byType(CustomPaint)); |
| // The scroll offset and paint extend should cancel out each other. |
| expect(painter, paints..save()..translate(y: 0.0)..scale()..circle()); |
| }); |
| |
| testWidgets('CustomScrollView overscroll indicator works well with [CustomScrollView.center] and [OverscrollIndicatorNotification.paintOffset]', (WidgetTester tester) async { |
| final Key centerKey = UniqueKey(); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: ScrollConfiguration( |
| behavior: const TestScrollBehavior2(), |
| child: NotificationListener<OverscrollIndicatorNotification>( |
| onNotification: (OverscrollIndicatorNotification notification) { |
| if (notification.leading) { |
| notification.paintOffset = 50.0; |
| } |
| return false; |
| }, |
| child: CustomScrollView( |
| center: centerKey, |
| physics: const AlwaysScrollableScrollPhysics(), |
| slivers: <Widget>[ |
| SliverList( |
| delegate: SliverChildBuilderDelegate( |
| (BuildContext context, int index) => Text('First sliver $index'), |
| childCount: 2, |
| ), |
| ), |
| SliverList( |
| key: centerKey, |
| delegate: SliverChildBuilderDelegate( |
| (BuildContext context, int index) => Text('Second sliver $index'), |
| childCount: 5, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('First sliver 1'), findsNothing); |
| |
| await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0)); // offset will be magnified ten times |
| expect(find.text('First sliver 1'), findsOneWidget); |
| final RenderObject painter = tester.renderObject(find.byType(CustomPaint)); |
| // The OverscrollIndicator should respect the [OverscrollIndicatorNotification.paintOffset] setting. |
| expect(painter, paints..save()..translate(y: 50.0)..scale()..circle()); |
| }); |
| |
| testWidgets('The OverscrollIndicator should not overflow the scrollable view edge', (WidgetTester tester) async { |
| // Regressing test for https://github.com/flutter/flutter/issues/64149 |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: NotificationListener<OverscrollIndicatorNotification>( |
| onNotification: (OverscrollIndicatorNotification notification) { |
| notification.paintOffset = 50.0; // both the leading and trailing indicator have a 50.0 pixels offset. |
| return false; |
| }, |
| child: const CustomScrollView( |
| slivers: <Widget>[ |
| SliverToBoxAdapter(child: SizedBox(height: 2000.0)), |
| ], |
| ), |
| ), |
| ), |
| ); |
| final RenderObject painter = tester.renderObject(find.byType(CustomPaint)); |
| await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0)); |
| expect(painter, paints..save()..translate(y: 50.0)..scale()..circle()); |
| // Reverse scroll (30 pixels), and the offset < notification.paintOffset. |
| await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, -30.0)); |
| await tester.pump(); |
| // The OverscrollIndicator should move with the CustomScrollView. |
| expect(painter, paints..save()..translate(y: 50.0 - 30.0)..scale()..circle()); |
| |
| // Reverse scroll (30+20 pixels) and offset == notification.paintOffset. |
| await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, -20.0)); |
| await tester.pump(); |
| expect(painter, paints..save()..translate(y: 50.0 - 50.0)..scale()..circle()); |
| |
| // Reverse scroll (30+20+10 pixels) and offset > notification.paintOffset. |
| await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, -10.0)); |
| await tester.pump(); |
| // The OverscrollIndicator should not overflow the CustomScrollView's edge. |
| expect(painter, paints..save()..translate(y: 50.0 - 50.0)..scale()..circle()); |
| |
| await tester.pumpAndSettle(); // Finish the leading indicator. |
| |
| // trigger the trailing indicator |
| await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, -200.0)); |
| expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0)..scale()..circle()); |
| |
| // Reverse scroll (30 pixels), and the offset < notification.paintOffset. |
| await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, 30.0)); |
| await tester.pump(); |
| // The OverscrollIndicator should move with the CustomScrollView. |
| expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0 - 30.0)..scale()..circle()); |
| |
| // Reverse scroll (30+20 pixels) and offset == notification.paintOffset. |
| await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, 20.0)); |
| await tester.pump(); |
| expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0 - 50.0)..scale()..circle()); |
| |
| // Reverse scroll (30+20+10 pixels) and offset > notification.paintOffset. |
| await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, 10.0)); |
| await tester.pump(); |
| // The OverscrollIndicator should not overflow the CustomScrollView's edge. |
| expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0 - 50.0)..scale()..circle()); |
| }); |
| |
| group('[OverscrollIndicatorNotification.paintOffset] test', () { |
| testWidgets('Leading', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: NotificationListener<OverscrollIndicatorNotification>( |
| onNotification: (OverscrollIndicatorNotification notification) { |
| if (notification.leading) { |
| notification.paintOffset = 50.0; |
| } |
| return false; |
| }, |
| child: const CustomScrollView( |
| slivers: <Widget>[ |
| SliverToBoxAdapter(child: SizedBox(height: 2000.0)), |
| ], |
| ), |
| ), |
| ), |
| ); |
| final RenderObject painter = tester.renderObject(find.byType(CustomPaint)); |
| await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0)); |
| // The OverscrollIndicator should respect the [OverscrollIndicatorNotification.paintOffset] setting. |
| expect(painter, paints..save()..translate(y: 50.0)..scale()..circle()); |
| // Reverse scroll direction. |
| await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, -30.0)); |
| await tester.pump(); |
| // The OverscrollIndicator should move with the CustomScrollView. |
| expect(painter, paints..save()..translate(y: 50.0 - 30.0)..scale()..circle()); |
| }); |
| |
| testWidgets('Trailing', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: NotificationListener<OverscrollIndicatorNotification>( |
| onNotification: (OverscrollIndicatorNotification notification) { |
| if (!notification.leading) { |
| notification.paintOffset = 50.0; |
| } |
| return false; |
| }, |
| child: const CustomScrollView( |
| slivers: <Widget>[ |
| SliverToBoxAdapter(child: SizedBox(height: 2000.0)), |
| ], |
| ), |
| ), |
| ), |
| ); |
| final RenderObject painter = tester.renderObject(find.byType(CustomPaint)); |
| await tester.dragFrom(const Offset(200.0, 200.0), const Offset(200.0, -10000.0)); |
| await tester.pump(); |
| await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, -5.0)); |
| // The OverscrollIndicator should respect the [OverscrollIndicatorNotification.paintOffset] setting. |
| expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0)..scale()..circle()); |
| // Reverse scroll direction. |
| await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, 30.0)); |
| await tester.pump(); |
| // The OverscrollIndicator should move with the CustomScrollView. |
| expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0 - 30.0)..scale()..circle()); |
| }); |
| }); |
| } |
| |
| class TestScrollBehavior1 extends ScrollBehavior { |
| const TestScrollBehavior1(); |
| |
| @override |
| Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) { |
| return GlowingOverscrollIndicator( |
| axisDirection: details.direction, |
| color: const Color(0xFF00FF00), |
| child: child, |
| ); |
| } |
| } |
| |
| class TestScrollBehavior2 extends ScrollBehavior { |
| const TestScrollBehavior2(); |
| |
| @override |
| Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) { |
| return GlowingOverscrollIndicator( |
| axisDirection: details.direction, |
| color: const Color(0xFF0000FF), |
| child: child, |
| ); |
| } |
| } |