animated_list.dart 14.2 KB
Newer Older
Hans Muller's avatar
Hans Muller committed
1 2 3 4 5
// 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/animation.dart';
6
import 'package:flutter/foundation.dart';
Hans Muller's avatar
Hans Muller committed
7 8 9 10 11 12 13 14 15

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].
16
typedef AnimatedListItemBuilder = Widget Function(BuildContext context, int index, Animation<double> animation);
Hans Muller's avatar
Hans Muller committed
17

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

// The default insert/remove animation duration.
22
const Duration _kDuration = Duration(milliseconds: 300);
Hans Muller's avatar
Hans Muller committed
23 24 25 26 27

// 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);
28 29 30
  _ActiveItem.index(this.itemIndex)
    : controller = null,
      removedItemBuilder = null;
Hans Muller's avatar
Hans Muller committed
31 32 33 34 35 36 37 38 39 40 41

  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.
///
42
/// This widget's [AnimatedListState] can be used to dynamically insert or remove
Hans Muller's avatar
Hans Muller committed
43 44 45 46
/// 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].
47 48
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=ZtfItHwFlZ8}
Hans Muller's avatar
Hans Muller committed
49 50
class AnimatedList extends StatefulWidget {
  /// Creates a scrolling container that animates items when they are inserted or removed.
51
  const AnimatedList({
Hans Muller's avatar
Hans Muller committed
52 53
    Key key,
    @required this.itemBuilder,
54 55 56
    this.initialItemCount = 0,
    this.scrollDirection = Axis.vertical,
    this.reverse = false,
Hans Muller's avatar
Hans Muller committed
57 58 59
    this.controller,
    this.primary,
    this.physics,
60
    this.shrinkWrap = false,
Hans Muller's avatar
Hans Muller committed
61
    this.padding,
62 63 64
  }) : assert(itemBuilder != null),
       assert(initialItemCount != null && initialItemCount >= 0),
       super(key: key);
Hans Muller's avatar
Hans Muller committed
65 66 67 68 69 70

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

  /// The number of items the list will start with.
  ///
82 83
  /// 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
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
  /// 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.
110 111 112 113 114 115 116 117
  ///
  /// 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
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 152 153 154
  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.
155
  final EdgeInsetsGeometry padding;
Hans Muller's avatar
Hans Muller committed
156 157 158 159 160 161 162 163 164

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

  @override
184
  AnimatedListState createState() => AnimatedListState();
Hans Muller's avatar
Hans Muller committed
185 186 187 188 189
}

/// The state for a scrolling container that animates items when they are
/// inserted or removed.
///
190 191
/// 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
192 193 194
/// is needed.
///
/// When an item is removed with [removeItem] its animation is reversed.
195
/// The removed item's animation is passed to the [removeItem] builder
Hans Muller's avatar
Hans Muller committed
196 197 198 199 200 201
/// 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
202
/// GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
Hans Muller's avatar
Hans Muller committed
203
/// ...
204
/// AnimatedList(key: listKey, ...);
Hans Muller's avatar
Hans Muller committed
205 206 207 208
/// ...
/// listKey.currentState.insert(123);
/// ```
///
209 210
/// [AnimatedList] item input handlers can also refer to their [AnimatedListState]
/// with the static [AnimatedList.of] method.
211
class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixin<AnimatedList> {
Hans Muller's avatar
Hans Muller committed
212 213 214 215 216 217 218 219 220 221 222 223
  final List<_ActiveItem> _incomingItems = <_ActiveItem>[];
  final List<_ActiveItem> _outgoingItems = <_ActiveItem>[];
  int _itemsCount = 0;

  @override
  void initState() {
    super.initState();
    _itemsCount = widget.initialItemCount;
  }

  @override
  void dispose() {
224 225 226
    for (_ActiveItem item in _incomingItems)
      item.controller.dispose();
    for (_ActiveItem item in _outgoingItems)
Hans Muller's avatar
Hans Muller committed
227 228 229 230 231
      item.controller.dispose();
    super.dispose();
  }

  _ActiveItem _removeActiveItemAt(List<_ActiveItem> items, int itemIndex) {
232
    final int i = binarySearch(items, _ActiveItem.index(itemIndex));
Hans Muller's avatar
Hans Muller committed
233 234 235 236
    return i == -1 ? null : items.removeAt(i);
  }

  _ActiveItem _activeItemAt(List<_ActiveItem> items, int itemIndex) {
237
    final int i = binarySearch(items, _ActiveItem.index(itemIndex));
Hans Muller's avatar
Hans Muller committed
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 268 269
    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;
  }

270 271
  /// Insert an item at [index] and start an animation that will be passed
  /// to [AnimatedList.itemBuilder] when the item is visible.
Hans Muller's avatar
Hans Muller committed
272 273 274 275
  ///
  /// 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.
276
  void insertItem(int index, { Duration duration = _kDuration }) {
Hans Muller's avatar
Hans Muller committed
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
    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;
    }

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

303
    controller.forward().then<void>((_) {
Hans Muller's avatar
Hans Muller committed
304 305 306 307 308 309 310 311
      _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
312 313
  /// will no longer be passed to the [AnimatedList.itemBuilder]. However the
  /// item will still appear in the list for [duration] and during that time
314
  /// [builder] must construct its widget as needed.
Hans Muller's avatar
Hans Muller committed
315 316 317 318
  ///
  /// 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.
319
  void removeItem(int index, AnimatedListRemovedItemBuilder builder, { Duration duration = _kDuration }) {
Hans Muller's avatar
Hans Muller committed
320 321 322 323 324 325 326 327 328 329
    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
330 331
      ?? AnimationController(duration: duration, value: 1.0, vsync: this);
    final _ActiveItem outgoingItem = _ActiveItem.outgoing(controller, itemIndex, builder);
Hans Muller's avatar
Hans Muller committed
332 333 334 335 336 337
    setState(() {
      _outgoingItems
        ..add(outgoingItem)
        ..sort();
    });

338
    controller.reverse().then<void>((void value) {
Hans Muller's avatar
Hans Muller committed
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 367 368 369
      _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) {
370 371 372 373 374 375 376 377 378 379
    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,
Hans Muller's avatar
Hans Muller committed
380 381 382
    );
  }
}