diff --git a/packages/flutter/lib/src/material/reorderable_list.dart b/packages/flutter/lib/src/material/reorderable_list.dart index 58c7c33d53..a701292d53 100644 --- a/packages/flutter/lib/src/material/reorderable_list.dart +++ b/packages/flutter/lib/src/material/reorderable_list.dart @@ -289,115 +289,7 @@ class ReorderableListView extends StatefulWidget { _ReorderableListViewState createState() => _ReorderableListViewState(); } -// This top-level state manages an Overlay that contains the list and -// also any items being dragged on top fo the list. -// -// The Overlay doesn't properly keep state by building new overlay entries, -// and so we cache a single OverlayEntry for use as the list layer. -// That overlay entry then builds a _ReorderableListContent which may -// insert items being dragged into the Overlay above itself. class _ReorderableListViewState extends State { - // This entry contains the scrolling list itself. - late OverlayEntry _listOverlayEntry; - - @override - void initState() { - super.initState(); - _listOverlayEntry = OverlayEntry( - opaque: true, - builder: (BuildContext context) { - return _ReorderableListContent( - itemBuilder: widget.itemBuilder, - itemCount: widget.itemCount, - onReorder: widget.onReorder, - proxyDecorator: widget.proxyDecorator, - buildDefaultDragHandles: widget.buildDefaultDragHandles, - padding: widget.padding, - header: widget.header, - scrollDirection: widget.scrollDirection, - reverse: widget.reverse, - scrollController: widget.scrollController, - primary: widget.primary, - physics: widget.physics, - shrinkWrap: widget.shrinkWrap, - anchor: widget.anchor, - cacheExtent: widget.cacheExtent, - dragStartBehavior: widget.dragStartBehavior, - keyboardDismissBehavior: widget.keyboardDismissBehavior, - restorationId: widget.restorationId, - clipBehavior: widget.clipBehavior, - ); - }, - ); - } - - @override - void didUpdateWidget(ReorderableListView oldWidget) { - super.didUpdateWidget(oldWidget); - // As this depends on pretty much everything, it - // is ok to mark this as dirty unconditionally. - _listOverlayEntry.markNeedsBuild(); - } - - @override - Widget build(BuildContext context) { - assert(debugCheckHasMaterialLocalizations(context)); - return Overlay( - initialEntries: [ - _listOverlayEntry - ], - ); - } -} - -class _ReorderableListContent extends StatefulWidget { - const _ReorderableListContent({ - required this.itemBuilder, - required this.itemCount, - required this.onReorder, - required this.proxyDecorator, - required this.buildDefaultDragHandles, - required this.padding, - required this.header, - required this.scrollDirection, - required this.reverse, - required this.scrollController, - required this.primary, - required this.physics, - required this.shrinkWrap, - required this.anchor, - required this.cacheExtent, - required this.dragStartBehavior, - required this.keyboardDismissBehavior, - required this.restorationId, - required this.clipBehavior, - }); - - final IndexedWidgetBuilder itemBuilder; - final int itemCount; - final ReorderCallback onReorder; - final ReorderItemProxyDecorator? proxyDecorator; - final bool buildDefaultDragHandles; - final EdgeInsets? padding; - final Widget? header; - final Axis scrollDirection; - final bool reverse; - final ScrollController? scrollController; - final bool? primary; - final ScrollPhysics? physics; - final bool shrinkWrap; - final double anchor; - final double? cacheExtent; - final DragStartBehavior dragStartBehavior; - final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; - final String? restorationId; - final Clip clipBehavior; - - @override - _ReorderableListContentState createState() => _ReorderableListContentState(); -} - -class _ReorderableListContentState extends State<_ReorderableListContent> { Widget _wrapWithSemantics(Widget child, int index) { void reorder(int startIndex, int endIndex) { if (startIndex != endIndex) @@ -530,6 +422,9 @@ class _ReorderableListContentState extends State<_ReorderableListContent> { @override Widget build(BuildContext context) { + assert(debugCheckHasMaterialLocalizations(context)); + assert(debugCheckHasOverlay(context)); + // If there is a header we can't just apply the padding to the list, // so we wrap the CustomScrollView in the padding for the top, left and right // and only add the padding from the bottom to the sliver list (or the equivalent diff --git a/packages/flutter/test/material/reorderable_list_test.dart b/packages/flutter/test/material/reorderable_list_test.dart index 0baaace89d..e1d57cec0b 100644 --- a/packages/flutter/test/material/reorderable_list_test.dart +++ b/packages/flutter/test/material/reorderable_list_test.dart @@ -212,7 +212,8 @@ void main() { expect(getListHeight(), kDraggingListHeight); }); - testWidgets('Vertical drop area golden', (WidgetTester tester) async { + testWidgets('Vertical drag in progress golden image', (WidgetTester tester) async { + debugDisableShadows = false; final Widget reorderableListView = ReorderableListView( children: [ Container( @@ -234,23 +235,45 @@ void main() { color: Colors.green, ), ], - scrollDirection: Axis.vertical, onReorder: (int oldIndex, int newIndex) { }, ); await tester.pumpWidget(MaterialApp( - home: SizedBox( + home: Container( + color: Colors.white, height: itemHeight * 3, - child: reorderableListView, + // Wrap in an overlay so that the golden image includes the dragged item. + child: Overlay( + initialEntries: [ + OverlayEntry(builder: (BuildContext context) { + // Wrap the list in padding to test that the positioning + // is correct when the origin of the overlay is different + // from the list. + return Padding( + padding: const EdgeInsets.all(24), + child: reorderableListView, + ); + }), + ], + ), ), )); - await tester.startGesture(tester.getCenter(find.byKey(const Key('blue')))); + // Start dragging the second item. + final TestGesture drag = await tester.startGesture(tester.getCenter(find.byKey(const Key('blue')))); await tester.pump(kLongPressTimeout + kPressTimeout); + + // Drag it up to be partially over the top item. + await drag.moveBy(const Offset(0, -itemHeight / 3)); await tester.pumpAndSettle(); + + // Should be an image of the second item overlapping the bottom of the + // first with a gap between the first and third and a drop shadow on + // the dragged item. await expectLater( - find.byKey(const Key('blue')), + find.byType(ReorderableListView), matchesGoldenFile('reorderable_list_test.vertical.drop_area.png'), ); + debugDisableShadows = true; }); testWidgets('Preserves children states when the list parent changes the order', (WidgetTester tester) async { @@ -432,6 +455,11 @@ void main() { ], onReorder: (int oldIndex, int newIndex) { }, ); + final Widget overlay = Overlay( + initialEntries: [ + OverlayEntry(builder: (BuildContext context) => reorderableList) + ], + ); final Widget boilerplate = Localizations( locale: const Locale('en'), delegates: const >[ @@ -443,7 +471,7 @@ void main() { height: 100.0, child: Directionality( textDirection: TextDirection.ltr, - child: reorderableList, + child: overlay, ), ), ); @@ -795,7 +823,8 @@ void main() { expect(getListWidth(), kDraggingListWidth); }); - testWidgets('Horizontal drop area golden', (WidgetTester tester) async { + testWidgets('Horizontal drag in progress golden image', (WidgetTester tester) async { + debugDisableShadows = false; final Widget reorderableListView = ReorderableListView( children: [ Container( @@ -821,19 +850,42 @@ void main() { onReorder: (int oldIndex, int newIndex) { }, ); await tester.pumpWidget(MaterialApp( - home: SizedBox( + home: Container( + color: Colors.white, width: itemHeight * 3, - child: reorderableListView, + // Wrap in an overlay so that the golden image includes the dragged item. + child: Overlay( + initialEntries: [ + OverlayEntry(builder: (BuildContext context) { + // Wrap the list in padding to test that the positioning + // is correct when the origin of the overlay is different + // from the list. + return Padding( + padding: const EdgeInsets.all(24), + child: reorderableListView, + ); + }) + ], + ), ), )); - await tester.startGesture(tester.getCenter(find.byKey(const Key('blue')))); + // Start dragging the second item. + final TestGesture drag = await tester.startGesture(tester.getCenter(find.byKey(const Key('blue')))); await tester.pump(kLongPressTimeout + kPressTimeout); + + // Drag it left to be partially over the first item. + await drag.moveBy(const Offset(-itemHeight / 3, 0)); await tester.pumpAndSettle(); + + // Should be an image of the second item overlapping the right of the + // first with a gap between the first and third and a drop shadow on + // the dragged item. await expectLater( - find.byKey(const Key('blue')), + find.byType(ReorderableListView), matchesGoldenFile('reorderable_list_test.horizontal.drop_area.png'), ); + debugDisableShadows = true; }); testWidgets('Preserves children states when the list parent changes the order', (WidgetTester tester) async { @@ -1342,6 +1394,38 @@ void main() { expect(exception, isFlutterError); expect(exception.toString(), contains('Every item of ReorderableListView must have a key.')); }); + + testWidgets('Throws an error if no overlay present', (WidgetTester tester) async { + final Widget reorderableList = ReorderableListView( + children: const [ + SizedBox(width: 100.0, height: 100.0, child: Text('C'), key: Key('C')), + SizedBox(width: 100.0, height: 100.0, child: Text('B'), key: Key('B')), + SizedBox(width: 100.0, height: 100.0, child: Text('A'), key: Key('A')), + ], + onReorder: (int oldIndex, int newIndex) { }, + ); + final Widget boilerplate = Localizations( + locale: const Locale('en'), + delegates: const >[ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + child:SizedBox( + width: 100.0, + height: 100.0, + child: Directionality( + textDirection: TextDirection.ltr, + child: reorderableList, + ), + ), + ); + await tester.pumpWidget(boilerplate); + + final dynamic exception = tester.takeException(); + expect(exception, isFlutterError); + expect(exception.toString(), contains('No Overlay widget found')); + expect(exception.toString(), contains('ReorderableListView widgets require an Overlay widget ancestor')); + }); } Future longPressDrag(WidgetTester tester, Offset start, Offset end) async {