diff --git a/packages/flutter/lib/src/widgets/reorderable_list.dart b/packages/flutter/lib/src/widgets/reorderable_list.dart index 7bbbdc0fbc0..d177096fd8e 100644 --- a/packages/flutter/lib/src/widgets/reorderable_list.dart +++ b/packages/flutter/lib/src/widgets/reorderable_list.dart @@ -136,6 +136,9 @@ class ReorderableList extends StatefulWidget { final IndexedWidgetBuilder itemBuilder; /// The number of items in the list. + /// + /// It must be a non-negative integer. When zero, nothing is displayed and + /// the widget occupies no space. final int itemCount; /// A callback used by the list to report that a list item has been dragged diff --git a/packages/flutter/test/widgets/reorderable_list_test.dart b/packages/flutter/test/widgets/reorderable_list_test.dart index 88af85a6653..81cab536b16 100644 --- a/packages/flutter/test/widgets/reorderable_list_test.dart +++ b/packages/flutter/test/widgets/reorderable_list_test.dart @@ -8,6 +8,85 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; void main() { + testWidgets('negative itemCount should assert', (WidgetTester tester) async { + final List items = [1, 2, 3]; + await tester.pumpWidget(MaterialApp( + home: StatefulBuilder( + builder: (BuildContext outerContext, StateSetter setState) { + return CustomScrollView( + slivers: [ + SliverReorderableList( + itemCount: -1, + onReorder: (int fromIndex, int toIndex) { + setState(() { + if (toIndex > fromIndex) { + toIndex -= 1; + } + items.insert(toIndex, items.removeAt(fromIndex)); + }); + }, + itemBuilder: (BuildContext context, int index) { + return Container( + height: 100, + child: Text('item ${items[index]}'), + ); + }, + ), + ], + ); + } + ), + )); + expect(tester.takeException(), isA()); + }); + + testWidgets('zero itemCount should not build widget', (WidgetTester tester) async { + final List items = [1, 2, 3]; + await tester.pumpWidget(MaterialApp( + home: StatefulBuilder( + builder: (BuildContext outerContext, StateSetter setState) { + return CustomScrollView( + slivers: [ + SliverFixedExtentList( + itemExtent: 50.0, + delegate: SliverChildListDelegate([ + const Text('before'), + ]), + ), + SliverReorderableList( + itemCount: 0, + onReorder: (int fromIndex, int toIndex) { + setState(() { + if (toIndex > fromIndex) { + toIndex -= 1; + } + items.insert(toIndex, items.removeAt(fromIndex)); + }); + }, + itemBuilder: (BuildContext context, int index) { + return Container( + height: 100, + child: Text('item ${items[index]}'), + ); + }, + ), + SliverFixedExtentList( + itemExtent: 50.0, + delegate: SliverChildListDelegate([ + const Text('after'), + ]), + ), + ], + ); + } + ), + )); + + expect(find.text('before'), findsOneWidget); + expect(find.byType(SliverReorderableList), findsNothing); + expect(find.text('after'), findsOneWidget); + }); + testWidgets('SliverReorderableList, drag and drop, fixed height items', (WidgetTester tester) async { final List items = List.generate(8, (int index) => index); @@ -126,6 +205,69 @@ void main() { expect(getIconStyle().color, iconColor); expect(getTextStyle().color, textColor); }); + + testWidgets('SliverReorderableList - custom proxyDecorator', (WidgetTester tester) async { + const ValueKey fadeTransitionKey = ValueKey('reordered-fade'); + + await tester.pumpWidget( + TestList( + items: List.from([0, 1, 2, 3]), + proxyDecorator: ( + Widget child, + int index, + Animation animation, + ) { + return AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + final Tween fadeValues = Tween(begin: 1.0, end: 0.5); + final Animation fadeAnimation = animation.drive(fadeValues); + return FadeTransition( + key: fadeTransitionKey, + opacity: fadeAnimation, + child: child, + ); + }, + child: child, + ); + }, + ), + ); + + Finder getItemFadeTransition() => find.byKey(fadeTransitionKey); + + expect(getItemFadeTransition(), findsNothing); + + // Start gesture on first item + final TestGesture drag = await tester.startGesture(tester.getCenter(find.text('item 0'))); + await tester.pump(kPressTimeout); + + // Drag enough for transition animation defined in proxyDecorator to start. + await drag.moveBy(const Offset(0, 50)); + await tester.pump(); + + // At the start, opacity should be at 1.0. + expect(getItemFadeTransition(), findsOneWidget); + FadeTransition fadeTransition = tester.widget(getItemFadeTransition()); + expect(fadeTransition.opacity.value, 1.0); + + // Let animation run halfway. + await tester.pump(const Duration(milliseconds: 125)); + fadeTransition = tester.widget(getItemFadeTransition()); + expect(fadeTransition.opacity.value, greaterThan(0.5)); + expect(fadeTransition.opacity.value, lessThan(1.0)); + + // Allow animation to run to the end. + await tester.pumpAndSettle(); + expect(find.byKey(fadeTransitionKey), findsOneWidget); + fadeTransition = tester.widget(getItemFadeTransition()); + expect(fadeTransition.opacity.value, 0.5); + + // Finish reordering. + await drag.up(); + await tester.pumpAndSettle(); + expect(getItemFadeTransition(), findsNothing); + }); } class TestList extends StatefulWidget { @@ -133,12 +275,14 @@ class TestList extends StatefulWidget { Key? key, this.textColor, this.iconColor, + this.proxyDecorator, required this.items, }) : super(key: key); final List items; final Color? textColor; final Color? iconColor; + final ReorderItemProxyDecorator? proxyDecorator; @override _TestListState createState() => _TestListState(); @@ -185,6 +329,7 @@ class _TestListState extends State { items.insert(toIndex, items.removeAt(fromIndex)); }); }, + proxyDecorator: widget.proxyDecorator, ), ], );