Added "insertAll" and "removeAll" methods to AnimatedList (#115545)
* Added "insertAll" and "removeAll" method to AnimatedList
* Fixed doc
* Changes in documentation asked by reviewwer
* Removed unnecessary asserts.
* Doc changes asked by reviewer.
* Doc changes.
---------
Co-authored-by: Rashid Khabeer <rkhabeer84@gmail.com>
diff --git a/packages/flutter/lib/src/widgets/animated_scroll_view.dart b/packages/flutter/lib/src/widgets/animated_scroll_view.dart
index 7938d78..9df6301 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 @@
/// 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 @@
_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 @@
_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 @@
});
}
+ /// 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 }) {
+ for (int i = 0; i < length; i++) {
+ insertItem(index + i, duration: duration);
+ }
+ }
+
/// Remove the item at [index] and start an animation that will be passed
/// to [builder] when the item is visible.
///
@@ -1094,4 +1127,19 @@
setState(() => _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 6c3746b..7a86f65 100644
--- a/packages/flutter/test/widgets/animated_grid_test.dart
+++ b/packages/flutter/test/widgets/animated_grid_test.dart
@@ -103,6 +103,32 @@
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<double> 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 @@
expect(itemRight(2), 300.0);
});
+ testWidgets('insertAll', (WidgetTester tester) async {
+ final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>();
+
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: CustomScrollView(
+ slivers: <Widget>[
+ SliverAnimatedGrid(
+ key: listKey,
+ itemBuilder: (BuildContext context, int index, Animation<double> animation) {
+ return ScaleTransition(
+ key: ValueKey<int>(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<ScaleTransition>(find.byKey(ValueKey<int>(index), skipOffstage: false)).scale.value;
+ double itemLeft(int index) => tester.getTopLeft(find.byKey(ValueKey<int>(index), skipOffstage: false)).dx;
+ double itemRight(int index) => tester.getTopRight(find.byKey(ValueKey<int>(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<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>();
final List<int> items = <int>[0, 1, 2];
@@ -302,6 +384,58 @@
expect(itemRight(2), 200.0);
});
+ testWidgets('removeAll', (WidgetTester tester) async {
+ final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>();
+ final List<int> items = <int>[0, 1, 2];
+
+ Widget buildItem(BuildContext context, int item, Animation<double> animation) {
+ return ScaleTransition(
+ key: ValueKey<int>(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: <Widget>[
+ SliverAnimatedGrid(
+ key: listKey,
+ initialItemCount: 3,
+ itemBuilder: (BuildContext context, int index, Animation<double> 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<double> 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<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>();
diff --git a/packages/flutter/test/widgets/animated_list_test.dart b/packages/flutter/test/widgets/animated_list_test.dart
index 03f4d63..89a6ce6 100644
--- a/packages/flutter/test/widgets/animated_list_test.dart
+++ b/packages/flutter/test/widgets/animated_list_test.dart
@@ -96,6 +96,33 @@
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<double> 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 @@
expect(itemBottom(2), 300.0);
});
+ // Test for insertAllItems with SliverAnimatedList
+ testWidgets('insertAll', (WidgetTester tester) async {
+ final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
+
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: CustomScrollView(
+ slivers: <Widget>[
+ SliverAnimatedList(
+ key: listKey,
+ itemBuilder: (BuildContext context, int index, Animation<double> animation) {
+ return SizeTransition(
+ key: ValueKey<int>(index),
+ sizeFactor: animation,
+ child: SizedBox(
+ height: 100.0,
+ child: Center(child: Text('item $index')),
+ ),
+ );
+ },
+ ),
+ ],
+ ),
+ ),
+ );
+
+ double itemHeight(int index) => tester.getSize(find.byKey(ValueKey<int>(index), skipOffstage: false)).height;
+ double itemTop(int index) => tester.getTopLeft(find.byKey(ValueKey<int>(index), skipOffstage: false)).dy;
+ double itemBottom(int index) => tester.getBottomLeft(find.byKey(ValueKey<int>(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<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
final List<int> items = <int>[0, 1, 2];
@@ -293,6 +378,57 @@
expect(itemBottom(2), 200.0);
});
+ // Test for removeAllItems with SliverAnimatedList
+ testWidgets('removeAll', (WidgetTester tester) async {
+ final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
+ final List<int> items = <int>[0, 1, 2];
+
+ Widget buildItem(BuildContext context, int item, Animation<double> animation) {
+ return SizeTransition(
+ key: ValueKey<int>(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: <Widget>[
+ SliverAnimatedList(
+ key: listKey,
+ initialItemCount: 3,
+ itemBuilder: (BuildContext context, int index, Animation<double> 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<double> 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<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();