| // 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 '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 [AnimatedListState.removeItem]. |
| 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 dynamically 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. |
| const 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, |
| }) : 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 [AnimatedListState.insertItem] and less the total number of |
| /// items that have been removed with [AnimatedListState.removeItem]. |
| /// |
| /// Implementations of this callback should assume that |
| /// [AnimatedListState.removeItem] removes an item immediately. |
| final AnimatedListItemBuilder itemBuilder; |
| |
| /// 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]. |
| 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. |
| /// |
| /// A [ScrollController] serves several purposes. It can be used to control |
| /// the initial scroll position (see [ScrollController.initialScrollOffset]). |
| /// It can be used to control whether the scroll view should automatically |
| /// save and restore its scroll position in the [PageStorage] (see |
| /// [ScrollController.keepScrollOffset]). It can be used to read the current |
| /// scroll position (see [ScrollController.offset]), or change it (see |
| /// [ScrollController.animateTo]). |
| 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 EdgeInsetsGeometry 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(context != null); |
| assert(nullOk != 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/AnimatedListState-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 [AnimatedList.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 [AnimatedList.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 [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 }) { |
| 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<void>((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 [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 }) { |
| 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<void>((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, |
| ); |
| } |
| } |