// 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 { /// Creates a [TreeSliverNode] instance for use in a [TreeSliver]. TreeSliverNode(T content, {List>? children, bool expanded = false}) : _expanded = (children?.isNotEmpty ?? false) && expanded, _content = content, _children = children ?? >[]; /// 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> get children => _children; final List> _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? get parent => _parent; TreeSliverNode? _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 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 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 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 { /// Returns whether or not the given [TreeSliverNode] is expanded. bool isExpanded(TreeSliverNode 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 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 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? 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 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? _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 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 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? 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 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 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 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 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? result = context .findAncestorStateOfType<_TreeSliverState>(); if (result != null) { return result.controller; } throw FlutterError.fromParts([ 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>()?.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 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> 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 = const 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 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 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 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: [ // Icon for parent nodes TreeSliver.wrapChildToToggleNode( node: node, child: SizedBox.square( dimension: 30.0, child: node.children.isNotEmpty ? AnimatedRotation( key: ValueKey(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> createState() => _TreeSliverState(); } // Used in _SliverTreeState for code simplicity. typedef _AnimationRecord = ({ AnimationController controller, CurvedAnimation animation, UniqueKey key, }); class _TreeSliverState extends State> with TickerProviderStateMixin, TreeSliverStateMixin { TreeSliverController get controller => _treeController!; TreeSliverController? _treeController; final List> _activeNodes = >[]; bool _shouldUnpackNode(TreeSliverNode 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>? nodes, TreeSliverNode? parent, }) { if (nodes == null) { _activeNodes.clear(); nodes = widget.tree; } for (final TreeSliverNode 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, _AnimationRecord> _currentAnimationForParent = , _AnimationRecord>{}; final Map _activeAnimations = {}; @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 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 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 node) { return _getNode(node.content, widget.tree)?.isExpanded ?? false; } @override bool isActive(TreeSliverNode node) => _activeNodes.contains(node); @override TreeSliverNode? getNodeFor(T content) => _getNode(content, widget.tree); TreeSliverNode? _getNode(T content, List> tree) { final nextDepth = >[]; for (final 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 node) { if (_activeNodes.contains(node)) { return _activeNodes.indexOf(node); } return null; } @override void expandAll() { final activeNodesToExpand = >[]; _expandAll(widget.tree, activeNodesToExpand); activeNodesToExpand.reversed.forEach(toggleNode); } void _expandAll(List> tree, List> activeNodesToExpand) { for (final 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 activeNodesToCollapse = >[]; _collapseAll(widget.tree, activeNodesToCollapse); activeNodesToCollapse.reversed.forEach(toggleNode); } void _collapseAll(List> tree, List> activeNodesToCollapse) { for (final 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 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 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 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 { const _TreeNodeParentDataWidget({required this.depth, required super.child}) : assert(depth >= 0); final int depth; @override void applyParentData(RenderObject renderObject) { final parentData = renderObject.parentData! as TreeSliverNodeParentData; var 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 activeAnimations; final double indentation; @override RenderTreeSliver createRenderObject(BuildContext context) { final 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; } }