mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Feature: Add AnimatedList with separators (#144899)
This PR adds `AnimatedList.separated`. A widget like an AnimatedList with animated separators. `animated_list_separated.0.dart` extends `animated_list.0.dart` to work with `AnimatedList.separated` Related issue: https://github.com/flutter/flutter/issues/48226
This commit is contained in:
parent
cd8e84f465
commit
39472d9b61
@ -66,7 +66,8 @@ class _AnimatedListSampleState extends State<AnimatedListSample> {
|
||||
// Insert the "next item" into the list model.
|
||||
void _insert() {
|
||||
final int index = _selectedItem == null ? _list.length : _list.indexOf(_selectedItem!);
|
||||
_list.insert(index, _nextItem++);
|
||||
_list.insert(index, _nextItem);
|
||||
_nextItem++;
|
||||
}
|
||||
|
||||
// Remove the selected item from the list model.
|
||||
|
||||
@ -0,0 +1,278 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Flutter code sample for [AnimatedList.separated].
|
||||
|
||||
void main() {
|
||||
runApp(const AnimatedListSeparatedSample());
|
||||
}
|
||||
|
||||
class AnimatedListSeparatedSample extends StatefulWidget {
|
||||
const AnimatedListSeparatedSample({super.key});
|
||||
|
||||
@override
|
||||
State<AnimatedListSeparatedSample> createState() => _AnimatedListSeparatedSampleState();
|
||||
}
|
||||
|
||||
class _AnimatedListSeparatedSampleState extends State<AnimatedListSeparatedSample> {
|
||||
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
|
||||
late ListModel<int> _list;
|
||||
int? _selectedItem;
|
||||
late int _nextItem; // The next item inserted when the user presses the '+' button.
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_list = ListModel<int>(
|
||||
listKey: _listKey,
|
||||
initialItems: <int>[0, 1, 2],
|
||||
removedItemBuilder: _buildRemovedItem,
|
||||
);
|
||||
_nextItem = 3;
|
||||
}
|
||||
|
||||
// Used to build list items that haven't been removed.
|
||||
Widget _buildItem(BuildContext context, int index, Animation<double> animation) {
|
||||
return CardItem(
|
||||
animation: animation,
|
||||
item: _list[index],
|
||||
selected: _selectedItem == _list[index],
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedItem = _selectedItem == _list[index] ? null : _list[index];
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Used to build separators for items that haven't been removed.
|
||||
Widget _buildSeparator(BuildContext context, int index, Animation<double> animation) {
|
||||
return ItemSeparator(
|
||||
animation: animation,
|
||||
item: _list[index],
|
||||
);
|
||||
}
|
||||
|
||||
/// The builder function used to build items that have been removed.
|
||||
///
|
||||
/// Used to build an item after it has been removed from the list. This method
|
||||
/// is needed because a removed item remains visible until its animation has
|
||||
/// completed (even though it's gone as far as this ListModel is concerned).
|
||||
/// The widget will be used by the [AnimatedListState.removeItem] method's
|
||||
/// `itemBuilder` parameter.
|
||||
Widget _buildRemovedItem(int item, BuildContext context, Animation<double> animation) {
|
||||
return CardItem(
|
||||
animation: animation,
|
||||
item: item,
|
||||
// No gesture detector here: we don't want removed items to be interactive.
|
||||
);
|
||||
}
|
||||
|
||||
/// The builder function used to build a separator for an item that has been removed.
|
||||
///
|
||||
/// Used to build a separator after the corresponding item has been removed from the list.
|
||||
/// This method is needed because the separator of a removed item remains visible until its animation has completed.
|
||||
/// The widget will be passed to [AnimatedList.separated]
|
||||
/// via the [AnimatedList.removedSeparatorBuilder] parameter and used
|
||||
/// in the [AnimatedListState.removeItem] method.
|
||||
///
|
||||
/// The item parameter is null, because the corresponding item will
|
||||
/// have been removed from the list model by the time this builder is called.
|
||||
Widget _buildRemovedSeparator(BuildContext context, int index, Animation<double> animation) {
|
||||
return SizeTransition(
|
||||
sizeFactor: animation,
|
||||
child: ItemSeparator(
|
||||
animation: animation,
|
||||
item: null,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Insert the "next item" into the list model.
|
||||
void _insert() {
|
||||
final int index = _selectedItem == null ? _list.length : _list.indexOf(_selectedItem!);
|
||||
_list.insert(index, _nextItem);
|
||||
_nextItem++;
|
||||
}
|
||||
|
||||
// Remove the selected item from the list model.
|
||||
void _remove() {
|
||||
if (_selectedItem != null) {
|
||||
_list.removeAt(_list.indexOf(_selectedItem!));
|
||||
setState(() {
|
||||
_selectedItem = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('AnimatedList.separated'),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_circle),
|
||||
onPressed: _insert,
|
||||
tooltip: 'insert a new item',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove_circle),
|
||||
onPressed: _remove,
|
||||
tooltip: 'remove the selected item',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: AnimatedList.separated(
|
||||
key: _listKey,
|
||||
initialItemCount: _list.length,
|
||||
itemBuilder: _buildItem,
|
||||
separatorBuilder: _buildSeparator,
|
||||
removedSeparatorBuilder: _buildRemovedSeparator,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
typedef RemovedItemBuilder<T> = Widget Function(T item, BuildContext context, Animation<double> animation);
|
||||
|
||||
/// Keeps a Dart [List] in sync with an [AnimatedList.separated].
|
||||
///
|
||||
/// The [insert] and [removeAt] methods apply to both the internal list and
|
||||
/// the animated list that belongs to [listKey].
|
||||
///
|
||||
/// This class only exposes as much of the Dart List API as is needed by the
|
||||
/// sample app. More list methods are easily added, however methods that
|
||||
/// mutate the list must make the same changes to the animated list in terms
|
||||
/// of [AnimatedListState.insertItem] and [AnimatedListState.removeItem].
|
||||
class ListModel<E> {
|
||||
ListModel({
|
||||
required this.listKey,
|
||||
required this.removedItemBuilder,
|
||||
Iterable<E>? initialItems,
|
||||
}) : _items = List<E>.from(initialItems ?? <E>[]);
|
||||
|
||||
final GlobalKey<AnimatedListState> listKey;
|
||||
final RemovedItemBuilder<E> removedItemBuilder;
|
||||
final List<E> _items;
|
||||
|
||||
AnimatedListState? get _animatedList => listKey.currentState;
|
||||
|
||||
void insert(int index, E item) {
|
||||
_items.insert(index, item);
|
||||
_animatedList!.insertItem(index);
|
||||
}
|
||||
|
||||
E removeAt(int index) {
|
||||
final E removedItem = _items.removeAt(index);
|
||||
if (removedItem != null) {
|
||||
_animatedList!.removeItem(
|
||||
index,
|
||||
(BuildContext context, Animation<double> animation) {
|
||||
return removedItemBuilder(removedItem, context, animation);
|
||||
},
|
||||
);
|
||||
}
|
||||
return removedItem;
|
||||
}
|
||||
|
||||
int get length => _items.length;
|
||||
|
||||
E operator [](int index) => _items[index];
|
||||
|
||||
int indexOf(E item) => _items.indexOf(item);
|
||||
}
|
||||
|
||||
/// Displays its integer item as 'item N' on a Card whose color is based on
|
||||
/// the item's value.
|
||||
///
|
||||
/// The text is displayed in bright green if [selected] is
|
||||
/// true. This widget's height is based on the [animation] parameter, it
|
||||
/// varies from 0 to 80 as the animation varies from 0.0 to 1.0.
|
||||
class CardItem extends StatelessWidget {
|
||||
const CardItem({
|
||||
super.key,
|
||||
this.onTap,
|
||||
this.selected = false,
|
||||
required this.animation,
|
||||
required this.item,
|
||||
}) : assert(item >= 0);
|
||||
|
||||
final Animation<double> animation;
|
||||
final VoidCallback? onTap;
|
||||
final int item;
|
||||
final bool selected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
TextStyle textStyle = Theme.of(context).textTheme.headlineMedium!;
|
||||
if (selected) {
|
||||
textStyle = textStyle.copyWith(color: Colors.lightGreenAccent[400]);
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
height: 80.0,
|
||||
child: Card(
|
||||
color: Colors.primaries[item % Colors.primaries.length],
|
||||
child: Center(
|
||||
child: Text('Item $item', style: textStyle),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays its integer item as 'separator N' on a Card whose color is based on
|
||||
/// the corresponding item's value.
|
||||
///
|
||||
/// When the item parameter is null, the separator is displayed as 'Removing separator' with a default color.
|
||||
///
|
||||
/// This widget's height is based on the [animation] parameter, it
|
||||
/// varies from 0 to 40 as the animation varies from 0.0 to 1.0.
|
||||
class ItemSeparator extends StatelessWidget {
|
||||
const ItemSeparator({
|
||||
super.key,
|
||||
required this.animation,
|
||||
required this.item,
|
||||
}) : assert(item == null || item >= 0);
|
||||
|
||||
final Animation<double> animation;
|
||||
final int? item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TextStyle textStyle = Theme.of(context).textTheme.headlineSmall!;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
child: SizedBox(
|
||||
height: 40.0,
|
||||
child: Card(
|
||||
color: item == null ? Colors.grey : Colors.primaries[item! % Colors.primaries.length],
|
||||
child: Center(
|
||||
child: Text(item == null ? 'Removing separator' : 'Separator $item', style: textStyle),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_api_samples/widgets/animated_list/animated_list_separated.0.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'Items can be selected, added, and removed from AnimatedList.separated',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(const example.AnimatedListSeparatedSample());
|
||||
|
||||
expect(find.text('Item 0'), findsOneWidget);
|
||||
expect(find.text('Separator 0'), findsOneWidget);
|
||||
expect(find.text('Item 1'), findsOneWidget);
|
||||
expect(find.text('Separator 1'), findsOneWidget);
|
||||
expect(find.text('Item 2'), findsOneWidget);
|
||||
|
||||
// Add an item at the end of the list
|
||||
await tester.tap(find.byIcon(Icons.add_circle));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Separator 2'), findsOneWidget);
|
||||
expect(find.text('Item 3'), findsOneWidget);
|
||||
|
||||
// Select Item 1.
|
||||
await tester.tap(find.text('Item 1'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Add item at the top of the list
|
||||
await tester.tap(find.byIcon(Icons.add_circle));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Item 4'), findsOneWidget);
|
||||
// Contrary to the behavior for insertion at other places,
|
||||
// the Separator for the last item of the list will be added
|
||||
// before that item instead of after it.
|
||||
expect(find.text('Separator 4'), findsOneWidget);
|
||||
|
||||
// Remove selected item.
|
||||
await tester.tap(find.byIcon(Icons.remove_circle));
|
||||
|
||||
// Item animation is not completed.
|
||||
await tester.pump();
|
||||
expect(find.text('Item 1'), findsOneWidget);
|
||||
expect(find.text('Separator 1'), findsNothing);
|
||||
expect(find.text('Removing separator'), findsOneWidget);
|
||||
|
||||
// When the animation completes, Item 1 disappears.
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Item 1'), findsNothing);
|
||||
expect(find.text('Separator 1'), findsNothing);
|
||||
expect(find.text('Removing separator'), findsNothing);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -85,6 +85,92 @@ class AnimatedList extends _AnimatedScrollView {
|
||||
super.clipBehavior = Clip.hardEdge,
|
||||
}) : assert(initialItemCount >= 0);
|
||||
|
||||
/// A scrolling container that animates items with separators when they are inserted or removed.
|
||||
///
|
||||
/// This widget's [AnimatedListState] can be used to dynamically insert or
|
||||
/// remove items. To refer to the [AnimatedListState] either provide a
|
||||
/// [GlobalKey] or use the static [of] method from an item's input callback.
|
||||
///
|
||||
/// This widget is similar to one created by [ListView.separated].
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample application uses an [AnimatedList.separated] to create an effect when
|
||||
/// items are removed or added to the list.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/animated_list/animated_list_separated.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// By default, [AnimatedList.separated] will automatically pad the limits of the
|
||||
/// list's scrollable to avoid partial obstructions indicated by
|
||||
/// [MediaQuery]'s padding. To avoid this behavior, override with a
|
||||
/// zero [padding] property.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
/// The following example demonstrates how to override the default top and
|
||||
/// bottom padding using [MediaQuery.removePadding].
|
||||
///
|
||||
/// ```dart
|
||||
/// Widget myWidget(BuildContext context) {
|
||||
/// return MediaQuery.removePadding(
|
||||
/// context: context,
|
||||
/// removeTop: true,
|
||||
/// removeBottom: true,
|
||||
/// child: AnimatedList.separated(
|
||||
/// initialItemCount: 50,
|
||||
/// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
|
||||
/// return Card(
|
||||
/// color: Colors.amber,
|
||||
/// child: Center(child: Text('$index')),
|
||||
/// );
|
||||
/// },
|
||||
/// separatorBuilder: (BuildContext context, int index, Animation<double> animation) {
|
||||
/// return const Divider();
|
||||
/// },
|
||||
/// removedSeparatorBuilder: (BuildContext context, int index, Animation<double> animation) {
|
||||
/// return const Divider();
|
||||
/// }
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliverAnimatedList], a sliver that animates items when they are inserted
|
||||
/// or removed from a list.
|
||||
/// * [SliverAnimatedGrid], a sliver which animates items when they are
|
||||
/// inserted or removed from a grid.
|
||||
/// * [AnimatedGrid], a non-sliver scrolling container that animates items when
|
||||
/// they are inserted or removed in a grid.
|
||||
/// * [AnimatedList], which animates items added and removed from a list instead
|
||||
/// of a grid.
|
||||
AnimatedList.separated({
|
||||
super.key,
|
||||
required AnimatedItemBuilder itemBuilder,
|
||||
required AnimatedItemBuilder separatorBuilder,
|
||||
required AnimatedItemBuilder super.removedSeparatorBuilder,
|
||||
int initialItemCount = 0,
|
||||
super.scrollDirection = Axis.vertical,
|
||||
super.reverse = false,
|
||||
super.controller,
|
||||
super.primary,
|
||||
super.physics,
|
||||
super.shrinkWrap = false,
|
||||
super.padding,
|
||||
super.clipBehavior = Clip.hardEdge,
|
||||
}) : assert(initialItemCount >= 0),
|
||||
super(
|
||||
initialItemCount: _computeChildCountWithSeparators(initialItemCount),
|
||||
itemBuilder: (BuildContext context, int index, Animation<double> animation) {
|
||||
final int itemIndex = index ~/ 2;
|
||||
if (index.isEven) {
|
||||
return itemBuilder(context, itemIndex, animation);
|
||||
}
|
||||
return separatorBuilder(context, itemIndex, animation);
|
||||
},
|
||||
);
|
||||
|
||||
/// The state from the closest instance of this class that encloses the given
|
||||
/// context.
|
||||
///
|
||||
@ -148,12 +234,20 @@ class AnimatedList extends _AnimatedScrollView {
|
||||
return context.findAncestorStateOfType<AnimatedListState>();
|
||||
}
|
||||
|
||||
// Helper method to compute the actual child count when taking separators into account.
|
||||
static int _computeChildCountWithSeparators(int itemCount) {
|
||||
if (itemCount == 0) {
|
||||
return 0;
|
||||
}
|
||||
return itemCount * 2 - 1;
|
||||
}
|
||||
|
||||
@override
|
||||
AnimatedListState createState() => AnimatedListState();
|
||||
}
|
||||
|
||||
/// The [AnimatedListState] for [AnimatedList], a scrolling list container that animates items when they are
|
||||
/// inserted or removed.
|
||||
/// The [AnimatedListState] for [AnimatedList], a scrolling list container that
|
||||
/// animates items when they are inserted or removed.
|
||||
///
|
||||
/// When an item is inserted with [insertItem] an animation begins running. The
|
||||
/// animation is passed to [AnimatedList.itemBuilder] whenever the item's widget
|
||||
@ -163,9 +257,13 @@ class AnimatedList extends _AnimatedScrollView {
|
||||
/// The animation is passed to [AnimatedList.itemBuilder] whenever the item's widget
|
||||
/// is needed.
|
||||
///
|
||||
/// If using [AnimatedList.separated], the animation is also passed to
|
||||
/// `AnimatedList.separatorBuilder` whenever the separator'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.
|
||||
/// parameter. If using [AnimatedList.separated], the corresponding separator's
|
||||
/// animation is also passed to the [AnimatedList.removedSeparatorBuilder] parameter.
|
||||
///
|
||||
/// An app that needs to insert or remove items in response to an event
|
||||
/// can refer to the [AnimatedList]'s state with a global key:
|
||||
@ -427,6 +525,7 @@ abstract class _AnimatedScrollView extends StatefulWidget {
|
||||
const _AnimatedScrollView({
|
||||
super.key,
|
||||
required this.itemBuilder,
|
||||
this.removedSeparatorBuilder,
|
||||
this.initialItemCount = 0,
|
||||
this.scrollDirection = Axis.vertical,
|
||||
this.reverse = false,
|
||||
@ -456,6 +555,22 @@ abstract class _AnimatedScrollView extends StatefulWidget {
|
||||
/// {@endtemplate}
|
||||
final AnimatedItemBuilder itemBuilder;
|
||||
|
||||
/// {@template flutter.widgets.AnimatedScrollView.removedSeparatorBuilder}
|
||||
/// Called, as needed, to build separator widgets.
|
||||
///
|
||||
/// Separators are only built when they're scrolled into view.
|
||||
///
|
||||
/// The [AnimatedItemBuilder] index parameter indicates the
|
||||
/// separator's corresponding item's position in the scroll view. The value
|
||||
/// of the index parameter will be between 0 and [initialItemCount] plus the
|
||||
/// total number of items that have been inserted with [AnimatedListState.insertItem]
|
||||
/// and less the total number of items that have been removed with [AnimatedListState.removeItem].
|
||||
///
|
||||
/// Implementations of this callback should assume that
|
||||
/// `removeItem` removes an item immediately.
|
||||
/// {@endtemplate}
|
||||
final AnimatedItemBuilder? removedSeparatorBuilder;
|
||||
|
||||
/// {@template flutter.widgets.AnimatedScrollView.initialItemCount}
|
||||
/// The number of items the [AnimatedList] or [AnimatedGrid] will start with.
|
||||
///
|
||||
@ -546,51 +661,142 @@ abstract class _AnimatedScrollViewState<T extends _AnimatedScrollView> extends S
|
||||
/// to [AnimatedGrid.itemBuilder] or [AnimatedList.itemBuilder] when the item
|
||||
/// is visible.
|
||||
///
|
||||
/// If using [AnimatedList.separated] the animation will also be passed
|
||||
/// to `separatorBuilder`.
|
||||
///
|
||||
/// This method's semantics are the same as Dart's [List.insert] method: it
|
||||
/// increases the length of the list of items by one and shifts
|
||||
/// all items at or after [index] towards the end of the list of items.
|
||||
void insertItem(int index, { Duration duration = _kDuration }) {
|
||||
_sliverAnimatedMultiBoxKey.currentState!.insertItem(index, duration: duration);
|
||||
if (widget.removedSeparatorBuilder == null) {
|
||||
_sliverAnimatedMultiBoxKey.currentState!.insertItem(index, duration: duration);
|
||||
} else {
|
||||
final int itemIndex = _computeItemIndex(index);
|
||||
_sliverAnimatedMultiBoxKey.currentState!.insertItem(itemIndex, duration: duration);
|
||||
if (_itemsCount > 1) {
|
||||
// Because `insertItem` moves the items after the index, we need to insert the separator
|
||||
// at the same index as the item. If there is only one item, we don't need to insert a separator.
|
||||
_sliverAnimatedMultiBoxKey.currentState!.insertItem(itemIndex, 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.
|
||||
///
|
||||
/// If using [AnimatedList.separated] the animation will also be passed to `separatorBuilder`.
|
||||
void insertAllItems(int index, int length, { Duration duration = _kDuration, bool isAsync = false }) {
|
||||
_sliverAnimatedMultiBoxKey.currentState!.insertAllItems(index, length, duration: duration);
|
||||
if (widget.removedSeparatorBuilder == null) {
|
||||
_sliverAnimatedMultiBoxKey.currentState!.insertAllItems(index, length, duration: duration);
|
||||
} else {
|
||||
final int itemIndex = _computeItemIndex(index);
|
||||
final int lengthWithSeparators = _itemsCount == 0 ? length * 2 - 1 : length * 2;
|
||||
_sliverAnimatedMultiBoxKey.currentState!.insertAllItems(itemIndex, lengthWithSeparators, duration: duration);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the item at `index` and start an animation that will be passed to
|
||||
/// `builder` when the item is visible.
|
||||
/// Remove the item at [index] and start an animation that will be passed to
|
||||
/// [builder] when the item is visible.
|
||||
///
|
||||
/// If using [AnimatedList.separated], the animation will also be passed to the
|
||||
/// corresponding separator's [AnimatedList.removedSeparatorBuilder].
|
||||
///
|
||||
/// Items are removed immediately. After an item has been removed, its index
|
||||
/// will no longer be passed to the `itemBuilder`. However, the
|
||||
/// item will still appear for `duration` and during that time
|
||||
/// `builder` must construct its widget as needed.
|
||||
/// will no longer be passed to the [builder]. However, the
|
||||
/// item 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.remove] method: it
|
||||
/// decreases the length of items by one and shifts all items at or before
|
||||
/// `index` towards the beginning of the list of items.
|
||||
/// [index] towards the beginning of the list of items.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [AnimatedRemovedItemBuilder], which describes the arguments to the
|
||||
/// `builder` argument.
|
||||
/// [builder] argument.
|
||||
void removeItem(int index, AnimatedRemovedItemBuilder builder, { Duration duration = _kDuration }) {
|
||||
_sliverAnimatedMultiBoxKey.currentState!.removeItem(index, builder, duration: duration);
|
||||
final AnimatedItemBuilder? removedSeparatorBuilder = widget.removedSeparatorBuilder;
|
||||
if (removedSeparatorBuilder == null) {
|
||||
// There are no separators. Remove only the item.
|
||||
_sliverAnimatedMultiBoxKey.currentState!.removeItem(index, builder, duration: duration);
|
||||
} else {
|
||||
final int itemIndex = _computeItemIndex(index);
|
||||
// Remove the item
|
||||
_sliverAnimatedMultiBoxKey.currentState!.removeItem(itemIndex, builder, duration: duration);
|
||||
if (_itemsCount > 1) {
|
||||
if (itemIndex == _itemsCount - 1) {
|
||||
// The item was removed from the end of the list, so the separator to remove is the one at `last index` - 1.
|
||||
_sliverAnimatedMultiBoxKey.currentState!.removeItem(itemIndex - 1, _toRemovedItemBuilder(removedSeparatorBuilder, index - 1), duration: duration);
|
||||
} else {
|
||||
// The item was removed from the middle or beginning of the list,
|
||||
// so the corresponding separator took its place and needs to be removed at `itemIndex`.
|
||||
_sliverAnimatedMultiBoxKey.currentState!.removeItem(itemIndex, _toRemovedItemBuilder(removedSeparatorBuilder, index), duration: duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove all the items and start an animation that will be passed to
|
||||
/// `builder` when the items are visible.
|
||||
/// [builder] when the items are visible.
|
||||
///
|
||||
/// If using [AnimatedList.separated], the animation will also be passed
|
||||
/// to the corresponding separator's [AnimatedList.removedSeparatorBuilder].
|
||||
///
|
||||
/// Items are removed immediately. However, the
|
||||
/// items will still appear for `duration`, and during that time
|
||||
/// `builder` must construct its widget as needed.
|
||||
/// 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.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [AnimatedRemovedItemBuilder], which describes the arguments to the
|
||||
/// [builder] argument.
|
||||
void removeAllItems(AnimatedRemovedItemBuilder builder, { Duration duration = _kDuration }) {
|
||||
_sliverAnimatedMultiBoxKey.currentState!.removeAllItems(builder, duration: duration);
|
||||
final AnimatedItemBuilder? removedSeparatorBuilder = widget.removedSeparatorBuilder;
|
||||
if (removedSeparatorBuilder == null) {
|
||||
// There are no separators. We can remove all items with the same builder.
|
||||
_sliverAnimatedMultiBoxKey.currentState!.removeAllItems(builder, duration: duration);
|
||||
return;
|
||||
}
|
||||
|
||||
// There are separators. We need to remove items and separators separately
|
||||
// with the corresponding builders.
|
||||
for (int index = _itemsCount - 1; index >= 0 ; index--) {
|
||||
if (index.isEven) {
|
||||
_sliverAnimatedMultiBoxKey.currentState!.removeItem(index, builder, duration: duration);
|
||||
} else {
|
||||
// The index of the separator's corresponding item
|
||||
final int itemIndex = index ~/ 2;
|
||||
_sliverAnimatedMultiBoxKey.currentState!.removeItem(index, _toRemovedItemBuilder(removedSeparatorBuilder, itemIndex), duration: duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int get _itemsCount => _sliverAnimatedMultiBoxKey.currentState!._itemsCount;
|
||||
|
||||
// Helper method to compute the index for the item to insert or remove considering the separators in between.
|
||||
int _computeItemIndex(int index) {
|
||||
if (index == 0) {
|
||||
return index;
|
||||
}
|
||||
final int itemsAndSeparatorsCount = _itemsCount;
|
||||
final int separatorsCount = itemsAndSeparatorsCount ~/ 2;
|
||||
final int separatedItemsCount = _itemsCount - separatorsCount;
|
||||
|
||||
final bool isNewLastIndex = index == separatedItemsCount;
|
||||
final int indexAdjustedForSeparators = index * 2;
|
||||
return isNewLastIndex ? indexAdjustedForSeparators - 1 : indexAdjustedForSeparators;
|
||||
}
|
||||
|
||||
// Helper method to create an [AnimatedRemovedItemBuilder]
|
||||
// from an [AnimatedItemBuilder] for given [index].
|
||||
AnimatedRemovedItemBuilder _toRemovedItemBuilder(AnimatedItemBuilder builder, int index) {
|
||||
return (BuildContext context, Animation<double> animation) {
|
||||
return builder(context, index, animation);
|
||||
};
|
||||
}
|
||||
|
||||
Widget _wrap(Widget sliver, Axis direction) {
|
||||
@ -635,14 +841,22 @@ abstract class _AnimatedScrollViewState<T extends _AnimatedScrollView> extends S
|
||||
}
|
||||
}
|
||||
|
||||
/// Signature for the builder callback used by [AnimatedList] & [AnimatedGrid] to
|
||||
/// build their animated children.
|
||||
/// Signature for the builder callback used by [AnimatedList], [AnimatedList.separated]
|
||||
/// & [AnimatedGrid] to build their animated children.
|
||||
///
|
||||
/// The `context` argument is the build context where the widget will be
|
||||
/// created, the `index` is the index of the item to be built, and the
|
||||
/// `animation` is an [Animation] that should be used to animate an entry
|
||||
/// This signature is also used by [AnimatedList.separated] to build its separators and
|
||||
/// to animate their exit transition after their corresponding item has been removed.
|
||||
///
|
||||
/// The [context] argument is the build context where the widget will be
|
||||
/// created, the [index] is the index of the item to be built, and the
|
||||
/// [animation] is an [Animation] that should be used to animate an entry
|
||||
/// transition for the widget that is built.
|
||||
///
|
||||
/// For [AnimatedList.separated], the [index] is the index
|
||||
/// of the corresponding item of the separator that is built or removed.
|
||||
/// For [AnimatedList.separated] `removedSeparatorBuilder`, the [animation] should be used
|
||||
/// to animate an exit transition for the widget that is built.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [AnimatedRemovedItemBuilder], a builder that is for removing items with
|
||||
@ -653,8 +867,8 @@ typedef AnimatedItemBuilder = Widget Function(BuildContext context, int index, A
|
||||
/// [AnimatedGridState.removeItem] to animate their children after they have
|
||||
/// been removed.
|
||||
///
|
||||
/// The `context` argument is the build context where the widget will be
|
||||
/// created, and the `animation` is an [Animation] that should be used to
|
||||
/// The [context] argument is the build context where the widget will be
|
||||
/// created, and the [animation] is an [Animation] that should be used to
|
||||
/// animate an exit transition for the widget that is built.
|
||||
///
|
||||
/// See also:
|
||||
|
||||
@ -688,6 +688,470 @@ void main() {
|
||||
// Verify that the left/right padding is not applied.
|
||||
expect(innerMediaQueryPadding, const EdgeInsets.symmetric(horizontal: 30.0));
|
||||
});
|
||||
|
||||
testWidgets('AnimatedList.separated', (WidgetTester tester) async {
|
||||
tester.view.physicalSize = const Size(600, 1800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.reset);
|
||||
|
||||
Widget builder(BuildContext context, int index, Animation<double> animation) {
|
||||
return SizedBox(
|
||||
height: 100.0,
|
||||
child: Center(
|
||||
child: Text('item $index'),
|
||||
),
|
||||
);
|
||||
}
|
||||
Widget separatorBuilder(BuildContext context, int index, Animation<double> animation) {
|
||||
return SizedBox(
|
||||
height: 100.0,
|
||||
child: Center(
|
||||
child: Text('separator after item $index'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget itemRemovalBuilder(BuildContext context, int? index, Animation<double> animation) {
|
||||
final String text = index != null ? 'removing item $index' : 'removing item';
|
||||
return SizedBox(
|
||||
height: 100.0,
|
||||
child: Center(child: Text(text)),
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to wrap itemRemovalBuilder with index
|
||||
// to allow testing removal of an item at the expected index.
|
||||
// Null index is necessary for removeAllItems.
|
||||
AnimatedRemovedItemBuilder itemRemovalBuilderWrapper({int? index}) {
|
||||
return (BuildContext context, Animation<double> animation) {
|
||||
return itemRemovalBuilder(context, index, animation);
|
||||
};
|
||||
}
|
||||
|
||||
Widget separatorRemovalBuilder(BuildContext context, int index, Animation<double> animation) {
|
||||
return SizedBox(
|
||||
height: 100.0,
|
||||
child: Center(child: Text('removing separator after item $index')),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
List<Text> getItemsSeparatorsTexts(WidgetTester tester) {
|
||||
final Finder itemsSeparators = find.descendant(of: find.byType(SliverAnimatedList), matching: find.byType(Text));
|
||||
return itemsSeparators.allCandidates.map((Element e) => e.widget).whereType<Text>().toList();
|
||||
}
|
||||
|
||||
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: AnimatedList.separated(
|
||||
key: listKey,
|
||||
initialItemCount: 2,
|
||||
itemBuilder: builder,
|
||||
separatorBuilder: separatorBuilder,
|
||||
removedSeparatorBuilder: separatorRemovalBuilder,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Finder sliverAnimatedList = find.byType(SliverAnimatedList);
|
||||
expect(sliverAnimatedList, findsOneWidget);
|
||||
expect((sliverAnimatedList.evaluate().first.widget as SliverAnimatedList).initialItemCount, 3); // 2 items + 1 separator
|
||||
|
||||
List<Text> itemsSeparatorsTexts;
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 3);
|
||||
expect(itemsSeparatorsTexts[0].data, 'item 0');
|
||||
expect(itemsSeparatorsTexts[1].data, 'separator after item 0');
|
||||
expect(itemsSeparatorsTexts[2].data, 'item 1');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Begin testing
|
||||
|
||||
// insertItem - Insert at beginning of list
|
||||
listKey.currentState!.insertItem(0);
|
||||
await tester.pump();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 5);
|
||||
expect(itemsSeparatorsTexts[0].data, 'item 0');
|
||||
expect(itemsSeparatorsTexts[1].data, 'separator after item 0');
|
||||
expect(itemsSeparatorsTexts[2].data, 'item 1');
|
||||
expect(itemsSeparatorsTexts[3].data, 'separator after item 1');
|
||||
expect(itemsSeparatorsTexts[4].data, 'item 2');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// insertItem - Insert at end of list
|
||||
listKey.currentState!.insertItem(3);
|
||||
await tester.pump();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 7);
|
||||
expect(itemsSeparatorsTexts[0].data, 'item 0');
|
||||
expect(itemsSeparatorsTexts[1].data, 'separator after item 0');
|
||||
expect(itemsSeparatorsTexts[2].data, 'item 1');
|
||||
expect(itemsSeparatorsTexts[3].data, 'separator after item 1');
|
||||
expect(itemsSeparatorsTexts[4].data, 'item 2');
|
||||
expect(itemsSeparatorsTexts[5].data, 'separator after item 2');
|
||||
expect(itemsSeparatorsTexts[6].data, 'item 3');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// insertItem - Insert in middle of list
|
||||
listKey.currentState!.insertItem(2);
|
||||
await tester.pump();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 9);
|
||||
expect(itemsSeparatorsTexts[0].data, 'item 0');
|
||||
expect(itemsSeparatorsTexts[1].data, 'separator after item 0');
|
||||
expect(itemsSeparatorsTexts[2].data, 'item 1');
|
||||
expect(itemsSeparatorsTexts[3].data, 'separator after item 1');
|
||||
expect(itemsSeparatorsTexts[4].data, 'item 2');
|
||||
expect(itemsSeparatorsTexts[5].data, 'separator after item 2');
|
||||
expect(itemsSeparatorsTexts[6].data, 'item 3');
|
||||
expect(itemsSeparatorsTexts[7].data, 'separator after item 3');
|
||||
expect(itemsSeparatorsTexts[8].data, 'item 4');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// insertItem - Insert at negative index
|
||||
expect(() => listKey.currentState!.insertItem(-1), throwsAssertionError);
|
||||
|
||||
// insertItem - Insert at index greater than itemCount
|
||||
expect(() => listKey.currentState!.insertItem(42), throwsAssertionError);
|
||||
|
||||
// removeItem - Remove at beginning of list
|
||||
listKey.currentState!.removeItem(
|
||||
0,
|
||||
itemRemovalBuilderWrapper(index: 0),
|
||||
duration: const Duration(milliseconds: 100),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 9);
|
||||
expect(itemsSeparatorsTexts[0].data, 'removing item 0');
|
||||
expect(itemsSeparatorsTexts[1].data, 'removing separator after item 0');
|
||||
expect(itemsSeparatorsTexts[2].data, 'item 0');
|
||||
expect(itemsSeparatorsTexts[3].data, 'separator after item 0');
|
||||
expect(itemsSeparatorsTexts[4].data, 'item 1');
|
||||
expect(itemsSeparatorsTexts[5].data, 'separator after item 1');
|
||||
expect(itemsSeparatorsTexts[6].data, 'item 2');
|
||||
expect(itemsSeparatorsTexts[7].data, 'separator after item 2');
|
||||
expect(itemsSeparatorsTexts[8].data, 'item 3');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 7);
|
||||
expect(itemsSeparatorsTexts[0].data, 'item 0');
|
||||
expect(itemsSeparatorsTexts[1].data, 'separator after item 0');
|
||||
expect(itemsSeparatorsTexts[2].data, 'item 1');
|
||||
expect(itemsSeparatorsTexts[3].data, 'separator after item 1');
|
||||
expect(itemsSeparatorsTexts[4].data, 'item 2');
|
||||
expect(itemsSeparatorsTexts[5].data, 'separator after item 2');
|
||||
expect(itemsSeparatorsTexts[6].data, 'item 3');
|
||||
|
||||
// removeItem - Remove at end of list
|
||||
listKey.currentState!.removeItem(
|
||||
3,
|
||||
itemRemovalBuilderWrapper(index: 3),
|
||||
duration: const Duration(milliseconds: 100),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 7);
|
||||
expect(itemsSeparatorsTexts[0].data, 'item 0');
|
||||
expect(itemsSeparatorsTexts[1].data, 'separator after item 0');
|
||||
expect(itemsSeparatorsTexts[2].data, 'item 1');
|
||||
expect(itemsSeparatorsTexts[3].data, 'separator after item 1');
|
||||
expect(itemsSeparatorsTexts[4].data, 'item 2');
|
||||
expect(itemsSeparatorsTexts[5].data, 'removing separator after item 2');
|
||||
expect(itemsSeparatorsTexts[6].data, 'removing item 3');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// removeItem - Remove in middle of list
|
||||
listKey.currentState!.removeItem(
|
||||
1,
|
||||
itemRemovalBuilderWrapper(index: 1),
|
||||
duration: const Duration(milliseconds: 100),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 5);
|
||||
expect(itemsSeparatorsTexts[0].data, 'item 0');
|
||||
expect(itemsSeparatorsTexts[1].data, 'separator after item 0');
|
||||
expect(itemsSeparatorsTexts[2].data, 'removing item 1');
|
||||
expect(itemsSeparatorsTexts[3].data, 'removing separator after item 1');
|
||||
expect(itemsSeparatorsTexts[4].data, 'item 1');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 3);
|
||||
expect(itemsSeparatorsTexts[0].data, 'item 0');
|
||||
expect(itemsSeparatorsTexts[1].data, 'separator after item 0');
|
||||
expect(itemsSeparatorsTexts[2].data, 'item 1');
|
||||
|
||||
// removeItem - Remove at negative index
|
||||
expect(
|
||||
() => listKey.currentState!.removeItem(
|
||||
-1,
|
||||
itemRemovalBuilderWrapper(index: -1),
|
||||
duration: const Duration(milliseconds: 100),
|
||||
),
|
||||
throwsAssertionError,
|
||||
);
|
||||
|
||||
// removeItem - Remove at index greater than itemCount
|
||||
expect(
|
||||
() => listKey.currentState!.removeItem(
|
||||
42,
|
||||
itemRemovalBuilderWrapper(index: 42),
|
||||
duration: const Duration(milliseconds: 100),
|
||||
),
|
||||
throwsAssertionError,
|
||||
);
|
||||
|
||||
// insertAllItems - Insert no items
|
||||
listKey.currentState!.insertAllItems(0, 0);
|
||||
await tester.pump();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 3);
|
||||
expect(itemsSeparatorsTexts[0].data, 'item 0');
|
||||
expect(itemsSeparatorsTexts[1].data, 'separator after item 0');
|
||||
expect(itemsSeparatorsTexts[2].data, 'item 1');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// insertAllItems - Insert negative number of items
|
||||
listKey.currentState!.insertAllItems(0, -1);
|
||||
await tester.pump();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 3);
|
||||
expect(itemsSeparatorsTexts[0].data, 'item 0');
|
||||
expect(itemsSeparatorsTexts[1].data, 'separator after item 0');
|
||||
expect(itemsSeparatorsTexts[2].data, 'item 1');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// insertAllItems - Insert at beginning of list
|
||||
listKey.currentState!.insertAllItems(0, 2);
|
||||
await tester.pump();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 7);
|
||||
expect(itemsSeparatorsTexts[0].data, 'item 0');
|
||||
expect(itemsSeparatorsTexts[1].data, 'separator after item 0');
|
||||
expect(itemsSeparatorsTexts[2].data, 'item 1');
|
||||
expect(itemsSeparatorsTexts[3].data, 'separator after item 1');
|
||||
expect(itemsSeparatorsTexts[4].data, 'item 2');
|
||||
expect(itemsSeparatorsTexts[5].data, 'separator after item 2');
|
||||
expect(itemsSeparatorsTexts[6].data, 'item 3');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// insertAllItems - Insert at end of list
|
||||
listKey.currentState!.insertAllItems(4, 2);
|
||||
await tester.pump();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 11);
|
||||
expect(itemsSeparatorsTexts[0].data, 'item 0');
|
||||
expect(itemsSeparatorsTexts[1].data, 'separator after item 0');
|
||||
expect(itemsSeparatorsTexts[2].data, 'item 1');
|
||||
expect(itemsSeparatorsTexts[3].data, 'separator after item 1');
|
||||
expect(itemsSeparatorsTexts[4].data, 'item 2');
|
||||
expect(itemsSeparatorsTexts[5].data, 'separator after item 2');
|
||||
expect(itemsSeparatorsTexts[6].data, 'item 3');
|
||||
expect(itemsSeparatorsTexts[7].data, 'separator after item 3');
|
||||
expect(itemsSeparatorsTexts[8].data, 'item 4');
|
||||
expect(itemsSeparatorsTexts[9].data, 'separator after item 4');
|
||||
expect(itemsSeparatorsTexts[10].data, 'item 5');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// insertAllItems - Insert in middle of list
|
||||
listKey.currentState!.insertAllItems(3, 2);
|
||||
await tester.pump();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 15);
|
||||
expect(itemsSeparatorsTexts[0].data, 'item 0');
|
||||
expect(itemsSeparatorsTexts[1].data, 'separator after item 0');
|
||||
expect(itemsSeparatorsTexts[2].data, 'item 1');
|
||||
expect(itemsSeparatorsTexts[3].data, 'separator after item 1');
|
||||
expect(itemsSeparatorsTexts[4].data, 'item 2');
|
||||
expect(itemsSeparatorsTexts[5].data, 'separator after item 2');
|
||||
expect(itemsSeparatorsTexts[6].data, 'item 3');
|
||||
expect(itemsSeparatorsTexts[7].data, 'separator after item 3');
|
||||
expect(itemsSeparatorsTexts[8].data, 'item 4');
|
||||
expect(itemsSeparatorsTexts[9].data, 'separator after item 4');
|
||||
expect(itemsSeparatorsTexts[10].data, 'item 5');
|
||||
expect(itemsSeparatorsTexts[11].data, 'separator after item 5');
|
||||
expect(itemsSeparatorsTexts[12].data, 'item 6');
|
||||
expect(itemsSeparatorsTexts[13].data, 'separator after item 6');
|
||||
expect(itemsSeparatorsTexts[14].data, 'item 7');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// insertAllItems - Insert at negative index
|
||||
expect(() => listKey.currentState!.insertAllItems(-1, 2), throwsAssertionError);
|
||||
|
||||
// insertAllItems - Insert at index greater than itemCount
|
||||
expect(() => listKey.currentState!.insertAllItems(42, 2), throwsAssertionError);
|
||||
|
||||
// removeAllItems - Remove all items from list with multiple items
|
||||
listKey.currentState!.removeAllItems(
|
||||
itemRemovalBuilderWrapper(),
|
||||
duration: const Duration(milliseconds: 100),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 15);
|
||||
expect(itemsSeparatorsTexts[0].data, 'removing item');
|
||||
expect(itemsSeparatorsTexts[1].data, 'removing separator after item 0');
|
||||
expect(itemsSeparatorsTexts[2].data, 'removing item');
|
||||
expect(itemsSeparatorsTexts[3].data, 'removing separator after item 1');
|
||||
expect(itemsSeparatorsTexts[4].data, 'removing item');
|
||||
expect(itemsSeparatorsTexts[5].data, 'removing separator after item 2');
|
||||
expect(itemsSeparatorsTexts[6].data, 'removing item');
|
||||
expect(itemsSeparatorsTexts[7].data, 'removing separator after item 3');
|
||||
expect(itemsSeparatorsTexts[8].data, 'removing item');
|
||||
expect(itemsSeparatorsTexts[9].data, 'removing separator after item 4');
|
||||
expect(itemsSeparatorsTexts[10].data, 'removing item');
|
||||
expect(itemsSeparatorsTexts[11].data, 'removing separator after item 5');
|
||||
expect(itemsSeparatorsTexts[12].data, 'removing item');
|
||||
expect(itemsSeparatorsTexts[13].data, 'removing separator after item 6');
|
||||
expect(itemsSeparatorsTexts[14].data, 'removing item');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// removeItem - Remove from empty list
|
||||
expect(
|
||||
() => listKey.currentState!.removeItem(
|
||||
0,
|
||||
itemRemovalBuilderWrapper(index: 0),
|
||||
duration: const Duration(milliseconds: 100),
|
||||
),
|
||||
throwsAssertionError,
|
||||
);
|
||||
|
||||
// removeItem - Remove item from list with single item
|
||||
// Prepare
|
||||
listKey.currentState!.insertItem(0);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 1);
|
||||
expect(itemsSeparatorsTexts[0].data, 'item 0');
|
||||
|
||||
// Test
|
||||
listKey.currentState!.removeItem(
|
||||
0,
|
||||
itemRemovalBuilderWrapper(index: 0),
|
||||
duration: const Duration(milliseconds: 100),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 1);
|
||||
expect(itemsSeparatorsTexts[0].data, 'removing item 0');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 0);
|
||||
|
||||
// removeAllItems - Remove all items from empty list
|
||||
listKey.currentState!.removeAllItems(
|
||||
itemRemovalBuilderWrapper(),
|
||||
duration: const Duration(milliseconds: 100),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 0);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// removeAllItems - Remove all items from list with single item
|
||||
// Prepare
|
||||
listKey.currentState!.insertItem(0);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 1);
|
||||
expect(itemsSeparatorsTexts[0].data, 'item 0');
|
||||
|
||||
// Test
|
||||
listKey.currentState!.removeAllItems(
|
||||
itemRemovalBuilderWrapper(),
|
||||
duration: const Duration(milliseconds: 100),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 1);
|
||||
expect(itemsSeparatorsTexts[0].data, 'removing item');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 0);
|
||||
|
||||
// insertAllItems - Insert into empty list
|
||||
listKey.currentState!.insertAllItems(0, 2);
|
||||
await tester.pump();
|
||||
|
||||
itemsSeparatorsTexts = getItemsSeparatorsTexts(tester);
|
||||
|
||||
expect(itemsSeparatorsTexts.length, 3);
|
||||
expect(itemsSeparatorsTexts[0].data, 'item 0');
|
||||
expect(itemsSeparatorsTexts[1].data, 'separator after item 0');
|
||||
expect(itemsSeparatorsTexts[2].data, 'item 1');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user