From 3ff76f47fbafe6a3f3503c2d7317da07241855dc Mon Sep 17 00:00:00 2001 From: Yuqian Li Date: Thu, 13 Aug 2020 15:01:41 -0700 Subject: [PATCH] Add clipBehavior to ListView, GridView, PageView (#63147) These widgets are missing from https://github.com/flutter/flutter/pull/59364 With this change, developers can use clipBehavior for https://github.com/flutter/flutter/issues/59424 --- .../lib/src/widgets/nested_scroll_view.dart | 4 +- .../flutter/lib/src/widgets/page_view.dart | 12 ++ .../flutter/lib/src/widgets/scroll_view.dart | 31 ++++++ .../flutter/test/widgets/grid_view_test.dart | 104 ++++++++++++++++++ .../flutter/test/widgets/list_view_test.dart | 87 +++++++++++++++ .../flutter/test/widgets/page_view_test.dart | 37 +++++++ 6 files changed, 273 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/widgets/nested_scroll_view.dart b/packages/flutter/lib/src/widgets/nested_scroll_view.dart index 31e7c4e8a6..7c1baf3fcb 100644 --- a/packages/flutter/lib/src/widgets/nested_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/nested_scroll_view.dart @@ -656,7 +656,7 @@ class _NestedScrollViewCustomScrollView extends CustomScrollView { @required ScrollController controller, @required List slivers, @required this.handle, - @required this.clipBehavior, + @required Clip clipBehavior, DragStartBehavior dragStartBehavior = DragStartBehavior.start, String restorationId, }) : super( @@ -667,10 +667,10 @@ class _NestedScrollViewCustomScrollView extends CustomScrollView { slivers: slivers, dragStartBehavior: dragStartBehavior, restorationId: restorationId, + clipBehavior: clipBehavior, ); final SliverOverlapAbsorberHandle handle; - final Clip clipBehavior; @override Widget buildViewport( diff --git a/packages/flutter/lib/src/widgets/page_view.dart b/packages/flutter/lib/src/widgets/page_view.dart index 6a9990f577..50447c2917 100644 --- a/packages/flutter/lib/src/widgets/page_view.dart +++ b/packages/flutter/lib/src/widgets/page_view.dart @@ -587,7 +587,9 @@ class PageView extends StatefulWidget { this.dragStartBehavior = DragStartBehavior.start, this.allowImplicitScrolling = false, this.restorationId, + this.clipBehavior = Clip.hardEdge, }) : assert(allowImplicitScrolling != null), + assert(clipBehavior != null), controller = controller ?? _defaultPageController, childrenDelegate = SliverChildListDelegate(children), super(key: key); @@ -623,7 +625,9 @@ class PageView extends StatefulWidget { this.dragStartBehavior = DragStartBehavior.start, this.allowImplicitScrolling = false, this.restorationId, + this.clipBehavior = Clip.hardEdge, }) : assert(allowImplicitScrolling != null), + assert(clipBehavior != null), controller = controller ?? _defaultPageController, childrenDelegate = SliverChildBuilderDelegate(itemBuilder, childCount: itemCount), super(key: key); @@ -722,8 +726,10 @@ class PageView extends StatefulWidget { this.dragStartBehavior = DragStartBehavior.start, this.allowImplicitScrolling = false, this.restorationId, + this.clipBehavior = Clip.hardEdge, }) : assert(childrenDelegate != null), assert(allowImplicitScrolling != null), + assert(clipBehavior != null), controller = controller ?? _defaultPageController, super(key: key); @@ -794,6 +800,11 @@ class PageView extends StatefulWidget { /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; + /// {@macro flutter.widgets.Clip} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + @override _PageViewState createState() => _PageViewState(); } @@ -856,6 +867,7 @@ class _PageViewState extends State { cacheExtentStyle: CacheExtentStyle.viewport, axisDirection: axisDirection, offset: position, + clipBehavior: widget.clipBehavior, slivers: [ SliverFillViewport( viewportFraction: widget.controller.viewportFraction, diff --git a/packages/flutter/lib/src/widgets/scroll_view.dart b/packages/flutter/lib/src/widgets/scroll_view.dart index 2fd269c704..f4130f357d 100644 --- a/packages/flutter/lib/src/widgets/scroll_view.dart +++ b/packages/flutter/lib/src/widgets/scroll_view.dart @@ -92,10 +92,12 @@ abstract class ScrollView extends StatelessWidget { this.dragStartBehavior = DragStartBehavior.start, this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, this.restorationId, + this.clipBehavior = Clip.hardEdge, }) : assert(scrollDirection != null), assert(reverse != null), assert(shrinkWrap != null), assert(dragStartBehavior != null), + assert(clipBehavior != null), assert(!(controller != null && primary == true), 'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. ' 'You cannot both set primary to true and pass an explicit controller.' @@ -264,6 +266,11 @@ abstract class ScrollView extends StatelessWidget { /// {@macro flutter.widgets.scrollable.restorationId} final String restorationId; + /// {@macro flutter.widgets.Clip} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + /// Returns the [AxisDirection] in which the scroll view scrolls. /// /// Combines the [scrollDirection] with the [reverse] boolean to obtain the @@ -312,6 +319,7 @@ abstract class ScrollView extends StatelessWidget { axisDirection: axisDirection, offset: offset, slivers: slivers, + clipBehavior: clipBehavior, ); } return Viewport( @@ -321,6 +329,7 @@ abstract class ScrollView extends StatelessWidget { cacheExtent: cacheExtent, center: center, anchor: anchor, + clipBehavior: clipBehavior, ); } @@ -574,6 +583,7 @@ class CustomScrollView extends ScrollView { int semanticChildCount, DragStartBehavior dragStartBehavior = DragStartBehavior.start, String restorationId, + Clip clipBehavior = Clip.hardEdge, }) : super( key: key, scrollDirection: scrollDirection, @@ -588,6 +598,7 @@ class CustomScrollView extends ScrollView { semanticChildCount: semanticChildCount, dragStartBehavior: dragStartBehavior, restorationId: restorationId, + clipBehavior: clipBehavior, ); /// The slivers to place inside the viewport. @@ -623,6 +634,7 @@ abstract class BoxScrollView extends ScrollView { DragStartBehavior dragStartBehavior = DragStartBehavior.start, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String restorationId, + Clip clipBehavior = Clip.hardEdge, }) : super( key: key, scrollDirection: scrollDirection, @@ -636,6 +648,7 @@ abstract class BoxScrollView extends ScrollView { dragStartBehavior: dragStartBehavior, keyboardDismissBehavior: keyboardDismissBehavior, restorationId: restorationId, + clipBehavior: clipBehavior, ); /// The amount of space by which to inset the children. @@ -1042,6 +1055,7 @@ class ListView extends BoxScrollView { DragStartBehavior dragStartBehavior = DragStartBehavior.start, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String restorationId, + Clip clipBehavior = Clip.hardEdge, }) : childrenDelegate = SliverChildListDelegate( children, addAutomaticKeepAlives: addAutomaticKeepAlives, @@ -1062,6 +1076,7 @@ class ListView extends BoxScrollView { dragStartBehavior: dragStartBehavior, keyboardDismissBehavior: keyboardDismissBehavior, restorationId: restorationId, + clipBehavior: clipBehavior, ); /// Creates a scrollable, linear array of widgets that are created on demand. @@ -1115,6 +1130,7 @@ class ListView extends BoxScrollView { DragStartBehavior dragStartBehavior = DragStartBehavior.start, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String restorationId, + Clip clipBehavior = Clip.hardEdge, }) : assert(itemCount == null || itemCount >= 0), assert(semanticChildCount == null || semanticChildCount <= itemCount), childrenDelegate = SliverChildBuilderDelegate( @@ -1138,6 +1154,7 @@ class ListView extends BoxScrollView { dragStartBehavior: dragStartBehavior, keyboardDismissBehavior: keyboardDismissBehavior, restorationId: restorationId, + clipBehavior: clipBehavior, ); /// Creates a fixed-length scrollable linear array of list "items" separated @@ -1206,6 +1223,7 @@ class ListView extends BoxScrollView { DragStartBehavior dragStartBehavior = DragStartBehavior.start, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String restorationId, + Clip clipBehavior = Clip.hardEdge, }) : assert(itemBuilder != null), assert(separatorBuilder != null), assert(itemCount != null && itemCount >= 0), @@ -1249,6 +1267,7 @@ class ListView extends BoxScrollView { dragStartBehavior: dragStartBehavior, keyboardDismissBehavior: keyboardDismissBehavior, restorationId: restorationId, + clipBehavior: clipBehavior, ); /// Creates a scrollable, linear array of widgets with a custom child model. @@ -1349,6 +1368,7 @@ class ListView extends BoxScrollView { DragStartBehavior dragStartBehavior = DragStartBehavior.start, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String restorationId, + Clip clipBehavior = Clip.hardEdge, }) : assert(childrenDelegate != null), super( key: key, @@ -1364,6 +1384,7 @@ class ListView extends BoxScrollView { dragStartBehavior: dragStartBehavior, keyboardDismissBehavior: keyboardDismissBehavior, restorationId: restorationId, + clipBehavior: clipBehavior, ); /// If non-null, forces the children to have the given extent in the scroll @@ -1642,6 +1663,7 @@ class GridView extends BoxScrollView { List children = const [], int semanticChildCount, DragStartBehavior dragStartBehavior = DragStartBehavior.start, + Clip clipBehavior = Clip.hardEdge, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String restorationId, }) : assert(gridDelegate != null), @@ -1665,6 +1687,7 @@ class GridView extends BoxScrollView { dragStartBehavior: dragStartBehavior, keyboardDismissBehavior: keyboardDismissBehavior, restorationId: restorationId, + clipBehavior: clipBehavior, ); /// Creates a scrollable, 2D array of widgets that are created on demand. @@ -1706,6 +1729,7 @@ class GridView extends BoxScrollView { DragStartBehavior dragStartBehavior = DragStartBehavior.start, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String restorationId, + Clip clipBehavior = Clip.hardEdge, }) : assert(gridDelegate != null), childrenDelegate = SliverChildBuilderDelegate( itemBuilder, @@ -1728,6 +1752,7 @@ class GridView extends BoxScrollView { dragStartBehavior: dragStartBehavior, keyboardDismissBehavior: keyboardDismissBehavior, restorationId: restorationId, + clipBehavior: clipBehavior, ); /// Creates a scrollable, 2D array of widgets with both a custom @@ -1753,6 +1778,7 @@ class GridView extends BoxScrollView { DragStartBehavior dragStartBehavior = DragStartBehavior.start, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String restorationId, + Clip clipBehavior = Clip.hardEdge, }) : assert(gridDelegate != null), assert(childrenDelegate != null), super( @@ -1769,6 +1795,7 @@ class GridView extends BoxScrollView { dragStartBehavior: dragStartBehavior, keyboardDismissBehavior: keyboardDismissBehavior, restorationId: restorationId, + clipBehavior: clipBehavior, ); /// Creates a scrollable, 2D array of widgets with a fixed number of tiles in @@ -1807,6 +1834,7 @@ class GridView extends BoxScrollView { DragStartBehavior dragStartBehavior = DragStartBehavior.start, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String restorationId, + Clip clipBehavior = Clip.hardEdge, }) : gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, mainAxisSpacing: mainAxisSpacing, @@ -1833,6 +1861,7 @@ class GridView extends BoxScrollView { dragStartBehavior: dragStartBehavior, keyboardDismissBehavior: keyboardDismissBehavior, restorationId: restorationId, + clipBehavior: clipBehavior, ); /// Creates a scrollable, 2D array of widgets with tiles that each have a @@ -1871,6 +1900,7 @@ class GridView extends BoxScrollView { DragStartBehavior dragStartBehavior = DragStartBehavior.start, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String restorationId, + Clip clipBehavior = Clip.hardEdge, }) : gridDelegate = SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: maxCrossAxisExtent, mainAxisSpacing: mainAxisSpacing, @@ -1897,6 +1927,7 @@ class GridView extends BoxScrollView { dragStartBehavior: dragStartBehavior, keyboardDismissBehavior: keyboardDismissBehavior, restorationId: restorationId, + clipBehavior: clipBehavior, ); /// A delegate that controls the layout of the children within the [GridView]. diff --git a/packages/flutter/test/widgets/grid_view_test.dart b/packages/flutter/test/widgets/grid_view_test.dart index 4785c58e6f..791f12f30f 100644 --- a/packages/flutter/test/widgets/grid_view_test.dart +++ b/packages/flutter/test/widgets/grid_view_test.dart @@ -6,9 +6,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; import '../rendering/mock_canvas.dart'; +import '../rendering/rendering_tester.dart'; import 'states.dart'; void main() { @@ -602,4 +604,106 @@ void main() { expect(find.byKey(const ValueKey(4)), findsOneWidget); expect(counters[4], 2); }); + + testWidgets('GridView respects clipBehavior', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GridView( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), + children: [Container(height: 2000.0)], + ), + ), + ); + + // 1st, check that the render object has received the default clip behavior. + final RenderViewport renderObject = tester.allRenderObjects.whereType().first; + expect(renderObject.clipBehavior, equals(Clip.hardEdge)); + + // 2nd, check that the painting context has received the default clip behavior. + final TestClipPaintingContext context = TestClipPaintingContext(); + renderObject.paint(context, Offset.zero); + expect(context.clipBehavior, equals(Clip.hardEdge)); + + // 3rd, pump a new widget to check that the render object can update its clip behavior. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GridView( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), + children: [Container(height: 2000.0)], + clipBehavior: Clip.antiAlias, + ), + ), + ); + expect(renderObject.clipBehavior, equals(Clip.antiAlias)); + + // 4th, check that a non-default clip behavior can be sent to the painting context. + renderObject.paint(context, Offset.zero); + expect(context.clipBehavior, equals(Clip.antiAlias)); + }); + + testWidgets('GridView.builder respects clipBehavior', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), + itemCount: 10, + itemBuilder: (BuildContext _, int __) => Container(height: 2000.0), + clipBehavior: Clip.antiAlias, + ), + ), + ); + final RenderViewport renderObject = tester.allRenderObjects.whereType().first; + expect(renderObject.clipBehavior, equals(Clip.antiAlias)); + }); + + testWidgets('GridView.custom respects clipBehavior', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GridView.custom( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), + childrenDelegate: SliverChildBuilderDelegate( + (BuildContext context, int index) => Container(height: 2000.0), + childCount: 1, + ), + clipBehavior: Clip.antiAlias, + ), + ), + ); + final RenderViewport renderObject = tester.allRenderObjects.whereType().first; + expect(renderObject.clipBehavior, equals(Clip.antiAlias)); + }); + + testWidgets('GridView.count respects clipBehavior', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GridView.count( + crossAxisCount: 3, + children: [Container(height: 2000.0)], + clipBehavior: Clip.antiAlias, + ), + ), + ); + final RenderViewport renderObject = tester.allRenderObjects.whereType().first; + expect(renderObject.clipBehavior, equals(Clip.antiAlias)); + }); + + testWidgets('GridView.extent respects clipBehavior', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GridView.extent( + maxCrossAxisExtent: 1000, + children: [Container(height: 2000.0)], + clipBehavior: Clip.antiAlias, + ), + ), + ); + final RenderViewport renderObject = tester.allRenderObjects.whereType().first; + expect(renderObject.clipBehavior, equals(Clip.antiAlias)); + }); } diff --git a/packages/flutter/test/widgets/list_view_test.dart b/packages/flutter/test/widgets/list_view_test.dart index 3fa9395aef..cae80c3c7d 100644 --- a/packages/flutter/test/widgets/list_view_test.dart +++ b/packages/flutter/test/widgets/list_view_test.dart @@ -4,10 +4,13 @@ // @dart = 2.8 +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import '../rendering/mock_canvas.dart'; +import '../rendering/rendering_tester.dart'; class TestSliverChildListDelegate extends SliverChildListDelegate { TestSliverChildListDelegate(List children) : super(children); @@ -575,4 +578,88 @@ void main() { await tester.pumpWidget(buildListView(scrollDirection: Axis.horizontal)); expect(controller.position.viewportDimension, 100.0); }); + + testWidgets('ListView respects clipBehavior', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ListView( + children: [Container(height: 2000.0)], + ), + ), + ); + + // 1st, check that the render object has received the default clip behavior. + final RenderViewport renderObject = tester.allRenderObjects.whereType().first; + expect(renderObject.clipBehavior, equals(Clip.hardEdge)); + + // 2nd, check that the painting context has received the default clip behavior. + final TestClipPaintingContext context = TestClipPaintingContext(); + renderObject.paint(context, Offset.zero); + expect(context.clipBehavior, equals(Clip.hardEdge)); + + // 3rd, pump a new widget to check that the render object can update its clip behavior. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ListView( + children: [Container(height: 2000.0)], + clipBehavior: Clip.antiAlias, + ), + ), + ); + expect(renderObject.clipBehavior, equals(Clip.antiAlias)); + + // 4th, check that a non-default clip behavior can be sent to the painting context. + renderObject.paint(context, Offset.zero); + expect(context.clipBehavior, equals(Clip.antiAlias)); + }); + + testWidgets('ListView.builder respects clipBehavior', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ListView.builder( + itemCount: 10, + itemBuilder: (BuildContext _, int __) => Container(height: 2000.0), + clipBehavior: Clip.antiAlias, + ), + ), + ); + final RenderViewport renderObject = tester.allRenderObjects.whereType().first; + expect(renderObject.clipBehavior, equals(Clip.antiAlias)); + }); + + testWidgets('ListView.custom respects clipBehavior', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ListView.custom( + childrenDelegate: SliverChildBuilderDelegate( + (BuildContext context, int index) => Container(height: 2000.0), + childCount: 1, + ), + clipBehavior: Clip.antiAlias, + ), + ), + ); + final RenderViewport renderObject = tester.allRenderObjects.whereType().first; + expect(renderObject.clipBehavior, equals(Clip.antiAlias)); + }); + + testWidgets('ListView.separated respects clipBehavior', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ListView.separated( + itemCount: 10, + itemBuilder: (BuildContext _, int __) => Container(height: 2000.0), + separatorBuilder: (BuildContext _, int __) => const Divider(), + clipBehavior: Clip.antiAlias, + ), + ), + ); + final RenderViewport renderObject = tester.allRenderObjects.whereType().first; + expect(renderObject.clipBehavior, equals(Clip.antiAlias)); + }); } diff --git a/packages/flutter/test/widgets/page_view_test.dart b/packages/flutter/test/widgets/page_view_test.dart index 6149f6a9f1..d3a3ce4877 100644 --- a/packages/flutter/test/widgets/page_view_test.dart +++ b/packages/flutter/test/widgets/page_view_test.dart @@ -10,6 +10,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; +import '../rendering/rendering_tester.dart'; import 'semantics_tester.dart'; import 'states.dart'; @@ -951,4 +952,40 @@ void main() { semantics.dispose(); }); + + testWidgets('PageView respects clipBehavior', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: PageView( + children: [Container(height: 2000.0)], + ), + ), + ); + + // 1st, check that the render object has received the default clip behavior. + final RenderViewport renderObject = tester.allRenderObjects.whereType().first; + expect(renderObject.clipBehavior, equals(Clip.hardEdge)); + + // 2nd, check that the painting context has received the default clip behavior. + final TestClipPaintingContext context = TestClipPaintingContext(); + renderObject.paint(context, Offset.zero); + expect(context.clipBehavior, equals(Clip.hardEdge)); + + // 3rd, pump a new widget to check that the render object can update its clip behavior. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: PageView( + children: [Container(height: 2000.0)], + clipBehavior: Clip.antiAlias, + ), + ), + ); + expect(renderObject.clipBehavior, equals(Clip.antiAlias)); + + // 4th, check that a non-default clip behavior can be sent to the painting context. + renderObject.paint(context, Offset.zero); + expect(context.clipBehavior, equals(Clip.antiAlias)); + }); }