From fd71de13fddae5ef2646c36e2e553b6df948ced5 Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Fri, 25 Feb 2022 16:06:15 +0200 Subject: [PATCH] [ReorderableListView] Add `footer` (#92086) --- .../lib/src/material/reorderable_list.dart | 40 ++++++---- .../test/material/reorderable_list_test.dart | 78 +++++++++++++++++-- 2 files changed, 99 insertions(+), 19 deletions(-) diff --git a/packages/flutter/lib/src/material/reorderable_list.dart b/packages/flutter/lib/src/material/reorderable_list.dart index 06f3ed0d4c..39fa4c9642 100644 --- a/packages/flutter/lib/src/material/reorderable_list.dart +++ b/packages/flutter/lib/src/material/reorderable_list.dart @@ -72,6 +72,7 @@ class ReorderableListView extends StatefulWidget { this.buildDefaultDragHandles = true, this.padding, this.header, + this.footer, this.scrollDirection = Axis.vertical, this.reverse = false, this.scrollController, @@ -141,6 +142,7 @@ class ReorderableListView extends StatefulWidget { this.buildDefaultDragHandles = true, this.padding, this.header, + this.footer, this.scrollDirection = Axis.vertical, this.reverse = false, this.scrollController, @@ -214,6 +216,11 @@ class ReorderableListView extends StatefulWidget { /// If null, no header will appear before the list. final Widget? header; + /// A non-reorderable footer item to show after the items of the list. + /// + /// If null, no footer will appear after the list. + final Widget? footer; + /// {@macro flutter.widgets.scroll_view.scrollDirection} final Axis scrollDirection; @@ -426,39 +433,41 @@ class _ReorderableListViewState extends State { assert(debugCheckHasMaterialLocalizations(context)); assert(debugCheckHasOverlay(context)); - // If there is a header we can't just apply the padding to the list, - // so we break it up into padding for the header and padding for the list. + // If there is a header or footer we can't just apply the padding to the list, + // so we break it up into padding for the header, footer and padding for the list. final EdgeInsets padding = widget.padding ?? EdgeInsets.zero; late final EdgeInsets headerPadding; + late final EdgeInsets footerPadding; late final EdgeInsets listPadding; - if (widget.header == null) { + if (widget.header == null && widget.footer == null) { headerPadding = EdgeInsets.zero; + footerPadding = EdgeInsets.zero; listPadding = padding; - } else { + } else if (widget.header != null || widget.footer != null) { switch (widget.scrollDirection) { case Axis.horizontal: if (widget.reverse) { - // Header on the right headerPadding = EdgeInsets.fromLTRB(0, padding.top, padding.right, padding.bottom); - listPadding = EdgeInsets.fromLTRB(padding.left, padding.top, 0, padding.bottom); + listPadding = EdgeInsets.fromLTRB(widget.footer != null ? 0 : padding.left, padding.top, widget.header != null ? 0 : padding.right, padding.bottom); + footerPadding = EdgeInsets.fromLTRB(padding.left, padding.top, 0, padding.bottom); } else { - // Header on the left headerPadding = EdgeInsets.fromLTRB(padding.left, padding.top, 0, padding.bottom); - listPadding = EdgeInsets.fromLTRB(0, padding.top, padding.right, padding.bottom); + listPadding = EdgeInsets.fromLTRB(widget.header != null ? 0 : padding.left, padding.top, widget.footer != null ? 0 : padding.right, padding.bottom); + footerPadding = EdgeInsets.fromLTRB(0, padding.top, padding.right, padding.bottom); } break; case Axis.vertical: if (widget.reverse) { - // Header on the bottom headerPadding = EdgeInsets.fromLTRB(padding.left, 0, padding.right, padding.bottom); - listPadding = EdgeInsets.fromLTRB(padding.left, padding.top, padding.right, 0); + listPadding = EdgeInsets.fromLTRB(padding.left, widget.footer != null ? 0 : padding.top, padding.right, widget.header != null ? 0 : padding.bottom); + footerPadding = EdgeInsets.fromLTRB(padding.left, padding.top, padding.right, 0); } else { - // Header on the top headerPadding = EdgeInsets.fromLTRB(padding.left, padding.top, padding.right, 0); - listPadding = EdgeInsets.fromLTRB(padding.left, 0, padding.right, padding.bottom); + listPadding = EdgeInsets.fromLTRB(padding.left, widget.header != null ? 0 : padding.top, padding.right, widget.footer != null ? 0 : padding.bottom); + footerPadding = EdgeInsets.fromLTRB(padding.left, 0, padding.right, padding.bottom); } - break; + break; } } @@ -494,6 +503,11 @@ class _ReorderableListViewState extends State { proxyDecorator: widget.proxyDecorator ?? _proxyDecorator, ), ), + if (widget.footer != null) + SliverPadding( + padding: footerPadding, + sliver: SliverToBoxAdapter(child: widget.footer), + ), ], ); } diff --git a/packages/flutter/test/material/reorderable_list_test.dart b/packages/flutter/test/material/reorderable_list_test.dart index 08fb57692b..2e8e8a6da1 100644 --- a/packages/flutter/test/material/reorderable_list_test.dart +++ b/packages/flutter/test/material/reorderable_list_test.dart @@ -36,6 +36,7 @@ void main() { Widget build({ Widget? header, + Widget? footer, Axis scrollDirection = Axis.vertical, bool reverse = false, EdgeInsets padding = EdgeInsets.zero, @@ -51,6 +52,7 @@ void main() { width: itemHeight * 10, child: ReorderableListView( header: header, + footer: footer, scrollDirection: scrollDirection, onReorder: onReorder, reverse: reverse, @@ -157,6 +159,20 @@ void main() { expect(listItems, orderedEquals(['Item 2', 'Item 3', 'Item 4', 'Item 1'])); }); + testWidgets('properly reorders with a footer', (WidgetTester tester) async { + await tester.pumpWidget(build(footer: const Text('Footer Text'))); + expect(find.text('Footer Text'), findsOneWidget); + expect(listItems, orderedEquals(originalListItems)); + await longPressDrag( + tester, + tester.getCenter(find.text('Item 1')), + tester.getCenter(find.text('Item 4')) + const Offset(0.0, itemHeight * 2), + ); + await tester.pumpAndSettle(); + expect(find.text('Footer Text'), findsOneWidget); + expect(listItems, orderedEquals(['Item 2', 'Item 3', 'Item 4', 'Item 1'])); + }); + testWidgets('properly determines the vertical drop area extents', (WidgetTester tester) async { final Widget reorderableListView = ReorderableListView( onReorder: (int oldIndex, int newIndex) { }, @@ -764,6 +780,29 @@ void main() { expect(listItems, orderedEquals(['Item 2', 'Item 4', 'Item 3', 'Item 1'])); }); + testWidgets('properly reorders with a footer', (WidgetTester tester) async { + await tester.pumpWidget(build(footer: const Text('Footer Text'), scrollDirection: Axis.horizontal)); + expect(find.text('Footer Text'), findsOneWidget); + expect(listItems, orderedEquals(originalListItems)); + await longPressDrag( + tester, + tester.getCenter(find.text('Item 1')), + tester.getCenter(find.text('Item 4')) + const Offset(itemHeight * 2, 0.0), + ); + await tester.pumpAndSettle(); + expect(find.text('Footer Text'), findsOneWidget); + expect(listItems, orderedEquals(['Item 2', 'Item 3', 'Item 4', 'Item 1'])); + await tester.pumpWidget(build(footer: const Text('Footer Text'), scrollDirection: Axis.horizontal)); + await longPressDrag( + tester, + tester.getCenter(find.text('Item 4')), + tester.getCenter(find.text('Item 3')), + ); + await tester.pumpAndSettle(); + expect(find.text('Footer Text'), findsOneWidget); + expect(listItems, orderedEquals(['Item 2', 'Item 4', 'Item 3', 'Item 1'])); + }); + testWidgets('properly determines the horizontal drop area extents', (WidgetTester tester) async { final Widget reorderableListView = ReorderableListView( scrollDirection: Axis.horizontal, @@ -1242,7 +1281,7 @@ void main() { }); group('Padding', () { - testWidgets('Padding with no header', (WidgetTester tester) async { + testWidgets('Padding with no header & footer', (WidgetTester tester) async { const EdgeInsets padding = EdgeInsets.fromLTRB(10, 20, 30, 40); // Vertical @@ -1256,35 +1295,62 @@ void main() { expect(tester.getRect(find.byKey(const Key('Item 4'))), const Rect.fromLTRB(154, 20, 202, 560)); }); - testWidgets('Padding with header', (WidgetTester tester) async { + testWidgets('Padding with header or footer', (WidgetTester tester) async { const EdgeInsets padding = EdgeInsets.fromLTRB(10, 20, 30, 40); const Key headerKey = Key('Header'); + const Key footerKey = Key('Footer'); const Widget verticalHeader = SizedBox(key: headerKey, height: 10); const Widget horizontalHeader = SizedBox(key: headerKey, width: 10); + const Widget verticalFooter = SizedBox(key: footerKey, height: 10); + const Widget horizontalFooter = SizedBox(key: footerKey, width: 10); - // Vertical + // Vertical Header await tester.pumpWidget(build(padding: padding, header: verticalHeader)); expect(tester.getRect(find.byKey(headerKey)), const Rect.fromLTRB(10, 20, 770, 30)); expect(tester.getRect(find.byKey(const Key('Item 1'))), const Rect.fromLTRB(10, 30, 770, 78)); expect(tester.getRect(find.byKey(const Key('Item 4'))), const Rect.fromLTRB(10, 174, 770, 222)); - // Vertical, reversed + // Vertical Footer + await tester.pumpWidget(build(padding: padding, footer: verticalFooter)); + expect(tester.getRect(find.byKey(footerKey)), const Rect.fromLTRB(10, 212, 770, 222)); + expect(tester.getRect(find.byKey(const Key('Item 1'))), const Rect.fromLTRB(10, 20, 770, 68)); + expect(tester.getRect(find.byKey(const Key('Item 4'))), const Rect.fromLTRB(10, 164, 770, 212)); + + // Vertical Header, reversed await tester.pumpWidget(build(padding: padding, header: verticalHeader, reverse: true)); expect(tester.getRect(find.byKey(headerKey)), const Rect.fromLTRB(10, 550, 770, 560)); expect(tester.getRect(find.byKey(const Key('Item 1'))), const Rect.fromLTRB(10, 502, 770, 550)); expect(tester.getRect(find.byKey(const Key('Item 4'))), const Rect.fromLTRB(10, 358, 770, 406)); - // Horizontal + // Vertical Footer, reversed + await tester.pumpWidget(build(padding: padding, footer: verticalFooter, reverse: true)); + expect(tester.getRect(find.byKey(footerKey)), const Rect.fromLTRB(10, 358, 770, 368)); + expect(tester.getRect(find.byKey(const Key('Item 1'))), const Rect.fromLTRB(10, 512, 770, 560)); + expect(tester.getRect(find.byKey(const Key('Item 4'))), const Rect.fromLTRB(10, 368, 770, 416)); + + // Horizontal Header await tester.pumpWidget(build(padding: padding, header: horizontalHeader, scrollDirection: Axis.horizontal)); expect(tester.getRect(find.byKey(headerKey)), const Rect.fromLTRB(10, 20, 20, 560)); expect(tester.getRect(find.byKey(const Key('Item 1'))), const Rect.fromLTRB(20, 20, 68, 560)); expect(tester.getRect(find.byKey(const Key('Item 4'))), const Rect.fromLTRB(164, 20, 212, 560)); - // Horizontal, reversed + // // Horizontal Footer + await tester.pumpWidget(build(padding: padding, footer: horizontalFooter, scrollDirection: Axis.horizontal)); + expect(tester.getRect(find.byKey(footerKey)), const Rect.fromLTRB(202, 20, 212, 560)); + expect(tester.getRect(find.byKey(const Key('Item 1'))), const Rect.fromLTRB(10, 20, 58, 560)); + expect(tester.getRect(find.byKey(const Key('Item 4'))), const Rect.fromLTRB(154, 20, 202, 560)); + + // Horizontal Header, reversed await tester.pumpWidget(build(padding: padding, header: horizontalHeader, scrollDirection: Axis.horizontal, reverse: true)); expect(tester.getRect(find.byKey(headerKey)), const Rect.fromLTRB(760, 20, 770, 560)); expect(tester.getRect(find.byKey(const Key('Item 1'))), const Rect.fromLTRB(712, 20, 760, 560)); expect(tester.getRect(find.byKey(const Key('Item 4'))), const Rect.fromLTRB(568, 20, 616, 560)); + + // // Horizontal Footer, reversed + await tester.pumpWidget(build(padding: padding, footer: horizontalFooter, scrollDirection: Axis.horizontal, reverse: true)); + expect(tester.getRect(find.byKey(footerKey)), const Rect.fromLTRB(568, 20, 578, 560)); + expect(tester.getRect(find.byKey(const Key('Item 1'))), const Rect.fromLTRB(722, 20, 770, 560)); + expect(tester.getRect(find.byKey(const Key('Item 4'))), const Rect.fromLTRB(578, 20, 626, 560)); }); });