From f9905fc43c5d3207a36d7647604b6d7f8dfa90eb Mon Sep 17 00:00:00 2001 From: Viren Khatri Date: Tue, 4 May 2021 00:13:18 +0530 Subject: [PATCH] prototypeItem added to ReorderableList and ReorderableListView (#81604) prototypeItem added to ReorderableList and ReorderableListView along with tests --- .../lib/src/material/reorderable_list.dart | 14 ++++ .../lib/src/widgets/reorderable_list.dart | 36 ++++++-- .../flutter/lib/src/widgets/scroll_view.dart | 4 +- .../test/material/reorderable_list_test.dart | 77 +++++++++++++++-- .../test/widgets/reorderable_list_test.dart | 82 +++++++++++++++++++ .../test/widgets/scroll_view_test.dart | 43 +++++++++- 6 files changed, 242 insertions(+), 14 deletions(-) diff --git a/packages/flutter/lib/src/material/reorderable_list.dart b/packages/flutter/lib/src/material/reorderable_list.dart index 0403b61377..e1f168e306 100644 --- a/packages/flutter/lib/src/material/reorderable_list.dart +++ b/packages/flutter/lib/src/material/reorderable_list.dart @@ -77,6 +77,7 @@ class ReorderableListView extends StatefulWidget { required List children, required this.onReorder, this.itemExtent, + this.prototypeItem, this.proxyDecorator, this.buildDefaultDragHandles = true, this.padding, @@ -96,6 +97,10 @@ class ReorderableListView extends StatefulWidget { }) : assert(scrollDirection != null), assert(onReorder != null), assert(children != null), + assert( + itemExtent == null || prototypeItem == null, + 'You can only pass itemExtent or prototypeItem, not both', + ), assert( children.every((Widget w) => w.key != null), 'All children of this widget must have a key.', @@ -170,6 +175,7 @@ class ReorderableListView extends StatefulWidget { required this.itemCount, required this.onReorder, this.itemExtent, + this.prototypeItem, this.proxyDecorator, this.buildDefaultDragHandles = true, this.padding, @@ -189,6 +195,10 @@ class ReorderableListView extends StatefulWidget { }) : assert(scrollDirection != null), assert(itemCount >= 0), assert(onReorder != null), + assert( + itemExtent == null || prototypeItem == null, + 'You can only pass itemExtent or prototypeItem, not both', + ), assert(buildDefaultDragHandles != null), super(key: key); @@ -328,6 +338,9 @@ class ReorderableListView extends StatefulWidget { /// {@macro flutter.widgets.list_view.itemExtent} final double? itemExtent; + /// {@macro flutter.widgets.list_view.prototypeItem} + final Widget? prototypeItem; + @override _ReorderableListViewState createState() => _ReorderableListViewState(); } @@ -551,6 +564,7 @@ class _ReorderableListViewState extends State { sliver: SliverReorderableList( itemBuilder: _itemBuilder, itemExtent: widget.itemExtent, + prototypeItem: widget.prototypeItem, itemCount: widget.itemCount, onReorder: widget.onReorder, proxyDecorator: widget.proxyDecorator ?? _proxyDecorator, diff --git a/packages/flutter/lib/src/widgets/reorderable_list.dart b/packages/flutter/lib/src/widgets/reorderable_list.dart index dd8428c1ca..f1bddac7d9 100644 --- a/packages/flutter/lib/src/widgets/reorderable_list.dart +++ b/packages/flutter/lib/src/widgets/reorderable_list.dart @@ -18,6 +18,7 @@ import 'scroll_position.dart'; import 'scroll_view.dart'; import 'scrollable.dart'; import 'sliver.dart'; +import 'sliver_prototype_extent_list.dart'; import 'ticker_provider.dart'; import 'transitions.dart'; @@ -113,6 +114,7 @@ class ReorderableList extends StatefulWidget { required this.itemCount, required this.onReorder, this.itemExtent, + this.prototypeItem, this.proxyDecorator, this.padding, this.scrollDirection = Axis.vertical, @@ -128,6 +130,10 @@ class ReorderableList extends StatefulWidget { this.restorationId, this.clipBehavior = Clip.hardEdge, }) : assert(itemCount >= 0), + assert( + itemExtent == null || prototypeItem == null, + 'You can only pass itemExtent or prototypeItem, not both', + ), super(key: key); /// {@template flutter.widgets.reorderable_list.itemBuilder} @@ -215,6 +221,9 @@ class ReorderableList extends StatefulWidget { /// {@macro flutter.widgets.list_view.itemExtent} final double? itemExtent; + /// {@macro flutter.widgets.list_view.prototypeItem} + final Widget? prototypeItem; + /// The state from the closest instance of this class that encloses the given /// context. /// @@ -342,6 +351,7 @@ class ReorderableListState extends State { sliver: SliverReorderableList( key: _sliverReorderableListKey, itemExtent: widget.itemExtent, + prototypeItem: widget.prototypeItem, itemBuilder: widget.itemBuilder, itemCount: widget.itemCount, onReorder: widget.onReorder, @@ -386,8 +396,13 @@ class SliverReorderableList extends StatefulWidget { required this.itemCount, required this.onReorder, this.itemExtent, + this.prototypeItem, this.proxyDecorator, }) : assert(itemCount >= 0), + assert( + itemExtent == null || prototypeItem == null, + 'You can only pass itemExtent or prototypeItem, not both', + ), super(key: key); /// {@macro flutter.widgets.reorderable_list.itemBuilder} @@ -405,6 +420,9 @@ class SliverReorderableList extends StatefulWidget { /// {@macro flutter.widgets.list_view.itemExtent} final double? itemExtent; + /// {@macro flutter.widgets.list_view.prototypeItem} + final Widget? prototypeItem; + @override SliverReorderableListState createState() => SliverReorderableListState(); @@ -841,12 +859,18 @@ class SliverReorderableListState extends State with Ticke // list extent stable we add a dummy entry to the end. childCount: widget.itemCount + (_dragInfo != null ? 1 : 0), ); - return widget.itemExtent != null - ? SliverFixedExtentList( - itemExtent: widget.itemExtent!, - delegate: childrenDelegate, - ) - : SliverList(delegate: childrenDelegate); + if (widget.itemExtent != null) { + return SliverFixedExtentList( + delegate: childrenDelegate, + itemExtent: widget.itemExtent!, + ); + } else if (widget.prototypeItem != null) { + return SliverPrototypeExtentList( + delegate: childrenDelegate, + prototypeItem: widget.prototypeItem!, + ); + } + return SliverList(delegate: childrenDelegate); } } diff --git a/packages/flutter/lib/src/widgets/scroll_view.dart b/packages/flutter/lib/src/widgets/scroll_view.dart index 5c4e64e366..5196fc742e 100644 --- a/packages/flutter/lib/src/widgets/scroll_view.dart +++ b/packages/flutter/lib/src/widgets/scroll_view.dart @@ -1490,11 +1490,12 @@ class ListView extends BoxScrollView { /// * [SliverFixedExtentList], the sliver used internally when this property /// is provided. It constrains its box children to have a specific given /// extent along the main axis. - /// {@endtemplate} /// * The [prototypeItem] property, which allows forcing the children's /// extent to be the same as the given widget. + /// {@endtemplate} final double? itemExtent; + /// {@template flutter.widgets.list_view.prototypeItem} /// If non-null, forces the children to have the same extent as the given /// widget in the scroll direction. /// @@ -1510,6 +1511,7 @@ class ListView extends BoxScrollView { /// extent as a prototype item along the main axis. /// * The [itemExtent] property, which allows forcing the children's extent /// to a given value. + /// {@endtemplate} final Widget? prototypeItem; /// A delegate that provides the children for the [ListView]. diff --git a/packages/flutter/test/material/reorderable_list_test.dart b/packages/flutter/test/material/reorderable_list_test.dart index 90a42f1f3b..133f52e348 100644 --- a/packages/flutter/test/material/reorderable_list_test.dart +++ b/packages/flutter/test/material/reorderable_list_test.dart @@ -1506,6 +1506,35 @@ void main() { expect(exception.toString(), contains('ReorderableListView widgets require an Overlay widget ancestor')); }); + testWidgets('ReorderableListView asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { + expect(() => ReorderableListView( + children: const [], + itemExtent: 30, + prototypeItem: const SizedBox(), + onReorder: (int fromIndex, int toIndex) { }, + ), throwsAssertionError); + }); + + testWidgets('ReorderableListView.builder asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { + final List numbers = [0,1,2]; + expect(() => ReorderableListView.builder( + itemBuilder: (BuildContext context, int index) { + return SizedBox( + key: ValueKey(numbers[index]), + height: 20 + numbers[index] * 10, + child: ReorderableDragStartListener( + index: index, + child: Text(numbers[index].toString()), + ) + ); + }, + itemCount: numbers.length, + itemExtent: 30, + prototypeItem: const SizedBox(), + onReorder: (int fromIndex, int toIndex) { }, + ), throwsAssertionError); + }); + testWidgets('if itemExtent is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { final List numbers = [0,1,2]; @@ -1528,13 +1557,49 @@ void main() { }, itemCount: numbers.length, itemExtent: 30, - onReorder: (int fromIndex, int toIndex) { - if (fromIndex < toIndex) { - toIndex--; - } - final int value = numbers.removeAt(fromIndex); - numbers.insert(toIndex, value); + onReorder: (int fromIndex, int toIndex) { }, + ); + }, + ), + ), + ) + ); + + final double item0Height = tester.getSize(find.text('0').hitTestable()).height; + final double item1Height = tester.getSize(find.text('1').hitTestable()).height; + final double item2Height = tester.getSize(find.text('2').hitTestable()).height; + + expect(item0Height, 30.0); + expect(item1Height, 30.0); + expect(item2Height, 30.0); + }); + + testWidgets('if prototypeItem is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { + final List numbers = [0,1,2]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return ReorderableListView.builder( + itemBuilder: (BuildContext context, int index) { + return SizedBox( + key: ValueKey(numbers[index]), + // children with different heights + height: 20 + numbers[index] * 10, + child: ReorderableDragStartListener( + index: index, + child: Text(numbers[index].toString()), + ) + ); }, + itemCount: numbers.length, + prototypeItem: const SizedBox( + height: 30, + child: Text('3'), + ), + onReorder: (int oldIndex, int newIndex) { }, ); }, ), diff --git a/packages/flutter/test/widgets/reorderable_list_test.dart b/packages/flutter/test/widgets/reorderable_list_test.dart index 02a9cb8ad8..0808bbd269 100644 --- a/packages/flutter/test/widgets/reorderable_list_test.dart +++ b/packages/flutter/test/widgets/reorderable_list_test.dart @@ -268,6 +268,46 @@ void main() { expect(getItemFadeTransition(), findsNothing); }); + testWidgets('ReorderableList asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { + final List numbers = [0,1,2]; + expect(() => ReorderableList( + itemBuilder: (BuildContext context, int index) { + return SizedBox( + key: ValueKey(numbers[index]), + height: 20 + numbers[index] * 10, + child: ReorderableDragStartListener( + index: index, + child: Text(numbers[index].toString()), + ) + ); + }, + itemCount: numbers.length, + itemExtent: 30, + prototypeItem: const SizedBox(), + onReorder: (int fromIndex, int toIndex) { }, + ), throwsAssertionError); + }); + + testWidgets('SliverReorderableList asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { + final List numbers = [0,1,2]; + expect(() => SliverReorderableList( + itemBuilder: (BuildContext context, int index) { + return SizedBox( + key: ValueKey(numbers[index]), + height: 20 + numbers[index] * 10, + child: ReorderableDragStartListener( + index: index, + child: Text(numbers[index].toString()), + ) + ); + }, + itemCount: numbers.length, + itemExtent: 30, + prototypeItem: const SizedBox(), + onReorder: (int fromIndex, int toIndex) { }, + ), throwsAssertionError); + }); + testWidgets('if itemExtent is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { final List numbers = [0,1,2]; @@ -312,6 +352,48 @@ void main() { expect(item1Height, 30.0); expect(item2Height, 30.0); }); + + testWidgets('if prototypeItem is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { + final List numbers = [0,1,2]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return ReorderableList( + itemBuilder: (BuildContext context, int index) { + return SizedBox( + key: ValueKey(numbers[index]), + // children with different heights + height: 20 + numbers[index] * 10, + child: ReorderableDragStartListener( + index: index, + child: Text(numbers[index].toString()), + ) + ); + }, + itemCount: numbers.length, + prototypeItem: const SizedBox( + height: 30, + child: Text('3'), + ), + onReorder: (int oldIndex, int newIndex) { }, + ); + }, + ), + ), + ) + ); + + final double item0Height = tester.getSize(find.text('0').hitTestable()).height; + final double item1Height = tester.getSize(find.text('1').hitTestable()).height; + final double item2Height = tester.getSize(find.text('2').hitTestable()).height; + + expect(item0Height, 30.0); + expect(item1Height, 30.0); + expect(item2Height, 30.0); + }); } class TestList extends StatefulWidget { diff --git a/packages/flutter/test/widgets/scroll_view_test.dart b/packages/flutter/test/widgets/scroll_view_test.dart index cb1441a3b0..81e5063b02 100644 --- a/packages/flutter/test/widgets/scroll_view_test.dart +++ b/packages/flutter/test/widgets/scroll_view_test.dart @@ -1233,7 +1233,7 @@ void main() { expect(finder, findsOneWidget); }); - testWidgets('ListView asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { + testWidgets('ListView asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { expect(() => ListView( itemExtent: 100, prototypeItem: const SizedBox(), @@ -1372,4 +1372,45 @@ void main() { expect(item1Height, 30.0); expect(item2Height, 30.0); }); + + testWidgets('if prototypeItem is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { + final List numbers = [0,1,2]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return ListView.builder( + itemBuilder: (BuildContext context, int index) { + return SizedBox( + key: ValueKey(numbers[index]), + // children with different heights + height: 20 + numbers[index] * 10, + child: ReorderableDragStartListener( + index: index, + child: Text(numbers[index].toString()), + ) + ); + }, + itemCount: numbers.length, + prototypeItem: const SizedBox( + height: 30, + child: Text('3'), + ), + ); + }, + ), + ), + ) + ); + + final double item0Height = tester.getSize(find.text('0').hitTestable()).height; + final double item1Height = tester.getSize(find.text('1').hitTestable()).height; + final double item2Height = tester.getSize(find.text('2').hitTestable()).height; + + expect(item0Height, 30.0); + expect(item1Height, 30.0); + expect(item2Height, 30.0); + }); }