Skip to content

Commit 40b5e4c

Browse files
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 <[email protected]>
1 parent 7bf1e99 commit 40b5e4c

File tree

3 files changed

+318
-0
lines changed

3 files changed

+318
-0
lines changed

packages/flutter/lib/src/widgets/animated_scroll_view.dart

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ class AnimatedList extends _AnimatedScrollView {
128128
/// animation is passed to [AnimatedList.itemBuilder] whenever the item's widget
129129
/// is needed.
130130
///
131+
/// When multiple items are inserted with [insertAllItems] an animation begins running.
132+
/// The animation is passed to [AnimatedList.itemBuilder] whenever the item's widget
133+
/// is needed.
134+
///
131135
/// When an item is removed with [removeItem] its animation is reversed.
132136
/// The removed item's animation is passed to the [removeItem] builder
133137
/// parameter.
@@ -486,6 +490,13 @@ abstract class _AnimatedScrollViewState<T extends _AnimatedScrollView> extends S
486490
_sliverAnimatedMultiBoxKey.currentState!.insertItem(index, duration: duration);
487491
}
488492

493+
/// Insert multiple items at [index] and start an animation that will be passed
494+
/// to [AnimatedGrid.itemBuilder] or [AnimatedList.itemBuilder] when the items
495+
/// are visible.
496+
void insertAllItems(int index, int length, { Duration duration = _kDuration, bool isAsync = false }) {
497+
_sliverAnimatedMultiBoxKey.currentState!.insertAllItems(index, length, duration: duration);
498+
}
499+
489500
/// Remove the item at `index` and start an animation that will be passed to
490501
/// `builder` when the item is visible.
491502
///
@@ -506,6 +517,19 @@ abstract class _AnimatedScrollViewState<T extends _AnimatedScrollView> extends S
506517
_sliverAnimatedMultiBoxKey.currentState!.removeItem(index, builder, duration: duration);
507518
}
508519

520+
/// Remove all the items and start an animation that will be passed to
521+
/// `builder` when the items are visible.
522+
///
523+
/// Items are removed immediately. However, the
524+
/// items will still appear for `duration`, and during that time
525+
/// `builder` must construct its widget as needed.
526+
///
527+
/// This method's semantics are the same as Dart's [List.clear] method: it
528+
/// removes all the items in the list.
529+
void removeAllItems(AnimatedRemovedItemBuilder builder, { Duration duration = _kDuration }) {
530+
_sliverAnimatedMultiBoxKey.currentState!.removeAllItems(builder, duration: duration);
531+
}
532+
509533
Widget _wrap(Widget sliver) {
510534
return CustomScrollView(
511535
scrollDirection: widget.scrollDirection,
@@ -1046,6 +1070,15 @@ abstract class _SliverAnimatedMultiBoxAdaptorState<T extends _SliverAnimatedMult
10461070
});
10471071
}
10481072

1073+
/// Insert multiple items at [index] and start an animation that will be passed
1074+
/// to [AnimatedGrid.itemBuilder] or [AnimatedList.itemBuilder] when the items
1075+
/// are visible.
1076+
void insertAllItems(int index, int length, { Duration duration = _kDuration }) {
1077+
for (int i = 0; i < length; i++) {
1078+
insertItem(index + i, duration: duration);
1079+
}
1080+
}
1081+
10491082
/// Remove the item at [index] and start an animation that will be passed
10501083
/// to [builder] when the item is visible.
10511084
///
@@ -1094,4 +1127,19 @@ abstract class _SliverAnimatedMultiBoxAdaptorState<T extends _SliverAnimatedMult
10941127
setState(() => _itemsCount -= 1);
10951128
});
10961129
}
1130+
1131+
/// Remove all the items and start an animation that will be passed to
1132+
/// `builder` when the items are visible.
1133+
///
1134+
/// Items are removed immediately. However, the
1135+
/// items will still appear for `duration` and during that time
1136+
/// `builder` must construct its widget as needed.
1137+
///
1138+
/// This method's semantics are the same as Dart's [List.clear] method: it
1139+
/// removes all the items in the list.
1140+
void removeAllItems(AnimatedRemovedItemBuilder builder, { Duration duration = _kDuration }) {
1141+
for(int i = _itemsCount - 1 ; i >= 0; i--) {
1142+
removeItem(i, builder, duration: duration);
1143+
}
1144+
}
10971145
}

