Refactor Animated[List, Grid, SliverList, SliverGrid] to share common code (#113793)
This commit is contained in:
parent
1cfdac4b23
commit
0e70a97e2e
@ -462,9 +462,9 @@ class SliverGridDelegateWithMaxCrossAxisExtent extends SliverGridDelegate {
|
||||
SliverGridLayout getLayout(SliverConstraints constraints) {
|
||||
assert(_debugAssertIsValid(constraints.crossAxisExtent));
|
||||
int crossAxisCount = (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)).ceil();
|
||||
// TODO(gspencergoog): Figure out why we need this in release mode (and only
|
||||
// in release mode). https://github.com/flutter/flutter/issues/113109
|
||||
crossAxisCount = crossAxisCount < 1 ? 1 : crossAxisCount;
|
||||
// Ensure a minimum count of 1, can be zero and result in an infinite extent
|
||||
// below when the window size is 0.
|
||||
crossAxisCount = math.max(1, crossAxisCount);
|
||||
final double usableCrossAxisExtent = math.max(
|
||||
0.0,
|
||||
constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1),
|
||||
|
@ -1,700 +0,0 @@
|
||||
// Copyright 2014 The Flutter 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/foundation.dart';
|
||||
|
||||
import 'animated_grid.dart';
|
||||
import 'basic.dart';
|
||||
import 'framework.dart';
|
||||
import 'scroll_controller.dart';
|
||||
import 'scroll_physics.dart';
|
||||
import 'scroll_view.dart';
|
||||
import 'sliver.dart';
|
||||
import 'ticker_provider.dart';
|
||||
|
||||
/// Signature for the builder callback used by [AnimatedList].
|
||||
///
|
||||
/// This is deprecated, use the identical [AnimatedItemBuilder] instead.
|
||||
@Deprecated(
|
||||
'Use AnimatedItemBuilder instead. '
|
||||
'This feature was deprecated after v3.5.0-4.0.pre.',
|
||||
)
|
||||
typedef AnimatedListItemBuilder = Widget Function(BuildContext context, int index, Animation<double> animation);
|
||||
|
||||
/// Signature for the builder callback used by [AnimatedListState.removeItem].
|
||||
///
|
||||
/// This is deprecated, use the identical [AnimatedRemovedItemBuilder]
|
||||
/// instead.
|
||||
@Deprecated(
|
||||
'Use AnimatedRemovedItemBuilder instead. '
|
||||
'This feature was deprecated after v3.5.0-4.0.pre.',
|
||||
)
|
||||
typedef AnimatedListRemovedItemBuilder = Widget Function(BuildContext context, Animation<double> animation);
|
||||
|
||||
// The default insert/remove animation duration.
|
||||
const Duration _kDuration = 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 AnimatedRemovedItemBuilder? 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].
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=ZtfItHwFlZ8}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample application uses an [AnimatedList] to create an effect when
|
||||
/// items are removed or added to the list.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/animated_list/animated_list.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliverAnimatedList], a sliver that animates items when they are inserted
|
||||
/// or removed from a list.
|
||||
/// * [SliverAnimatedGrid], a sliver which animates items when they are
|
||||
/// inserted or removed from a grid.
|
||||
/// * [AnimatedGrid], a non-sliver scrolling container that animates items when
|
||||
/// they are inserted or removed in a grid.
|
||||
class AnimatedList extends StatefulWidget {
|
||||
/// Creates a scrolling container that animates items when they are inserted
|
||||
/// or removed.
|
||||
const AnimatedList({
|
||||
super.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,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
}) : 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 [AnimatedItemBuilder] 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 AnimatedItemBuilder itemBuilder;
|
||||
|
||||
/// {@template flutter.widgets.animatedList.initialItemCount}
|
||||
/// 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].
|
||||
/// {@endtemplate}
|
||||
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;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.hardEdge].
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// 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
|
||||
/// assert in debug mode and throw an exception in release mode.
|
||||
///
|
||||
/// This method can be expensive (it walks the element tree).
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [maybeOf], a similar function that will return null if no
|
||||
/// [AnimatedList] ancestor is found.
|
||||
static AnimatedListState of(BuildContext context) {
|
||||
assert(context != null);
|
||||
final AnimatedListState? result = context.findAncestorStateOfType<AnimatedListState>();
|
||||
assert(() {
|
||||
if (result == null) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary('AnimatedList.of() called with a context that does not contain an AnimatedList.'),
|
||||
ErrorDescription(
|
||||
'No AnimatedList ancestor could be found starting from the context that was passed to AnimatedList.of().',
|
||||
),
|
||||
ErrorHint(
|
||||
'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:\n'
|
||||
' https://api.flutter.dev/flutter/widgets/AnimatedListState-class.html',
|
||||
),
|
||||
context.describeElement('The context used was'),
|
||||
]);
|
||||
}
|
||||
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.
|
||||
///
|
||||
/// This method can be expensive (it walks the element tree).
|
||||
///
|
||||
/// 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>();
|
||||
}
|
||||
|
||||
@override
|
||||
AnimatedListState createState() => 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
|
||||
/// // (e.g. in a stateful widget)
|
||||
/// GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
|
||||
///
|
||||
/// // ...
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return AnimatedList(
|
||||
/// key: listKey,
|
||||
/// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
|
||||
/// return const Placeholder();
|
||||
/// },
|
||||
/// );
|
||||
/// }
|
||||
///
|
||||
/// // ...
|
||||
///
|
||||
/// void _updateList() {
|
||||
/// // adds "123" to the AnimatedList
|
||||
/// listKey.currentState!.insertItem(123);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [AnimatedList] item input handlers can also refer to their [AnimatedListState]
|
||||
/// with the static [AnimatedList.of] method.
|
||||
class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixin<AnimatedList> {
|
||||
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 }) {
|
||||
_sliverAnimatedListKey.currentState!.insertItem(index, duration: duration);
|
||||
}
|
||||
|
||||
/// 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, AnimatedRemovedItemBuilder builder, { Duration duration = _kDuration }) {
|
||||
_sliverAnimatedListKey.currentState!.removeItem(index, builder, duration: duration);
|
||||
}
|
||||
|
||||
@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,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
slivers: <Widget>[
|
||||
SliverPadding(
|
||||
padding: widget.padding ?? EdgeInsets.zero,
|
||||
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.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample application uses a [SliverAnimatedList] to create an animated
|
||||
/// effect when items are removed or added to the list.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/animated_list/sliver_animated_list.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [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.
|
||||
/// * [SliverAnimatedGrid], a sliver which animates items when they are
|
||||
/// inserted into or removed from a grid.
|
||||
/// * [AnimatedGrid], a non-sliver scrolling container that animates items when
|
||||
/// they are inserted into or removed from a grid.
|
||||
class SliverAnimatedList extends StatefulWidget {
|
||||
/// Creates a sliver that animates items when they are inserted or removed.
|
||||
const SliverAnimatedList({
|
||||
super.key,
|
||||
required this.itemBuilder,
|
||||
this.findChildIndexCallback,
|
||||
this.initialItemCount = 0,
|
||||
}) : 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 [AnimatedItemBuilder] 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 AnimatedItemBuilder itemBuilder;
|
||||
|
||||
/// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback}
|
||||
final ChildIndexGetter? findChildIndexCallback;
|
||||
|
||||
/// {@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.
|
||||
///
|
||||
/// If no [SliverAnimatedList] surrounds the context given, then this function
|
||||
/// will assert in debug mode and throw an exception in release mode.
|
||||
///
|
||||
/// This method can be expensive (it walks the element tree).
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [maybeOf], a similar function that will return null if no
|
||||
/// [SliverAnimatedList] ancestor is found.
|
||||
static SliverAnimatedListState of(BuildContext context) {
|
||||
assert(context != null);
|
||||
final SliverAnimatedListState? result = context.findAncestorStateOfType<SliverAnimatedListState>();
|
||||
assert(() {
|
||||
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: '
|
||||
'https://api.flutter.dev/flutter/widgets/SliverAnimatedListState-class.html\n'
|
||||
'The context used was:\n'
|
||||
' $context',
|
||||
);
|
||||
}
|
||||
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.
|
||||
///
|
||||
/// This method can be expensive (it walks the element tree).
|
||||
///
|
||||
/// 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>();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// // (e.g. in a stateful widget)
|
||||
/// GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
|
||||
///
|
||||
/// // ...
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return AnimatedList(
|
||||
/// key: listKey,
|
||||
/// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
|
||||
/// return const Placeholder();
|
||||
/// },
|
||||
/// );
|
||||
/// }
|
||||
///
|
||||
/// // ...
|
||||
///
|
||||
/// void _updateList() {
|
||||
/// // adds "123" to the AnimatedList
|
||||
/// listKey.currentState!.insertItem(123);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [SliverAnimatedList] item input handlers can also refer to their
|
||||
/// [SliverAnimatedListState] with the static [SliverAnimatedList.of] method.
|
||||
class SliverAnimatedListState extends State<SliverAnimatedList> 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 (final _ActiveItem item in _incomingItems.followedBy(_outgoingItems)) {
|
||||
item.controller!.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_ActiveItem? _removeActiveItemAt(List<_ActiveItem> items, int itemIndex) {
|
||||
final int i = binarySearch(items, _ActiveItem.index(itemIndex));
|
||||
return i == -1 ? null : items.removeAt(i);
|
||||
}
|
||||
|
||||
_ActiveItem? _activeItemAt(List<_ActiveItem> items, int itemIndex) {
|
||||
final int i = binarySearch(items, _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 (final _ActiveItem item in _outgoingItems) {
|
||||
if (item.itemIndex <= itemIndex) {
|
||||
itemIndex += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return itemIndex;
|
||||
}
|
||||
|
||||
int _itemIndexToIndex(int itemIndex) {
|
||||
int index = itemIndex;
|
||||
for (final _ActiveItem item in _outgoingItems) {
|
||||
assert(item.itemIndex != itemIndex);
|
||||
if (item.itemIndex < itemIndex) {
|
||||
index -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
SliverChildDelegate _createDelegate() {
|
||||
return SliverChildBuilderDelegate(
|
||||
_itemBuilder,
|
||||
childCount: _itemsCount,
|
||||
findChildIndexCallback: widget.findChildIndexCallback == null
|
||||
? null
|
||||
: (Key key) {
|
||||
final int? index = widget.findChildIndexCallback!(key);
|
||||
return index != null ? _indexToItemIndex(index) : null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Insert an item at [index] and start an animation that will be passed to
|
||||
/// [SliverAnimatedList.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 (final _ActiveItem item in _incomingItems) {
|
||||
if (item.itemIndex >= itemIndex) {
|
||||
item.itemIndex += 1;
|
||||
}
|
||||
}
|
||||
for (final _ActiveItem item in _outgoingItems) {
|
||||
if (item.itemIndex >= itemIndex) {
|
||||
item.itemIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
final AnimationController controller = AnimationController(
|
||||
duration: duration,
|
||||
vsync: this,
|
||||
);
|
||||
final _ActiveItem incomingItem = _ActiveItem.incoming(
|
||||
controller,
|
||||
itemIndex,
|
||||
);
|
||||
setState(() {
|
||||
_incomingItems
|
||||
..add(incomingItem)
|
||||
..sort();
|
||||
_itemsCount += 1;
|
||||
});
|
||||
|
||||
controller.forward().then<void>((_) {
|
||||
_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 [SliverAnimatedList.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, AnimatedRemovedItemBuilder 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
|
||||
?? AnimationController(duration: duration, value: 1.0, vsync: this);
|
||||
final _ActiveItem outgoingItem = _ActiveItem.outgoing(controller, itemIndex, builder);
|
||||
setState(() {
|
||||
_outgoingItems
|
||||
..add(outgoingItem)
|
||||
..sort();
|
||||
});
|
||||
|
||||
controller.reverse().then<void>((void value) {
|
||||
_removeActiveItemAt(_outgoingItems, outgoingItem.itemIndex)!.controller!.dispose();
|
||||
|
||||
// Decrement the incoming and outgoing item indices to account
|
||||
// for the removal.
|
||||
for (final _ActiveItem item in _incomingItems) {
|
||||
if (item.itemIndex > outgoingItem.itemIndex) {
|
||||
item.itemIndex -= 1;
|
||||
}
|
||||
}
|
||||
for (final _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 SliverList(
|
||||
delegate: _createDelegate(),
|
||||
);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -19,8 +19,7 @@ export 'foundation.dart' show UniqueKey;
|
||||
export 'rendering.dart' show TextSelectionHandleType;
|
||||
export 'src/widgets/actions.dart';
|
||||
export 'src/widgets/animated_cross_fade.dart';
|
||||
export 'src/widgets/animated_grid.dart';
|
||||
export 'src/widgets/animated_list.dart';
|
||||
export 'src/widgets/animated_scroll_view.dart';
|
||||
export 'src/widgets/animated_size.dart';
|
||||
export 'src/widgets/animated_switcher.dart';
|
||||
export 'src/widgets/annotated_region.dart';
|
||||
|
Loading…
x
Reference in New Issue
Block a user