Unverified Commit 0e70a97e authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Refactor Animated[List, Grid, SliverList, SliverGrid] to share common code (#113793)

parent 1cfdac4b
...@@ -462,9 +462,9 @@ class SliverGridDelegateWithMaxCrossAxisExtent extends SliverGridDelegate { ...@@ -462,9 +462,9 @@ class SliverGridDelegateWithMaxCrossAxisExtent extends SliverGridDelegate {
SliverGridLayout getLayout(SliverConstraints constraints) { SliverGridLayout getLayout(SliverConstraints constraints) {
assert(_debugAssertIsValid(constraints.crossAxisExtent)); assert(_debugAssertIsValid(constraints.crossAxisExtent));
int crossAxisCount = (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)).ceil(); int crossAxisCount = (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)).ceil();
// TODO(gspencergoog): Figure out why we need this in release mode (and only // Ensure a minimum count of 1, can be zero and result in an infinite extent
// in release mode). https://github.com/flutter/flutter/issues/113109 // below when the window size is 0.
crossAxisCount = crossAxisCount < 1 ? 1 : crossAxisCount; crossAxisCount = math.max(1, crossAxisCount);
final double usableCrossAxisExtent = math.max( final double usableCrossAxisExtent = math.max(
0.0, 0.0,
constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1), constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1),
......
// 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(),
);
}
}
...@@ -12,54 +12,173 @@ import 'scroll_view.dart'; ...@@ -12,54 +12,173 @@ import 'scroll_view.dart';
import 'sliver.dart'; import 'sliver.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
/// Signature for the builder callback used by widgets like [AnimatedGrid] to /// A scrolling container that animates items when they are inserted or removed.
/// build their animated children.
/// ///
/// The `context` argument is the build context where the widget will be /// This widget's [AnimatedListState] can be used to dynamically insert or
/// created, the `index` is the index of the item to be built, and the /// remove items. To refer to the [AnimatedListState] either provide a
/// `animation` is an [Animation] that should be used to animate an entry /// [GlobalKey] or use the static [of] method from an item's input callback.
/// transition for the widget that is built.
/// ///
/// See also: /// This widget is similar to one created by [ListView.builder].
/// ///
/// * [AnimatedRemovedItemBuilder], a builder that is for removing items with /// {@youtube 560 315 https://www.youtube.com/watch?v=ZtfItHwFlZ8}
/// animations instead of adding them.
typedef AnimatedItemBuilder = Widget Function(BuildContext context, int index, Animation<double> animation);
/// Signature for the builder callback used by widgets like [AnimatedGrid] (in
/// [AnimatedGridState.removeItem]) to animated their children after they have
/// been removed.
/// ///
/// The `context` argument is the build context where the widget will be /// {@tool dartpad}
/// created, and the `animation` is an [Animation] that should be used to /// This sample application uses an [AnimatedList] to create an effect when
/// animate an exit transition for the widget that is built. /// 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: /// See also:
/// ///
/// * [AnimatedItemBuilder], a builder that is for adding items with animations /// * [SliverAnimatedList], a sliver that animates items when they are inserted
/// instead of removing them. /// or removed from a list.
typedef AnimatedRemovedItemBuilder = Widget Function(BuildContext context, Animation<double> animation); /// * [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 _AnimatedScrollView {
/// Creates a scrolling container that animates items when they are inserted
/// or removed.
const AnimatedList({
super.key,
required super.itemBuilder,
super.initialItemCount = 0,
super.scrollDirection = Axis.vertical,
super.reverse = false,
super.controller,
super.primary,
super.physics,
super.shrinkWrap = false,
super.padding,
super.clipBehavior = Clip.hardEdge,
}) : assert(itemBuilder != null),
assert(initialItemCount != null && initialItemCount >= 0);
// The default insert/remove animation duration. /// The state from the closest instance of this class that encloses the given
const Duration _kDuration = Duration(milliseconds: 300); /// 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).
///
/// This method does not create a dependency, and so will not cause rebuilding
/// when the state changes.
///
/// 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 = AnimatedList.maybeOf(context);
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!;
}
// Incoming and outgoing AnimatedGrid items. /// The [AnimatedListState] from the closest instance of [AnimatedList] that encloses the given
class _ActiveItem implements Comparable<_ActiveItem> { /// context.
_ActiveItem.incoming(this.controller, this.itemIndex) : removedItemBuilder = null; ///
_ActiveItem.outgoing(this.controller, this.itemIndex, this.removedItemBuilder); /// This method is typically used by [AnimatedList] item widgets that insert
_ActiveItem.index(this.itemIndex) /// or remove items in response to user input.
: controller = null, ///
removedItemBuilder = null; /// If no [AnimatedList] surrounds the context given, then this function will
/// return null.
///
/// This method can be expensive (it walks the element tree).
///
/// This method does not create a dependency, and so will not cause rebuilding
/// when the state changes.
///
/// 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>();
}
final AnimationController? controller; @override
final AnimatedRemovedItemBuilder? removedItemBuilder; AnimatedListState createState() => AnimatedListState();
int itemIndex; }
/// The [AnimatedListState] for [AnimatedList], a scrolling list 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 _AnimatedScrollViewState<AnimatedList> {
@override @override
int compareTo(_ActiveItem other) => itemIndex - other.itemIndex; Widget build(BuildContext context) {
return _wrap(
SliverAnimatedList(
key: _sliverAnimatedMultiBoxKey,
itemBuilder: widget.itemBuilder,
initialItemCount: widget.initialItemCount,
),
);
}
} }
/// A scrolling container that animates items when they are inserted or removed /// A scrolling container that animates items when they are inserted into or removed from a grid.
/// in a grid. /// in a grid.
/// ///
/// This widget's [AnimatedGridState] can be used to dynamically insert or /// This widget's [AnimatedGridState] can be used to dynamically insert or
...@@ -83,51 +202,200 @@ class _ActiveItem implements Comparable<_ActiveItem> { ...@@ -83,51 +202,200 @@ class _ActiveItem implements Comparable<_ActiveItem> {
/// a list instead of a grid. /// a list instead of a grid.
/// * [AnimatedList], which animates items added and removed from a list instead /// * [AnimatedList], which animates items added and removed from a list instead
/// of a grid. /// of a grid.
class AnimatedGrid extends StatefulWidget { class AnimatedGrid extends _AnimatedScrollView {
/// Creates a scrolling container that animates items when they are inserted /// Creates a scrolling container that animates items when they are inserted
/// or removed. /// or removed.
const AnimatedGrid({ const AnimatedGrid({
super.key, super.key,
required this.itemBuilder, required super.itemBuilder,
required this.gridDelegate, required this.gridDelegate,
super.initialItemCount = 0,
super.scrollDirection = Axis.vertical,
super.reverse = false,
super.controller,
super.primary,
super.physics,
super.padding,
super.clipBehavior = Clip.hardEdge,
}) : assert(itemBuilder != null),
assert(initialItemCount != null && initialItemCount >= 0);
/// {@template flutter.widgets.AnimatedGrid.gridDelegate}
/// A delegate that controls the layout of the children within the
/// [AnimatedGrid].
///
/// See also:
///
/// * [SliverGridDelegateWithFixedCrossAxisCount], which creates a layout with
/// a fixed number of tiles in the cross axis.
/// * [SliverGridDelegateWithMaxCrossAxisExtent], which creates a layout with
/// tiles that have a maximum cross-axis extent.
/// {@endtemplate}
final SliverGridDelegate gridDelegate;
/// The state from the closest instance of this class that encloses the given
/// context.
///
/// This method is typically used by [AnimatedGrid] item widgets that insert
/// or remove items in response to user input.
///
/// If no [AnimatedGrid] 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).
///
/// This method does not create a dependency, and so will not cause rebuilding
/// when the state changes.
///
/// See also:
///
/// * [maybeOf], a similar function that will return null if no
/// [AnimatedGrid] ancestor is found.
static AnimatedGridState of(BuildContext context) {
assert(context != null);
final AnimatedGridState? result = AnimatedGrid.maybeOf(context);
assert(() {
if (result == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('AnimatedGrid.of() called with a context that does not contain an AnimatedGrid.'),
ErrorDescription(
'No AnimatedGrid ancestor could be found starting from the context that was passed to AnimatedGrid.of().',
),
ErrorHint(
'This can happen when the context provided is from the same StatefulWidget that '
'built the AnimatedGrid. Please see the AnimatedGrid documentation for examples '
'of how to refer to an AnimatedGridState object:\n'
' https://api.flutter.dev/flutter/widgets/AnimatedGridState-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 [AnimatedGrid] item widgets that insert
/// or remove items in response to user input.
///
/// If no [AnimatedGrid] surrounds the context given, then this function will
/// return null.
///
/// This method can be expensive (it walks the element tree).
///
/// This method does not create a dependency, and so will not cause rebuilding
/// when the state changes.
///
/// See also:
///
/// * [of], a similar function that will throw if no [AnimatedGrid] ancestor
/// is found.
static AnimatedGridState? maybeOf(BuildContext context) {
assert(context != null);
return context.findAncestorStateOfType<AnimatedGridState>();
}
@override
AnimatedGridState createState() => AnimatedGridState();
}
/// The [State] for an [AnimatedGrid] 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 [AnimatedGrid.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 [AnimatedGrid]'s state with a global key:
///
/// ```dart
/// // (e.g. in a stateful widget)
/// GlobalKey<AnimatedGridState> gridKey = GlobalKey<AnimatedGridState>();
///
/// // ...
///
/// @override
/// Widget build(BuildContext context) {
/// return AnimatedGrid(
/// key: gridKey,
/// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
/// return const Placeholder();
/// },
/// gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 100.0),
/// );
/// }
///
/// // ...
///
/// void _updateGrid() {
/// // adds "123" to the AnimatedGrid
/// gridKey.currentState!.insertItem(123);
/// }
/// ```
///
/// [AnimatedGrid] item input handlers can also refer to their [AnimatedGridState]
/// with the static [AnimatedGrid.of] method.
class AnimatedGridState extends _AnimatedScrollViewState<AnimatedGrid> {
@override
Widget build(BuildContext context) {
return _wrap(
SliverAnimatedGrid(
key: _sliverAnimatedMultiBoxKey,
gridDelegate: widget.gridDelegate,
itemBuilder: widget.itemBuilder,
initialItemCount: widget.initialItemCount,
),
);
}
}
abstract class _AnimatedScrollView extends StatefulWidget {
/// Creates a scrolling container that animates items when they are inserted
/// or removed.
const _AnimatedScrollView({
super.key,
required this.itemBuilder,
this.initialItemCount = 0, this.initialItemCount = 0,
this.scrollDirection = Axis.vertical, this.scrollDirection = Axis.vertical,
this.reverse = false, this.reverse = false,
this.controller, this.controller,
this.primary, this.primary,
this.physics, this.physics,
this.shrinkWrap = false,
this.padding, this.padding,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
}) : assert(itemBuilder != null), }) : assert(itemBuilder != null),
assert(initialItemCount != null && initialItemCount >= 0); assert(initialItemCount != null && initialItemCount >= 0);
/// Called, as needed, to build grid item widgets. /// {@template flutter.widgets.AnimatedScrollView.itemBuilder}
/// Called, as needed, to build children widgets.
/// ///
/// Grid items are only built when they're scrolled into view. /// Children are only built when they're scrolled into view.
/// ///
/// The [AnimatedItemBuilder] index parameter indicates the item's position in /// The [AnimatedItemBuilder] index parameter indicates the item's
/// the grid. The value of the index parameter will be between 0 and /// position in the scroll view. The value of the index parameter will be
/// [initialItemCount] plus the total number of items that have been inserted /// between 0 and [initialItemCount] plus the total number of items that have
/// with [AnimatedGridState.insertItem] and less the total number of items /// been inserted with [AnimatedListState.insertItem] or
/// that have been removed with [AnimatedGridState.removeItem]. /// [AnimatedGridState.insertItem] and less the total number of items that
/// have been removed with [AnimatedListState.removeItem] or
/// [AnimatedGridState.removeItem].
/// ///
/// Implementations of this callback should assume that /// Implementations of this callback should assume that
/// [AnimatedGridState.removeItem] removes an item immediately. /// `removeItem` removes an item immediately.
/// {@endtemplate}
final AnimatedItemBuilder itemBuilder; final AnimatedItemBuilder itemBuilder;
/// A delegate that controls the layout of the children within the /// {@template flutter.widgets.AnimatedScrollView.initialItemCount}
/// [AnimatedGrid]. /// The number of items the [AnimatedList] or [AnimatedGrid] will start with.
///
/// See also:
///
/// * [SliverGridDelegateWithFixedCrossAxisCount], which creates a layout with
/// a fixed number of tiles in the cross axis.
/// * [SliverGridDelegateWithMaxCrossAxisExtent], which creates a layout with
/// tiles that have a maximum cross-axis extent.
final SliverGridDelegate gridDelegate;
/// {@template flutter.widgets.AnimatedGrid.initialItemCount}
/// The number of items the grid will start with.
/// ///
/// The appearance of the initial items is not animated. They /// The appearance of the initial items is not animated. They
/// are created, as needed, by [itemBuilder] with an animation parameter /// are created, as needed, by [itemBuilder] with an animation parameter
...@@ -168,196 +436,320 @@ class AnimatedGrid extends StatefulWidget { ...@@ -168,196 +436,320 @@ class AnimatedGrid extends StatefulWidget {
/// [ScrollController.animateTo]). /// [ScrollController.animateTo]).
final ScrollController? controller; final ScrollController? controller;
/// Whether this is the primary scroll view associated with the parent /// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController]. /// [PrimaryScrollController].
/// ///
/// On iOS, this identifies the scroll view that will scroll to top in /// On iOS, this identifies the scroll view that will scroll to top in
/// response to a tap in the status bar. /// response to a tap in the status bar.
/// ///
/// Defaults to true when [scrollDirection] is [Axis.vertical] and /// Defaults to true when [scrollDirection] is [Axis.vertical] and
/// [controller] is null. /// [controller] is null.
final bool? primary; final bool? primary;
/// How the scroll view should respond to user input.
///
/// For example, this 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;
}
abstract class _AnimatedScrollViewState<T extends _AnimatedScrollView> extends State<T> with TickerProviderStateMixin {
final GlobalKey<_SliverAnimatedMultiBoxAdaptorState<_SliverAnimatedMultiBoxAdaptor>> _sliverAnimatedMultiBoxKey = GlobalKey();
/// Insert an item at [index] and start an animation that will be passed
/// to [AnimatedGrid.itemBuilder] or [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 of items by one and shifts
/// all items at or after [index] towards the end of the list of items.
void insertItem(int index, { Duration duration = _kDuration }) {
_sliverAnimatedMultiBoxKey.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 `itemBuilder`. However, the
/// item will still appear 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 items by one and shifts all items at or before
/// `index` towards the beginning of the list of items.
///
/// See also:
///
/// * [AnimatedRemovedItemBuilder], which describes the arguments to the
/// `builder` argument.
void removeItem(int index, AnimatedRemovedItemBuilder builder, { Duration duration = _kDuration }) {
_sliverAnimatedMultiBoxKey.currentState!.removeItem(index, builder, duration: duration);
}
Widget _wrap(Widget sliver) {
return CustomScrollView(
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
controller: widget.controller,
primary: widget.primary,
physics: widget.physics,
clipBehavior: widget.clipBehavior,
slivers: <Widget>[
SliverPadding(
padding: widget.padding ?? EdgeInsets.zero,
sliver: sliver,
),
],
);
}
}
/// 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 [AnimatedList] & [AnimatedGrid] to
/// build their animated children.
///
/// The `context` argument is the build context where the widget will be
/// created, the `index` is the index of the item to be built, and the
/// `animation` is an [Animation] that should be used to animate an entry
/// transition for the widget that is built.
///
/// See also:
///
/// * [AnimatedRemovedItemBuilder], a builder that is for removing items with
/// animations instead of adding them.
typedef AnimatedItemBuilder = 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);
/// Signature for the builder callback used in [AnimatedListState.removeItem] and
/// [AnimatedGridState.removeItem] to animate their children after they have
/// been removed.
///
/// The `context` argument is the build context where the widget will be
/// created, and the `animation` is an [Animation] that should be used to
/// animate an exit transition for the widget that is built.
///
/// See also:
///
/// * [AnimatedItemBuilder], a builder that is for adding items with animations
/// instead of removing them.
typedef AnimatedRemovedItemBuilder = Widget Function(BuildContext context, Animation<double> animation);
// The default insert/remove animation duration.
const Duration _kDuration = Duration(milliseconds: 300);
/// How the scroll view should respond to user input. // Incoming and outgoing animated items.
/// class _ActiveItem implements Comparable<_ActiveItem> {
/// For example, determines how the scroll view continues to animate after the _ActiveItem.incoming(this.controller, this.itemIndex) : removedItemBuilder = null;
/// user stops dragging the scroll view. _ActiveItem.outgoing(this.controller, this.itemIndex, this.removedItemBuilder);
/// _ActiveItem.index(this.itemIndex)
/// Defaults to matching platform conventions. : controller = null,
final ScrollPhysics? physics; removedItemBuilder = null;
/// The amount of space by which to inset the children. final AnimationController? controller;
final EdgeInsetsGeometry? padding; final AnimatedRemovedItemBuilder? removedItemBuilder;
int itemIndex;
/// {@macro flutter.material.Material.clipBehavior} @override
/// int compareTo(_ActiveItem other) => itemIndex - other.itemIndex;
/// Defaults to [Clip.hardEdge]. }
final Clip clipBehavior;
/// The state from the closest instance of this class that encloses the given /// A [SliverList] 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 a list 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 _SliverAnimatedMultiBoxAdaptor {
/// Creates a [SliverList] that animates items when they are inserted or
/// removed.
const SliverAnimatedList({
super.key,
required super.itemBuilder,
super.findChildIndexCallback,
super.initialItemCount = 0,
}) : assert(itemBuilder != null),
assert(initialItemCount != null && initialItemCount >= 0);
@override
SliverAnimatedListState createState() => SliverAnimatedListState();
/// The [SliverAnimatedListState] from the closest instance of this class that encloses the given
/// context. /// context.
/// ///
/// This method is typically used by [AnimatedGrid] item widgets that insert /// This method is typically used by [SliverAnimatedList] item widgets that
/// or remove items in response to user input. /// insert or remove items in response to user input.
/// ///
/// If no [AnimatedGrid] surrounds the context given, then this function will /// If no [SliverAnimatedList] surrounds the context given, then this function
/// assert in debug mode and throw an exception in release mode. /// will assert in debug mode and throw an exception in release mode.
/// ///
/// This method can be expensive (it walks the element tree). /// This method can be expensive (it walks the element tree).
/// ///
/// This method does not create a dependency, and so will not cause rebuilding
/// when the state changes.
///
/// See also: /// See also:
/// ///
/// * [maybeOf], a similar function that will return null if no /// * [maybeOf], a similar function that will return null if no
/// [AnimatedGrid] ancestor is found. /// [SliverAnimatedList] ancestor is found.
static AnimatedGridState of(BuildContext context) { static SliverAnimatedListState of(BuildContext context) {
assert(context != null); assert(context != null);
final AnimatedGridState? result = context.findAncestorStateOfType<AnimatedGridState>(); final SliverAnimatedListState? result = SliverAnimatedList.maybeOf(context);
assert(() { assert(() {
if (result == null) { if (result == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[ throw FlutterError(
ErrorSummary('AnimatedGrid.of() called with a context that does not contain an AnimatedGrid.'), 'SliverAnimatedList.of() called with a context that does not contain a SliverAnimatedList.\n'
ErrorDescription( 'No SliverAnimatedListState ancestor could be found starting from the '
'No AnimatedGrid ancestor could be found starting from the context that was passed to AnimatedGrid.of().', 'context that was passed to SliverAnimatedListState.of(). This can '
), 'happen when the context provided is from the same StatefulWidget that '
ErrorHint( 'built the AnimatedList. Please see the SliverAnimatedList documentation '
'This can happen when the context provided is from the same StatefulWidget that ' 'for examples of how to refer to an AnimatedListState object: '
'built the AnimatedGrid. Please see the AnimatedGrid documentation for examples ' 'https://api.flutter.dev/flutter/widgets/SliverAnimatedListState-class.html\n'
'of how to refer to an AnimatedGridState object:\n' 'The context used was:\n'
' https://api.flutter.dev/flutter/widgets/AnimatedGridState-class.html', ' $context',
), );
context.describeElement('The context used was'),
]);
} }
return true; return true;
}()); }());
return result!; return result!;
} }
/// The state from the closest instance of this class that encloses the given /// The [SliverAnimatedListState] from the closest instance of this class that encloses the given
/// context. /// context.
/// ///
/// This method is typically used by [AnimatedGrid] item widgets that insert /// This method is typically used by [SliverAnimatedList] item widgets that
/// or remove items in response to user input. /// insert or remove items in response to user input.
/// ///
/// If no [AnimatedGrid] surrounds the context given, then this function will /// If no [SliverAnimatedList] surrounds the context given, then this function
/// return null. /// will return null.
/// ///
/// This method can be expensive (it walks the element tree). /// This method can be expensive (it walks the element tree).
/// ///
/// This method does not create a dependency, and so will not cause rebuilding
/// when the state changes.
///
/// See also: /// See also:
/// ///
/// * [of], a similar function that will throw if no [AnimatedGrid] ancestor /// * [of], a similar function that will throw if no [SliverAnimatedList]
/// is found. /// ancestor is found.
static AnimatedGridState? maybeOf(BuildContext context) { static SliverAnimatedListState? maybeOf(BuildContext context) {
assert(context != null); assert(context != null);
return context.findAncestorStateOfType<AnimatedGridState>(); return context.findAncestorStateOfType<SliverAnimatedListState>();
} }
@override
AnimatedGridState createState() => AnimatedGridState();
} }
/// The state for a scrolling container that animates items when they are /// The state for a [SliverAnimatedList] that animates items when they are
/// inserted or removed. /// inserted or removed.
/// ///
/// When an item is inserted with [insertItem] an animation begins running. The /// When an item is inserted with [insertItem] an animation begins running. The
/// animation is passed to [AnimatedGrid.itemBuilder] whenever the item's widget /// animation is passed to [SliverAnimatedList.itemBuilder] whenever the item's
/// is needed. /// widget is needed.
/// ///
/// When an item is removed with [removeItem] its animation is reversed. /// When an item is removed with [removeItem] its animation is reversed.
/// The removed item's animation is passed to the [removeItem] builder /// The removed item's animation is passed to the [removeItem] builder
/// parameter. /// parameter.
/// ///
/// An app that needs to insert or remove items in response to an event /// An app that needs to insert or remove items in response to an event
/// can refer to the [AnimatedGrid]'s state with a global key: /// can refer to the [SliverAnimatedList]'s state with a global key:
/// ///
/// ```dart /// ```dart
/// // (e.g. in a stateful widget) /// // (e.g. in a stateful widget)
/// GlobalKey<AnimatedGridState> gridKey = GlobalKey<AnimatedGridState>(); /// GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
/// ///
/// // ... /// // ...
/// ///
/// @override /// @override
/// Widget build(BuildContext context) { /// Widget build(BuildContext context) {
/// return AnimatedGrid( /// return AnimatedList(
/// key: gridKey, /// key: listKey,
/// itemBuilder: (BuildContext context, int index, Animation<double> animation) { /// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
/// return const Placeholder(); /// return const Placeholder();
/// }, /// },
/// gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 100.0),
/// ); /// );
/// } /// }
/// ///
/// // ... /// // ...
/// ///
/// void _updateGrid() { /// void _updateList() {
/// // adds "123" to the AnimatedGrid /// // adds "123" to the AnimatedList
/// gridKey.currentState!.insertItem(123); /// listKey.currentState!.insertItem(123);
/// } /// }
/// ``` /// ```
/// ///
/// [AnimatedGrid] item input handlers can also refer to their [AnimatedGridState] /// [SliverAnimatedList] item input handlers can also refer to their
/// with the static [AnimatedGrid.of] method. /// [SliverAnimatedListState] with the static [SliverAnimatedList.of] method.
class AnimatedGridState extends State<AnimatedGrid> with TickerProviderStateMixin<AnimatedGrid> { class SliverAnimatedListState extends _SliverAnimatedMultiBoxAdaptorState<SliverAnimatedList> {
final GlobalKey<SliverAnimatedGridState> _sliverAnimatedGridKey = GlobalKey();
/// Insert an item at [index] and start an animation that will be passed
/// to [AnimatedGrid.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 of items in the grid by one and shifts
/// all items at or after [index] towards the end of the list of items in the
/// grid.
void insertItem(int index, {Duration duration = _kDuration}) {
_sliverAnimatedGridKey.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 [AnimatedGrid.itemBuilder]. However, the
/// item will still appear in the grid 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 of items in the grid by one and shifts
/// all items at or before `index` towards the beginning of the list of items
/// in the grid.
///
/// See also:
///
/// - [AnimatedRemovedItemBuilder], which describes the arguments to the
/// `builder` argument.
void removeItem(int index, AnimatedRemovedItemBuilder builder, {Duration duration = _kDuration}) {
_sliverAnimatedGridKey.currentState!.removeItem(index, builder, duration: duration);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomScrollView( return SliverList(
scrollDirection: widget.scrollDirection, delegate: _createDelegate(),
reverse: widget.reverse,
controller: widget.controller,
primary: widget.primary,
physics: widget.physics,
clipBehavior: widget.clipBehavior,
slivers: <Widget>[
SliverPadding(
padding: widget.padding ?? EdgeInsets.zero,
sliver: SliverAnimatedGrid(
key: _sliverAnimatedGridKey,
gridDelegate: widget.gridDelegate,
itemBuilder: widget.itemBuilder,
initialItemCount: widget.initialItemCount,
),
),
],
); );
} }
} }
/// A sliver that animates items when they are inserted or removed in a grid. /// A [SliverGrid] that animates items when they are inserted or removed.
/// ///
/// This widget's [SliverAnimatedGridState] can be used to dynamically insert or /// This widget's [SliverAnimatedGridState] can be used to dynamically insert or
/// remove items. To refer to the [SliverAnimatedGridState] either provide a /// remove items. To refer to the [SliverAnimatedGridState] either provide a
...@@ -380,51 +772,24 @@ class AnimatedGridState extends State<AnimatedGrid> with TickerProviderStateMixi ...@@ -380,51 +772,24 @@ class AnimatedGridState extends State<AnimatedGrid> with TickerProviderStateMixi
/// * [SliverList], which displays a non-animated list of items. /// * [SliverList], which displays a non-animated list of items.
/// * [SliverAnimatedList], which animates items added and removed from a list /// * [SliverAnimatedList], which animates items added and removed from a list
/// instead of a grid. /// instead of a grid.
class SliverAnimatedGrid extends StatefulWidget { class SliverAnimatedGrid extends _SliverAnimatedMultiBoxAdaptor {
/// Creates a sliver that animates items when they are inserted or removed. /// Creates a [SliverGrid] that animates items when they are inserted or
/// removed.
const SliverAnimatedGrid({ const SliverAnimatedGrid({
super.key, super.key,
required this.itemBuilder, required super.itemBuilder,
required this.gridDelegate, required this.gridDelegate,
this.findChildIndexCallback, super.findChildIndexCallback,
this.initialItemCount = 0, super.initialItemCount = 0,
}) : assert(itemBuilder != null), }) : assert(itemBuilder != null),
assert(initialItemCount != null && initialItemCount >= 0); assert(initialItemCount != null && initialItemCount >= 0);
/// Called, as needed, to build grid item widgets.
///
/// Grid items are only built when they're scrolled into view.
///
/// The [AnimatedItemBuilder] index parameter indicates the item's position in
/// the grid. The value of the index parameter will be between 0 and
/// [initialItemCount] plus the total number of items that have been inserted
/// with [SliverAnimatedGridState.insertItem] and less the total number of
/// items that have been removed with [SliverAnimatedGridState.removeItem].
///
/// Implementations of this callback should assume that
/// [SliverAnimatedGridState.removeItem] removes an item immediately.
final AnimatedItemBuilder itemBuilder;
/// A delegate that controls the layout of the children within the
/// [SliverAnimatedGrid].
///
/// See also:
///
/// * [SliverGridDelegateWithFixedCrossAxisCount], which creates a layout with
/// a fixed number of tiles in the cross axis.
/// * [SliverGridDelegateWithMaxCrossAxisExtent], which creates a layout with
/// tiles that have a maximum cross-axis extent.
final SliverGridDelegate gridDelegate;
/// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback}
final ChildIndexGetter? findChildIndexCallback;
/// {@macro flutter.widgets.AnimatedGrid.initialItemCount}
final int initialItemCount;
@override @override
SliverAnimatedGridState createState() => SliverAnimatedGridState(); SliverAnimatedGridState createState() => SliverAnimatedGridState();
/// {@macro flutter.widgets.AnimatedGrid.gridDelegate}
final SliverGridDelegate gridDelegate;
/// The state from the closest instance of this class that encloses the given /// The state from the closest instance of this class that encloses the given
/// context. /// context.
/// ///
...@@ -483,7 +848,7 @@ class SliverAnimatedGrid extends StatefulWidget { ...@@ -483,7 +848,7 @@ class SliverAnimatedGrid extends StatefulWidget {
} }
} }
/// The state for a sliver that animates items when they are /// The state for a [SliverAnimatedGrid] that animates items when they are
/// inserted or removed. /// inserted or removed.
/// ///
/// When an item is inserted with [insertItem] an animation begins running. The /// When an item is inserted with [insertItem] an animation begins running. The
...@@ -524,10 +889,38 @@ class SliverAnimatedGrid extends StatefulWidget { ...@@ -524,10 +889,38 @@ class SliverAnimatedGrid extends StatefulWidget {
/// ///
/// [SliverAnimatedGrid] item input handlers can also refer to their /// [SliverAnimatedGrid] item input handlers can also refer to their
/// [SliverAnimatedGridState] with the static [SliverAnimatedGrid.of] method. /// [SliverAnimatedGridState] with the static [SliverAnimatedGrid.of] method.
class SliverAnimatedGridState extends State<SliverAnimatedGrid> with TickerProviderStateMixin { class SliverAnimatedGridState extends _SliverAnimatedMultiBoxAdaptorState<SliverAnimatedGrid> {
final List<_ActiveItem> _incomingItems = <_ActiveItem>[];
final List<_ActiveItem> _outgoingItems = <_ActiveItem>[]; @override
int _itemsCount = 0; Widget build(BuildContext context) {
return SliverGrid(
gridDelegate: widget.gridDelegate,
delegate: _createDelegate(),
);
}
}
abstract class _SliverAnimatedMultiBoxAdaptor extends StatefulWidget {
/// Creates a sliver that animates items when they are inserted or removed.
const _SliverAnimatedMultiBoxAdaptor({
super.key,
required this.itemBuilder,
this.findChildIndexCallback,
this.initialItemCount = 0,
}) : assert(itemBuilder != null),
assert(initialItemCount != null && initialItemCount >= 0);
/// {@macro flutter.widgets.AnimatedScrollView.itemBuilder}
final AnimatedItemBuilder itemBuilder;
/// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback}
final ChildIndexGetter? findChildIndexCallback;
/// {@macro flutter.widgets.AnimatedScrollView.initialItemCount}
final int initialItemCount;
}
abstract class _SliverAnimatedMultiBoxAdaptorState<T extends _SliverAnimatedMultiBoxAdaptor> extends State<T> with TickerProviderStateMixin {
@override @override
void initState() { void initState() {
...@@ -543,6 +936,10 @@ class SliverAnimatedGridState extends State<SliverAnimatedGrid> with TickerProvi ...@@ -543,6 +936,10 @@ class SliverAnimatedGridState extends State<SliverAnimatedGrid> with TickerProvi
super.dispose(); super.dispose();
} }
final List<_ActiveItem> _incomingItems = <_ActiveItem>[];
final List<_ActiveItem> _outgoingItems = <_ActiveItem>[];
int _itemsCount = 0;
_ActiveItem? _removeActiveItemAt(List<_ActiveItem> items, int itemIndex) { _ActiveItem? _removeActiveItemAt(List<_ActiveItem> items, int itemIndex) {
final int i = binarySearch(items, _ActiveItem.index(itemIndex)); final int i = binarySearch(items, _ActiveItem.index(itemIndex));
return i == -1 ? null : items.removeAt(i); return i == -1 ? null : items.removeAt(i);
...@@ -554,10 +951,11 @@ class SliverAnimatedGridState extends State<SliverAnimatedGrid> with TickerProvi ...@@ -554,10 +951,11 @@ class SliverAnimatedGridState extends State<SliverAnimatedGrid> with TickerProvi
} }
// The insertItem() and removeItem() index parameters are defined as if the // The insertItem() and removeItem() index parameters are defined as if the
// removeItem() operation removed the corresponding grid entry immediately. // removeItem() operation removed the corresponding list/grid entry
// The entry is only actually removed from the grid when the remove animation // immediately. The entry is only actually removed from the
// finishes. The entry is added to _outgoingItems when removeItem is called // ListView/GridView when the remove animation finishes. The entry is added
// and removed from _outgoingItems when the remove animation finishes. // to _outgoingItems when removeItem is called and removed from
// _outgoingItems when the remove animation finishes.
int _indexToItemIndex(int index) { int _indexToItemIndex(int index) {
int itemIndex = index; int itemIndex = index;
...@@ -597,14 +995,32 @@ class SliverAnimatedGridState extends State<SliverAnimatedGrid> with TickerProvi ...@@ -597,14 +995,32 @@ class SliverAnimatedGridState extends State<SliverAnimatedGrid> with TickerProvi
); );
} }
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,
);
}
/// Insert an item at [index] and start an animation that will be passed to /// Insert an item at [index] and start an animation that will be passed to
/// [SliverAnimatedGrid.itemBuilder] when the item is visible. /// [SliverAnimatedGrid.itemBuilder] or [SliverAnimatedList.itemBuilder] when
/// the item is visible.
/// ///
/// This method's semantics are the same as Dart's [List.insert] method: it /// This method's semantics are the same as Dart's [List.insert] method: it
/// increases the length of the list of items in the grid by one and shifts /// increases the length of the list of items by one and shifts
/// all items at or after [index] towards the end of the list of items in the /// all items at or after [index] towards the end of the list of items.
/// grid. void insertItem(int index, { Duration duration = _kDuration }) {
void insertItem(int index, {Duration duration = _kDuration}) {
assert(index != null && index >= 0); assert(index != null && index >= 0);
assert(duration != null); assert(duration != null);
...@@ -648,15 +1064,15 @@ class SliverAnimatedGridState extends State<SliverAnimatedGrid> with TickerProvi ...@@ -648,15 +1064,15 @@ class SliverAnimatedGridState extends State<SliverAnimatedGrid> with TickerProvi
/// to [builder] when the item is visible. /// to [builder] when the item is visible.
/// ///
/// Items are removed immediately. After an item has been removed, its index /// Items are removed immediately. After an item has been removed, its index
/// will no longer be passed to the [SliverAnimatedGrid.itemBuilder]. However /// will no longer be passed to the subclass' [SliverAnimatedGrid.itemBuilder]
/// the item will still appear in the grid for [duration] and during that time /// or [SliverAnimatedList.itemBuilder]. However the item will still appear
/// [builder] must construct its widget as needed. /// 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 /// This method's semantics are the same as Dart's [List.remove] method: it
/// decreases the length of the list of items in the grid by one and shifts /// decreases the length of items by one and shifts
/// all items at or before [index] towards the beginning of the list of items /// all items at or before [index] towards the beginning of the list of items.
/// in the grid. void removeItem(int index, AnimatedRemovedItemBuilder builder, { Duration duration = _kDuration }) {
void removeItem(int index, AnimatedRemovedItemBuilder builder, {Duration duration = _kDuration}) {
assert(index != null && index >= 0); assert(index != null && index >= 0);
assert(builder != null); assert(builder != null);
assert(duration != null); assert(duration != null);
...@@ -694,30 +1110,4 @@ class SliverAnimatedGridState extends State<SliverAnimatedGrid> with TickerProvi ...@@ -694,30 +1110,4 @@ class SliverAnimatedGridState extends State<SliverAnimatedGrid> with TickerProvi
setState(() => _itemsCount -= 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 SliverGrid(
gridDelegate: widget.gridDelegate,
delegate: _createDelegate(),
);
}
} }
...@@ -19,8 +19,7 @@ export 'foundation.dart' show UniqueKey; ...@@ -19,8 +19,7 @@ export 'foundation.dart' show UniqueKey;
export 'rendering.dart' show TextSelectionHandleType; export 'rendering.dart' show TextSelectionHandleType;
export 'src/widgets/actions.dart'; export 'src/widgets/actions.dart';
export 'src/widgets/animated_cross_fade.dart'; export 'src/widgets/animated_cross_fade.dart';
export 'src/widgets/animated_grid.dart'; export 'src/widgets/animated_scroll_view.dart';
export 'src/widgets/animated_list.dart';
export 'src/widgets/animated_size.dart'; export 'src/widgets/animated_size.dart';
export 'src/widgets/animated_switcher.dart'; export 'src/widgets/animated_switcher.dart';
export 'src/widgets/annotated_region.dart'; export 'src/widgets/annotated_region.dart';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment