diff --git a/packages/flutter/lib/src/material/reorderable_list.dart b/packages/flutter/lib/src/material/reorderable_list.dart index 8fbd37d1f0..0403b61377 100644 --- a/packages/flutter/lib/src/material/reorderable_list.dart +++ b/packages/flutter/lib/src/material/reorderable_list.dart @@ -76,6 +76,7 @@ class ReorderableListView extends StatefulWidget { Key? key, required List children, required this.onReorder, + this.itemExtent, this.proxyDecorator, this.buildDefaultDragHandles = true, this.padding, @@ -168,6 +169,7 @@ class ReorderableListView extends StatefulWidget { required this.itemBuilder, required this.itemCount, required this.onReorder, + this.itemExtent, this.proxyDecorator, this.buildDefaultDragHandles = true, this.padding, @@ -323,6 +325,9 @@ class ReorderableListView extends StatefulWidget { /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; + /// {@macro flutter.widgets.list_view.itemExtent} + final double? itemExtent; + @override _ReorderableListViewState createState() => _ReorderableListViewState(); } @@ -545,6 +550,7 @@ class _ReorderableListViewState extends State { padding: listPadding, sliver: SliverReorderableList( itemBuilder: _itemBuilder, + itemExtent: widget.itemExtent, 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 2c47270d6e..dd8428c1ca 100644 --- a/packages/flutter/lib/src/widgets/reorderable_list.dart +++ b/packages/flutter/lib/src/widgets/reorderable_list.dart @@ -112,6 +112,7 @@ class ReorderableList extends StatefulWidget { required this.itemBuilder, required this.itemCount, required this.onReorder, + this.itemExtent, this.proxyDecorator, this.padding, this.scrollDirection = Axis.vertical, @@ -211,6 +212,9 @@ class ReorderableList extends StatefulWidget { /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; + /// {@macro flutter.widgets.list_view.itemExtent} + final double? itemExtent; + /// The state from the closest instance of this class that encloses the given /// context. /// @@ -337,6 +341,7 @@ class ReorderableListState extends State { padding: widget.padding ?? EdgeInsets.zero, sliver: SliverReorderableList( key: _sliverReorderableListKey, + itemExtent: widget.itemExtent, itemBuilder: widget.itemBuilder, itemCount: widget.itemCount, onReorder: widget.onReorder, @@ -380,6 +385,7 @@ class SliverReorderableList extends StatefulWidget { required this.itemBuilder, required this.itemCount, required this.onReorder, + this.itemExtent, this.proxyDecorator, }) : assert(itemCount >= 0), super(key: key); @@ -396,6 +402,9 @@ class SliverReorderableList extends StatefulWidget { /// {@macro flutter.widgets.reorderable_list.proxyDecorator} final ReorderItemProxyDecorator? proxyDecorator; + /// {@macro flutter.widgets.list_view.itemExtent} + final double? itemExtent; + @override SliverReorderableListState createState() => SliverReorderableListState(); @@ -825,15 +834,19 @@ class SliverReorderableListState extends State with Ticke @override Widget build(BuildContext context) { assert(debugCheckHasOverlay(context)); - return SliverList( + final SliverChildBuilderDelegate childrenDelegate = SliverChildBuilderDelegate( + _itemBuilder, // When dragging, the dragged item is still in the list but has been replaced // by a zero height SizedBox, so that the gap can move around. To make the // list extent stable we add a dummy entry to the end. - delegate: SliverChildBuilderDelegate( - _itemBuilder, - childCount: widget.itemCount + (_dragInfo != null ? 1 : 0), - ), + childCount: widget.itemCount + (_dragInfo != null ? 1 : 0), ); + return widget.itemExtent != null + ? SliverFixedExtentList( + itemExtent: widget.itemExtent!, + delegate: childrenDelegate, + ) + : SliverList(delegate: childrenDelegate); } } diff --git a/packages/flutter/lib/src/widgets/scroll_view.dart b/packages/flutter/lib/src/widgets/scroll_view.dart index e4c7eca49a..5c4e64e366 100644 --- a/packages/flutter/lib/src/widgets/scroll_view.dart +++ b/packages/flutter/lib/src/widgets/scroll_view.dart @@ -1476,6 +1476,7 @@ class ListView extends BoxScrollView { clipBehavior: clipBehavior, ); + /// {@template flutter.widgets.list_view.itemExtent} /// If non-null, forces the children to have the given extent in the scroll /// direction. /// @@ -1489,6 +1490,7 @@ 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. final double? itemExtent; diff --git a/packages/flutter/test/material/reorderable_list_test.dart b/packages/flutter/test/material/reorderable_list_test.dart index 63c23091b6..90a42f1f3b 100644 --- a/packages/flutter/test/material/reorderable_list_test.dart +++ b/packages/flutter/test/material/reorderable_list_test.dart @@ -1505,6 +1505,51 @@ void main() { expect(exception.toString(), contains('No Overlay widget found')); expect(exception.toString(), contains('ReorderableListView widgets require an Overlay widget ancestor')); }); + + testWidgets('if itemExtent 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, + itemExtent: 30, + onReorder: (int fromIndex, int toIndex) { + if (fromIndex < toIndex) { + toIndex--; + } + final int value = numbers.removeAt(fromIndex); + numbers.insert(toIndex, value); + }, + ); + }, + ), + ), + ) + ); + + 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); + }); } Future longPressDrag(WidgetTester tester, Offset start, Offset end) async { diff --git a/packages/flutter/test/widgets/reorderable_list_test.dart b/packages/flutter/test/widgets/reorderable_list_test.dart index 02b5d1a472..02a9cb8ad8 100644 --- a/packages/flutter/test/widgets/reorderable_list_test.dart +++ b/packages/flutter/test/widgets/reorderable_list_test.dart @@ -267,6 +267,51 @@ void main() { await tester.pumpAndSettle(); expect(getItemFadeTransition(), findsNothing); }); + + testWidgets('if itemExtent 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, + itemExtent: 30, + onReorder: (int fromIndex, int toIndex) { + if (fromIndex < toIndex) { + toIndex--; + } + final int value = numbers.removeAt(fromIndex); + numbers.insert(toIndex, value); + }, + ); + }, + ), + ), + ) + ); + + 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 f1df0f3b1a..cb1441a3b0 100644 --- a/packages/flutter/test/widgets/scroll_view_test.dart +++ b/packages/flutter/test/widgets/scroll_view_test.dart @@ -1334,4 +1334,42 @@ void main() { equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 50.0)), ); }); + + testWidgets('if itemExtent 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, + itemExtent: 30, + ); + }, + ), + ), + ) + ); + + 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); + }); }