mirror of
https://github.com/flutter/flutter
synced 2024-10-13 03:32:55 +00:00
This reverts commit d2de911d50
.
This commit is contained in:
parent
d2de911d50
commit
ea03ac2b84
|
@ -10,7 +10,6 @@ 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].
|
||||
|
@ -76,13 +75,11 @@ 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.
|
||||
|
@ -210,150 +207,6 @@ 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;
|
||||
|
@ -366,9 +219,10 @@ class SliverAnimatedListState extends State<SliverAnimatedList> with TickerProvi
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
for (_ActiveItem item in _incomingItems.followedBy(_outgoingItems)) {
|
||||
for (_ActiveItem item in _incomingItems)
|
||||
item.controller.dispose();
|
||||
for (_ActiveItem item in _outgoingItems)
|
||||
item.controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -411,12 +265,8 @@ class SliverAnimatedListState extends State<SliverAnimatedList> with TickerProvi
|
|||
return index;
|
||||
}
|
||||
|
||||
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.
|
||||
/// 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
|
||||
|
@ -457,8 +307,8 @@ class SliverAnimatedListState extends State<SliverAnimatedList> with TickerProvi
|
|||
/// 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 [SliverAnimatedList.itemBuilder]. However
|
||||
/// the item will still appear in the list for [duration] and during that time
|
||||
/// 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:
|
||||
|
@ -515,8 +365,16 @@ class SliverAnimatedListState extends State<SliverAnimatedList> with TickerProvi
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverList(
|
||||
delegate: _createDelegate(),
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,15 +6,36 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
void main() {
|
||||
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'),
|
||||
testWidgets('AnimatedList initialItemCount', (WidgetTester tester) async {
|
||||
final Map<int, Animation<double>> animations = <int, Animation<double>>{};
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: AnimatedList(
|
||||
initialItemCount: 2,
|
||||
itemBuilder: (BuildContext context, int index, Animation<double> animation) {
|
||||
animations[index] = animation;
|
||||
return SizedBox(
|
||||
height: 100.0,
|
||||
child: Center(
|
||||
child: Text('item $index'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
};
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('item 0'), findsOneWidget);
|
||||
expect(find.text('item 1'), findsOneWidget);
|
||||
expect(animations.containsKey(0), true);
|
||||
expect(animations.containsKey(1), true);
|
||||
expect(animations[0].value, 1.0);
|
||||
expect(animations[1].value, 1.0);
|
||||
});
|
||||
|
||||
testWidgets('AnimatedList insert', (WidgetTester tester) async {
|
||||
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
|
||||
|
||||
await tester.pumpWidget(
|
||||
|
@ -22,286 +43,141 @@ void main() {
|
|||
textDirection: TextDirection.ltr,
|
||||
child: AnimatedList(
|
||||
key: listKey,
|
||||
initialItemCount: 2,
|
||||
itemBuilder: builder,
|
||||
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'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byWidgetPredicate((Widget widget) {
|
||||
return widget is SliverAnimatedList
|
||||
&& widget.initialItemCount == 2
|
||||
&& widget.itemBuilder == builder;
|
||||
}), findsOneWidget);
|
||||
double itemHeight(int index) => tester.getSize(find.byKey(ValueKey<int>(index), skipOffstage: false)).height;
|
||||
double itemTop(int index) => tester.getTopLeft(find.byKey(ValueKey<int>(index), skipOffstage: false)).dy;
|
||||
double itemBottom(int index) => tester.getBottomLeft(find.byKey(ValueKey<int>(index), skipOffstage: false)).dy;
|
||||
|
||||
listKey.currentState.insertItem(0);
|
||||
listKey.currentState.insertItem(0, duration: const Duration(milliseconds: 100));
|
||||
await tester.pump();
|
||||
|
||||
// Newly inserted item 0's height should animate from 0 to 100
|
||||
expect(itemHeight(0), 0.0);
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
expect(itemHeight(0), 50.0);
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
expect(itemHeight(0), 100.0);
|
||||
|
||||
// The list now contains one fully expanded item at the top:
|
||||
expect(find.text('item 0'), findsOneWidget);
|
||||
expect(itemTop(0), 0.0);
|
||||
expect(itemBottom(0), 100.0);
|
||||
|
||||
listKey.currentState.insertItem(0, duration: const Duration(milliseconds: 100));
|
||||
listKey.currentState.insertItem(0, 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.
|
||||
// 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('AnimatedList remove', (WidgetTester tester) async {
|
||||
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
|
||||
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: AnimatedList(
|
||||
key: listKey,
|
||||
initialItemCount: 3,
|
||||
itemBuilder: (BuildContext context, int index, Animation<double> animation) {
|
||||
return buildItem(context, items[index], animation);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
double itemTop(int index) => tester.getTopLeft(find.byKey(ValueKey<int>(index))).dy;
|
||||
double itemBottom(int index) => tester.getBottomLeft(find.byKey(ValueKey<int>(index))).dy;
|
||||
|
||||
expect(find.text('item 0'), findsOneWidget);
|
||||
expect(find.text('item 1'), findsOneWidget);
|
||||
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));
|
||||
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();
|
||||
expect(find.text('removing item'), findsOneWidget);
|
||||
expect(find.text('item 2'), findsNothing);
|
||||
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);
|
||||
|
||||
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;
|
||||
return SizedBox(
|
||||
height: 100.0,
|
||||
child: Center(
|
||||
child: Text('item $index'),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('item 0'), findsOneWidget);
|
||||
expect(find.text('item 1'), findsOneWidget);
|
||||
expect(animations.containsKey(0), true);
|
||||
expect(animations.containsKey(1), true);
|
||||
expect(animations[0].value, 1.0);
|
||||
expect(animations[1].value, 1.0);
|
||||
});
|
||||
|
||||
testWidgets('insert', (WidgetTester tester) async {
|
||||
final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverAnimatedList(
|
||||
key: listKey,
|
||||
itemBuilder: (BuildContext context, int index, Animation<double> animation) {
|
||||
return SizeTransition(
|
||||
key: ValueKey<int>(index),
|
||||
axis: Axis.vertical,
|
||||
sizeFactor: animation,
|
||||
child: SizedBox(
|
||||
height: 100.0,
|
||||
child: Center(
|
||||
child: Text('item $index'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
double itemHeight(int index) => tester.getSize(find.byKey(ValueKey<int>(index), skipOffstage: false)).height;
|
||||
double itemTop(int index) => tester.getTopLeft(find.byKey(ValueKey<int>(index), skipOffstage: false)).dy;
|
||||
double itemBottom(int index) => tester.getBottomLeft(find.byKey(ValueKey<int>(index), skipOffstage: false)).dy;
|
||||
|
||||
listKey.currentState.insertItem(0, duration: const Duration(milliseconds: 100));
|
||||
await tester.pump();
|
||||
|
||||
// Newly inserted item 0's height should animate from 0 to 100
|
||||
expect(itemHeight(0), 0.0);
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
expect(itemHeight(0), 50.0);
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
expect(itemHeight(0), 100.0);
|
||||
|
||||
// The list now contains one fully expanded item at the top:
|
||||
expect(find.text('item 0'), findsOneWidget);
|
||||
expect(itemTop(0), 0.0);
|
||||
expect(itemBottom(0), 100.0);
|
||||
|
||||
listKey.currentState.insertItem(0, duration: const Duration(milliseconds: 100));
|
||||
listKey.currentState.insertItem(0, 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.
|
||||
// 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);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
double itemTop(int index) => tester.getTopLeft(find.byKey(ValueKey<int>(index))).dy;
|
||||
double itemBottom(int index) => tester.getBottomLeft(find.byKey(ValueKey<int>(index))).dy;
|
||||
|
||||
expect(find.text('item 0'), findsOneWidget);
|
||||
expect(find.text('item 1'), findsOneWidget);
|
||||
expect(find.text('item 2'), findsOneWidget);
|
||||
|
||||
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>[
|
||||
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);
|
||||
});
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue