Unverified Commit 83878683 authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Re-landing SliverAnimatedList (#42485)

parent 6db4c44d
...@@ -10,6 +10,7 @@ import 'framework.dart'; ...@@ -10,6 +10,7 @@ import 'framework.dart';
import 'scroll_controller.dart'; import 'scroll_controller.dart';
import 'scroll_physics.dart'; import 'scroll_physics.dart';
import 'scroll_view.dart'; import 'scroll_view.dart';
import 'sliver.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
/// Signature for the builder callback used by [AnimatedList]. /// Signature for the builder callback used by [AnimatedList].
...@@ -39,15 +40,234 @@ class _ActiveItem implements Comparable<_ActiveItem> { ...@@ -39,15 +40,234 @@ class _ActiveItem implements Comparable<_ActiveItem> {
/// A scrolling container that animates items when they are inserted or removed. /// A scrolling container that animates items when they are inserted or removed.
/// ///
/// This widget's [AnimatedListState] can be used to dynamically insert or remove /// This widget's [AnimatedListState] can be used to dynamically insert or
/// items. To refer to the [AnimatedListState] either provide a [GlobalKey] or /// remove items. To refer to the [AnimatedListState] either provide a
/// use the static [of] method from an item's input callback. /// [GlobalKey] or use the static [of] method from an item's input callback.
/// ///
/// This widget is similar to one created by [ListView.builder]. /// This widget is similar to one created by [ListView.builder].
/// ///
/// {@youtube 560 315 https://www.youtube.com/watch?v=ZtfItHwFlZ8} /// {@youtube 560 315 https://www.youtube.com/watch?v=ZtfItHwFlZ8}
///
/// {@tool snippet --template=freeform}
/// This sample application uses an [AnimatedList] to create an effect when
/// items are removed or added to the list.
///
/// ```dart imports
/// import 'package:flutter/foundation.dart';
/// import 'package:flutter/material.dart';
/// ```
///
/// ```dart
/// class AnimatedListSample extends StatefulWidget {
/// @override
/// _AnimatedListSampleState createState() => _AnimatedListSampleState();
/// }
///
/// class _AnimatedListSampleState extends State<AnimatedListSample> {
/// final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
/// ListModel<int> _list;
/// int _selectedItem;
/// 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 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 this ListModel is
/// // concerned). The widget will be used by the
/// // [AnimatedListState.removeItem] method's
/// // [AnimatedListRemovedItemBuilder] parameter.
/// Widget _buildRemovedItem(int item, BuildContext context, Animation<double> animation) {
/// return CardItem(
/// animation: animation,
/// item: item,
/// selected: false,
/// // No gesture detector here: we don't want removed items to be interactive.
/// );
/// }
///
/// // Insert the "next item" into the list model.
/// void _insert() {
/// final int index = _selectedItem == null ? _list.length : _list.indexOf(_selectedItem);
/// _list.insert(index, _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'),
/// 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(
/// key: _listKey,
/// initialItemCount: _list.length,
/// itemBuilder: _buildItem,
/// ),
/// ),
/// ),
/// );
/// }
/// }
///
/// /// Keeps a Dart [List] in sync with an [AnimatedList].
/// ///
/// /// 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 [AnimatedList.removeItem].
/// class ListModel<E> {
/// ListModel({
/// @required this.listKey,
/// @required this.removedItemBuilder,
/// Iterable<E> initialItems,
/// }) : assert(listKey != null),
/// assert(removedItemBuilder != null),
/// _items = List<E>.from(initialItems ?? <E>[]);
///
/// final GlobalKey<AnimatedListState> listKey;
/// final dynamic 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) => 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 128 as the animation varies from 0.0 to 1.0.
/// class CardItem extends StatelessWidget {
/// const CardItem({
/// Key key,
/// @required this.animation,
/// this.onTap,
/// @required this.item,
/// this.selected: false
/// }) : assert(animation != null),
/// assert(item != null && item >= 0),
/// assert(selected != null),
/// super(key: key);
///
/// final Animation<double> animation;
/// final VoidCallback onTap;
/// final int item;
/// final bool selected;
///
/// @override
/// Widget build(BuildContext context) {
/// TextStyle textStyle = Theme.of(context).textTheme.display1;
/// if (selected)
/// textStyle = textStyle.copyWith(color: Colors.lightGreenAccent[400]);
/// return Padding(
/// padding: const EdgeInsets.all(2.0),
/// child: SizeTransition(
/// axis: Axis.vertical,
/// sizeFactor: animation,
/// child: GestureDetector(
/// behavior: HitTestBehavior.opaque,
/// onTap: onTap,
/// child: SizedBox(
/// height: 128.0,
/// child: Card(
/// color: Colors.primaries[item % Colors.primaries.length],
/// child: Center(
/// child: Text('Item $item', style: textStyle),
/// ),
/// ),
/// ),
/// ),
/// ),
/// );
/// }
/// }
///
/// void main() {
/// runApp(AnimatedListSample());
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [SliverAnimatedList], a sliver that animates items when they are inserted
/// or removed from a list.
class AnimatedList extends StatefulWidget { class AnimatedList extends StatefulWidget {
/// Creates a scrolling container that animates items when they are inserted or removed. /// Creates a scrolling container that animates items when they are inserted
/// or removed.
const AnimatedList({ const AnimatedList({
Key key, Key key,
@required this.itemBuilder, @required this.itemBuilder,
...@@ -77,11 +297,13 @@ class AnimatedList extends StatefulWidget { ...@@ -77,11 +297,13 @@ class AnimatedList extends StatefulWidget {
/// [AnimatedListState.removeItem] removes an item immediately. /// [AnimatedListState.removeItem] removes an item immediately.
final AnimatedListItemBuilder itemBuilder; final AnimatedListItemBuilder itemBuilder;
/// {@template flutter.widgets.animatedList.initialItemCount}
/// The number of items the list will start with. /// The number of items the list will start with.
/// ///
/// The appearance of the initial items is not animated. They /// The appearance of the initial items is not animated. They
/// are created, as needed, by [itemBuilder] with an animation parameter /// are created, as needed, by [itemBuilder] with an animation parameter
/// of [kAlwaysCompleteAnimation]. /// of [kAlwaysCompleteAnimation].
/// {@endtemplate}
final int initialItemCount; final int initialItemCount;
/// The axis along which the scroll view scrolls. /// The axis along which the scroll view scrolls.
...@@ -154,10 +376,11 @@ class AnimatedList extends StatefulWidget { ...@@ -154,10 +376,11 @@ class AnimatedList extends StatefulWidget {
/// The amount of space by which to inset the children. /// The amount of space by which to inset the children.
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
/// The state from the closest instance of this class that encloses the given context. /// The state from the closest instance of this class that encloses the given
/// context.
/// ///
/// This method is typically used by [AnimatedList] item widgets that insert or /// This method is typically used by [AnimatedList] item widgets that insert
/// remove items in response to user input. /// or remove items in response to user input.
/// ///
/// ```dart /// ```dart
/// AnimatedListState animatedList = AnimatedList.of(context); /// AnimatedListState animatedList = AnimatedList.of(context);
...@@ -170,11 +393,11 @@ class AnimatedList extends StatefulWidget { ...@@ -170,11 +393,11 @@ class AnimatedList extends StatefulWidget {
return result; return result;
throw FlutterError( throw FlutterError(
'AnimatedList.of() called with a context that does not contain an AnimatedList.\n' 'AnimatedList.of() called with a context that does not contain an AnimatedList.\n'
'No AnimatedList ancestor could be found starting from the context that was passed to AnimatedList.of(). ' 'No AnimatedList ancestor could be found starting from the context that '
'This can happen when the context provided is from the same StatefulWidget that ' 'was passed to AnimatedList.of(). This can happen when the context '
'built the AnimatedList. Please see the AnimatedList documentation for examples ' 'provided is from the same StatefulWidget that built the AnimatedList. '
'of how to refer to an AnimatedListState object: ' 'Please see the AnimatedList documentation for examples of how to refer to '
' https://api.flutter.dev/flutter/widgets/AnimatedListState-class.html \n' 'an AnimatedListState object: https://api.flutter.dev/flutter/widgets/AnimatedListState-class.html \n'
'The context used was:\n' 'The context used was:\n'
' $context' ' $context'
); );
...@@ -209,6 +432,389 @@ class AnimatedList extends StatefulWidget { ...@@ -209,6 +432,389 @@ class AnimatedList extends StatefulWidget {
/// [AnimatedList] item input handlers can also refer to their [AnimatedListState] /// [AnimatedList] item input handlers can also refer to their [AnimatedListState]
/// with the static [AnimatedList.of] method. /// with the static [AnimatedList.of] method.
class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixin<AnimatedList> { class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixin<AnimatedList> {
final GlobalKey<SliverAnimatedListState> _sliverAnimatedListKey = GlobalKey();
/// Insert an item at [index] and start an animation that will be passed
/// to [AnimatedList.itemBuilder] when the item is visible.
///
/// This method's semantics are the same as Dart's [List.insert] method:
/// it increases the length of the list by one and shifts all items at or
/// after [index] towards the end of the list.
void insertItem(int index, { Duration duration = _kDuration }) {
_sliverAnimatedListKey.currentState.insertItem(index, duration: duration);
}
/// Remove the item at [index] and start an animation that will be passed
/// to [builder] when the item is visible.
///
/// Items are removed immediately. After an item has been removed, its index
/// will no longer be passed to the [AnimatedList.itemBuilder]. However the
/// item will still appear in the list 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 the list by one and shifts all items at or
/// before [index] towards the beginning of the list.
void removeItem(int index, AnimatedListRemovedItemBuilder builder, { Duration duration = _kDuration }) {
_sliverAnimatedListKey.currentState.removeItem(index, builder, duration: duration);
}
@override
Widget build(BuildContext context) {
return CustomScrollView(
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
controller: widget.controller,
primary: widget.primary,
physics: widget.physics,
shrinkWrap: widget.shrinkWrap,
slivers: <Widget>[
SliverPadding(
padding: widget.padding ?? const EdgeInsets.all(0),
sliver: SliverAnimatedList(
key: _sliverAnimatedListKey,
itemBuilder: widget.itemBuilder,
initialItemCount: widget.initialItemCount,
),
),
],
);
}
}
/// A sliver that animates items when they are inserted or removed.
///
/// This widget's [SliverAnimatedListState] can be used to dynamically insert or
/// remove items. To refer to the [SliverAnimatedListState] either provide a
/// [GlobalKey] or use the static [SliverAnimatedList.of] method from an item's
/// input callback.
///
/// {@tool snippet --template=freeform}
/// This sample application uses a [SliverAnimatedList] to create an animated
/// effect when items are removed or added to the list.
///
/// ```dart imports
/// import 'package:flutter/foundation.dart';
/// import 'package:flutter/material.dart';
/// ```
///
/// ```dart
/// void main() => runApp(SliverAnimatedListSample());
///
/// class SliverAnimatedListSample extends StatefulWidget {
/// @override
/// _SliverAnimatedListSampleState createState() => _SliverAnimatedListSampleState();
/// }
///
/// class _SliverAnimatedListSampleState extends State<SliverAnimatedListSample> {
/// final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey<SliverAnimatedListState>();
/// final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
/// ListModel<int> _list;
/// int _selectedItem;
/// 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 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 this ListModel is
/// // concerned). The widget will be used by the
/// // [AnimatedListState.removeItem] method's
/// // [AnimatedListRemovedItemBuilder] parameter.
/// Widget _buildRemovedItem(int item, BuildContext context, Animation<double> animation) {
/// return CardItem(
/// animation: animation,
/// item: item,
/// selected: false,
/// );
/// }
///
/// // Insert the "next item" into the list model.
/// void _insert() {
/// final int index = _selectedItem == null ? _list.length : _list.indexOf(_selectedItem);
/// _list.insert(index, _nextItem++);
/// }
///
/// // Remove the selected item from the list model.
/// void _remove() {
/// if (_selectedItem != null) {
/// _list.removeAt(_list.indexOf(_selectedItem));
/// setState(() {
/// _selectedItem = null;
/// });
/// } else {
/// _scaffoldKey.currentState.showSnackBar(SnackBar(
/// content: Text(
/// 'Select an item to remove from the list.',
/// style: TextStyle(fontSize: 20),
/// ),
/// ));
/// }
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// home: Scaffold(
/// key: _scaffoldKey,
/// body: CustomScrollView(
/// slivers: <Widget>[
/// SliverAppBar(
/// title: Text(
/// 'SliverAnimatedList',
/// style: TextStyle(fontSize: 30),
/// ),
/// expandedHeight: 100,
/// centerTitle: true,
/// backgroundColor: Colors.amber[900],
/// leading: IconButton(
/// icon: const Icon(Icons.add_circle),
/// onPressed: _insert,
/// tooltip: 'Insert a new item.',
/// iconSize: 48,
/// ),
/// actions: [
/// IconButton(
/// icon: const Icon(Icons.remove_circle),
/// onPressed: _remove,
/// tooltip: 'Remove the selected item.',
/// iconSize: 48,
/// ),
/// ],
/// ),
/// SliverAnimatedList(
/// key: _listKey,
/// initialItemCount: _list.length,
/// itemBuilder: _buildItem,
/// ),
/// ],
/// ),
/// ),
/// );
/// }
/// }
///
/// // Keeps a Dart [List] in sync with an [AnimatedList].
/// //
/// // 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 [AnimatedList.removeItem].
/// class ListModel<E> {
/// ListModel({
/// @required this.listKey,
/// @required this.removedItemBuilder,
/// Iterable<E> initialItems,
/// }) : assert(listKey != null),
/// assert(removedItemBuilder != null),
/// _items = List<E>.from(initialItems ?? <E>[]);
///
/// final GlobalKey<SliverAnimatedListState> listKey;
/// final dynamic removedItemBuilder;
/// final List<E> _items;
///
/// SliverAnimatedListState 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) => 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 card turns gray when [selected] is true. This widget's height
/// // is based on the [animation] parameter. It varies as the animation value
/// // transitions from 0.0 to 1.0.
/// class CardItem extends StatelessWidget {
/// const CardItem({
/// Key key,
/// @required this.animation,
/// @required this.item,
/// this.onTap,
/// this.selected = false,
/// }) : assert(animation != null),
/// assert(item != null && item >= 0),
/// assert(selected != null),
/// super(key: key);
///
/// final Animation<double> animation;
/// final VoidCallback onTap;
/// final int item;
/// final bool selected;
///
/// @override
/// Widget build(BuildContext context) {
/// return Padding(
/// padding:
/// const EdgeInsets.only(
/// left: 2.0,
/// right: 2.0,
/// top: 2.0,
/// bottom: 0.0,
/// ),
/// child: SizeTransition(
/// axis: Axis.vertical,
/// sizeFactor: animation,
/// child: GestureDetector(
/// onTap: onTap,
/// child: SizedBox(
/// height: 120.0,
/// child: Card(
/// color: selected
/// ? Colors.black12
/// : Colors.primaries[item % Colors.primaries.length],
/// child: Center(
/// child: Text(
/// 'Item $item',
/// style: Theme.of(context).textTheme.display1,
/// ),
/// ),
/// ),
/// ),
/// ),
/// ),
/// );
/// }
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [SliverList], which does not animate items when they are inserted or
/// removed.
/// * [AnimatedList], a non-sliver scrolling container that animates items when
/// they are inserted or removed.
class SliverAnimatedList extends StatefulWidget {
/// Creates a sliver that animates items when they are inserted or removed.
const SliverAnimatedList({
Key key,
@required this.itemBuilder,
this.initialItemCount = 0,
}) : assert(itemBuilder != null),
assert(initialItemCount != null && initialItemCount >= 0),
super(key: key);
/// Called, as needed, to build list item widgets.
///
/// List items are only built when they're scrolled into view.
///
/// The [AnimatedListItemBuilder] index parameter indicates the item's
/// position in the list. The value of the index parameter will be between 0
/// and [initialItemCount] plus the total number of items that have been
/// inserted with [SliverAnimatedListState.insertItem] and less the total
/// number of items that have been removed with
/// [SliverAnimatedListState.removeItem].
///
/// Implementations of this callback should assume that
/// [SliverAnimatedListState.removeItem] removes an item immediately.
final AnimatedListItemBuilder itemBuilder;
/// {@macro flutter.widgets.animatedList.initialItemCount}
final int initialItemCount;
@override
SliverAnimatedListState createState() => SliverAnimatedListState();
/// The state from the closest instance of this class that encloses the given
/// context.
///
/// This method is typically used by [SliverAnimatedList] item widgets that
/// insert or remove items in response to user input.
///
/// ```dart
/// SliverAnimatedListState animatedList = SliverAnimatedList.of(context);
/// ```
static SliverAnimatedListState of(BuildContext context, {bool nullOk = false}) {
assert(context != null);
assert(nullOk != null);
final SliverAnimatedListState result = context.ancestorStateOfType(const TypeMatcher<SliverAnimatedListState>());
if (nullOk || result != null)
return result;
throw FlutterError(
'SliverAnimatedList.of() called with a context that does not contain a SliverAnimatedList.\n'
'No SliverAnimatedListState ancestor could be found starting from the '
'context that was passed to SliverAnimatedListState.of(). This can '
'happen when the context provided is from the same StatefulWidget that '
'built the AnimatedList. Please see the SliverAnimatedList documentation '
'for examples of how to refer to an AnimatedListState object: '
'https://docs.flutter.io/flutter/widgets/SliverAnimatedListState-class.html \n'
'The context used was:\n'
' $context');
}
}
/// The state for a sliver 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 [SliverAnimatedList.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.
///
/// An app that needs to insert or remove items in response to an event
/// can refer to the [SliverAnimatedList]'s state with a global key:
///
/// ```dart
/// GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
/// ...
/// SliverAnimatedList(key: listKey, ...);
/// ...
/// listKey.currentState.insert(123);
/// ```
///
/// [SliverAnimatedList] item input handlers can also refer to their
/// [SliverAnimatedListState] with the static [SliverAnimatedList.of] method.
class SliverAnimatedListState extends State<SliverAnimatedList> with TickerProviderStateMixin {
final List<_ActiveItem> _incomingItems = <_ActiveItem>[]; final List<_ActiveItem> _incomingItems = <_ActiveItem>[];
final List<_ActiveItem> _outgoingItems = <_ActiveItem>[]; final List<_ActiveItem> _outgoingItems = <_ActiveItem>[];
int _itemsCount = 0; int _itemsCount = 0;
...@@ -221,10 +827,9 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi ...@@ -221,10 +827,9 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
@override @override
void dispose() { void dispose() {
for (_ActiveItem item in _incomingItems) for (_ActiveItem item in _incomingItems.followedBy(_outgoingItems)) {
item.controller.dispose();
for (_ActiveItem item in _outgoingItems)
item.controller.dispose(); item.controller.dispose();
}
super.dispose(); super.dispose();
} }
...@@ -267,8 +872,12 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi ...@@ -267,8 +872,12 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
return index; return index;
} }
/// Insert an item at [index] and start an animation that will be passed SliverChildDelegate _createDelegate() {
/// to [AnimatedList.itemBuilder] when the item is visible. return SliverChildBuilderDelegate(_itemBuilder, childCount: _itemsCount);
}
/// Insert an item at [index] and start an animation that will be passed to
/// [SliverAnimatedList.itemBuilder] when the item is visible.
/// ///
/// This method's semantics are the same as Dart's [List.insert] method: /// This method's semantics are the same as Dart's [List.insert] method:
/// it increases the length of the list by one and shifts all items at or /// it increases the length of the list by one and shifts all items at or
...@@ -291,8 +900,14 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi ...@@ -291,8 +900,14 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
item.itemIndex += 1; item.itemIndex += 1;
} }
final AnimationController controller = AnimationController(duration: duration, vsync: this); final AnimationController controller = AnimationController(
final _ActiveItem incomingItem = _ActiveItem.incoming(controller, itemIndex); duration: duration,
vsync: this,
);
final _ActiveItem incomingItem = _ActiveItem.incoming(
controller,
itemIndex,
);
setState(() { setState(() {
_incomingItems _incomingItems
..add(incomingItem) ..add(incomingItem)
...@@ -309,8 +924,8 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi ...@@ -309,8 +924,8 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
/// to [builder] when the item is visible. /// to [builder] when the item is visible.
/// ///
/// Items are removed immediately. After an item has been removed, its index /// Items are removed immediately. After an item has been removed, its index
/// will no longer be passed to the [AnimatedList.itemBuilder]. However the /// will no longer be passed to the [SliverAnimatedList.itemBuilder]. However
/// item will still appear in the list for [duration] and during that time /// the item will still appear in the list for [duration] and during that time
/// [builder] must construct its widget as needed. /// [builder] must construct its widget as needed.
/// ///
/// This method's semantics are the same as Dart's [List.remove] method: /// This method's semantics are the same as Dart's [List.remove] method:
...@@ -349,34 +964,32 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi ...@@ -349,34 +964,32 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
item.itemIndex -= 1; item.itemIndex -= 1;
} }
setState(() { setState(() => _itemsCount -= 1);
_itemsCount -= 1;
});
}); });
} }
Widget _itemBuilder(BuildContext context, int itemIndex) { Widget _itemBuilder(BuildContext context, int itemIndex) {
final _ActiveItem outgoingItem = _activeItemAt(_outgoingItems, itemIndex); final _ActiveItem outgoingItem = _activeItemAt(_outgoingItems, itemIndex);
if (outgoingItem != null) if (outgoingItem != null) {
return outgoingItem.removedItemBuilder(context, outgoingItem.controller.view); return outgoingItem.removedItemBuilder(
context,
outgoingItem.controller.view,
);
}
final _ActiveItem incomingItem = _activeItemAt(_incomingItems, itemIndex); final _ActiveItem incomingItem = _activeItemAt(_incomingItems, itemIndex);
final Animation<double> animation = incomingItem?.controller?.view ?? kAlwaysCompleteAnimation; final Animation<double> animation = incomingItem?.controller?.view ?? kAlwaysCompleteAnimation;
return widget.itemBuilder(context, _itemIndexToIndex(itemIndex), animation); return widget.itemBuilder(
context,
_itemIndexToIndex(itemIndex),
animation,
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView.builder( return SliverList(
itemBuilder: _itemBuilder, delegate: _createDelegate(),
itemCount: _itemsCount,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
controller: widget.controller,
primary: widget.primary,
physics: widget.physics,
shrinkWrap: widget.shrinkWrap,
padding: widget.padding,
); );
} }
} }
...@@ -6,13 +6,67 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -6,13 +6,67 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
void main() { void main() {
testWidgets('AnimatedList initialItemCount', (WidgetTester tester) async { testWidgets('AnimatedList', (WidgetTester tester) async {
final Map<int, Animation<double>> animations = <int, Animation<double>>{}; final AnimatedListItemBuilder builder = (BuildContext context, int index, Animation<double> animation) {
return SizedBox(
height: 100.0,
child: Center(
child: Text('item $index'),
),
);
};
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: AnimatedList( child: AnimatedList(
key: listKey,
initialItemCount: 2,
itemBuilder: builder,
),
),
);
expect(find.byWidgetPredicate((Widget widget) {
return widget is SliverAnimatedList
&& widget.initialItemCount == 2
&& widget.itemBuilder == builder;
}), findsOneWidget);
listKey.currentState.insertItem(0);
await tester.pump();
expect(find.text('item 2'), findsOneWidget);
listKey.currentState.removeItem(
2,
(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'), findsOneWidget);
expect(find.text('item 2'), findsNothing);
await tester.pumpAndSettle(const Duration(milliseconds: 100));
expect(find.text('removing item'), findsNothing);
});
group('SliverAnimatedList', () {
testWidgets('initialItemCount', (WidgetTester tester) async {
final Map<int, Animation<double>> animations = <int, Animation<double>>{};
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
slivers: <Widget>[
SliverAnimatedList(
initialItemCount: 2, initialItemCount: 2,
itemBuilder: (BuildContext context, int index, Animation<double> animation) { itemBuilder: (BuildContext context, int index, Animation<double> animation) {
animations[index] = animation; animations[index] = animation;
...@@ -23,6 +77,8 @@ void main() { ...@@ -23,6 +77,8 @@ void main() {
), ),
); );
}, },
)
],
), ),
), ),
); );
...@@ -35,13 +91,15 @@ void main() { ...@@ -35,13 +91,15 @@ void main() {
expect(animations[1].value, 1.0); expect(animations[1].value, 1.0);
}); });
testWidgets('AnimatedList insert', (WidgetTester tester) async { testWidgets('insert', (WidgetTester tester) async {
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>(); final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: AnimatedList( child: CustomScrollView(
slivers: <Widget>[
SliverAnimatedList(
key: listKey, key: listKey,
itemBuilder: (BuildContext context, int index, Animation<double> animation) { itemBuilder: (BuildContext context, int index, Animation<double> animation) {
return SizeTransition( return SizeTransition(
...@@ -50,12 +108,12 @@ void main() { ...@@ -50,12 +108,12 @@ void main() {
sizeFactor: animation, sizeFactor: animation,
child: SizedBox( child: SizedBox(
height: 100.0, height: 100.0,
child: Center( child: Center(child: Text('item $index')),
child: Text('item $index'),
),
), ),
); );
}, },
)
],
), ),
), ),
); );
...@@ -64,7 +122,10 @@ void main() { ...@@ -64,7 +122,10 @@ void main() {
double itemTop(int index) => tester.getTopLeft(find.byKey(ValueKey<int>(index), skipOffstage: false)).dy; 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; double itemBottom(int index) => tester.getBottomLeft(find.byKey(ValueKey<int>(index), skipOffstage: false)).dy;
listKey.currentState.insertItem(0, duration: const Duration(milliseconds: 100)); listKey.currentState.insertItem(
0,
duration: const Duration(milliseconds: 100),
);
await tester.pump(); await tester.pump();
// Newly inserted item 0's height should animate from 0 to 100 // Newly inserted item 0's height should animate from 0 to 100
...@@ -79,11 +140,18 @@ void main() { ...@@ -79,11 +140,18 @@ void main() {
expect(itemTop(0), 0.0); expect(itemTop(0), 0.0);
expect(itemBottom(0), 100.0); expect(itemBottom(0), 100.0);
listKey.currentState.insertItem(0, duration: const Duration(milliseconds: 100)); listKey.currentState.insertItem(
listKey.currentState.insertItem(0, duration: const Duration(milliseconds: 100)); 0,
duration: const Duration(milliseconds: 100),
);
listKey.currentState.insertItem(
0,
duration: const Duration(milliseconds: 100),
);
await tester.pump(); await tester.pump();
// The height of the newly inserted items at index 0 and 1 should animate from 0 to 100. // The height of the newly inserted items at index 0 and 1 should animate
// from 0 to 100.
// The height of the original item, now at index 2, should remain 100. // The height of the original item, now at index 2, should remain 100.
expect(itemHeight(0), 0.0); expect(itemHeight(0), 0.0);
expect(itemHeight(1), 0.0); expect(itemHeight(1), 0.0);
...@@ -109,8 +177,8 @@ void main() { ...@@ -109,8 +177,8 @@ void main() {
expect(itemBottom(2), 300.0); expect(itemBottom(2), 300.0);
}); });
testWidgets('AnimatedList remove', (WidgetTester tester) async { testWidgets('remove', (WidgetTester tester) async {
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>(); final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
final List<int> items = <int>[0, 1, 2]; final List<int> items = <int>[0, 1, 2];
Widget buildItem(BuildContext context, int item, Animation<double> animation) { Widget buildItem(BuildContext context, int item, Animation<double> animation) {
...@@ -130,12 +198,16 @@ void main() { ...@@ -130,12 +198,16 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: AnimatedList( child: CustomScrollView(
slivers: <Widget>[
SliverAnimatedList(
key: listKey, key: listKey,
initialItemCount: 3, initialItemCount: 3,
itemBuilder: (BuildContext context, int index, Animation<double> animation) { itemBuilder: (BuildContext context, int index, Animation<double> animation) {
return buildItem(context, items[index], animation); return buildItem(context, items[index], animation);
}, },
)
],
), ),
), ),
); );
...@@ -148,7 +220,8 @@ void main() { ...@@ -148,7 +220,8 @@ void main() {
expect(find.text('item 2'), findsOneWidget); expect(find.text('item 2'), findsOneWidget);
items.removeAt(0); items.removeAt(0);
listKey.currentState.removeItem(0, listKey.currentState.removeItem(
0,
(BuildContext context, Animation<double> animation) => buildItem(context, 0, animation), (BuildContext context, Animation<double> animation) => buildItem(context, 0, animation),
duration: const Duration(milliseconds: 100), duration: const Duration(milliseconds: 100),
); );
...@@ -180,4 +253,70 @@ void main() { ...@@ -180,4 +253,70 @@ void main() {
expect(itemTop(2), 100.0); expect(itemTop(2), 100.0);
expect(itemBottom(2), 200.0); expect(itemBottom(2), 200.0);
}); });
testWidgets('works in combination with other slivers', (WidgetTester tester) async {
final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate(<Widget>[
const SizedBox(height: 100),
const SizedBox(height: 100),
]),
),
SliverAnimatedList(
key: listKey,
initialItemCount: 3,
itemBuilder: (BuildContext context, int index, Animation<double> animation) {
return SizedBox(
height: 100,
child: Text('item $index'),
);
},
),
],
),
),
);
expect(tester.getTopLeft(find.text('item 0')).dy, 200);
expect(tester.getTopLeft(find.text('item 1')).dy, 300);
listKey.currentState.insertItem(3);
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('item 3')).dy, 500);
listKey.currentState.removeItem(0,
(BuildContext context, Animation<double> animation) {
return SizeTransition(
sizeFactor: animation,
key: const ObjectKey('removing'),
child: const SizedBox(
height: 100,
child: Text('removing'),
),
);
},
duration: const Duration(seconds: 1),
);
await tester.pump();
expect(find.text('item 3'), findsNothing);
await tester.pump(const Duration(milliseconds: 500));
expect(
tester.getSize(find.byKey(const ObjectKey('removing'))).height,
50,
);
expect(tester.getTopLeft(find.text('item 0')).dy, 250);
await tester.pumpAndSettle();
expect(find.text('removing'), findsNothing);
expect(tester.getTopLeft(find.text('item 0')).dy, 200);
});
});
} }
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