Unverified Commit 40b5e4cb authored by Haroon Awan's avatar Haroon Awan Committed by GitHub

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: 's avatarRashid Khabeer <rkhabeer84@gmail.com>
parent 7bf1e99e
...@@ -128,6 +128,10 @@ class AnimatedList extends _AnimatedScrollView { ...@@ -128,6 +128,10 @@ class AnimatedList extends _AnimatedScrollView {
/// animation is passed to [AnimatedList.itemBuilder] whenever the item's widget /// animation is passed to [AnimatedList.itemBuilder] whenever the item's widget
/// is needed. /// 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. /// When an item is removed with [removeItem] its animation is reversed.
/// The removed item's animation is passed to the [removeItem] builder /// The removed item's animation is passed to the [removeItem] builder
/// parameter. /// parameter.
...@@ -486,6 +490,13 @@ abstract class _AnimatedScrollViewState<T extends _AnimatedScrollView> extends S ...@@ -486,6 +490,13 @@ abstract class _AnimatedScrollViewState<T extends _AnimatedScrollView> extends S
_sliverAnimatedMultiBoxKey.currentState!.insertItem(index, duration: duration); _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 /// Remove the item at `index` and start an animation that will be passed to
/// `builder` when the item is visible. /// `builder` when the item is visible.
/// ///
...@@ -506,6 +517,19 @@ abstract class _AnimatedScrollViewState<T extends _AnimatedScrollView> extends S ...@@ -506,6 +517,19 @@ abstract class _AnimatedScrollViewState<T extends _AnimatedScrollView> extends S
_sliverAnimatedMultiBoxKey.currentState!.removeItem(index, builder, duration: duration); _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) { Widget _wrap(Widget sliver) {
return CustomScrollView( return CustomScrollView(
scrollDirection: widget.scrollDirection, scrollDirection: widget.scrollDirection,
...@@ -1046,6 +1070,15 @@ abstract class _SliverAnimatedMultiBoxAdaptorState<T extends _SliverAnimatedMult ...@@ -1046,6 +1070,15 @@ abstract class _SliverAnimatedMultiBoxAdaptorState<T extends _SliverAnimatedMult
}); });
} }
/// 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 /// Remove the item at [index] and start an animation that will be passed
/// to [builder] when the item is visible. /// to [builder] when the item is visible.
/// ///
...@@ -1094,4 +1127,19 @@ abstract class _SliverAnimatedMultiBoxAdaptorState<T extends _SliverAnimatedMult ...@@ -1094,4 +1127,19 @@ abstract class _SliverAnimatedMultiBoxAdaptorState<T extends _SliverAnimatedMult
setState(() => _itemsCount -= 1); 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);
}
}
} }
...@@ -103,6 +103,32 @@ void main() { ...@@ -103,6 +103,32 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('removing item'), findsNothing); 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', () { group('SliverAnimatedGrid', () {
...@@ -224,6 +250,62 @@ void main() { ...@@ -224,6 +250,62 @@ void main() {
expect(itemRight(2), 300.0); 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 { testWidgets('remove', (WidgetTester tester) async {
final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>(); final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>();
final List<int> items = <int>[0, 1, 2]; final List<int> items = <int>[0, 1, 2];
...@@ -302,6 +384,58 @@ void main() { ...@@ -302,6 +384,58 @@ void main() {
expect(itemRight(2), 200.0); 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 { testWidgets('works in combination with other slivers', (WidgetTester tester) async {
final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>(); final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>();
......
...@@ -96,6 +96,33 @@ void main() { ...@@ -96,6 +96,33 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('removing item'), findsNothing); 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', () { group('SliverAnimatedList', () {
...@@ -217,6 +244,64 @@ void main() { ...@@ -217,6 +244,64 @@ void main() {
expect(itemBottom(2), 300.0); 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 { testWidgets('remove', (WidgetTester tester) async {
final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>(); final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
final List<int> items = <int>[0, 1, 2]; final List<int> items = <int>[0, 1, 2];
...@@ -293,6 +378,57 @@ void main() { ...@@ -293,6 +378,57 @@ void main() {
expect(itemBottom(2), 200.0); 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 { testWidgets('works in combination with other slivers', (WidgetTester tester) async {
final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>(); final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment