blob: 5e8ef084f39c6fc4eddb0a27862e7e7812e91c39 [file] [log] [blame]
// 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.
/// @docImport 'viewport.dart';
library;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'icon.dart';
import 'icon_data.dart';
import 'implicit_animations.dart';
import 'scroll_delegate.dart';
import 'sliver.dart';
import 'text.dart';
import 'ticker_provider.dart';
const double _kDefaultRowExtent = 40.0;
/// A data structure for configuring children of a [TreeSliver].
///
/// A [TreeSliverNode.content] can be of any type [T], but must correspond with
/// the same type of the [TreeSliver].
///
/// The values returned by [depth], [parent] and [isExpanded] getters are
/// managed by the [TreeSliver]'s state.
class TreeSliverNode<T> {
/// Creates a [TreeSliverNode] instance for use in a [TreeSliver].
TreeSliverNode(T content, {List<TreeSliverNode<T>>? children, bool expanded = false})
: _expanded = (children?.isNotEmpty ?? false) && expanded,
_content = content,
_children = children ?? <TreeSliverNode<T>>[];
/// The subject matter of the node.
///
/// Must correspond with the type of [TreeSliver].
T get content => _content;
final T _content;
/// Other [TreeSliverNode]s that this node will be [parent] to.
///
/// Modifying the children of nodes in a [TreeSliver] will cause the tree to be
/// rebuilt so that newly added active nodes are reflected in the tree.
List<TreeSliverNode<T>> get children => _children;
final List<TreeSliverNode<T>> _children;
/// Whether or not this node is expanded in the tree.
///
/// Cannot be expanded if there are no children.
bool get isExpanded => _expanded;
bool _expanded;
/// The number of parent nodes between this node and the root of the tree.
int? get depth => _depth;
int? _depth;
/// The parent [TreeSliverNode] of this node.
TreeSliverNode<T>? get parent => _parent;
TreeSliverNode<T>? _parent;
@override
String toString() {
return 'TreeSliverNode: $content, depth: ${depth == 0 ? 'root' : depth}, '
'${children.isEmpty ? 'leaf' : 'parent, expanded: $isExpanded'}';
}
}
/// Signature for a function that creates a [Widget] to represent the given
/// [TreeSliverNode] in the [TreeSliver].
///
/// Used by [TreeSliver.treeNodeBuilder] to build rows on demand for the
/// tree.
typedef TreeSliverNodeBuilder =
Widget Function(
BuildContext context,
TreeSliverNode<Object?> node,
AnimationStyle animationStyle,
);
/// Signature for a function that returns an extent for the given
/// [TreeSliverNode] in the [TreeSliver].
///
/// Used by [TreeSliver.treeRowExtentBuilder] to size rows on demand in the
/// tree. The provided [SliverLayoutDimensions] provide information about the
/// current scroll state and [Viewport] dimensions.
///
/// See also:
///
/// * [SliverVariedExtentList], which uses a similar item extent builder for
/// dynamic child sizing in the list.
typedef TreeSliverRowExtentBuilder =
double Function(TreeSliverNode<Object?> node, SliverLayoutDimensions dimensions);
/// Signature for a function that is called when a [TreeSliverNode] is toggled,
/// changing its expanded state.
///
/// See also:
///
/// * [TreeSliver.onNodeToggle], for controlling node expansion
/// programmatically.
typedef TreeSliverNodeCallback = void Function(TreeSliverNode<Object?> node);
/// A mixin for classes implementing a tree structure as expected by a
/// [TreeSliverController].
///
/// Used by [TreeSliver] to implement an interface for the
/// [TreeSliverController].
///
/// This allows the [TreeSliverController] to be used in other widgets that
/// implement this interface.
///
/// The type [T] correlates to the type of [TreeSliver] and [TreeSliverNode],
/// representing the type of [TreeSliverNode.content].
mixin TreeSliverStateMixin<T> {
/// Returns whether or not the given [TreeSliverNode] is expanded.
bool isExpanded(TreeSliverNode<T> node);
/// Returns whether or not the given [TreeSliverNode] is enclosed within its
/// parent [TreeSliverNode].
///
/// If the [TreeSliverNode.parent] [isExpanded] (and all its parents are
/// expanded), or this is a root node, the given node is active and this
/// method will return true. This does not reflect whether or not the node is
/// visible in the [Viewport].
bool isActive(TreeSliverNode<T> node);
/// Switches the given [TreeSliverNode]s expanded state.
///
/// May trigger an animation to reveal or hide the node's children based on
/// the [TreeSliver.toggleAnimationStyle].
///
/// If the node does not have any children, nothing will happen.
void toggleNode(TreeSliverNode<T> node);
/// Closes all parent [TreeSliverNode]s in the tree.
void collapseAll();
/// Expands all parent [TreeSliverNode]s in the tree.
void expandAll();
/// Retrieves the [TreeSliverNode] containing the associated content, if it
/// exists.
///
/// If no node exists, this will return null. This does not reflect whether
/// or not a node [isActive], or if it is visible in the viewport.
TreeSliverNode<T>? getNodeFor(T content);
/// Returns the current row index of the given [TreeSliverNode].
///
/// If the node is not currently active in the tree, meaning its parent is
/// collapsed, this will return null.
int? getActiveIndexFor(TreeSliverNode<T> node);
}
/// Enables control over the [TreeSliverNode]s of a [TreeSliver].
///
/// It can be useful to expand or collapse nodes of the tree
/// programmatically, for example to reconfigure an existing node
/// based on a system event. To do so, create a [TreeSliver]
/// with a [TreeSliverController] that's owned by a stateful widget
/// or look up the tree's automatically created [TreeSliverController]
/// with [TreeSliverController.of]
///
/// The controller's methods to expand or collapse nodes cause the
/// the [TreeSliver] to rebuild, so they may not be called from
/// a build method.
class TreeSliverController {
/// Create a controller to be used with [TreeSliver.controller].
TreeSliverController();
TreeSliverStateMixin<Object?>? _state;
/// Whether the given [TreeSliverNode] built with this controller is in an
/// expanded state.
///
/// See also:
///
/// * [expandNode], which expands a given [TreeSliverNode].
/// * [collapseNode], which collapses a given [TreeSliverNode].
/// * [TreeSliver.controller] to create a TreeSliver with a controller.
bool isExpanded(TreeSliverNode<Object?> node) {
assert(_state != null);
return _state!.isExpanded(node);
}
/// Whether or not the given [TreeSliverNode] is enclosed within its parent
/// [TreeSliverNode].
///
/// If the [TreeSliverNode.parent] [isExpanded], or this is a root node, the
/// given node is active and this method will return true. This does not
/// reflect whether or not the node is visible in the [Viewport].
bool isActive(TreeSliverNode<Object?> node) {
assert(_state != null);
return _state!.isActive(node);
}
/// Returns the [TreeSliverNode] containing the associated content, if it
/// exists.
///
/// If no node exists, this will return null. This does not reflect whether
/// or not a node [isActive], or if it is currently visible in the viewport.
TreeSliverNode<Object?>? getNodeFor(Object? content) {
assert(_state != null);
return _state!.getNodeFor(content);
}
/// Switches the given [TreeSliverNode]s expanded state.
///
/// May trigger an animation to reveal or hide the node's children based on
/// the [TreeSliver.toggleAnimationStyle].
///
/// If the node does not have any children, nothing will happen.
void toggleNode(TreeSliverNode<Object?> node) {
assert(_state != null);
return _state!.toggleNode(node);
}
/// Expands the [TreeSliverNode] that was built with this controller.
///
/// If the node is already in the expanded state (see [isExpanded]), calling
/// this method has no effect.
///
/// Calling this method may cause the [TreeSliver] to rebuild, so it may
/// not be called from a build method.
///
/// Calling this method will trigger the [TreeSliver.onNodeToggle]
/// callback.
///
/// See also:
///
/// * [collapseNode], which collapses the [TreeSliverNode].
/// * [isExpanded] to check whether the tile is expanded.
/// * [TreeSliver.controller] to create a TreeSliver with a controller.
void expandNode(TreeSliverNode<Object?> node) {
assert(_state != null);
if (!node.isExpanded) {
_state!.toggleNode(node);
}
}
/// Expands all parent [TreeSliverNode]s in the tree.
void expandAll() {
assert(_state != null);
_state!.expandAll();
}
/// Closes all parent [TreeSliverNode]s in the tree.
void collapseAll() {
assert(_state != null);
_state!.collapseAll();
}
/// Collapses the [TreeSliverNode] that was built with this controller.
///
/// If the node is already in the collapsed state (see [isExpanded]), calling
/// this method has no effect.
///
/// Calling this method may cause the [TreeSliver] to rebuild, so it may
/// not be called from a build method.
///
/// Calling this method will trigger the [TreeSliver.onNodeToggle]
/// callback.
///
/// See also:
///
/// * [expandNode], which expands the tile.
/// * [isExpanded] to check whether the tile is expanded.
/// * [TreeSliver.controller] to create a TreeSliver with a controller.
void collapseNode(TreeSliverNode<Object?> node) {
assert(_state != null);
if (node.isExpanded) {
_state!.toggleNode(node);
}
}
/// Returns the current row index of the given [TreeSliverNode].
///
/// If the node is not currently active in the tree, meaning its parent is
/// collapsed, this will return null.
int? getActiveIndexFor(TreeSliverNode<Object?> node) {
assert(_state != null);
return _state!.getActiveIndexFor(node);
}
/// Finds the [TreeSliverController] for the closest [TreeSliver] instance
/// that encloses the given context.
///
/// If no [TreeSliver] encloses the given context, calling this
/// method will cause an assert in debug mode, and throw an
/// exception in release mode.
///
/// To return null if there is no [TreeSliver] use [maybeOf] instead.
///
/// Typical usage of the [TreeSliverController.of] function is to call it
/// from within the `build` method of a descendant of a [TreeSliver].
///
/// When the [TreeSliver] is actually created in the same `build`
/// function as the callback that refers to the controller, then the
/// `context` argument to the `build` function can't be used to find
/// the [TreeSliverController] (since it's "above" the widget
/// being returned in the widget tree). In cases like that you can
/// add a [Builder] widget, which provides a new scope with a
/// [BuildContext] that is "under" the [TreeSliver].
static TreeSliverController of(BuildContext context) {
final _TreeSliverState<Object?>? result =
context.findAncestorStateOfType<_TreeSliverState<Object?>>();
if (result != null) {
return result.controller;
}
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'TreeController.of() called with a context that does not contain a '
'TreeSliver.',
),
ErrorDescription(
'No TreeSliver ancestor could be found starting from the context that '
'was passed to TreeController.of(). '
'This usually happens when the context provided is from the same '
'StatefulWidget as that whose build function actually creates the '
'TreeSliver widget being sought.',
),
ErrorHint(
'There are several ways to avoid this problem. The simplest is to use '
'a Builder to get a context that is "under" the TreeSliver.',
),
ErrorHint(
'A more efficient solution is to split your build function into '
'several widgets. This introduces a new context from which you can '
'obtain the TreeSliver. In this solution, you would have an outer '
'widget that creates the TreeSliver populated by instances of your new '
'inner widgets, and then in these inner widgets you would use '
'TreeController.of().',
),
context.describeElement('The context used was'),
]);
}
/// Finds the [TreeSliver] from the closest instance of this class that
/// encloses the given context and returns its [TreeSliverController].
///
/// If no [TreeSliver] encloses the given context then return null.
/// To throw an exception instead, use [of] instead of this function.
///
/// See also:
///
/// * [of], a similar function to this one that throws if no [TreeSliver]
/// encloses the given context. Also includes some sample code in its
/// documentation.
static TreeSliverController? maybeOf(BuildContext context) {
return context.findAncestorStateOfType<_TreeSliverState<Object?>>()?.controller;
}
}
int _kDefaultSemanticIndexCallback(Widget _, int localIndex) => localIndex;
/// A widget that displays [TreeSliverNode]s that expand and collapse in a
/// vertically and horizontally scrolling [Viewport].
///
/// The type [T] correlates to the type of [TreeSliver] and [TreeSliverNode],
/// representing the type of [TreeSliverNode.content].
///
/// The rows of the tree are laid out on demand by the [Viewport]'s render
/// object, using [TreeSliver.treeNodeBuilder]. This will only be called for the
/// nodes that are visible, or within the [Viewport.cacheExtent].
///
/// The [TreeSliver.treeNodeBuilder] returns the [Widget] that represents the
/// given [TreeSliverNode].
///
/// The [TreeSliver.treeRowExtentBuilder] returns a double representing the
/// extent of a given node in the main axis.
///
/// Providing a [TreeSliverController] will enable querying and controlling the
/// state of nodes in the tree.
///
/// A [TreeSliver] only supports a vertical axis direction of
/// [AxisDirection.down] and a horizontal axis direction of
/// [AxisDirection.right].
///
///{@tool dartpad}
/// This example uses a [TreeSliver] to display nodes, highlighting nodes as
/// they are selected.
///
/// ** See code in examples/api/lib/widgets/sliver/sliver_tree.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example shows a highly customized [TreeSliver] configured to
/// [TreeSliverIndentationType.none]. This allows the indentation to be handled
/// by the developer in [TreeSliver.treeNodeBuilder], where a decoration is
/// used to fill the indented space.
///
/// ** See code in examples/api/lib/widgets/sliver/sliver_tree.1.dart **
/// {@end-tool}
class TreeSliver<T> extends StatefulWidget {
/// Creates an instance of a [TreeSliver] for displaying [TreeSliverNode]s
/// that animate expanding and collapsing of nodes.
const TreeSliver({
super.key,
required this.tree,
this.treeNodeBuilder = TreeSliver.defaultTreeNodeBuilder,
this.treeRowExtentBuilder = TreeSliver.defaultTreeRowExtentBuilder,
this.controller,
this.onNodeToggle,
this.toggleAnimationStyle,
this.indentation = TreeSliverIndentationType.standard,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
this.semanticIndexOffset = 0,
this.findChildIndexCallback,
});
/// The list of [TreeSliverNode]s that may be displayed in the [TreeSliver].
///
/// Beyond root nodes, whether or not a given [TreeSliverNode] is displayed
/// depends on the [TreeSliverNode.isExpanded] value of its parent. The
/// [TreeSliver] will set the [TreeSliverNode.parent] and
/// [TreeSliverNode.depth] as nodes are built on demand to ensure the
/// integrity of the tree.
final List<TreeSliverNode<T>> tree;
/// Called to build and entry of the [TreeSliver] for the given node.
///
/// By default, if this is unset, the [TreeSliver.defaultTreeNodeBuilder]
/// is used.
final TreeSliverNodeBuilder treeNodeBuilder;
/// Called to calculate the extent of the widget built for the given
/// [TreeSliverNode].
///
/// By default, if this is unset, the
/// [TreeSliver.defaultTreeRowExtentBuilder] is used.
///
/// See also:
///
/// * [SliverVariedExtentList.itemExtentBuilder], a very similar method that
/// allows users to dynamically compute extents on demand.
final TreeSliverRowExtentBuilder treeRowExtentBuilder;
/// If provided, the controller can be used to expand and collapse
/// [TreeSliverNode]s, or lookup information about the current state of the
/// [TreeSliver].
final TreeSliverController? controller;
/// Called when a [TreeSliverNode] expands or collapses.
///
/// This will not be called if a [TreeSliverNode] does not have any children.
final TreeSliverNodeCallback? onNodeToggle;
/// The default [AnimationStyle] for expanding and collapsing nodes in the
/// [TreeSliver].
///
/// The default [AnimationStyle.duration] uses
/// [TreeSliver.defaultAnimationDuration], which is 150 milliseconds.
///
/// The default [AnimationStyle.curve] uses [TreeSliver.defaultAnimationCurve],
/// which is [Curves.linear].
///
/// To disable the tree animation, use [AnimationStyle.noAnimation].
final AnimationStyle? toggleAnimationStyle;
/// The number of pixels children will be offset by in the cross axis based on
/// their [TreeSliverNode.depth].
///
/// {@macro flutter.rendering.TreeSliverIndentationType}
final TreeSliverIndentationType indentation;
/// {@macro flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives}
final bool addAutomaticKeepAlives;
/// {@macro flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries}
final bool addRepaintBoundaries;
/// {@macro flutter.widgets.SliverChildBuilderDelegate.addSemanticIndexes}
final bool addSemanticIndexes;
/// {@macro flutter.widgets.SliverChildBuilderDelegate.semanticIndexCallback}
final SemanticIndexCallback semanticIndexCallback;
/// {@macro flutter.widgets.SliverChildBuilderDelegate.semanticIndexOffset}
final int semanticIndexOffset;
/// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback}
final int? Function(Key)? findChildIndexCallback;
/// The default [AnimationStyle] used for node expand and collapse animations,
/// when one has not been provided in [toggleAnimationStyle].
static AnimationStyle defaultToggleAnimationStyle = AnimationStyle(
curve: defaultAnimationCurve,
duration: defaultAnimationDuration,
);
/// A default of [Curves.linear], which is used in the tree's expanding and
/// collapsing node animation.
static const Curve defaultAnimationCurve = Curves.linear;
/// A default [Duration] of 150 milliseconds, which is used in the tree's
/// expanding and collapsing node animation.
static const Duration defaultAnimationDuration = Duration(milliseconds: 150);
/// A wrapper method for triggering the expansion or collapse of a
/// [TreeSliverNode].
///
/// Used as part of [TreeSliver.defaultTreeNodeBuilder] to wrap the leading
/// icon of parent [TreeSliverNode]s such that tapping on it triggers the
/// animation.
///
/// If defining your own [TreeSliver.treeNodeBuilder], this method can be used
/// to wrap any part, or all, of the returned widget in order to trigger the
/// change in state for the node.
static Widget wrapChildToToggleNode({
required TreeSliverNode<Object?> node,
required Widget child,
}) {
return Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
TreeSliverController.of(context).toggleNode(node);
},
child: child,
);
},
);
}
/// Returns the fixed default extent for rows in the tree, which is 40 pixels.
///
/// Used by [TreeSliver.treeRowExtentBuilder].
static double defaultTreeRowExtentBuilder(
TreeSliverNode<Object?> node,
SliverLayoutDimensions dimensions,
) {
return _kDefaultRowExtent;
}
/// Returns the default tree row for a given [TreeSliverNode].
///
/// Used by [TreeSliver.treeNodeBuilder].
///
/// This will return a [Row] containing the [toString] of
/// [TreeSliverNode.content]. If the [TreeSliverNode] is a parent of
/// additional nodes, a arrow icon will precede the content, and will trigger
/// an expand and collapse animation when tapped.
static Widget defaultTreeNodeBuilder(
BuildContext context,
TreeSliverNode<Object?> node,
AnimationStyle toggleAnimationStyle,
) {
final Duration animationDuration =
toggleAnimationStyle.duration ?? TreeSliver.defaultAnimationDuration;
final Curve animationCurve = toggleAnimationStyle.curve ?? TreeSliver.defaultAnimationCurve;
final int index = TreeSliverController.of(context).getActiveIndexFor(node)!;
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
// Icon for parent nodes
TreeSliver.wrapChildToToggleNode(
node: node,
child: SizedBox.square(
dimension: 30.0,
child:
node.children.isNotEmpty
? AnimatedRotation(
key: ValueKey<int>(index),
turns: node.isExpanded ? 0.25 : 0.0,
duration: animationDuration,
curve: animationCurve,
// Renders a unicode right-facing arrow. >
child: const Icon(IconData(0x25BA), size: 14),
)
: null,
),
),
// Spacer
const SizedBox(width: 8.0),
// Content
Text(node.content.toString()),
],
),
);
}
@override
State<TreeSliver<T>> createState() => _TreeSliverState<T>();
}
// Used in _SliverTreeState for code simplicity.
typedef _AnimationRecord =
({AnimationController controller, CurvedAnimation animation, UniqueKey key});
class _TreeSliverState<T> extends State<TreeSliver<T>>
with TickerProviderStateMixin, TreeSliverStateMixin<T> {
TreeSliverController get controller => _treeController!;
TreeSliverController? _treeController;
final List<TreeSliverNode<T>> _activeNodes = <TreeSliverNode<T>>[];
bool _shouldUnpackNode(TreeSliverNode<T> node) {
if (node.children.isEmpty) {
// No children to unpack.
return false;
}
if (_currentAnimationForParent[node] != null) {
// Whether expanding or collapsing, the child nodes are still active, so
// unpack.
return true;
}
// If we are not animating, respect node.isExpanded.
return node.isExpanded;
}
void _unpackActiveNodes({
int depth = 0,
List<TreeSliverNode<T>>? nodes,
TreeSliverNode<T>? parent,
}) {
if (nodes == null) {
_activeNodes.clear();
nodes = widget.tree;
}
for (final TreeSliverNode<T> node in nodes) {
node._depth = depth;
node._parent = parent;
_activeNodes.add(node);
if (_shouldUnpackNode(node)) {
_unpackActiveNodes(depth: depth + 1, nodes: node.children, parent: node);
}
}
}
final Map<TreeSliverNode<T>, _AnimationRecord> _currentAnimationForParent =
<TreeSliverNode<T>, _AnimationRecord>{};
final Map<UniqueKey, TreeSliverNodesAnimation> _activeAnimations =
<UniqueKey, TreeSliverNodesAnimation>{};
@override
void initState() {
_unpackActiveNodes();
assert(
widget.controller?._state == null,
'The provided TreeSliverController is already associated with another '
'TreeSliver. A TreeSliverController can only be associated with one '
'TreeSliver.',
);
_treeController = widget.controller ?? TreeSliverController();
_treeController!._state = this;
super.initState();
}
@override
void didUpdateWidget(TreeSliver<T> oldWidget) {
super.didUpdateWidget(oldWidget);
// Internal or provided, there is always a tree controller.
assert(_treeController != null);
if (oldWidget.controller == null && widget.controller != null) {
// A new tree controller has been provided, update and dispose of the
// internally generated one.
_treeController!._state = null;
_treeController = widget.controller;
_treeController!._state = this;
} else if (oldWidget.controller != null && widget.controller == null) {
// A tree controller had been provided, but was removed. We need to create
// one internally.
assert(oldWidget.controller == _treeController);
oldWidget.controller!._state = null;
_treeController = TreeSliverController();
_treeController!._state = this;
} else if (oldWidget.controller != widget.controller) {
assert(oldWidget.controller != null);
assert(widget.controller != null);
assert(oldWidget.controller == _treeController);
// The tree is still being provided a controller, but it has changed. Just
// update it.
_treeController!._state = null;
_treeController = widget.controller;
_treeController!._state = this;
}
// Internal or provided, there is always a tree controller.
assert(_treeController != null);
assert(_treeController!._state != null);
_unpackActiveNodes();
}
@override
void dispose() {
_treeController!._state = null;
for (final _AnimationRecord record in _currentAnimationForParent.values) {
record.animation.dispose();
record.controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return _SliverTree(
itemCount: _activeNodes.length,
activeAnimations: _activeAnimations,
itemBuilder: (BuildContext context, int index) {
final TreeSliverNode<T> node = _activeNodes[index];
Widget child = widget.treeNodeBuilder(
context,
node,
widget.toggleAnimationStyle ?? TreeSliver.defaultToggleAnimationStyle,
);
if (widget.addRepaintBoundaries) {
child = RepaintBoundary(child: child);
}
if (widget.addSemanticIndexes) {
final int? semanticIndex = widget.semanticIndexCallback(child, index);
if (semanticIndex != null) {
child = IndexedSemantics(
index: semanticIndex + widget.semanticIndexOffset,
child: child,
);
}
}
return _TreeNodeParentDataWidget(depth: node.depth!, child: child);
},
itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
return widget.treeRowExtentBuilder(_activeNodes[index], dimensions);
},
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
findChildIndexCallback: widget.findChildIndexCallback,
indentation: widget.indentation.value,
);
}
// TreeStateMixin Implementation
@override
bool isExpanded(TreeSliverNode<T> node) {
return _getNode(node.content, widget.tree)?.isExpanded ?? false;
}
@override
bool isActive(TreeSliverNode<T> node) => _activeNodes.contains(node);
@override
TreeSliverNode<T>? getNodeFor(T content) => _getNode(content, widget.tree);
TreeSliverNode<T>? _getNode(T content, List<TreeSliverNode<T>> tree) {
final List<TreeSliverNode<T>> nextDepth = <TreeSliverNode<T>>[];
for (final TreeSliverNode<T> node in tree) {
if (node.content == content) {
return node;
}
if (node.children.isNotEmpty) {
nextDepth.addAll(node.children);
}
}
if (nextDepth.isNotEmpty) {
return _getNode(content, nextDepth);
}
return null;
}
@override
int? getActiveIndexFor(TreeSliverNode<T> node) {
if (_activeNodes.contains(node)) {
return _activeNodes.indexOf(node);
}
return null;
}
@override
void expandAll() {
final List<TreeSliverNode<T>> activeNodesToExpand = <TreeSliverNode<T>>[];
_expandAll(widget.tree, activeNodesToExpand);
activeNodesToExpand.reversed.forEach(toggleNode);
}
void _expandAll(List<TreeSliverNode<T>> tree, List<TreeSliverNode<T>> activeNodesToExpand) {
for (final TreeSliverNode<T> node in tree) {
if (node.children.isNotEmpty) {
// This is a parent node.
// Expand all the children, and their children.
_expandAll(node.children, activeNodesToExpand);
if (!node.isExpanded) {
// The node itself needs to be expanded.
if (_activeNodes.contains(node)) {
// This is an active node in the tree, add to
// the list to toggle once all hidden nodes
// have been handled.
activeNodesToExpand.add(node);
} else {
// This is a hidden node. Update its expanded state.
node._expanded = true;
}
}
}
}
}
@override
void collapseAll() {
final List<TreeSliverNode<T>> activeNodesToCollapse = <TreeSliverNode<T>>[];
_collapseAll(widget.tree, activeNodesToCollapse);
activeNodesToCollapse.reversed.forEach(toggleNode);
}
void _collapseAll(List<TreeSliverNode<T>> tree, List<TreeSliverNode<T>> activeNodesToCollapse) {
for (final TreeSliverNode<T> node in tree) {
if (node.children.isNotEmpty) {
// This is a parent node.
// Collapse all the children, and their children.
_collapseAll(node.children, activeNodesToCollapse);
if (node.isExpanded) {
// The node itself needs to be collapsed.
if (_activeNodes.contains(node)) {
// This is an active node in the tree, add to
// the list to toggle once all hidden nodes
// have been handled.
activeNodesToCollapse.add(node);
} else {
// This is a hidden node. Update its expanded state.
node._expanded = false;
}
}
}
}
}
void _updateActiveAnimations() {
// The indexes of various child node animations can change constantly based
// on more nodes being expanded or collapsed. Compile the indexes and their
// animations keys each time we build with an updated active node list.
_activeAnimations.clear();
for (final TreeSliverNode<T> node in _currentAnimationForParent.keys) {
final _AnimationRecord animationRecord = _currentAnimationForParent[node]!;
final int leadingChildIndex = _activeNodes.indexOf(node) + 1;
final TreeSliverNodesAnimation animatingChildren = (
fromIndex: leadingChildIndex,
toIndex: leadingChildIndex + node.children.length - 1,
value: animationRecord.animation.value,
);
_activeAnimations[animationRecord.key] = animatingChildren;
}
}
@override
void toggleNode(TreeSliverNode<T> node) {
assert(_activeNodes.contains(node));
if (node.children.isEmpty) {
// No state to change.
return;
}
setState(() {
node._expanded = !node._expanded;
if (widget.onNodeToggle != null) {
widget.onNodeToggle!(node);
}
if (_currentAnimationForParent[node] != null) {
// Dispose of the old animation if this node was already animating.
_currentAnimationForParent[node]!.animation.dispose();
}
// If animation is disabled or the duration is zero, we skip the animation
// and immediately update the active nodes. This prevents the app from freezing
// due to the tree being incorrectly updated when the animation duration is zero.
// This is because, in this case, the node's children are no longer active.
if (widget.toggleAnimationStyle == AnimationStyle.noAnimation ||
widget.toggleAnimationStyle?.duration == Duration.zero) {
_unpackActiveNodes();
return;
}
final AnimationController controller =
_currentAnimationForParent[node]?.controller ??
AnimationController(
value: node._expanded ? 0.0 : 1.0,
vsync: this,
duration:
widget.toggleAnimationStyle?.duration ?? TreeSliver.defaultAnimationDuration,
)
..addStatusListener((AnimationStatus status) {
switch (status) {
case AnimationStatus.dismissed:
case AnimationStatus.completed:
_currentAnimationForParent[node]!.animation.dispose();
_currentAnimationForParent[node]!.controller.dispose();
_currentAnimationForParent.remove(node);
_updateActiveAnimations();
// If the node is collapsing, we need to unpack the active
// nodes to remove the ones that were removed from the tree.
// This is only necessary if the node is collapsing.
if (!node._expanded) {
_unpackActiveNodes();
}
case AnimationStatus.forward:
case AnimationStatus.reverse:
}
})
..addListener(() {
setState(() {
_updateActiveAnimations();
});
});
switch (controller.status) {
case AnimationStatus.forward:
case AnimationStatus.reverse:
// We're interrupting an animation already in progress.
controller.stop();
case AnimationStatus.dismissed:
case AnimationStatus.completed:
}
final CurvedAnimation newAnimation = CurvedAnimation(
parent: controller,
curve: widget.toggleAnimationStyle?.curve ?? TreeSliver.defaultAnimationCurve,
);
_currentAnimationForParent[node] = (
controller: controller,
animation: newAnimation,
// This key helps us keep track of the lifetime of this animation in the
// render object, since the indexes can change at any time.
key: UniqueKey(),
);
switch (node._expanded) {
case true:
// Expanding
_unpackActiveNodes();
controller.forward();
case false:
// Collapsing
controller.reverse();
}
});
}
}
class _TreeNodeParentDataWidget extends ParentDataWidget<TreeSliverNodeParentData> {
const _TreeNodeParentDataWidget({required this.depth, required super.child}) : assert(depth >= 0);
final int depth;
@override
void applyParentData(RenderObject renderObject) {
final TreeSliverNodeParentData parentData =
renderObject.parentData! as TreeSliverNodeParentData;
bool needsLayout = false;
if (parentData.depth != depth) {
assert(depth >= 0);
parentData.depth = depth;
needsLayout = true;
}
if (needsLayout) {
renderObject.parent?.markNeedsLayout();
}
}
@override
Type get debugTypicalAncestorWidgetClass => _SliverTree;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IntProperty('depth', depth));
}
}
class _SliverTree extends SliverVariedExtentList {
_SliverTree({
required NullableIndexedWidgetBuilder itemBuilder,
required super.itemExtentBuilder,
required this.activeAnimations,
required this.indentation,
ChildIndexGetter? findChildIndexCallback,
required int itemCount,
bool addAutomaticKeepAlives = true,
}) : super(
delegate: SliverChildBuilderDelegate(
itemBuilder,
findChildIndexCallback: findChildIndexCallback,
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: false, // Added in the _SliverTreeState
addSemanticIndexes: false, // Added in the _SliverTreeState
),
);
final Map<UniqueKey, TreeSliverNodesAnimation> activeAnimations;
final double indentation;
@override
RenderTreeSliver createRenderObject(BuildContext context) {
final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement;
return RenderTreeSliver(
itemExtentBuilder: itemExtentBuilder,
activeAnimations: activeAnimations,
indentation: indentation,
childManager: element,
);
}
@override
void updateRenderObject(BuildContext context, RenderTreeSliver renderObject) {
renderObject
..itemExtentBuilder = itemExtentBuilder
..activeAnimations = activeAnimations
..indentation = indentation;
}
}