animated_list.dart 23.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Hans Muller's avatar
Hans Muller committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/foundation.dart';
Hans Muller's avatar
Hans Muller committed
6 7 8 9 10 11

import 'basic.dart';
import 'framework.dart';
import 'scroll_controller.dart';
import 'scroll_physics.dart';
import 'scroll_view.dart';
12
import 'sliver.dart';
Hans Muller's avatar
Hans Muller committed
13 14 15
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
  final AnimationController? controller;
  final AnimatedListRemovedItemBuilder? removedItemBuilder;
Hans Muller's avatar
Hans Muller committed
34 35 36 37 38 39 40 41
  int itemIndex;

  @override
  int compareTo(_ActiveItem other) => itemIndex - other.itemIndex;
}

/// A scrolling container that animates items when they are inserted or removed.
///
42 43 44
/// 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.
Hans Muller's avatar
Hans Muller committed
45 46
///
/// This widget is similar to one created by [ListView.builder].
47 48
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=ZtfItHwFlZ8}
49
///
50
/// {@tool dartpad}
51 52 53
/// This sample application uses an [AnimatedList] to create an effect when
/// items are removed or added to the list.
///
54
/// ** See code in examples/api/lib/widgets/animated_list/animated_list.0.dart **
55 56 57 58
/// {@end-tool}
///
/// See also:
///
59 60
///  * [SliverAnimatedList], a sliver that animates items when they are inserted
///    or removed from a list.
Hans Muller's avatar
Hans Muller committed
61
class AnimatedList extends StatefulWidget {
62 63
  /// Creates a scrolling container that animates items when they are inserted
  /// or removed.
64
  const AnimatedList({
65
    super.key,
66
    required this.itemBuilder,
67 68 69
    this.initialItemCount = 0,
    this.scrollDirection = Axis.vertical,
    this.reverse = false,
Hans Muller's avatar
Hans Muller committed
70 71 72
    this.controller,
    this.primary,
    this.physics,
73
    this.shrinkWrap = false,
Hans Muller's avatar
Hans Muller committed
74
    this.padding,
75
    this.clipBehavior = Clip.hardEdge,
76
  }) : assert(itemBuilder != null),
77
       assert(initialItemCount != null && initialItemCount >= 0);
Hans Muller's avatar
Hans Muller committed
78 79 80 81 82 83

  /// 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
84
  /// position in the list. The value of the index parameter will be between 0
85 86 87
  /// 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
88
  ///
89 90
  /// Implementations of this callback should assume that
  /// [AnimatedListState.removeItem] removes an item immediately.
Hans Muller's avatar
Hans Muller committed
91 92
  final AnimatedListItemBuilder itemBuilder;

93
  /// {@template flutter.widgets.animatedList.initialItemCount}
Hans Muller's avatar
Hans Muller committed
94 95
  /// The number of items the list will start with.
  ///
96 97
  /// 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
98
  /// of [kAlwaysCompleteAnimation].
99
  /// {@endtemplate}
Hans Muller's avatar
Hans Muller committed
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
  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.
125 126 127 128 129 130 131 132
  ///
  /// 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]).
133
  final ScrollController? controller;
Hans Muller's avatar
Hans Muller committed
134 135 136 137 138 139 140 141 142

  /// 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.
143
  final bool? primary;
Hans Muller's avatar
Hans Muller committed
144 145 146 147 148 149 150

  /// 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.
151
  final ScrollPhysics? physics;
Hans Muller's avatar
Hans Muller committed
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169

