animated_list.dart 14.2 KB
Newer Older
Hans Muller's avatar
Hans Muller committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// 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].
17
typedef AnimatedListItemBuilder = Widget Function(BuildContext context, int index, Animation<double> animation);
Hans Muller's avatar
Hans Muller committed
18

19
/// Signature for the builder callback used by [AnimatedListState.removeItem].
20
typedef AnimatedListRemovedItemBuilder = Widget Function(BuildContext context, Animation<double> animation);
Hans Muller's avatar
Hans Muller committed
21 22

// The default insert/remove animation duration.
23
const Duration _kDuration = Duration(milliseconds: 300);
Hans Muller's avatar
Hans Muller committed
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

// 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.
///
41
/// This widget's [AnimatedListState] can be used to dynamically insert or remove
Hans Muller's avatar
Hans Muller committed
42 43 44 45 46 47
/// 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.
48
  const AnimatedList({
Hans Muller's avatar
Hans Muller committed
49 50
    Key key,
    @required this.itemBuilder,
51 52 53
    this.initialItemCount = 0,
    this.scrollDirection = Axis.vertical,
    this.reverse = false,
Hans Muller's avatar
Hans Muller committed
54 55 56
    this.controller,
    this.primary,
    this.physics,
57
    this.shrinkWrap = false,
Hans Muller's avatar
Hans Muller committed
58
    this.padding,
59 60 61
  }) : assert(itemBuilder != null),
       assert(initialItemCount != null && initialItemCount >= 0),
       super(key: key);
Hans Muller's avatar
Hans Muller committed
62 63 64 65 66 67

  /// 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
68
  /// position in the list. The value of the index parameter will be between 0
69 70 71
  /// 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].
Hans Muller's avatar
Hans Muller committed
72
  ///
73 74
  /// Implementations of this callback should assume that
  /// [AnimatedListState.removeItem] removes an item immediately.
Hans Muller's avatar
Hans Muller committed
75 76 77 78
  final AnimatedListItemBuilder itemBuilder;

  /// The number of items the list will start with.
  ///
79 80
  /// The appearance of the initial items is not animated. They
  /// are created, as needed, by [itemBuilder] with an animation parameter
Hans Muller's avatar
Hans Muller committed
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
  /// 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.
107 108 109 110 111 112 113 114
  ///
  /// 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]).
Hans Muller's avatar
Hans Muller committed
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
  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.
152
  final EdgeInsetsGeometry padding;
Hans Muller's avatar
Hans Muller committed
153 154 155 156 157 158 159 160 161

  /// 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);
  /// ```
162
  static AnimatedListState of(BuildContext context, { bool nullOk = false }) {
Hans Muller's avatar
Hans Muller committed
163
    assert(context != null);
Ian Hickson's avatar
Ian Hickson committed
164
    assert(nullOk != null);
Hans Muller's avatar
Hans Muller committed
165 166 167
    final AnimatedListState result = context.ancestorStateOfType(const TypeMatcher<AnimatedListState>());
    if (nullOk || result != null)
      return result;
168
    throw FlutterError(
169
      'AnimatedList.of() called with a context that does not contain an AnimatedList.\n'
Hans Muller's avatar
Hans Muller committed
170 171 172 173
      '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: '
174
      '  https://docs.flutter.io/flutter/widgets/AnimatedListState-class.html \n'
Hans Muller's avatar
Hans Muller committed
175 176 177 178 179 180
      'The context used was:\n'
      '  $context'
    );
  }

  @override
181
  AnimatedListState createState() => AnimatedListState();
Hans Muller's avatar
Hans Muller committed
182 183 184 185 186
}

/// The state for a scrolling container that animates items when they are
/// inserted or removed.
///
187 188
/// When an item is inserted with [insertItem] an animation begins running. The
/// animation is passed to [AnimatedList.itemBuilder] whenever the item's widget
Hans Muller's avatar
Hans Muller committed
189 190 191
/// is needed.
///
/// When an item is removed with [removeItem] its animation is reversed.
192
/// The removed item's animation is passed to the [removeItem] builder
Hans Muller's avatar
Hans Muller committed
193 194 195 196 197 198
/// 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
199
/// GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
Hans Muller's avatar
Hans Muller committed
200
/// ...
201
/// AnimatedList(key: listKey, ...);
Hans Muller's avatar
Hans Muller committed
202 203 204 205
/// ...
/// listKey.currentState.insert(123);
/// ```
///
206 207
/// [AnimatedList] item input handlers can also refer to their [AnimatedListState]
/// with the static [AnimatedList.of] method.
208
class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixin<AnimatedList> {
Hans Muller's avatar
Hans Muller committed
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
  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) {
229
    final int i = binarySearch(items, _ActiveItem.index(itemIndex));
Hans Muller's avatar
Hans Muller committed
230 231 232 233
    return i == -1 ? null : items.removeAt(i);
  }

  _ActiveItem _activeItemAt(List<_ActiveItem> items, int itemIndex) {
234
    final int i = binarySearch(items, _ActiveItem.index(itemIndex));
Hans Muller's avatar
Hans Muller committed
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
    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
268
  /// to [AnimatedList.itemBuilder] when the item is visible.
Hans Muller's avatar
Hans Muller committed
269 270 271 272
  ///
  /// 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.
273
  void insertItem(int index, { Duration duration = _kDuration }) {
Hans Muller's avatar
Hans Muller committed
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
    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;
    }

291 292
    final AnimationController controller = AnimationController(duration: duration, vsync: this);
    final _ActiveItem incomingItem = _ActiveItem.incoming(controller, itemIndex);
Hans Muller's avatar
Hans Muller committed
293 294 295 296 297 298 299
    setState(() {
      _incomingItems
        ..add(incomingItem)
        ..sort();
      _itemsCount += 1;
    });

300
    controller.forward().then<void>((_) {
Hans Muller's avatar
Hans Muller committed
301 302 303 304 305 306 307 308
      _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
309 310 311
  /// 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.
Hans Muller's avatar
Hans Muller committed
312 313 314 315
  ///
  /// 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.
316
  void removeItem(int index, AnimatedListRemovedItemBuilder builder, { Duration duration = _kDuration }) {
Hans Muller's avatar
Hans Muller committed
317 318 319 320 321 322 323 324 325 326
    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
327 328
      ?? AnimationController(duration: duration, value: 1.0, vsync: this);
    final _ActiveItem outgoingItem = _ActiveItem.outgoing(controller, itemIndex, builder);
Hans Muller's avatar
Hans Muller committed
329 330 331 332 333 334
    setState(() {
      _outgoingItems
        ..add(outgoingItem)
        ..sort();
    });

335
    controller.reverse().then<void>((void value) {
Hans Muller's avatar
Hans Muller committed
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366
      _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) {
367
    return ListView.builder(
Hans Muller's avatar
Hans Muller committed
368 369 370 371 372 373 374 375 376 377 378 379
      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,
    );
  }
}