Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
83878683
Unverified
Commit
83878683
authored
Oct 21, 2019
by
Kate Lovett
Committed by
GitHub
Oct 21, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Re-landing SliverAnimatedList (#42485)
parent
6db4c44d
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
930 additions
and
178 deletions
+930
-178
animated_list.dart
packages/flutter/lib/src/widgets/animated_list.dart
+650
-37
animated_list_test.dart
packages/flutter/test/widgets/animated_list_test.dart
+280
-141
No files found.
packages/flutter/lib/src/widgets/animated_list.dart
View file @
83878683
...
@@ -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 tha
t '
'
was passed to AnimatedList.of(). This can happen when the contex
t '
'
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
,
);
);
}
}
}
}
packages/flutter/test/widgets/animated_list_test.dart
View file @
83878683
...
@@ -6,178 +6,317 @@ import 'package:flutter_test/flutter_test.dart';
...
@@ -6,178 +6,317 @@ 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
,
initialItemCount:
2
,
itemBuilder:
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
)
{
itemBuilder:
builder
,
animations
[
index
]
=
animation
;
return
SizedBox
(
height:
100.0
,
child:
Center
(
child:
Text
(
'item
$index
'
),
),
);
},
),
),
),
),
);
);
expect
(
find
.
text
(
'item 0'
),
findsOneWidget
);
expect
(
find
.
byWidgetPredicate
((
Widget
widget
)
{
expect
(
find
.
text
(
'item 1'
),
findsOneWidget
);
return
widget
is
SliverAnimatedList
expect
(
animations
.
containsKey
(
0
),
true
);
&&
widget
.
initialItemCount
==
2
expect
(
animations
.
containsKey
(
1
),
true
);
&&
widget
.
itemBuilder
==
builder
;
expect
(
animations
[
0
].
value
,
1.0
);
}),
findsOneWidget
);
expect
(
animations
[
1
].
value
,
1.0
);
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
);
});
});
testWidgets
(
'AnimatedList insert'
,
(
WidgetTester
tester
)
async
{
group
(
'SliverAnimatedList'
,
()
{
final
GlobalKey
<
AnimatedListState
>
listKey
=
GlobalKey
<
AnimatedListState
>();
testWidgets
(
'initialItemCount'
,
(
WidgetTester
tester
)
async
{
final
Map
<
int
,
Animation
<
double
>>
animations
=
<
int
,
Animation
<
double
>>{};
await
tester
.
pumpWidget
(
await
tester
.
pumpWidget
(
Directionality
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
textDirection:
TextDirection
.
ltr
,
child:
AnimatedList
(
child:
CustomScrollView
(
key:
listKey
,
slivers:
<
Widget
>[
itemBuilder:
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
)
{
SliverAnimatedList
(
return
SizeTransition
(
initialItemCount:
2
,
key:
ValueKey
<
int
>(
index
),
itemBuilder:
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
)
{
axis:
Axis
.
vertical
,
animations
[
index
]
=
animation
;
sizeFactor:
animation
,
return
SizedBox
(
child:
SizedBox
(
height:
100.0
,
height:
100.0
,
child:
Center
(
child:
Center
(
child:
Text
(
'item
$index
'
),
child:
Text
(
'item
$index
'
),
),
),
);
),
},
);
)
},
],
),
),
),
),
);
);
double
itemHeight
(
int
index
)
=>
tester
.
getSize
(
find
.
byKey
(
ValueKey
<
int
>(
index
),
skipOffstage:
false
)).
height
;
expect
(
find
.
text
(
'item 0'
),
findsOneWidget
);
double
itemTop
(
int
index
)
=>
tester
.
getTopLeft
(
find
.
byKey
(
ValueKey
<
int
>(
index
),
skipOffstage:
false
)).
dy
;
expect
(
find
.
text
(
'item 1'
),
findsOneWidget
);
double
itemBottom
(
int
index
)
=>
tester
.
getBottomLeft
(
find
.
byKey
(
ValueKey
<
int
>(
index
),
skipOffstage:
false
)).
dy
;
expect
(
animations
.
containsKey
(
0
),
true
);
expect
(
animations
.
containsKey
(
1
),
true
);
expect
(
animations
[
0
].
value
,
1.0
);
expect
(
animations
[
1
].
value
,
1.0
);
});
listKey
.
currentState
.
insertItem
(
0
,
duration:
const
Duration
(
milliseconds:
100
));
testWidgets
(
'insert'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pump
();
final
GlobalKey
<
SliverAnimatedListState
>
listKey
=
GlobalKey
<
SliverAnimatedListState
>
();
// Newly inserted item 0's height should animate from 0 to 100
await
tester
.
pumpWidget
(
expect
(
itemHeight
(
0
),
0.0
);
Directionality
(
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
textDirection:
TextDirection
.
ltr
,
expect
(
itemHeight
(
0
),
50.0
);
child:
CustomScrollView
(
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
slivers:
<
Widget
>[
expect
(
itemHeight
(
0
),
100.0
);
SliverAnimatedList
(
key:
listKey
,
itemBuilder:
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
)
{
return
SizeTransition
(
key:
ValueKey
<
int
>(
index
),
axis:
Axis
.
vertical
,
sizeFactor:
animation
,
child:
SizedBox
(
height:
100.0
,
child:
Center
(
child:
Text
(
'item
$index
'
)),
),
);
},
)
],
),
),
);
// The list now contains one fully expanded item at the top:
double
itemHeight
(
int
index
)
=>
tester
.
getSize
(
find
.
byKey
(
ValueKey
<
int
>(
index
),
skipOffstage:
false
)).
height
;
expect
(
find
.
text
(
'item 0'
),
findsOneWidget
);
double
itemTop
(
int
index
)
=>
tester
.
getTopLeft
(
find
.
byKey
(
ValueKey
<
int
>(
index
),
skipOffstage:
false
)).
dy
;
expect
(
itemTop
(
0
),
0.0
);
double
itemBottom
(
int
index
)
=>
tester
.
getBottomLeft
(
find
.
byKey
(
ValueKey
<
int
>(
index
),
skipOffstage:
false
)).
dy
;
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
,
await
tester
.
pump
();
duration:
const
Duration
(
milliseconds:
100
),
);
await
tester
.
pump
();
// The height of the newly inserted items at index 0 and 1 should animate from 0 to 100.
// Newly inserted item 0's height should animate from 0 to 100
// The height of the original item, now at index 2, should remain 100.
expect
(
itemHeight
(
0
),
0.0
);
expect
(
itemHeight
(
0
),
0.0
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
itemHeight
(
1
),
0.0
);
expect
(
itemHeight
(
0
),
50.0
);
expect
(
itemHeight
(
2
),
100.0
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
itemHeight
(
0
),
100.0
);
expect
(
itemHeight
(
0
),
50.0
);
expect
(
itemHeight
(
1
),
50.0
);
expect
(
itemHeight
(
2
),
100.0
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
itemHeight
(
0
),
100.0
);
expect
(
itemHeight
(
1
),
100.0
);
expect
(
itemHeight
(
2
),
100.0
);
// The newly inserted "item 1" and "item 2" appear above "item 0"
expect
(
find
.
text
(
'item 0'
),
findsOneWidget
);
expect
(
find
.
text
(
'item 1'
),
findsOneWidget
);
expect
(
find
.
text
(
'item 2'
),
findsOneWidget
);
expect
(
itemTop
(
0
),
0.0
);
expect
(
itemBottom
(
0
),
100.0
);
expect
(
itemTop
(
1
),
100.0
);
expect
(
itemBottom
(
1
),
200.0
);
expect
(
itemTop
(
2
),
200.0
);
expect
(
itemBottom
(
2
),
300.0
);
});
testWidgets
(
'AnimatedList remove'
,
(
WidgetTester
tester
)
async
{
// The list now contains one fully expanded item at the top:
final
GlobalKey
<
AnimatedListState
>
listKey
=
GlobalKey
<
AnimatedListState
>();
expect
(
find
.
text
(
'item 0'
),
findsOneWidget
);
final
List
<
int
>
items
=
<
int
>[
0
,
1
,
2
];
expect
(
itemTop
(
0
),
0.0
);
expect
(
itemBottom
(
0
),
100.0
);
Widget
buildItem
(
BuildContext
context
,
int
item
,
Animation
<
double
>
animation
)
{
return
SizeTransition
(
listKey
.
currentState
.
insertItem
(
key:
ValueKey
<
int
>(
item
),
0
,
axis:
Axis
.
vertical
,
duration:
const
Duration
(
milliseconds:
100
),
sizeFactor:
animation
,
);
child:
SizedBox
(
listKey
.
currentState
.
insertItem
(
height:
100.0
,
0
,
child:
Center
(
duration:
const
Duration
(
milliseconds:
100
),
child:
Text
(
'item
$item
'
,
textDirection:
TextDirection
.
ltr
),
);
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 original item, now at index 2, should remain 100.
expect
(
itemHeight
(
0
),
0.0
);
expect
(
itemHeight
(
1
),
0.0
);
expect
(
itemHeight
(
2
),
100.0
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
itemHeight
(
0
),
50.0
);
expect
(
itemHeight
(
1
),
50.0
);
expect
(
itemHeight
(
2
),
100.0
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
itemHeight
(
0
),
100.0
);
expect
(
itemHeight
(
1
),
100.0
);
expect
(
itemHeight
(
2
),
100.0
);
// The newly inserted "item 1" and "item 2" appear above "item 0"
expect
(
find
.
text
(
'item 0'
),
findsOneWidget
);
expect
(
find
.
text
(
'item 1'
),
findsOneWidget
);
expect
(
find
.
text
(
'item 2'
),
findsOneWidget
);
expect
(
itemTop
(
0
),
0.0
);
expect
(
itemBottom
(
0
),
100.0
);
expect
(
itemTop
(
1
),
100.0
);
expect
(
itemBottom
(
1
),
200.0
);
expect
(
itemTop
(
2
),
200.0
);
expect
(
itemBottom
(
2
),
300.0
);
});
testWidgets
(
'remove'
,
(
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
),
axis:
Axis
.
vertical
,
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
);
},
)
],
),
),
),
),
);
);
}
await
tester
.
pumpWidget
(
double
itemTop
(
int
index
)
=>
tester
.
getTopLeft
(
find
.
byKey
(
ValueKey
<
int
>(
index
))).
dy
;
Directionality
(
double
itemBottom
(
int
index
)
=>
tester
.
getBottomLeft
(
find
.
byKey
(
ValueKey
<
int
>(
index
))).
dy
;
textDirection:
TextDirection
.
ltr
,
child:
AnimatedList
(
expect
(
find
.
text
(
'item 0'
),
findsOneWidget
);
key:
listKey
,
expect
(
find
.
text
(
'item 1'
),
findsOneWidget
);
initialItemCount:
3
,
expect
(
find
.
text
(
'item 2'
),
findsOneWidget
);
itemBuilder:
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
)
{
return
buildItem
(
context
,
items
[
index
],
animation
);
items
.
removeAt
(
0
);
},
listKey
.
currentState
.
removeItem
(
0
,
(
BuildContext
context
,
Animation
<
double
>
animation
)
=>
buildItem
(
context
,
0
,
animation
),
duration:
const
Duration
(
milliseconds:
100
),
);
// Items 0, 1, 2 at 0, 100, 200. All heights 100.
expect
(
itemTop
(
0
),
0.0
);
expect
(
itemBottom
(
0
),
100.0
);
expect
(
itemTop
(
1
),
100.0
);
expect
(
itemBottom
(
1
),
200.0
);
expect
(
itemTop
(
2
),
200.0
);
expect
(
itemBottom
(
2
),
300.0
);
// Newly removed item 0's height should animate from 100 to 0 over 100ms
// Items 0, 1, 2 at 0, 50, 150. Item 0's height is 50.
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
itemTop
(
0
),
0.0
);
expect
(
itemBottom
(
0
),
50.0
);
expect
(
itemTop
(
1
),
50.0
);
expect
(
itemBottom
(
1
),
150.0
);
expect
(
itemTop
(
2
),
150.0
);
expect
(
itemBottom
(
2
),
250.0
);
// Items 1, 2 at 0, 100.
await
tester
.
pumpAndSettle
();
expect
(
itemTop
(
1
),
0.0
);
expect
(
itemBottom
(
1
),
100.0
);
expect
(
itemTop
(
2
),
100.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
'
),
);
},
),
],
),
),
),
),
);
);
double
itemTop
(
int
index
)
=>
tester
.
getTopLeft
(
find
.
byKey
(
ValueKey
<
int
>(
index
))).
dy
;
expect
(
tester
.
getTopLeft
(
find
.
text
(
'item 0'
)).
dy
,
200
)
;
double
itemBottom
(
int
index
)
=>
tester
.
getBottomLeft
(
find
.
byKey
(
ValueKey
<
int
>(
index
))).
dy
;
expect
(
tester
.
getTopLeft
(
find
.
text
(
'item 1'
)).
dy
,
300
)
;
expect
(
find
.
text
(
'item 0'
),
findsOneWidget
);
listKey
.
currentState
.
insertItem
(
3
);
expect
(
find
.
text
(
'item 1'
),
findsOneWidget
);
await
tester
.
pumpAndSettle
(
);
expect
(
find
.
text
(
'item 2'
),
findsOneWidget
);
expect
(
tester
.
getTopLeft
(
find
.
text
(
'item 3'
)).
dy
,
500
);
items
.
removeAt
(
0
);
listKey
.
currentState
.
removeItem
(
0
,
listKey
.
currentState
.
removeItem
(
0
,
(
BuildContext
context
,
Animation
<
double
>
animation
)
{
(
BuildContext
context
,
Animation
<
double
>
animation
)
=>
buildItem
(
context
,
0
,
animation
),
return
SizeTransition
(
duration:
const
Duration
(
milliseconds:
100
),
sizeFactor:
animation
,
);
key:
const
ObjectKey
(
'removing'
),
child:
const
SizedBox
(
height:
100
,
child:
Text
(
'removing'
),
),
);
},
duration:
const
Duration
(
seconds:
1
),
);
// Items 0, 1, 2 at 0, 100, 200. All heights 100.
await
tester
.
pump
();
expect
(
itemTop
(
0
),
0.0
);
expect
(
find
.
text
(
'item 3'
),
findsNothing
);
expect
(
itemBottom
(
0
),
100.0
);
expect
(
itemTop
(
1
),
100.0
);
expect
(
itemBottom
(
1
),
200.0
);
expect
(
itemTop
(
2
),
200.0
);
expect
(
itemBottom
(
2
),
300.0
);
// Newly removed item 0's height should animate from 100 to 0 over 100ms
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
);
// Items 0, 1, 2 at 0, 50, 150. Item 0's height is 50.
await
tester
.
pumpAndSettle
();
await
tester
.
pump
();
expect
(
find
.
text
(
'removing'
),
findsNothing
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
tester
.
getTopLeft
(
find
.
text
(
'item 0'
)).
dy
,
200
);
expect
(
itemTop
(
0
),
0.0
);
});
expect
(
itemBottom
(
0
),
50.0
);
expect
(
itemTop
(
1
),
50.0
);
expect
(
itemBottom
(
1
),
150.0
);
expect
(
itemTop
(
2
),
150.0
);
expect
(
itemBottom
(
2
),
250.0
);
// Items 1, 2 at 0, 100.
await
tester
.
pumpAndSettle
();
expect
(
itemTop
(
1
),
0.0
);
expect
(
itemBottom
(
1
),
100.0
);
expect
(
itemTop
(
2
),
100.0
);
expect
(
itemBottom
(
2
),
200.0
);
});
});
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment