AnimatedList (#9649)
This commit is contained in:
parent
6716904385
commit
acf102be95
373
packages/flutter/lib/src/widgets/animated_list.dart
Normal file
373
packages/flutter/lib/src/widgets/animated_list.dart
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart' show binarySearch;
|
||||||
|
|
||||||
|
import 'package:flutter/animation.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import 'basic.dart';
|
||||||
|
import 'framework.dart';
|
||||||
|
import 'scroll_controller.dart';
|
||||||
|
import 'scroll_physics.dart';
|
||||||
|
import 'scroll_view.dart';
|
||||||
|
import 'ticker_provider.dart';
|
||||||
|
|
||||||
|
/// Signature for the builder callback used by [AnimatedList].
|
||||||
|
typedef Widget AnimatedListItemBuilder(BuildContext context, int index, Animation<double> animation);
|
||||||
|
|
||||||
|
/// Signature for the builder callback used by [AnimatedList.remove].
|
||||||
|
typedef Widget AnimatedListRemovedItemBuilder(BuildContext context, Animation<double> animation);
|
||||||
|
|
||||||
|
// The default insert/remove animation duration.
|
||||||
|
const Duration _kDuration = const Duration(milliseconds: 300);
|
||||||
|
|
||||||
|
// Incoming and outgoing AnimatedList items.
|
||||||
|
class _ActiveItem implements Comparable<_ActiveItem> {
|
||||||
|
_ActiveItem.incoming(this.controller, this.itemIndex) : removedItemBuilder = null;
|
||||||
|
_ActiveItem.outgoing(this.controller, this.itemIndex, this.removedItemBuilder);
|
||||||
|
_ActiveItem.index(this.itemIndex) : controller = null, removedItemBuilder = null;
|
||||||
|
|
||||||
|
final AnimationController controller;
|
||||||
|
final AnimatedListRemovedItemBuilder removedItemBuilder;
|
||||||
|
int itemIndex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int compareTo(_ActiveItem other) => itemIndex - other.itemIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A scrolling container that animates items when they are inserted or removed.
|
||||||
|
///
|
||||||
|
/// This widget's [AnimatedListState] can be used to dynmically insert or remove
|
||||||
|
/// items. To refer to the [AnimatedListState] either provide a [GlobalKey] or
|
||||||
|
/// use the static [of] method from an item's input callback.
|
||||||
|
///
|
||||||
|
/// This widget is similar to one created by [ListView.builder].
|
||||||
|
class AnimatedList extends StatefulWidget {
|
||||||
|
/// Creates a scrolling container that animates items when they are inserted or removed.
|
||||||
|
AnimatedList({
|
||||||
|
Key key,
|
||||||
|
@required this.itemBuilder,
|
||||||
|
this.initialItemCount: 0,
|
||||||
|
this.scrollDirection: Axis.vertical,
|
||||||
|
this.reverse: false,
|
||||||
|
this.controller,
|
||||||
|
this.primary,
|
||||||
|
this.physics,
|
||||||
|
this.shrinkWrap: false,
|
||||||
|
this.padding,
|
||||||
|
}) : super(key: key) {
|
||||||
|
assert(itemBuilder != null);
|
||||||
|
assert(initialItemCount != null && initialItemCount >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// posiition 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 [AnimatedListState.insertItem] and less the total number of items
|
||||||
|
/// that have been removed with [AnimatedList.removeItem].
|
||||||
|
///
|
||||||
|
/// Implementations of this callback should assume that [AnimatedList.removeItem]
|
||||||
|
/// removes an item immediately.
|
||||||
|
final AnimatedListItemBuilder itemBuilder;
|
||||||
|
|
||||||
|
/// The number of items the list will start with.
|
||||||
|
///
|
||||||
|
/// The appareance of the initial items is not animated. They are
|
||||||
|
/// are created, as needed, by [itemBuilder] with an animation paramter
|
||||||
|
/// of [kAlwaysCompleteAnimation].
|
||||||
|
final int initialItemCount;
|
||||||
|
|
||||||
|
/// The axis along which the scroll view scrolls.
|
||||||
|
///
|
||||||
|
/// Defaults to [Axis.vertical].
|
||||||
|
final Axis scrollDirection;
|
||||||
|
|
||||||
|
/// Whether the scroll view scrolls in the reading direction.
|
||||||
|
///
|
||||||
|
/// For example, if the reading direction is left-to-right and
|
||||||
|
/// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
|
||||||
|
/// left to right when [reverse] is false and from right to left when
|
||||||
|
/// [reverse] is true.
|
||||||
|
///
|
||||||
|
/// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
|
||||||
|
/// scrolls from top to bottom when [reverse] is false and from bottom to top
|
||||||
|
/// when [reverse] is true.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
final bool reverse;
|
||||||
|
|
||||||
|
/// An object that can be used to control the position to which this scroll
|
||||||
|
/// view is scrolled.
|
||||||
|
///
|
||||||
|
/// Must be null if [primary] is true.
|
||||||
|
final ScrollController controller;
|
||||||
|
|
||||||
|
/// Whether this is the primary scroll view associated with the parent
|
||||||
|
/// [PrimaryScrollController].
|
||||||
|
///
|
||||||
|
/// On iOS, this identifies the scroll view that will scroll to top in
|
||||||
|
/// response to a tap in the status bar.
|
||||||
|
///
|
||||||
|
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
|
||||||
|
/// [controller] is null.
|
||||||
|
final bool primary;
|
||||||
|
|
||||||
|
/// How the scroll view should respond to user input.
|
||||||
|
///
|
||||||
|
/// For example, determines how the scroll view continues to animate after the
|
||||||
|
/// user stops dragging the scroll view.
|
||||||
|
///
|
||||||
|
/// Defaults to matching platform conventions.
|
||||||
|
final ScrollPhysics physics;
|
||||||
|
|
||||||
|
/// Whether the extent of the scroll view in the [scrollDirection] should be
|
||||||
|
/// determined by the contents being viewed.
|
||||||
|
///
|
||||||
|
/// If the scroll view does not shrink wrap, then the scroll view will expand
|
||||||
|
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
|
||||||
|
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
|
||||||
|
/// be true.
|
||||||
|
///
|
||||||
|
/// Shrink wrapping the content of the scroll view is significantly more
|
||||||
|
/// expensive than expanding to the maximum allowed size because the content
|
||||||
|
/// can expand and contract during scrolling, which means the size of the
|
||||||
|
/// scroll view needs to be recomputed whenever the scroll position changes.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
final bool shrinkWrap;
|
||||||
|
|
||||||
|
/// The amount of space by which to inset the children.
|
||||||
|
final EdgeInsets padding;
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// remove items in response to user input.
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// AnimatedListState animatedList = AnimatedList.of(context);
|
||||||
|
/// ```
|
||||||
|
static AnimatedListState of(BuildContext context, { bool nullOk: false }) {
|
||||||
|
assert(nullOk != null);
|
||||||
|
assert(context != null);
|
||||||
|
final AnimatedListState result = context.ancestorStateOfType(const TypeMatcher<AnimatedListState>());
|
||||||
|
if (nullOk || result != null)
|
||||||
|
return result;
|
||||||
|
throw new FlutterError(
|
||||||
|
'AnimatedList.of() called with a context that does not contain a AnimatedList.\n'
|
||||||
|
'No AnimatedList ancestor could be found starting from the context that was passed to AnimatedList.of(). '
|
||||||
|
'This can happen when the context provided is from the same StatefulWidget that '
|
||||||
|
'built the AnimatedList. Please see the AnimatedList documentation for examples '
|
||||||
|
'of how to refer to an AnimatedListState object: '
|
||||||
|
' https://docs.flutter.io/flutter/widgets/AnimatedState-class.html\n'
|
||||||
|
'The context used was:\n'
|
||||||
|
' $context'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AnimatedListState createState() => new AnimatedListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The state for a scrolling container that animates items when they are
|
||||||
|
/// inserted or removed.
|
||||||
|
///
|
||||||
|
/// When an item is inserted with [insertItem] an animation begins running.
|
||||||
|
/// The animation is passed to [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 [AnimatedList]'s state with a global key:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// GlobalKey<AnimatedListState> listKey = new GlobalKey<AnimatedListState>();
|
||||||
|
/// ...
|
||||||
|
/// new AnimatedList(key: listKey, ...);
|
||||||
|
/// ...
|
||||||
|
/// listKey.currentState.insert(123);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// AnimatedList item input handlers can also refer to their [AnimatedListState]
|
||||||
|
/// with the static [of] method.
|
||||||
|
class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixin {
|
||||||
|
final List<_ActiveItem> _incomingItems = <_ActiveItem>[];
|
||||||
|
final List<_ActiveItem> _outgoingItems = <_ActiveItem>[];
|
||||||
|
int _itemsCount = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_itemsCount = widget.initialItemCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
for (_ActiveItem item in _incomingItems)
|
||||||
|
item.controller.dispose();
|
||||||
|
for (_ActiveItem item in _outgoingItems)
|
||||||
|
item.controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_ActiveItem _removeActiveItemAt(List<_ActiveItem> items, int itemIndex) {
|
||||||
|
final int i = binarySearch(items, new _ActiveItem.index(itemIndex));
|
||||||
|
return i == -1 ? null : items.removeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ActiveItem _activeItemAt(List<_ActiveItem> items, int itemIndex) {
|
||||||
|
final int i = binarySearch(items, new _ActiveItem.index(itemIndex));
|
||||||
|
return i == -1 ? null : items[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The insertItem() and removeItem() index parameters are defined as if the
|
||||||
|
// removeItem() operation removed the corresponding list entry immediately.
|
||||||
|
// The entry is only actually removed from the ListView when the remove animation
|
||||||
|
// finishes. The entry is added to _outgoingItems when removeItem is called
|
||||||
|
// and removed from _outgoingItems when the remove animation finishes.
|
||||||
|
|
||||||
|
int _indexToItemIndex(int index) {
|
||||||
|
int itemIndex = index;
|
||||||
|
for (_ActiveItem item in _outgoingItems) {
|
||||||
|
if (item.itemIndex <= itemIndex)
|
||||||
|
itemIndex += 1;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return itemIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _itemIndexToIndex(int itemIndex) {
|
||||||
|
int index = itemIndex;
|
||||||
|
for (_ActiveItem item in _outgoingItems) {
|
||||||
|
assert(item.itemIndex != itemIndex);
|
||||||
|
if (item.itemIndex < itemIndex)
|
||||||
|
index -= 1;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert an item at [index] and start an animation that will be passed
|
||||||
|
/// to [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 }) {
|
||||||
|
assert(index != null && index >= 0);
|
||||||
|
assert(duration != null);
|
||||||
|
|
||||||
|
final int itemIndex = _indexToItemIndex(index);
|
||||||
|
assert(itemIndex >= 0 && itemIndex <= _itemsCount);
|
||||||
|
|
||||||
|
// Increment the incoming and outgoing item indices to account
|
||||||
|
// for the insertion.
|
||||||
|
for (_ActiveItem item in _incomingItems) {
|
||||||
|
if (item.itemIndex >= itemIndex)
|
||||||
|
item.itemIndex += 1;
|
||||||
|
}
|
||||||
|
for (_ActiveItem item in _outgoingItems) {
|
||||||
|
if (item.itemIndex >= itemIndex)
|
||||||
|
item.itemIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final AnimationController controller = new AnimationController(duration: duration, vsync: this);
|
||||||
|
final _ActiveItem incomingItem = new _ActiveItem.incoming(controller, itemIndex);
|
||||||
|
setState(() {
|
||||||
|
_incomingItems
|
||||||
|
..add(incomingItem)
|
||||||
|
..sort();
|
||||||
|
_itemsCount += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.forward().then((Null value) {
|
||||||
|
_removeActiveItemAt(_incomingItems, incomingItem.itemIndex).controller.dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 [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 }) {
|
||||||
|
assert(index != null && index >= 0);
|
||||||
|
assert(builder != null);
|
||||||
|
assert(duration != null);
|
||||||
|
|
||||||
|
final int itemIndex = _indexToItemIndex(index);
|
||||||
|
assert(itemIndex >= 0 && itemIndex < _itemsCount);
|
||||||
|
assert(_activeItemAt(_outgoingItems, itemIndex) == null);
|
||||||
|
|
||||||
|
final _ActiveItem incomingItem = _removeActiveItemAt(_incomingItems, itemIndex);
|
||||||
|
final AnimationController controller = incomingItem?.controller
|
||||||
|
?? new AnimationController(duration: duration, value: 1.0, vsync: this);
|
||||||
|
final _ActiveItem outgoingItem = new _ActiveItem.outgoing(controller, itemIndex, builder);
|
||||||
|
setState(() {
|
||||||
|
_outgoingItems
|
||||||
|
..add(outgoingItem)
|
||||||
|
..sort();
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.reverse().then((Null value) {
|
||||||
|
_removeActiveItemAt(_outgoingItems, outgoingItem.itemIndex).controller.dispose();
|
||||||
|
|
||||||
|
// Decrement the incoming and outgoing item indices to account
|
||||||
|
// for the removal.
|
||||||
|
for (_ActiveItem item in _incomingItems) {
|
||||||
|
if (item.itemIndex > outgoingItem.itemIndex)
|
||||||
|
item.itemIndex -= 1;
|
||||||
|
}
|
||||||
|
for (_ActiveItem item in _outgoingItems) {
|
||||||
|
if (item.itemIndex > outgoingItem.itemIndex)
|
||||||
|
item.itemIndex -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_itemsCount -= 1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _itemBuilder(BuildContext context, int itemIndex) {
|
||||||
|
final _ActiveItem outgoingItem = _activeItemAt(_outgoingItems, itemIndex);
|
||||||
|
if (outgoingItem != null)
|
||||||
|
return outgoingItem.removedItemBuilder(context, outgoingItem.controller.view);
|
||||||
|
|
||||||
|
final _ActiveItem incomingItem = _activeItemAt(_incomingItems, itemIndex);
|
||||||
|
final Animation<double> animation = incomingItem?.controller?.view ?? kAlwaysCompleteAnimation;
|
||||||
|
return widget.itemBuilder(context, _itemIndexToIndex(itemIndex), animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return new 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ library widgets;
|
|||||||
export 'package:vector_math/vector_math_64.dart' show Matrix4;
|
export 'package:vector_math/vector_math_64.dart' show Matrix4;
|
||||||
|
|
||||||
export 'src/widgets/animated_cross_fade.dart';
|
export 'src/widgets/animated_cross_fade.dart';
|
||||||
|
export 'src/widgets/animated_list.dart';
|
||||||
export 'src/widgets/animated_size.dart';
|
export 'src/widgets/animated_size.dart';
|
||||||
export 'src/widgets/app.dart';
|
export 'src/widgets/app.dart';
|
||||||
export 'src/widgets/async.dart';
|
export 'src/widgets/async.dart';
|
||||||
|
176
packages/flutter/test/widgets/animated_list_test.dart
Normal file
176
packages/flutter/test/widgets/animated_list_test.dart
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter_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>>{};
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
new AnimatedList(
|
||||||
|
initialItemCount: 2,
|
||||||
|
itemBuilder: (BuildContext context, int index, Animation<double> animation) {
|
||||||
|
animations[index] = animation;
|
||||||
|
return new SizedBox(
|
||||||
|
height: 100.0,
|
||||||
|
child: new Center(
|
||||||
|
child: new 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 = new GlobalKey<AnimatedListState>();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
new AnimatedList(
|
||||||
|
key: listKey,
|
||||||
|
itemBuilder: (BuildContext context, int index, Animation<double> animation) {
|
||||||
|
return new SizeTransition(
|
||||||
|
key: new ValueKey<int>(index),
|
||||||
|
axis: Axis.vertical,
|
||||||
|
sizeFactor: animation,
|
||||||
|
child: new SizedBox(
|
||||||
|
height: 100.0,
|
||||||
|
child: new Center(
|
||||||
|
child: new Text('item $index'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
double itemHeight(int index) => tester.getSize(find.byKey(new ValueKey<int>(index))).height;
|
||||||
|
double itemTop(int index) => tester.getTopLeft(find.byKey(new ValueKey<int>(index))).dy;
|
||||||
|
double itemBottom(int index) => tester.getBottomLeft(find.byKey(new ValueKey<int>(index))).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('AnimatedList remove', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<AnimatedListState> listKey = new GlobalKey<AnimatedListState>();
|
||||||
|
final List<int> items = <int>[0, 1, 2];
|
||||||
|
|
||||||
|
Widget buildItem(BuildContext context, int item, Animation<double> animation) {
|
||||||
|
return new SizeTransition(
|
||||||
|
key: new ValueKey<int>(item),
|
||||||
|
axis: Axis.vertical,
|
||||||
|
sizeFactor: animation,
|
||||||
|
child: new SizedBox(
|
||||||
|
height: 100.0,
|
||||||
|
child: new Center(
|
||||||
|
child: new Text('item $item'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
new 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(new ValueKey<int>(index))).dy;
|
||||||
|
double itemBottom(int index) => tester.getBottomLeft(find.byKey(new 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),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Item's 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
|
||||||
|
|
||||||
|
// Item's 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);
|
||||||
|
|
||||||
|
// Item's 0, 1, 2 at 0, 0, 0. Item 0's height is 0.
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(itemTop(0), 0.0);
|
||||||
|
expect(itemBottom(0), 0.0);
|
||||||
|
expect(itemTop(1), 0.0);
|
||||||
|
expect(itemBottom(1), 100.0);
|
||||||
|
expect(itemTop(2), 100.0);
|
||||||
|
expect(itemBottom(2), 200.0);
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user