  /// 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.
170
  final EdgeInsetsGeometry? padding;
Hans Muller's avatar
Hans Muller committed
171

172 173 174 175 176
  /// {@macro flutter.material.Material.clipBehavior}
  ///
  /// Defaults to [Clip.hardEdge].
  final Clip clipBehavior;

177 178
  /// The state from the closest instance of this class that encloses the given
  /// context.
Hans Muller's avatar
Hans Muller committed
179
  ///
180 181
  /// This method is typically used by [AnimatedList] item widgets that insert
  /// or remove items in response to user input.
Hans Muller's avatar
Hans Muller committed
182
  ///
183 184 185
  /// If no [AnimatedList] surrounds the context given, then this function will
  /// assert in debug mode and throw an exception in release mode.
  ///
186 187
  /// This method can be expensive (it walks the element tree).
  ///
188 189 190 191 192
  /// See also:
  ///
  ///  * [maybeOf], a similar function that will return null if no
  ///    [AnimatedList] ancestor is found.
  static AnimatedListState of(BuildContext context) {
Hans Muller's avatar
Hans Muller committed
193
    assert(context != null);
194
    final AnimatedListState? result = context.findAncestorStateOfType<AnimatedListState>();
195
    assert(() {
196 197
      if (result == null) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
198
          ErrorSummary('AnimatedList.of() called with a context that does not contain an AnimatedList.'),
199
          ErrorDescription(
200 201
            'No AnimatedList ancestor could be found starting from the context that was passed to AnimatedList.of().',
          ),
202 203 204
          ErrorHint(
            'This can happen when the context provided is from the same StatefulWidget that '
            'built the AnimatedList. Please see the AnimatedList documentation for examples '
205 206
            'of how to refer to an AnimatedListState object:\n'
            '  https://api.flutter.dev/flutter/widgets/AnimatedListState-class.html',
207
          ),
208
          context.describeElement('The context used was'),
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
        ]);
      }
      return true;
    }());
    return result!;
  }

  /// 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.
  ///
  /// If no [AnimatedList] surrounds the context given, then this function will
  /// return null.
  ///
225 226
  /// This method can be expensive (it walks the element tree).
  ///
227 228 229 230 231 232 233
  /// See also:
  ///
  ///  * [of], a similar function that will throw if no [AnimatedList] ancestor
  ///    is found.
  static AnimatedListState? maybeOf(BuildContext context) {
    assert(context != null);
    return context.findAncestorStateOfType<AnimatedListState>();
Hans Muller's avatar
Hans Muller committed
234 235 236
  }

  @override
237
  AnimatedListState createState() => AnimatedListState();
Hans Muller's avatar
Hans Muller committed
238 239 240 241 242
}

/// The state for a scrolling container that animates items when they are
/// inserted or removed.
///
243 244
/// 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
245 246 247
/// is needed.
///
/// When an item is removed with [removeItem] its animation is reversed.
248
/// The removed item's animation is passed to the [removeItem] builder
Hans Muller's avatar
Hans Muller committed
249 250 251 252 253 254
/// 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
255
/// GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
Hans Muller's avatar
Hans Muller committed
256
/// ...
257
/// AnimatedList(key: listKey, ...);
Hans Muller's avatar
Hans Muller committed
258 259 260 261
/// ...
/// listKey.currentState.insert(123);
/// ```
///
262 263
/// [AnimatedList] item input handlers can also refer to their [AnimatedListState]
/// with the static [AnimatedList.of] method.
264
class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixin<AnimatedList> {
265 266 267 268 269 270 271 272 273
  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 }) {
274
    _sliverAnimatedListKey.currentState!.insertItem(index, duration: duration);
275 276 277 278 279 280 281 282 283 284 285 286 287 288
  }

  /// 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 }) {
289
    _sliverAnimatedListKey.currentState!.removeItem(index, builder, duration: duration);
290 291 292 293 294 295 296 297 298 299 300
  }

  @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,
301
      clipBehavior: widget.clipBehavior,
