diff --git a/packages/flutter/lib/src/widgets/animated_scroll_view.dart b/packages/flutter/lib/src/widgets/animated_scroll_view.dart index 7938d78b67..9df630146a 100644 --- a/packages/flutter/lib/src/widgets/animated_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/animated_scroll_view.dart @@ -128,6 +128,10 @@ class AnimatedList extends _AnimatedScrollView { /// animation is passed to [AnimatedList.itemBuilder] whenever the item's widget /// is needed. /// +/// When multiple items are inserted with [insertAllItems] an animation begins running. +/// The animation is passed to [AnimatedList.itemBuilder] whenever the item's widget +/// is needed. +/// /// When an item is removed with [removeItem] its animation is reversed. /// The removed item's animation is passed to the [removeItem] builder /// parameter. @@ -486,6 +490,13 @@ abstract class _AnimatedScrollViewState extends S _sliverAnimatedMultiBoxKey.currentState!.insertItem(index, duration: duration); } + /// Insert multiple items at [index] and start an animation that will be passed + /// to [AnimatedGrid.itemBuilder] or [AnimatedList.itemBuilder] when the items + /// are visible. + void insertAllItems(int index, int length, { Duration duration = _kDuration, bool isAsync = false }) { + _sliverAnimatedMultiBoxKey.currentState!.insertAllItems(index, length, duration: duration); + } + /// Remove the item at `index` and start an animation that will be passed to /// `builder` when the item is visible. /// @@ -506,6 +517,19 @@ abstract class _AnimatedScrollViewState extends S _sliverAnimatedMultiBoxKey.currentState!.removeItem(index, builder, duration: duration); } + /// Remove all the items and start an animation that will be passed to + /// `builder` when the items are visible. + /// + /// Items are removed immediately. However, the + /// items will still appear for `duration`, and during that time + /// `builder` must construct its widget as needed. + /// + /// This method's semantics are the same as Dart's [List.clear] method: it + /// removes all the items in the list. + void removeAllItems(AnimatedRemovedItemBuilder builder, { Duration duration = _kDuration }) { + _sliverAnimatedMultiBoxKey.currentState!.removeAllItems(builder, duration: duration); + } + Widget _wrap(Widget sliver) { return CustomScrollView( scrollDirection: widget.scrollDirection, @@ -1046,6 +1070,15 @@ abstract class _SliverAnimatedMultiBoxAdaptorState _itemsCount -= 1); }); } + + /// Remove all the items and start an animation that will be passed to + /// `builder` when the items are visible. + /// + /// Items are removed immediately. However, the + /// items will still appear for `duration` and during that time + /// `builder` must construct its widget as needed. + /// + /// This method's semantics are the same as Dart's [List.clear] method: it + /// removes all the items in the list. + void removeAllItems(AnimatedRemovedItemBuilder builder, { Duration duration = _kDuration }) { + for(int i = _itemsCount - 1 ; i >= 0; i--) { + removeItem(i, builder, duration: duration); + } + } } diff --git a/packages/flutter/test/widgets/animated_grid_test.dart b/packages/flutter/test/widgets/animated_grid_test.dart index 6c3746b209..7a86f65081 100644 --- a/packages/flutter/test/widgets/animated_grid_test.dart +++ b/packages/flutter/test/widgets/animated_grid_test.dart @@ -103,6 +103,32 @@ void main() { await tester.pumpAndSettle(); expect(find.text('removing item'), findsNothing); + + listKey.currentState!.insertAllItems(0, 2); + await tester.pump(); + expect(find.text('item 2'), findsOneWidget); + expect(find.text('item 3'), findsOneWidget); + + // Test for removeAllItems. + listKey.currentState!.removeAllItems( + (BuildContext context, Animation animation) { + return const SizedBox( + height: 100.0, + child: Center(child: Text('removing item')), + ); + }, + duration: const Duration(milliseconds: 100), + ); + + await tester.pump(); + expect(find.text('removing item'), findsWidgets); + expect(find.text('item 0'), findsNothing); + expect(find.text('item 1'), findsNothing); + expect(find.text('item 2'), findsNothing); + expect(find.text('item 3'), findsNothing); + + await tester.pumpAndSettle(); + expect(find.text('removing item'), findsNothing); }); group('SliverAnimatedGrid', () { @@ -224,6 +250,62 @@ void main() { expect(itemRight(2), 300.0); }); + testWidgets('insertAll', (WidgetTester tester) async { + final GlobalKey listKey = GlobalKey(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( + slivers: [ + SliverAnimatedGrid( + key: listKey, + itemBuilder: (BuildContext context, int index, Animation animation) { + return ScaleTransition( + key: ValueKey(index), + scale: animation, + child: SizedBox( + height: 100.0, + child: Center(child: Text('item $index')), + ), + ); + }, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 100.0, + ), + ), + ], + ), + ), + ); + + double itemScale(int index) => + tester.widget(find.byKey(ValueKey(index), skipOffstage: false)).scale.value; + double itemLeft(int index) => tester.getTopLeft(find.byKey(ValueKey(index), skipOffstage: false)).dx; + double itemRight(int index) => tester.getTopRight(find.byKey(ValueKey(index), skipOffstage: false)).dx; + + listKey.currentState!.insertAllItems(0, 2, duration: const Duration(milliseconds: 100)); + await tester.pump(); + + // Newly inserted items 0 & 1's scale should animate from 0 to 1 + expect(itemScale(0), 0.0); + expect(itemScale(1), 0.0); + await tester.pump(const Duration(milliseconds: 50)); + expect(itemScale(0), 0.5); + expect(itemScale(1), 0.5); + await tester.pump(const Duration(milliseconds: 50)); + expect(itemScale(0), 1.0); + expect(itemScale(1), 1.0); + + // The list now contains two fully expanded items at the top: + expect(find.text('item 0'), findsOneWidget); + expect(find.text('item 1'), findsOneWidget); + expect(itemLeft(0), 0.0); + expect(itemRight(0), 100.0); + expect(itemLeft(1), 100.0); + expect(itemRight(1), 200.0); + }); + testWidgets('remove', (WidgetTester tester) async { final GlobalKey listKey = GlobalKey(); final List items = [0, 1, 2]; @@ -302,6 +384,58 @@ void main() { expect(itemRight(2), 200.0); }); + testWidgets('removeAll', (WidgetTester tester) async { + final GlobalKey listKey = GlobalKey(); + final List items = [0, 1, 2]; + + Widget buildItem(BuildContext context, int item, Animation animation) { + return ScaleTransition( + key: ValueKey(item), + scale: animation, + child: SizedBox( + height: 100.0, + child: Center( + child: Text('item $item', textDirection: TextDirection.ltr), + ), + ), + ); + } + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( + slivers: [ + SliverAnimatedGrid( + key: listKey, + initialItemCount: 3, + itemBuilder: (BuildContext context, int index, Animation animation) { + return buildItem(context, items[index], animation); + }, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 100.0, + ), + ), + ], + ), + ), + ); + expect(find.text('item 0'), findsOneWidget); + expect(find.text('item 1'), findsOneWidget); + expect(find.text('item 2'), findsOneWidget); + + items.clear(); + listKey.currentState!.removeAllItems((BuildContext context, Animation animation) => buildItem(context, 0, animation), + duration: const Duration(milliseconds: 100), + ); + + await tester.pumpAndSettle(); + + expect(find.text('item 0'), findsNothing); + expect(find.text('item 1'), findsNothing); + expect(find.text('item 2'), findsNothing); + }); + testWidgets('works in combination with other slivers', (WidgetTester tester) async { final GlobalKey listKey = GlobalKey(); diff --git a/packages/flutter/test/widgets/animated_list_test.dart b/packages/flutter/test/widgets/animated_list_test.dart index 03f4d6353c..89a6ce6ac9 100644 --- a/packages/flutter/test/widgets/animated_list_test.dart +++ b/packages/flutter/test/widgets/animated_list_test.dart @@ -96,6 +96,33 @@ void main() { await tester.pumpAndSettle(); expect(find.text('removing item'), findsNothing); + + // Test for insertAllItems + listKey.currentState!.insertAllItems(0, 2); + await tester.pump(); + expect(find.text('item 2'), findsOneWidget); + expect(find.text('item 3'), findsOneWidget); + + // Test for removeAllItems + listKey.currentState!.removeAllItems( + (BuildContext context, Animation animation) { + return const SizedBox( + height: 100.0, + child: Center(child: Text('removing item')), + ); + }, + duration: const Duration(milliseconds: 100), + ); + + await tester.pump(); + expect(find.text('removing item'), findsWidgets); + expect(find.text('item 0'), findsNothing); + expect(find.text('item 1'), findsNothing); + expect(find.text('item 2'), findsNothing); + expect(find.text('item 3'), findsNothing); + + await tester.pumpAndSettle(); + expect(find.text('removing item'), findsNothing); }); group('SliverAnimatedList', () { @@ -217,6 +244,64 @@ void main() { expect(itemBottom(2), 300.0); }); + // Test for insertAllItems with SliverAnimatedList + testWidgets('insertAll', (WidgetTester tester) async { + final GlobalKey listKey = GlobalKey(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( + slivers: [ + SliverAnimatedList( + key: listKey, + itemBuilder: (BuildContext context, int index, Animation animation) { + return SizeTransition( + key: ValueKey(index), + sizeFactor: animation, + child: SizedBox( + height: 100.0, + child: Center(child: Text('item $index')), + ), + ); + }, + ), + ], + ), + ), + ); + + double itemHeight(int index) => tester.getSize(find.byKey(ValueKey(index), skipOffstage: false)).height; + double itemTop(int index) => tester.getTopLeft(find.byKey(ValueKey(index), skipOffstage: false)).dy; + double itemBottom(int index) => tester.getBottomLeft(find.byKey(ValueKey(index), skipOffstage: false)).dy; + + listKey.currentState!.insertAllItems( + 0, + 2, + duration: const Duration(milliseconds: 100), + ); + await tester.pump(); + + // Newly inserted item 0 & 1's height should animate from 0 to 100 + expect(itemHeight(0), 0.0); + expect(itemHeight(1), 0.0); + await tester.pump(const Duration(milliseconds: 50)); + expect(itemHeight(0), 50.0); + expect(itemHeight(1), 50.0); + await tester.pump(const Duration(milliseconds: 50)); + expect(itemHeight(0), 100.0); + expect(itemHeight(1), 100.0); + + // The list now contains two fully expanded items at the top: + expect(find.text('item 0'), findsOneWidget); + expect(find.text('item 1'), findsOneWidget); + expect(itemTop(0), 0.0); + expect(itemBottom(0), 100.0); + expect(itemTop(1), 100.0); + expect(itemBottom(1), 200.0); + }); + + // Test for removeAllItems with SliverAnimatedList testWidgets('remove', (WidgetTester tester) async { final GlobalKey listKey = GlobalKey(); final List items = [0, 1, 2]; @@ -293,6 +378,57 @@ void main() { expect(itemBottom(2), 200.0); }); + // Test for removeAllItems with SliverAnimatedList + testWidgets('removeAll', (WidgetTester tester) async { + final GlobalKey listKey = GlobalKey(); + final List items = [0, 1, 2]; + + Widget buildItem(BuildContext context, int item, Animation animation) { + return SizeTransition( + key: ValueKey(item), + sizeFactor: animation, + child: SizedBox( + height: 100.0, + child: Center( + child: Text('item $item', textDirection: TextDirection.ltr), + ), + ), + ); + } + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( + slivers: [ + SliverAnimatedList( + key: listKey, + initialItemCount: 3, + itemBuilder: (BuildContext context, int index, Animation animation) { + return buildItem(context, items[index], animation); + }, + ), + ], + ), + ), + ); + + expect(find.text('item 0'), findsOneWidget); + expect(find.text('item 1'), findsOneWidget); + expect(find.text('item 2'), findsOneWidget); + + items.clear(); + listKey.currentState!.removeAllItems((BuildContext context, Animation animation) => buildItem(context, 0, animation), + duration: const Duration(milliseconds: 100), + ); + + await tester.pumpAndSettle(); + + expect(find.text('item 0'), findsNothing); + expect(find.text('item 1'), findsNothing); + expect(find.text('item 2'), findsNothing); + }); + testWidgets('works in combination with other slivers', (WidgetTester tester) async { final GlobalKey listKey = GlobalKey();