packages/flutter/test/widgets/animated_grid_test.dart

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,32 @@ void main() {
103103

104104
await tester.pumpAndSettle();
105105
expect(find.text('removing item'), findsNothing);
106+
107+
listKey.currentState!.insertAllItems(0, 2);
108+
await tester.pump();
109+
expect(find.text('item 2'), findsOneWidget);
110+
expect(find.text('item 3'), findsOneWidget);
111+
112+
// Test for removeAllItems.
113+
listKey.currentState!.removeAllItems(
114+
(BuildContext context, Animation<double> animation) {
115+
return const SizedBox(
116+
height: 100.0,
117+
child: Center(child: Text('removing item')),
118+
);
119+
},
120+
duration: const Duration(milliseconds: 100),
121+
);
122+
123+
await tester.pump();
124+
expect(find.text('removing item'), findsWidgets);
125+
expect(find.text('item 0'), findsNothing);
126+
expect(find.text('item 1'), findsNothing);
127+
expect(find.text('item 2'), findsNothing);
128+
expect(find.text('item 3'), findsNothing);
129+
130+
await tester.pumpAndSettle();
131+
expect(find.text('removing item'), findsNothing);
106132
});
107133

108134
group('SliverAnimatedGrid', () {
@@ -224,6 +250,62 @@ void main() {
224250
expect(itemRight(2), 300.0);
225251
});
226252

253+
testWidgets('insertAll', (WidgetTester tester) async {
254+
final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>();
255+
256+
await tester.pumpWidget(
257+
Directionality(
258+
textDirection: TextDirection.ltr,
259+
child: CustomScrollView(
260+
slivers: <Widget>[
261+
SliverAnimatedGrid(
262+
key: listKey,
263+
itemBuilder: (BuildContext context, int index, Animation<double> animation) {
264+
return ScaleTransition(
265+
key: ValueKey<int>(index),
266+
scale: animation,
267+
child: SizedBox(
268+
height: 100.0,
269+
child: Center(child: Text('item $index')),
270+
),
271+
);
272+
},
273+
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
274+
maxCrossAxisExtent: 100.0,
275+
),
276+
),
277+
],
278+
),
279+
),
280+
);
281+
282+
double itemScale(int index) =>
283+
tester.widget<ScaleTransition>(find.byKey(ValueKey<int>(index), skipOffstage: false)).scale.value;
284+
double itemLeft(int index) => tester.getTopLeft(find.byKey(ValueKey<int>(index), skipOffstage: false)).dx;
285+
double itemRight(int index) => tester.getTopRight(find.byKey(ValueKey<int>(index), skipOffstage: false)).dx;
286+
287+
listKey.currentState!.insertAllItems(0, 2, duration: const Duration(milliseconds: 100));
288+
await tester.pump();
289+
290+
// Newly inserted items 0 & 1's scale should animate from 0 to 1
291+
expect(itemScale(0), 0.0);
292+
expect(itemScale(1), 0.0);
293+
await tester.pump(const Duration(milliseconds: 50));
294+
expect(itemScale(0), 0.5);
295+
expect(itemScale(1), 0.5);
296+
await tester.pump(const Duration(milliseconds: 50));
297+
expect(itemScale(0), 1.0);
298+
expect(itemScale(1), 1.0);
299+
300+
// The list now contains two fully expanded items at the top:
301+
expect(find.text('item 0'), findsOneWidget);
302+
expect(find.text('item 1'), findsOneWidget);
303+
expect(itemLeft(0), 0.0);
304+
expect(itemRight(0), 100.0);
305+
expect(itemLeft(1), 100.0);
306+
expect(itemRight(1), 200.0);
307+
});
308+
227309
testWidgets('remove', (WidgetTester tester) async {
228310
final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>();
229311
final List<int> items = <int>[0, 1, 2];
@@ -302,6 +384,58 @@ void main() {
302384
expect(itemRight(2), 200.0);
303385
});
304386

387+
testWidgets('removeAll', (WidgetTester tester) async {
388+
final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>();
389+
final List<int> items = <int>[0, 1, 2];
390+
391+
Widget buildItem(BuildContext context, int item, Animation<double> animation) {
392+
return ScaleTransition(
393+
key: ValueKey<int>(item),
394+
scale: animation,
395+
child: SizedBox(
396+
height: 100.0,
397+
child: Center(
398+
child: Text('item $item', textDirection: TextDirection.ltr),
399+
),
400+
),
401+
);
402+
}
403+
404+
await tester.pumpWidget(
405+
Directionality(
406+
textDirection: TextDirection.ltr,
407+
child: CustomScrollView(
408+
slivers: <Widget>[
409+
SliverAnimatedGrid(
410+
key: listKey,
411+
initialItemCount: 3,
412+
itemBuilder: (BuildContext context, int index, Animation<double> animation) {
413+
return buildItem(context, items[index], animation);
414+
},
415+
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
416+
maxCrossAxisExtent: 100.0,
417+
),
418+
),
419+
],
420+
),
421+
),
422+
);
423+
expect(find.text('item 0'), findsOneWidget);
424+
expect(find.text('item 1'), findsOneWidget);
425+
expect(find.text('item 2'), findsOneWidget);
426+
427+
items.clear();
428+
listKey.currentState!.removeAllItems((BuildContext context, Animation<double> animation) => buildItem(context, 0, animation),
429+
duration: const Duration(milliseconds: 100),
430+
);
431+
432+
await tester.pumpAndSettle();
433+
434+
expect(find.text('item 0'), findsNothing);
435+
expect(find.text('item 1'), findsNothing);
436+
expect(find.text('item 2'), findsNothing);
437+
});
438+
305439
testWidgets('works in combination with other slivers', (WidgetTester tester) async {
306440
final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>();
307441

packages/flutter/test/widgets/animated_list_test.dart

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,33 @@ void main() {
9696

9797
await tester.pumpAndSettle();
9898
expect(find.text('removing item'), findsNothing);
99+
100+
// Test for insertAllItems
101+
listKey.currentState!.insertAllItems(0, 2);
102+
await tester.pump();
103+
expect(find.text('item 2'), findsOneWidget);
104+
expect(find.text('item 3'), findsOneWidget);
105+
106+
// Test for removeAllItems
107+
listKey.currentState!.removeAllItems(
108+
(BuildContext context, Animation<double> animation) {
109+
return const SizedBox(
110+
height: 100.0,
111+
child: Center(child: Text('removing item')),
112+
);
113+
},
114+
duration: const Duration(milliseconds: 100),
115+
);
116+
117+
await tester.pump();
118+
expect(find.text('removing item'), findsWidgets);
119+
expect(find.text('item 0'), findsNothing);
120+
expect(find.text('item 1'), findsNothing);
121+
expect(find.text('item 2'), findsNothing);
122+
expect(find.text('item 3'), findsNothing);
123+
124+
await tester.pumpAndSettle();
125+
expect(find.text('removing item'), findsNothing);
99126
});
100127

101128
group('SliverAnimatedList', () {
@@ -217,6 +244,64 @@ void main() {
217244
expect(itemBottom(2), 300.0);
218245
});
219246

247+
// Test for insertAllItems with SliverAnimatedList
248+
testWidgets('insertAll', (WidgetTester tester) async {
249+
final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
250+
251+
await tester.pumpWidget(
252+
Directionality(
253+
textDirection: TextDirection.ltr,
254+
child: CustomScrollView(
255+
slivers: <Widget>[
256+
SliverAnimatedList(
257+
key: listKey,
258+
itemBuilder: (BuildContext context, int index, Animation<double> animation) {
259+
return SizeTransition(
260+
key: ValueKey<int>(index),
261+
sizeFactor: animation,
262+
child: SizedBox(
263+
height: 100.0,
264+
child: Center(child: Text('item $index')),
265+
),
266+
);
267+
},
268+
),
269+
],
270+
),
271+
),
272+
);
273+
274+
double itemHeight(int index) => tester.getSize(find.byKey(ValueKey<int>(index), skipOffstage: false)).height;
275+
double itemTop(int index) => tester.getTopLeft(find.byKey(ValueKey<int>(index), skipOffstage: false)).dy;
276+
double itemBottom(int index) => tester.getBottomLeft(find.byKey(ValueKey<int>(index), skipOffstage: false)).dy;
277+
278+
listKey.currentState!.insertAllItems(
279+
0,
280+
2,
281+
duration: const Duration(milliseconds: 100),
282+
);
283+
await tester.pump();
284+
285+
// Newly inserted item 0 & 1's height should animate from 0 to 100
286+
expect(itemHeight(0), 0.0);
287+
expect(itemHeight(1), 0.0);
288+
await tester.pump(const Duration(milliseconds: 50));
289+
expect(itemHeight(0), 50.0);
290+
expect(itemHeight(1), 50.0);
291+
await tester.pump(const Duration(milliseconds: 50));
292+
expect(itemHeight(0), 100.0);
293+
expect(itemHeight(1), 100.0);
294+
295+
// The list now contains two fully expanded items at the top:
296+
expect(find.text('item 0'), findsOneWidget);
297+
expect(find.text('item 1'), findsOneWidget);
298+
expect(itemTop(0), 0.0);
299+
expect(itemBottom(0), 100.0);
300+
expect(itemTop(1), 100.0);
301+
expect(itemBottom(1), 200.0);
302+
});
303+
304+
// Test for removeAllItems with SliverAnimatedList
220305
testWidgets('remove', (WidgetTester tester) async {
221306
final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
222307
final List<int> items = <int>[0, 1, 2];
@@ -293,6 +378,57 @@ void main() {
293378
expect(itemBottom(2), 200.0);
294379
});
295380

381+
// Test for removeAllItems with SliverAnimatedList
382+
testWidgets('removeAll', (WidgetTester tester) async {
383+
final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
384+
final List<int> items = <int>[0, 1, 2];
385+
386+
Widget buildItem(BuildContext context, int item, Animation<double> animation) {
387+
return SizeTransition(
388+
key: ValueKey<int>(item),
389+
sizeFactor: animation,
390+
child: SizedBox(
391+
height: 100.0,
392+
child: Center(
393+
child: Text('item $item', textDirection: TextDirection.ltr),
394+
),
395+
),
396+
);
397+
}
398+
399+
await tester.pumpWidget(
400+
Directionality(
401+
textDirection: TextDirection.ltr,
402+
child: CustomScrollView(
403+
slivers: <Widget>[
404+
SliverAnimatedList(
405+
key: listKey,
406+
initialItemCount: 3,
407+
itemBuilder: (BuildContext context, int index, Animation<double> animation) {
408+
return buildItem(context, items[index], animation);
409+
},
410+
),
411+
],
412+
),
413+
),
414+
);
415+
416+
expect(find.text('item 0'), findsOneWidget);
417+
expect(find.text('item 1'), findsOneWidget);
418+
expect(find.text('item 2'), findsOneWidget);
419+
420+
items.clear();
421+
listKey.currentState!.removeAllItems((BuildContext context, Animation<double> animation) => buildItem(context, 0, animation),
422+
duration: const Duration(milliseconds: 100),
423+
);
424+
425+
await tester.pumpAndSettle();
426+
427+
expect(find.text('item 0'), findsNothing);
428+
expect(find.text('item 1'), findsNothing);
429+
expect(find.text('item 2'), findsNothing);
430+
});
431+
296432
testWidgets('works in combination with other slivers', (WidgetTester tester) async {
297433
final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
298434

0 commit comments

Comments
 (0)