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
d2de911d
Commit
d2de911d
authored
May 06, 2019
by
Simon Binder
Committed by
Michael Goderbauer
May 06, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Sliver animated list (#28834)
parent
538a1c66
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
424 additions
and
158 deletions
+424
-158
animated_list.dart
packages/flutter/lib/src/widgets/animated_list.dart
+159
-17
animated_list_test.dart
packages/flutter/test/widgets/animated_list_test.dart
+265
-141
No files found.
packages/flutter/lib/src/widgets/animated_list.dart
View file @
d2de911d
...
...
@@ -10,6 +10,7 @@ import 'framework.dart';
import
'scroll_controller.dart'
;
import
'scroll_physics.dart'
;
import
'scroll_view.dart'
;
import
'sliver.dart'
;
import
'ticker_provider.dart'
;
/// Signature for the builder callback used by [AnimatedList].
...
...
@@ -75,11 +76,13 @@ class AnimatedList extends StatefulWidget {
/// [AnimatedListState.removeItem] removes an item immediately.
final
AnimatedListItemBuilder
itemBuilder
;
/// {@template flutter.widgets.animatedList.initialItemCount}
/// The number of items the list will start with.
///
/// The appearance of the initial items is not animated. They
/// are created, as needed, by [itemBuilder] with an animation parameter
/// of [kAlwaysCompleteAnimation].
/// {@endtemplate}
final
int
initialItemCount
;
/// The axis along which the scroll view scrolls.
...
...
@@ -207,6 +210,150 @@ class AnimatedList extends StatefulWidget {
/// [AnimatedList] item input handlers can also refer to their [AnimatedListState]
/// with the static [AnimatedList.of] method.
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.
///
/// See also:
///
/// * [SliverList], which does not animate 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
>
_outgoingItems
=
<
_ActiveItem
>[];
int
_itemsCount
=
0
;
...
...
@@ -219,10 +366,9 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
@override
void
dispose
()
{
for
(
_ActiveItem
item
in
_incomingItems
)
item
.
controller
.
dispose
();
for
(
_ActiveItem
item
in
_outgoingItems
)
for
(
_ActiveItem
item
in
_incomingItems
.
followedBy
(
_outgoingItems
))
{
item
.
controller
.
dispose
();
}
super
.
dispose
();
}
...
...
@@ -265,8 +411,12 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
return
index
;
}
/// Insert an item at [index] and start an animation that will be passed
/// to [AnimatedList.itemBuilder] when the item is visible.
SliverChildDelegate
_createDelegate
()
{
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:
/// it increases the length of the list by one and shifts all items at or
...
...
@@ -307,8 +457,8 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
/// 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
/// will no longer be passed to the [
SliverAnimatedList.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:
...
...
@@ -365,16 +515,8 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
@override
Widget
build
(
BuildContext
context
)
{
return
ListView
.
builder
(
itemBuilder:
_itemBuilder
,
itemCount:
_itemsCount
,
scrollDirection:
widget
.
scrollDirection
,
reverse:
widget
.
reverse
,
controller:
widget
.
controller
,
primary:
widget
.
primary
,
physics:
widget
.
physics
,
shrinkWrap:
widget
.
shrinkWrap
,
padding:
widget
.
padding
,
return
SliverList
(
delegate:
_createDelegate
(),
);
}
}
packages/flutter/test/widgets/animated_list_test.dart
View file @
d2de911d
...
...
@@ -6,13 +6,64 @@ import 'package:flutter_test/flutter_test.dart';
import
'package:flutter/widgets.dart'
;
void
main
(
)
{
testWidgets
(
'AnimatedList initialItemCount'
,
(
WidgetTester
tester
)
async
{
final
Map
<
int
,
Animation
<
double
>>
animations
=
<
int
,
Animation
<
double
>>{};
testWidgets
(
'AnimatedList'
,
(
WidgetTester
tester
)
async
{
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
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
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
,
itemBuilder:
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
)
{
animations
[
index
]
=
animation
;
...
...
@@ -23,6 +74,8 @@ void main() {
),
);
},
)
],
),
),
);
...
...
@@ -35,13 +88,15 @@ void main() {
expect
(
animations
[
1
].
value
,
1.0
);
});
testWidgets
(
'AnimatedList
insert'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
AnimatedListState
>
listKey
=
GlobalKey
<
AnimatedListState
>();
testWidgets
(
'
insert'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
SliverAnimatedListState
>
listKey
=
GlobalKey
<
Sliver
AnimatedListState
>();
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
AnimatedList
(
child:
CustomScrollView
(
slivers:
<
Widget
>[
SliverAnimatedList
(
key:
listKey
,
itemBuilder:
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
)
{
return
SizeTransition
(
...
...
@@ -56,6 +111,8 @@ void main() {
),
);
},
)
],
),
),
);
...
...
@@ -109,8 +166,8 @@ void main() {
expect
(
itemBottom
(
2
),
300.0
);
});
testWidgets
(
'AnimatedList
remove'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
AnimatedListState
>
listKey
=
GlobalKey
<
AnimatedListState
>();
testWidgets
(
'
remove'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
SliverAnimatedListState
>
listKey
=
GlobalKey
<
Sliver
AnimatedListState
>();
final
List
<
int
>
items
=
<
int
>[
0
,
1
,
2
];
Widget
buildItem
(
BuildContext
context
,
int
item
,
Animation
<
double
>
animation
)
{
...
...
@@ -130,12 +187,16 @@ void main() {
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
AnimatedList
(
child:
CustomScrollView
(
slivers:
<
Widget
>[
SliverAnimatedList
(
key:
listKey
,
initialItemCount:
3
,
itemBuilder:
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
)
{
return
buildItem
(
context
,
items
[
index
],
animation
);
},
)
],
),
),
);
...
...
@@ -180,4 +241,67 @@ void main() {
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
>[
const
SliverList
(
delegate:
SliverChildListDelegate
(<
Widget
>[
SizedBox
(
height:
100
),
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
);
});
});
}
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