302 303
      slivers: <Widget>[
        SliverPadding(
304
          padding: widget.padding ?? EdgeInsets.zero,
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
          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.
///
323
/// {@tool dartpad}
324 325 326
/// This sample application uses a [SliverAnimatedList] to create an animated
/// effect when items are removed or added to the list.
///
327
/// ** See code in examples/api/lib/widgets/animated_list/sliver_animated_list.0.dart **
328 329 330 331
/// {@end-tool}
///
/// See also:
///
332 333 334 335
///  * [SliverList], which does not animate items when they are inserted or
///    removed.
///  * [AnimatedList], a non-sliver scrolling container that animates items when
///    they are inserted or removed.
336 337 338
class SliverAnimatedList extends StatefulWidget {
  /// Creates a sliver that animates items when they are inserted or removed.
  const SliverAnimatedList({
339
    super.key,
340
    required this.itemBuilder,
341
    this.findChildIndexCallback,
342 343
    this.initialItemCount = 0,
  }) : assert(itemBuilder != null),
344
       assert(initialItemCount != null && initialItemCount >= 0);
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360

  /// 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;

361 362 363
  /// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback}
  final ChildIndexGetter? findChildIndexCallback;

364 365 366 367 368 369 370 371 372 373 374 375
  /// {@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.
  ///
376 377 378
  /// If no [SliverAnimatedList] surrounds the context given, then this function
  /// will assert in debug mode and throw an exception in release mode.
  ///
379 380
  /// This method can be expensive (it walks the element tree).
  ///
381 382 383 384 385
  /// See also:
  ///
  ///  * [maybeOf], a similar function that will return null if no
  ///    [SliverAnimatedList] ancestor is found.
  static SliverAnimatedListState of(BuildContext context) {
386
    assert(context != null);
387
    final SliverAnimatedListState? result = context.findAncestorStateOfType<SliverAnimatedListState>();
388
    assert(() {
389 390 391 392 393 394 395 396
      if (result == null) {
        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: '
397
          'https://api.flutter.dev/flutter/widgets/SliverAnimatedListState-class.html\n'
398
          'The context used was:\n'
399 400
          '  $context',
        );
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415
      }
      return true;
    }());
    return result!;
  }

  /// 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.
  ///
  /// If no [SliverAnimatedList] surrounds the context given, then this function
  /// will return null.
  ///
416 417
  /// This method can be expensive (it walks the element tree).
  ///
418 419 420 421 422 423 424
  /// See also:
  ///
  ///  * [of], a similar function that will throw if no [SliverAnimatedList]
  ///    ancestor is found.
  static SliverAnimatedListState? maybeOf(BuildContext context) {
    assert(context != null);
    return context.findAncestorStateOfType<SliverAnimatedListState>();
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
  }
}

/// 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 {

Hans Muller's avatar
Hans Muller committed
454 455 456 457 458 459 460 461 462 463 464 465
  final List<_ActiveItem> _incomingItems = <_ActiveItem>[];
  final List<_ActiveItem> _outgoingItems = <_ActiveItem>[];
  int _itemsCount = 0;

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

  @override
  void dispose() {
466
    for (final _ActiveItem item in _incomingItems.followedBy(_outgoingItems)) {
467
      item.controller!.dispose();
468
    }
Hans Muller's avatar
Hans Muller committed
469 470 471
    super.dispose();
  }

472
  _ActiveItem? _removeActiveItemAt(List<_ActiveItem> items, int itemIndex) {
473
    final int i = binarySearch(items, _ActiveItem.index(itemIndex));
Hans Muller's avatar
Hans Muller committed
474 475 476
    return i == -1 ? null : items.removeAt(i);
  }

477
  _ActiveItem? _activeItemAt(List<_ActiveItem> items, int itemIndex) {
478
    final int i = binarySearch(items, _ActiveItem.index(itemIndex));
Hans Muller's avatar
Hans Muller committed
479 480 481 482 483 484 485 486 487 488 489
    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;
490
    for (final _ActiveItem item in _outgoingItems) {
491
      if (item.itemIndex <= itemIndex) {
Hans Muller's avatar
Hans Muller committed
492
        itemIndex += 1;
493
      } else {
Hans Muller's avatar
Hans Muller committed
494
        break;
495
      }
Hans Muller's avatar
Hans Muller committed
496 497 498 499 500 501
    }
    return itemIndex;
  }

  int _itemIndexToIndex(int itemIndex) {
    int index = itemIndex;
502
    for (final _ActiveItem item in _outgoingItems) {
Hans Muller's avatar
Hans Muller committed
503
      assert(item.itemIndex != itemIndex);
504
      if (item.itemIndex < itemIndex) {
Hans Muller's avatar
Hans Muller committed
505
        index -= 1;
506
      } else {
Hans Muller's avatar
Hans Muller committed
507
        break;
508
      }
Hans Muller's avatar
Hans Muller committed
509 510 511 512
    }
    return index;
  }

513
  SliverChildDelegate _createDelegate() {
514 515 516 517 518
    return SliverChildBuilderDelegate(
      _itemBuilder,
      childCount: _itemsCount,
      findChildIndexCallback: widget.findChildIndexCallback,
    );
519 520 521 522
  }

  /// Insert an item at [index] and start an animation that will be passed to
  /// [SliverAnimatedList.itemBuilder] when the item is visible.
Hans Muller's avatar
Hans Muller committed
523 524 525 526
  ///
  /// 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.
527
  void insertItem(int index, { Duration duration = _kDuration }) {
Hans Muller's avatar
Hans Muller committed
528 529 530 531 532 533 534 535
    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.
536
    for (final _ActiveItem item in _incomingItems) {
537
      if (item.itemIndex >= itemIndex) {
Hans Muller's avatar
Hans Muller committed
538
        item.itemIndex += 1;
539
      }
Hans Muller's avatar
Hans Muller committed
540
    }
541
    for (final _ActiveItem item in _outgoingItems) {
542
      if (item.itemIndex >= itemIndex) {
Hans Muller's avatar
Hans Muller committed
543
        item.itemIndex += 1;
544
      }
Hans Muller's avatar
Hans Muller committed
545 546
    }

547 548 549 550 551 552 553 554
    final AnimationController controller = AnimationController(
      duration: duration,
      vsync: this,
    );
    final _ActiveItem incomingItem = _ActiveItem.incoming(
      controller,
      itemIndex,
    );
Hans Muller's avatar
Hans Muller committed
555 556 557 558 559 560 561
    setState(() {
      _incomingItems
        ..add(incomingItem)
        ..sort();
      _itemsCount += 1;
    });

562
    controller.forward().then<void>((_) {
563
      _removeActiveItemAt(_incomingItems, incomingItem.itemIndex)!.controller!.dispose();
Hans Muller's avatar
Hans Muller committed
564 565 566 567 568 569 570
    });
  }

  /// 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
571 572
  /// will no longer be passed to the [SliverAnimatedList.itemBuilder]. However
  /// the item will still appear in the list for [duration] and during that time
573
  /// [builder] must construct its widget as needed.
Hans Muller's avatar
Hans Muller committed
574 575 576 577
  ///
  /// 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.
578
  void removeItem(int index, AnimatedListRemovedItemBuilder builder, { Duration duration = _kDuration }) {
Hans Muller's avatar
Hans Muller committed
579 580 581 582 583 584 585 586
    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);

587
    final _ActiveItem? incomingItem = _removeActiveItemAt(_incomingItems, itemIndex);
Hans Muller's avatar
Hans Muller committed
588
    final AnimationController controller = incomingItem?.controller
589 590
      ?? AnimationController(duration: duration, value: 1.0, vsync: this);
    final _ActiveItem outgoingItem = _ActiveItem.outgoing(controller, itemIndex, builder);
Hans Muller's avatar
Hans Muller committed
591 592 593 594 595 596
    setState(() {
      _outgoingItems
        ..add(outgoingItem)
        ..sort();
    });

597
    controller.reverse().then<void>((void value) {
598
      _removeActiveItemAt(_outgoingItems, outgoingItem.itemIndex)!.controller!.dispose();
Hans Muller's avatar
Hans Muller committed
599 600 601

      // Decrement the incoming and outgoing item indices to account
      // for the removal.
602
      for (final _ActiveItem item in _incomingItems) {
603
        if (item.itemIndex > outgoingItem.itemIndex) {
Hans Muller's avatar
Hans Muller committed
604
          item.itemIndex -= 1;
605
        }
Hans Muller's avatar
Hans Muller committed
606
      }
607
      for (final _ActiveItem item in _outgoingItems) {
608
        if (item.itemIndex > outgoingItem.itemIndex) {
Hans Muller's avatar
Hans Muller committed
609
          item.itemIndex -= 1;
610
        }
Hans Muller's avatar
Hans Muller committed
611 612
      }

613
      setState(() => _itemsCount -= 1);
Hans Muller's avatar
Hans Muller committed
614 615 616 617
    });
  }

  Widget _itemBuilder(BuildContext context, int itemIndex) {
618
    final _ActiveItem? outgoingItem = _activeItemAt(_outgoingItems, itemIndex);
619
    if (outgoingItem != null) {
620
      return outgoingItem.removedItemBuilder!(
621
        context,
622
        outgoingItem.controller!.view,
623 624
      );
    }
Hans Muller's avatar
Hans Muller committed
625

626
    final _ActiveItem? incomingItem = _activeItemAt(_incomingItems, itemIndex);
Hans Muller's avatar
Hans Muller committed
627
    final Animation<double> animation = incomingItem?.controller?.view ?? kAlwaysCompleteAnimation;
628 629 630 631 632
    return widget.itemBuilder(
      context,
      _itemIndexToIndex(itemIndex),
      animation,
    );
Hans Muller's avatar
Hans Muller committed
633 634 635 636
  }

  @override
  Widget build(BuildContext context) {
637 638
    return SliverList(
      delegate: _createDelegate(),
Hans Muller's avatar
Hans Muller committed
639 640 641
    );
  }
}