1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'viewport.dart';
6library;
7
8import 'package:flutter/foundation.dart';
9import 'package:flutter/rendering.dart';
10
11import 'basic.dart';
12import 'framework.dart';
13import 'gesture_detector.dart';
14import 'icon.dart';
15import 'icon_data.dart';
16import 'implicit_animations.dart';
17import 'scroll_delegate.dart';
18import 'sliver.dart';
19import 'text.dart';
20import 'ticker_provider.dart';
21
22const double _kDefaultRowExtent = 40.0;
23
24/// A data structure for configuring children of a [TreeSliver].
25///
26/// A [TreeSliverNode.content] can be of any type [T], but must correspond with
27/// the same type of the [TreeSliver].
28///
29/// The values returned by [depth], [parent] and [isExpanded] getters are
30/// managed by the [TreeSliver]'s state.
31class TreeSliverNode<T> {
32 /// Creates a [TreeSliverNode] instance for use in a [TreeSliver].
33 TreeSliverNode(
34 T content, {
35 List<TreeSliverNode<T>>? children,
36 bool expanded = false,
37 }) : _expanded = (children?.isNotEmpty ?? false) && expanded,
38 _content = content,
39 _children = children ?? <TreeSliverNode<T>>[];
40
41 /// The subject matter of the node.
42 ///
43 /// Must correspond with the type of [TreeSliver].
44 T get content => _content;
45 final T _content;
46
47 /// Other [TreeSliverNode]s that this node will be [parent] to.
48 ///
49 /// Modifying the children of nodes in a [TreeSliver] will cause the tree to be
50 /// rebuilt so that newly added active nodes are reflected in the tree.
51 List<TreeSliverNode<T>> get children => _children;
52 final List<TreeSliverNode<T>> _children;
53
54 /// Whether or not this node is expanded in the tree.
55 ///
56 /// Cannot be expanded if there are no children.
57 bool get isExpanded => _expanded;
58 bool _expanded;
59
60 /// The number of parent nodes between this node and the root of the tree.
61 int? get depth => _depth;
62 int? _depth;
63
64 /// The parent [TreeSliverNode] of this node.
65 TreeSliverNode<T>? get parent => _parent;
66 TreeSliverNode<T>? _parent;
67
68 @override
69 String toString() {
70 return 'TreeSliverNode: $content, depth: ${depth == 0 ? 'root' : depth}, '
71 '${children.isEmpty ? 'leaf' : 'parent, expanded: $isExpanded'}';
72 }
73}
74
75/// Signature for a function that creates a [Widget] to represent the given
76/// [TreeSliverNode] in the [TreeSliver].
77///
78/// Used by [TreeSliver.treeNodeBuilder] to build rows on demand for the
79/// tree.
80typedef TreeSliverNodeBuilder = Widget Function(
81 BuildContext context,
82 TreeSliverNode<Object?> node,
83 AnimationStyle animationStyle,
84);
85
86/// Signature for a function that returns an extent for the given
87/// [TreeSliverNode] in the [TreeSliver].
88///
89/// Used by [TreeSliver.treeRowExtentBuilder] to size rows on demand in the
90/// tree. The provided [SliverLayoutDimensions] provide information about the
91/// current scroll state and [Viewport] dimensions.
92///
93/// See also:
94///
95/// * [SliverVariedExtentList], which uses a similar item extent builder for
96/// dynamic child sizing in the list.
97typedef TreeSliverRowExtentBuilder = double Function(
98 TreeSliverNode<Object?> node,
99 SliverLayoutDimensions dimensions,
100);
101
102/// Signature for a function that is called when a [TreeSliverNode] is toggled,
103/// changing its expanded state.
104///
105/// See also:
106///
107/// * [TreeSliver.onNodeToggle], for controlling node expansion
108/// programmatically.
109typedef TreeSliverNodeCallback = void Function(TreeSliverNode<Object?> node);
110
111/// A mixin for classes implementing a tree structure as expected by a
112/// [TreeSliverController].
113///
114/// Used by [TreeSliver] to implement an interface for the
115/// [TreeSliverController].
116///
117/// This allows the [TreeSliverController] to be used in other widgets that
118/// implement this interface.
119///
120/// The type [T] correlates to the type of [TreeSliver] and [TreeSliverNode],
121/// representing the type of [TreeSliverNode.content].
122mixin TreeSliverStateMixin<T> {
123 /// Returns whether or not the given [TreeSliverNode] is expanded.
124 bool isExpanded(TreeSliverNode<T> node);
125
126 /// Returns whether or not the given [TreeSliverNode] is enclosed within its
127 /// parent [TreeSliverNode].
128 ///
129 /// If the [TreeSliverNode.parent] [isExpanded] (and all its parents are
130 /// expanded), or this is a root node, the given node is active and this
131 /// method will return true. This does not reflect whether or not the node is
132 /// visible in the [Viewport].
133 bool isActive(TreeSliverNode<T> node);
134
135 /// Switches the given [TreeSliverNode]s expanded state.
136 ///
137 /// May trigger an animation to reveal or hide the node's children based on
138 /// the [TreeSliver.toggleAnimationStyle].
139 ///
140 /// If the node does not have any children, nothing will happen.
141 void toggleNode(TreeSliverNode<T> node);
142
143 /// Closes all parent [TreeSliverNode]s in the tree.
144 void collapseAll();
145
146 /// Expands all parent [TreeSliverNode]s in the tree.
147 void expandAll();
148
149 /// Retrieves the [TreeSliverNode] containing the associated content, if it
150 /// exists.
151 ///
152 /// If no node exists, this will return null. This does not reflect whether
153 /// or not a node [isActive], or if it is visible in the viewport.
154 TreeSliverNode<T>? getNodeFor(T content);
155
156 /// Returns the current row index of the given [TreeSliverNode].
157 ///
158 /// If the node is not currently active in the tree, meaning its parent is
159 /// collapsed, this will return null.
160 int? getActiveIndexFor(TreeSliverNode<T> node);
161}
162
163/// Enables control over the [TreeSliverNode]s of a [TreeSliver].
164///
165/// It can be useful to expand or collapse nodes of the tree
166/// programmatically, for example to reconfigure an existing node
167/// based on a system event. To do so, create a [TreeSliver]
168/// with a [TreeSliverController] that's owned by a stateful widget
169/// or look up the tree's automatically created [TreeSliverController]
170/// with [TreeSliverController.of]
171///
172/// The controller's methods to expand or collapse nodes cause the
173/// the [TreeSliver] to rebuild, so they may not be called from
174/// a build method.
175class TreeSliverController {
176 /// Create a controller to be used with [TreeSliver.controller].
177 TreeSliverController();
178
179 TreeSliverStateMixin<Object?>? _state;
180
181 /// Whether the given [TreeSliverNode] built with this controller is in an
182 /// expanded state.
183 ///
184 /// See also:
185 ///
186 /// * [expandNode], which expands a given [TreeSliverNode].
187 /// * [collapseNode], which collapses a given [TreeSliverNode].
188 /// * [TreeSliver.controller] to create a TreeSliver with a controller.
189 bool isExpanded(TreeSliverNode<Object?> node) {
190 assert(_state != null);
191 return _state!.isExpanded(node);
192 }
193
194 /// Whether or not the given [TreeSliverNode] is enclosed within its parent
195 /// [TreeSliverNode].
196 ///
197 /// If the [TreeSliverNode.parent] [isExpanded], or this is a root node, the
198 /// given node is active and this method will return true. This does not
199 /// reflect whether or not the node is visible in the [Viewport].
200 bool isActive(TreeSliverNode<Object?> node) {
201 assert(_state != null);
202 return _state!.isActive(node);
203 }
204
205 /// Returns the [TreeSliverNode] containing the associated content, if it
206 /// exists.
207 ///
208 /// If no node exists, this will return null. This does not reflect whether
209 /// or not a node [isActive], or if it is currently visible in the viewport.
210 TreeSliverNode<Object?>? getNodeFor(Object? content) {
211 assert(_state != null);
212 return _state!.getNodeFor(content);
213 }
214
215 /// Switches the given [TreeSliverNode]s expanded state.
216 ///
217 /// May trigger an animation to reveal or hide the node's children based on
218 /// the [TreeSliver.toggleAnimationStyle].
219 ///
220 /// If the node does not have any children, nothing will happen.
221 void toggleNode(TreeSliverNode<Object?> node) {
222 assert(_state != null);
223 return _state!.toggleNode(node);
224 }
225
226 /// Expands the [TreeSliverNode] that was built with this controller.
227 ///
228 /// If the node is already in the expanded state (see [isExpanded]), calling
229 /// this method has no effect.
230 ///
231 /// Calling this method may cause the [TreeSliver] to rebuild, so it may
232 /// not be called from a build method.
233 ///
234 /// Calling this method will trigger the [TreeSliver.onNodeToggle]
235 /// callback.
236 ///
237 /// See also:
238 ///
239 /// * [collapseNode], which collapses the [TreeSliverNode].
240 /// * [isExpanded] to check whether the tile is expanded.
241 /// * [TreeSliver.controller] to create a TreeSliver with a controller.
242 void expandNode(TreeSliverNode<Object?> node) {
243 assert(_state != null);
244 if (!node.isExpanded) {
245 _state!.toggleNode(node);
246 }
247 }
248
249 /// Expands all parent [TreeSliverNode]s in the tree.
250 void expandAll() {
251 assert(_state != null);
252 _state!.expandAll();
253 }
254
255 /// Closes all parent [TreeSliverNode]s in the tree.
256 void collapseAll() {
257 assert(_state != null);
258 _state!.collapseAll();
259 }
260
261 /// Collapses the [TreeSliverNode] that was built with this controller.
262 ///
263 /// If the node is already in the collapsed state (see [isExpanded]), calling
264 /// this method has no effect.
265 ///
266 /// Calling this method may cause the [TreeSliver] to rebuild, so it may
267 /// not be called from a build method.
268 ///
269 /// Calling this method will trigger the [TreeSliver.onNodeToggle]
270 /// callback.
271 ///
272 /// See also:
273 ///
274 /// * [expandNode], which expands the tile.
275 /// * [isExpanded] to check whether the tile is expanded.
276 /// * [TreeSliver.controller] to create a TreeSliver with a controller.
277 void collapseNode(TreeSliverNode<Object?> node) {
278 assert(_state != null);
279 if (node.isExpanded) {
280 _state!.toggleNode(node);
281 }
282 }
283
284 /// Returns the current row index of the given [TreeSliverNode].
285 ///
286 /// If the node is not currently active in the tree, meaning its parent is
287 /// collapsed, this will return null.
288 int? getActiveIndexFor(TreeSliverNode<Object?> node) {
289 assert(_state != null);
290 return _state!.getActiveIndexFor(node);
291 }
292
293 /// Finds the [TreeSliverController] for the closest [TreeSliver] instance
294 /// that encloses the given context.
295 ///
296 /// If no [TreeSliver] encloses the given context, calling this
297 /// method will cause an assert in debug mode, and throw an
298 /// exception in release mode.
299 ///
300 /// To return null if there is no [TreeSliver] use [maybeOf] instead.
301 ///
302 /// Typical usage of the [TreeSliverController.of] function is to call it
303 /// from within the `build` method of a descendant of a [TreeSliver].
304 ///
305 /// When the [TreeSliver] is actually created in the same `build`
306 /// function as the callback that refers to the controller, then the
307 /// `context` argument to the `build` function can't be used to find
308 /// the [TreeSliverController] (since it's "above" the widget
309 /// being returned in the widget tree). In cases like that you can
310 /// add a [Builder] widget, which provides a new scope with a
311 /// [BuildContext] that is "under" the [TreeSliver].
312 static TreeSliverController of(BuildContext context) {
313 final _TreeSliverState<Object?>? result =
314 context.findAncestorStateOfType<_TreeSliverState<Object?>>();
315 if (result != null) {
316 return result.controller;
317 }
318 throw FlutterError.fromParts(<DiagnosticsNode>[
319 ErrorSummary(
320 'TreeController.of() called with a context that does not contain a '
321 'TreeSliver.',
322 ),
323 ErrorDescription(
324 'No TreeSliver ancestor could be found starting from the context that '
325 'was passed to TreeController.of(). '
326 'This usually happens when the context provided is from the same '
327 'StatefulWidget as that whose build function actually creates the '
328 'TreeSliver widget being sought.',
329 ),
330 ErrorHint(
331 'There are several ways to avoid this problem. The simplest is to use '
332 'a Builder to get a context that is "under" the TreeSliver.',
333 ),
334 ErrorHint(
335 'A more efficient solution is to split your build function into '
336 'several widgets. This introduces a new context from which you can '
337 'obtain the TreeSliver. In this solution, you would have an outer '
338 'widget that creates the TreeSliver populated by instances of your new '
339 'inner widgets, and then in these inner widgets you would use '
340 'TreeController.of().',
341 ),
342 context.describeElement('The context used was'),
343 ]);
344 }
345
346 /// Finds the [TreeSliver] from the closest instance of this class that
347 /// encloses the given context and returns its [TreeSliverController].
348 ///
349 /// If no [TreeSliver] encloses the given context then return null.
350 /// To throw an exception instead, use [of] instead of this function.
351 ///
352 /// See also:
353 ///
354 /// * [of], a similar function to this one that throws if no [TreeSliver]
355 /// encloses the given context. Also includes some sample code in its
356 /// documentation.
357 static TreeSliverController? maybeOf(BuildContext context) {
358 return context.findAncestorStateOfType<_TreeSliverState<Object?>>()?.controller;
359 }
360}
361
362int _kDefaultSemanticIndexCallback(Widget _, int localIndex) => localIndex;
363
364/// A widget that displays [TreeSliverNode]s that expand and collapse in a
365/// vertically and horizontally scrolling [Viewport].
366///
367/// The type [T] correlates to the type of [TreeSliver] and [TreeSliverNode],
368/// representing the type of [TreeSliverNode.content].
369///
370/// The rows of the tree are laid out on demand by the [Viewport]'s render
371/// object, using [TreeSliver.treeNodeBuilder]. This will only be called for the
372/// nodes that are visible, or within the [Viewport.cacheExtent].
373///
374/// The [TreeSliver.treeNodeBuilder] returns the [Widget] that represents the
375/// given [TreeSliverNode].
376///
377/// The [TreeSliver.treeRowExtentBuilder] returns a double representing the
378/// extent of a given node in the main axis.
379///
380/// Providing a [TreeSliverController] will enable querying and controlling the
381/// state of nodes in the tree.
382///
383/// A [TreeSliver] only supports a vertical axis direction of
384/// [AxisDirection.down] and a horizontal axis direction of
385/// [AxisDirection.right].
386///
387///{@tool dartpad}
388/// This example uses a [TreeSliver] to display nodes, highlighting nodes as
389/// they are selected.
390///
391/// ** See code in examples/api/lib/widgets/sliver/sliver_tree.0.dart **
392/// {@end-tool}
393///
394/// {@tool dartpad}
395/// This example shows a highly customized [TreeSliver] configured to
396/// [TreeSliverIndentationType.none]. This allows the indentation to be handled
397/// by the developer in [TreeSliver.treeNodeBuilder], where a decoration is
398/// used to fill the indented space.
399///
400/// ** See code in examples/api/lib/widgets/sliver/sliver_tree.1.dart **
401/// {@end-tool}
402class TreeSliver<T> extends StatefulWidget {
403 /// Creates an instance of a [TreeSliver] for displaying [TreeSliverNode]s
404 /// that animate expanding and collapsing of nodes.
405 const TreeSliver({
406 super.key,
407 required this.tree,
408 this.treeNodeBuilder = TreeSliver.defaultTreeNodeBuilder,
409 this.treeRowExtentBuilder = TreeSliver.defaultTreeRowExtentBuilder,
410 this.controller,
411 this.onNodeToggle,
412 this.toggleAnimationStyle,
413 this.indentation = TreeSliverIndentationType.standard,
414 this.addAutomaticKeepAlives = true,
415 this.addRepaintBoundaries = true,
416 this.addSemanticIndexes = true,
417 this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
418 this.semanticIndexOffset = 0,
419 this.findChildIndexCallback,
420 });
421
422 /// The list of [TreeSliverNode]s that may be displayed in the [TreeSliver].
423 ///
424 /// Beyond root nodes, whether or not a given [TreeSliverNode] is displayed
425 /// depends on the [TreeSliverNode.isExpanded] value of its parent. The
426 /// [TreeSliver] will set the [TreeSliverNode.parent] and
427 /// [TreeSliverNode.depth] as nodes are built on demand to ensure the
428 /// integrity of the tree.
429 final List<TreeSliverNode<T>> tree;
430
431 /// Called to build and entry of the [TreeSliver] for the given node.
432 ///
433 /// By default, if this is unset, the [TreeSliver.defaultTreeNodeBuilder]
434 /// is used.
435 final TreeSliverNodeBuilder treeNodeBuilder;
436
437 /// Called to calculate the extent of the widget built for the given
438 /// [TreeSliverNode].
439 ///
440 /// By default, if this is unset, the
441 /// [TreeSliver.defaultTreeRowExtentBuilder] is used.
442 ///
443 /// See also:
444 ///
445 /// * [SliverVariedExtentList.itemExtentBuilder], a very similar method that
446 /// allows users to dynamically compute extents on demand.
447 final TreeSliverRowExtentBuilder treeRowExtentBuilder;
448
449 /// If provided, the controller can be used to expand and collapse
450 /// [TreeSliverNode]s, or lookup information about the current state of the
451 /// [TreeSliver].
452 final TreeSliverController? controller;
453
454 /// Called when a [TreeSliverNode] expands or collapses.
455 ///
456 /// This will not be called if a [TreeSliverNode] does not have any children.
457 final TreeSliverNodeCallback? onNodeToggle;
458
459 /// The default [AnimationStyle] for expanding and collapsing nodes in the
460 /// [TreeSliver].
461 ///
462 /// The default [AnimationStyle.duration] uses
463 /// [TreeSliver.defaultAnimationDuration], which is 150 milliseconds.
464 ///
465 /// The default [AnimationStyle.curve] uses [TreeSliver.defaultAnimationCurve],
466 /// which is [Curves.linear].
467 ///
468 /// To disable the tree animation, use [AnimationStyle.noAnimation].
469 final AnimationStyle? toggleAnimationStyle;
470
471 /// The number of pixels children will be offset by in the cross axis based on
472 /// their [TreeSliverNode.depth].
473 ///
474 /// {@macro flutter.rendering.TreeSliverIndentationType}
475 final TreeSliverIndentationType indentation;
476
477 /// {@macro flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives}
478 final bool addAutomaticKeepAlives;
479
480 /// {@macro flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries}
481 final bool addRepaintBoundaries;
482
483 /// {@macro flutter.widgets.SliverChildBuilderDelegate.addSemanticIndexes}
484 final bool addSemanticIndexes;
485
486 /// {@macro flutter.widgets.SliverChildBuilderDelegate.semanticIndexCallback}
487 final SemanticIndexCallback semanticIndexCallback;
488
489 /// {@macro flutter.widgets.SliverChildBuilderDelegate.semanticIndexOffset}
490 final int semanticIndexOffset;
491
492 /// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback}
493 final int? Function(Key)? findChildIndexCallback;
494
495 /// The default [AnimationStyle] used for node expand and collapse animations,
496 /// when one has not been provided in [toggleAnimationStyle].
497 static AnimationStyle defaultToggleAnimationStyle = AnimationStyle(
498 curve: defaultAnimationCurve,
499 duration: defaultAnimationDuration,
500 );
501
502 /// A default of [Curves.linear], which is used in the tree's expanding and
503 /// collapsing node animation.
504 static const Curve defaultAnimationCurve = Curves.linear;
505
506 /// A default [Duration] of 150 milliseconds, which is used in the tree's
507 /// expanding and collapsing node animation.
508 static const Duration defaultAnimationDuration = Duration(milliseconds: 150);
509
510 /// A wrapper method for triggering the expansion or collapse of a
511 /// [TreeSliverNode].
512 ///
513 /// Used as part of [TreeSliver.defaultTreeNodeBuilder] to wrap the leading
514 /// icon of parent [TreeSliverNode]s such that tapping on it triggers the
515 /// animation.
516 ///
517 /// If defining your own [TreeSliver.treeNodeBuilder], this method can be used
518 /// to wrap any part, or all, of the returned widget in order to trigger the
519 /// change in state for the node.
520 static Widget wrapChildToToggleNode({
521 required TreeSliverNode<Object?> node,
522 required Widget child,
523 }) {
524 return Builder(builder: (BuildContext context) {
525 return GestureDetector(
526 onTap: () {
527 TreeSliverController.of(context).toggleNode(node);
528 },
529 child: child,
530 );
531 });
532 }
533
534 /// Returns the fixed default extent for rows in the tree, which is 40 pixels.
535 ///
536 /// Used by [TreeSliver.treeRowExtentBuilder].
537 static double defaultTreeRowExtentBuilder(
538 TreeSliverNode<Object?> node,
539 SliverLayoutDimensions dimensions,
540 ) {
541 return _kDefaultRowExtent;
542 }
543
544 /// Returns the default tree row for a given [TreeSliverNode].
545 ///
546 /// Used by [TreeSliver.treeNodeBuilder].
547 ///
548 /// This will return a [Row] containing the [toString] of
549 /// [TreeSliverNode.content]. If the [TreeSliverNode] is a parent of
550 /// additional nodes, a arrow icon will precede the content, and will trigger
551 /// an expand and collapse animation when tapped.
552 static Widget defaultTreeNodeBuilder(
553 BuildContext context,
554 TreeSliverNode<Object?> node,
555 AnimationStyle toggleAnimationStyle
556 ) {
557 final Duration animationDuration = toggleAnimationStyle.duration
558 ?? TreeSliver.defaultAnimationDuration;
559 final Curve animationCurve = toggleAnimationStyle.curve
560 ?? TreeSliver.defaultAnimationCurve;
561 final int index = TreeSliverController.of(context).getActiveIndexFor(node)!;
562 return Padding(
563 padding: const EdgeInsets.all(8.0),
564 child: Row(children: <Widget>[
565 // Icon for parent nodes
566 TreeSliver.wrapChildToToggleNode(
567 node: node,
568 child: SizedBox.square(
569 dimension: 30.0,
570 child: node.children.isNotEmpty
571 ? AnimatedRotation(
572 key: ValueKey<int>(index),
573 turns: node.isExpanded ? 0.25 : 0.0,
574 duration: animationDuration,
575 curve: animationCurve,
576 // Renders a unicode right-facing arrow. >
577 child: const Icon(IconData(0x25BA), size: 14),
578 )
579 : null,
580 ),
581 ),
582 // Spacer
583 const SizedBox(width: 8.0),
584 // Content
585 Text(node.content.toString()),
586 ]),
587 );
588 }
589
590 @override
591 State<TreeSliver<T>> createState() => _TreeSliverState<T>();
592}
593
594// Used in _SliverTreeState for code simplicity.
595typedef _AnimationRecord = ({
596 AnimationController controller,
597 CurvedAnimation animation,
598 UniqueKey key,
599});
600
601class _TreeSliverState<T> extends State<TreeSliver<T>> with TickerProviderStateMixin, TreeSliverStateMixin<T> {
602 TreeSliverController get controller => _treeController!;
603 TreeSliverController? _treeController;
604
605 final List<TreeSliverNode<T>> _activeNodes = <TreeSliverNode<T>>[];
606 bool _shouldUnpackNode(TreeSliverNode<T> node) {
607 if (node.children.isEmpty) {
608 // No children to unpack.
609 return false;
610 }
611 if (_currentAnimationForParent[node] != null) {
612 // Whether expanding or collapsing, the child nodes are still active, so
613 // unpack.
614 return true;
615 }
616 // If we are not animating, respect node.isExpanded.
617 return node.isExpanded;
618 }
619 void _unpackActiveNodes({
620 int depth = 0,
621 List<TreeSliverNode<T>>? nodes,
622 TreeSliverNode<T>? parent,
623 }) {
624 if (nodes == null) {
625 _activeNodes.clear();
626 nodes = widget.tree;
627 }
628 for (final TreeSliverNode<T> node in nodes) {
629 node._depth = depth;
630 node._parent = parent;
631 _activeNodes.add(node);
632 if (_shouldUnpackNode(node)) {
633 _unpackActiveNodes(
634 depth: depth + 1,
635 nodes: node.children,
636 parent: node,
637 );
638 }
639 }
640 }
641
642 final Map<TreeSliverNode<T>, _AnimationRecord> _currentAnimationForParent = <TreeSliverNode<T>, _AnimationRecord>{};
643 final Map<UniqueKey, TreeSliverNodesAnimation> _activeAnimations = <UniqueKey, TreeSliverNodesAnimation>{};
644
645 @override
646 void initState() {
647 _unpackActiveNodes();
648 assert(
649 widget.controller?._state == null,
650 'The provided TreeSliverController is already associated with another '
651 'TreeSliver. A TreeSliverController can only be associated with one '
652 'TreeSliver.',
653 );
654 _treeController = widget.controller ?? TreeSliverController();
655 _treeController!._state = this;
656 super.initState();
657 }
658
659 @override
660 void didUpdateWidget(TreeSliver<T> oldWidget) {
661 super.didUpdateWidget(oldWidget);
662 // Internal or provided, there is always a tree controller.
663 assert(_treeController != null);
664 if (oldWidget.controller == null && widget.controller != null) {
665 // A new tree controller has been provided, update and dispose of the
666 // internally generated one.
667 _treeController!._state = null;
668 _treeController = widget.controller;
669 _treeController!._state = this;
670 } else if (oldWidget.controller != null && widget.controller == null) {
671 // A tree controller had been provided, but was removed. We need to create
672 // one internally.
673 assert(oldWidget.controller == _treeController);
674 oldWidget.controller!._state = null;
675 _treeController = TreeSliverController();
676 _treeController!._state = this;
677 } else if (oldWidget.controller != widget.controller) {
678 assert(oldWidget.controller != null);
679 assert(widget.controller != null);
680 assert(oldWidget.controller == _treeController);
681 // The tree is still being provided a controller, but it has changed. Just
682 // update it.
683 _treeController!._state = null;
684 _treeController = widget.controller;
685 _treeController!._state = this;
686 }
687 // Internal or provided, there is always a tree controller.
688 assert(_treeController != null);
689 assert(_treeController!._state != null);
690 _unpackActiveNodes();
691 }
692
693 @override
694 void dispose() {
695 _treeController!._state = null;
696 for (final _AnimationRecord record in _currentAnimationForParent.values) {
697 record.animation.dispose();
698 record.controller.dispose();
699 }
700 super.dispose();
701 }
702
703 @override
704 Widget build(BuildContext context) {
705 return _SliverTree(
706 itemCount: _activeNodes.length,
707 activeAnimations: _activeAnimations,
708 itemBuilder: (BuildContext context, int index) {
709 final TreeSliverNode<T> node = _activeNodes[index];
710 Widget child = widget.treeNodeBuilder(
711 context,
712 node,
713 widget.toggleAnimationStyle ?? TreeSliver.defaultToggleAnimationStyle,
714 );
715
716 if (widget.addRepaintBoundaries) {
717 child = RepaintBoundary(child: child);
718 }
719 if (widget.addSemanticIndexes) {
720 final int? semanticIndex = widget.semanticIndexCallback(child, index);
721 if (semanticIndex != null) {
722 child = IndexedSemantics(
723 index: semanticIndex + widget.semanticIndexOffset,
724 child: child,
725 );
726 }
727 }
728
729 return _TreeNodeParentDataWidget(
730 depth: node.depth!,
731 child: child,
732 );
733 },
734 itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
735 return widget.treeRowExtentBuilder(_activeNodes[index], dimensions);
736 },
737 addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
738 findChildIndexCallback: widget.findChildIndexCallback,
739 indentation: widget.indentation.value,
740 );
741 }
742
743 // TreeStateMixin Implementation
744
745 @override
746 bool isExpanded(TreeSliverNode<T> node) {
747 return _getNode(node.content, widget.tree)?.isExpanded ?? false;
748 }
749
750 @override
751 bool isActive(TreeSliverNode<T> node) => _activeNodes.contains(node);
752
753 @override
754 TreeSliverNode<T>? getNodeFor(T content) => _getNode(content, widget.tree);
755 TreeSliverNode<T>? _getNode(T content, List<TreeSliverNode<T>> tree) {
756 final List<TreeSliverNode<T>> nextDepth = <TreeSliverNode<T>>[];
757 for (final TreeSliverNode<T> node in tree) {
758 if (node.content == content) {
759 return node;
760 }
761 if (node.children.isNotEmpty) {
762 nextDepth.addAll(node.children);
763 }
764 }
765 if (nextDepth.isNotEmpty) {
766 return _getNode(content, nextDepth);
767 }
768 return null;
769 }
770
771 @override
772 int? getActiveIndexFor(TreeSliverNode<T> node) {
773 if (_activeNodes.contains(node)) {
774 return _activeNodes.indexOf(node);
775 }
776 return null;
777 }
778
779 @override
780 void expandAll() {
781 final List<TreeSliverNode<T>> activeNodesToExpand = <TreeSliverNode<T>>[];
782 _expandAll(widget.tree, activeNodesToExpand);
783 activeNodesToExpand.reversed.forEach(toggleNode);
784 }
785 void _expandAll(
786 List<TreeSliverNode<T>> tree,
787 List<TreeSliverNode<T>> activeNodesToExpand,
788 ) {
789 for (final TreeSliverNode<T> node in tree) {
790 if (node.children.isNotEmpty) {
791 // This is a parent node.
792 // Expand all the children, and their children.
793 _expandAll(node.children, activeNodesToExpand);
794 if (!node.isExpanded) {
795 // The node itself needs to be expanded.
796 if (_activeNodes.contains(node)) {
797 // This is an active node in the tree, add to
798 // the list to toggle once all hidden nodes
799 // have been handled.
800 activeNodesToExpand.add(node);
801 } else {
802 // This is a hidden node. Update its expanded state.
803 node._expanded = true;
804 }
805 }
806 }
807 }
808 }
809
810 @override
811 void collapseAll() {
812 final List<TreeSliverNode<T>> activeNodesToCollapse = <TreeSliverNode<T>>[];
813 _collapseAll(widget.tree, activeNodesToCollapse);
814 activeNodesToCollapse.reversed.forEach(toggleNode);
815 }
816 void _collapseAll(
817 List<TreeSliverNode<T>> tree,
818 List<TreeSliverNode<T>> activeNodesToCollapse,
819 ) {
820 for (final TreeSliverNode<T> node in tree) {
821 if (node.children.isNotEmpty) {
822 // This is a parent node.
823 // Collapse all the children, and their children.
824 _collapseAll(node.children, activeNodesToCollapse);
825 if (node.isExpanded) {
826 // The node itself needs to be collapsed.
827 if (_activeNodes.contains(node)) {
828 // This is an active node in the tree, add to
829 // the list to toggle once all hidden nodes
830 // have been handled.
831 activeNodesToCollapse.add(node);
832 } else {
833 // This is a hidden node. Update its expanded state.
834 node._expanded = false;
835 }
836 }
837 }
838 }
839 }
840
841 void _updateActiveAnimations() {
842 // The indexes of various child node animations can change constantly based
843 // on more nodes being expanded or collapsed. Compile the indexes and their
844 // animations keys each time we build with an updated active node list.
845 _activeAnimations.clear();
846 for (final TreeSliverNode<T> node in _currentAnimationForParent.keys) {
847 final _AnimationRecord animationRecord = _currentAnimationForParent[node]!;
848 final int leadingChildIndex = _activeNodes.indexOf(node) + 1;
849 final TreeSliverNodesAnimation animatingChildren = (
850 fromIndex: leadingChildIndex,
851 toIndex: leadingChildIndex + node.children.length - 1,
852 value: animationRecord.animation.value,
853 );
854 _activeAnimations[animationRecord.key] = animatingChildren;
855 }
856 }
857
858 @override
859 void toggleNode(TreeSliverNode<T> node) {
860 assert(_activeNodes.contains(node));
861 if (node.children.isEmpty) {
862 // No state to change.
863 return;
864 }
865 setState(() {
866 node._expanded = !node._expanded;
867 if (widget.onNodeToggle != null) {
868 widget.onNodeToggle!(node);
869 }
870 if (_currentAnimationForParent[node] != null) {
871 // Dispose of the old animation if this node was already animating.
872 _currentAnimationForParent[node]!.animation.dispose();
873 }
874
875 // If animation is disabled or the duration is zero, we skip the animation
876 // and immediately update the active nodes. This prevents the app from freezing
877 // due to the tree being incorrectly updated when the animation duration is zero.
878 // This is because, in this case, the node's children are no longer active.
879 if (widget.toggleAnimationStyle == AnimationStyle.noAnimation || widget.toggleAnimationStyle?.duration == Duration.zero) {
880 _unpackActiveNodes();
881 return;
882 }
883
884 final AnimationController controller = _currentAnimationForParent[node]?.controller
885 ?? AnimationController(
886 value: node._expanded ? 0.0 : 1.0,
887 vsync: this,
888 duration: widget.toggleAnimationStyle?.duration
889 ?? TreeSliver.defaultAnimationDuration,
890 )..addStatusListener((AnimationStatus status) {
891 switch (status) {
892 case AnimationStatus.dismissed:
893 case AnimationStatus.completed:
894 _currentAnimationForParent[node]!.animation.dispose();
895 _currentAnimationForParent[node]!.controller.dispose();
896 _currentAnimationForParent.remove(node);
897 _updateActiveAnimations();
898 case AnimationStatus.forward:
899 case AnimationStatus.reverse:
900 }
901 })..addListener(() {
902 setState((){
903 _updateActiveAnimations();
904 });
905 });
906
907 switch (controller.status) {
908 case AnimationStatus.forward:
909 case AnimationStatus.reverse:
910 // We're interrupting an animation already in progress.
911 controller.stop();
912 case AnimationStatus.dismissed:
913 case AnimationStatus.completed:
914 }
915
916 final CurvedAnimation newAnimation = CurvedAnimation(
917 parent: controller,
918 curve: widget.toggleAnimationStyle?.curve ?? TreeSliver.defaultAnimationCurve,
919 );
920 _currentAnimationForParent[node] = (
921 controller: controller,
922 animation: newAnimation,
923 // This key helps us keep track of the lifetime of this animation in the
924 // render object, since the indexes can change at any time.
925 key: UniqueKey(),
926 );
927 switch (node._expanded) {
928 case true:
929 // Expanding
930 _unpackActiveNodes();
931 controller.forward();
932 case false:
933 // Collapsing
934 controller.reverse().then((_) {
935 _unpackActiveNodes();
936 });
937 }
938 });
939 }
940}
941
942class _TreeNodeParentDataWidget extends ParentDataWidget<TreeSliverNodeParentData> {
943 const _TreeNodeParentDataWidget({
944 required this.depth,
945 required super.child,
946 }) : assert(depth >= 0);
947
948 final int depth;
949
950 @override
951 void applyParentData(RenderObject renderObject) {
952 final TreeSliverNodeParentData parentData = renderObject.parentData! as TreeSliverNodeParentData;
953 bool needsLayout = false;
954
955 if (parentData.depth != depth) {
956 assert(depth >= 0);
957 parentData.depth = depth;
958 needsLayout = true;
959 }
960
961 if (needsLayout) {
962 renderObject.parent?.markNeedsLayout();
963 }
964 }
965
966 @override
967 Type get debugTypicalAncestorWidgetClass => _SliverTree;
968
969 @override
970 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
971 super.debugFillProperties(properties);
972 properties.add(IntProperty('depth', depth));
973 }
974}
975
976class _SliverTree extends SliverVariedExtentList {
977 _SliverTree({
978 required NullableIndexedWidgetBuilder itemBuilder,
979 required super.itemExtentBuilder,
980 required this.activeAnimations,
981 required this.indentation,
982 ChildIndexGetter? findChildIndexCallback,
983 required int itemCount,
984 bool addAutomaticKeepAlives = true,
985 }) : super(delegate: SliverChildBuilderDelegate(
986 itemBuilder,
987 findChildIndexCallback: findChildIndexCallback,
988 childCount: itemCount,
989 addAutomaticKeepAlives: addAutomaticKeepAlives,
990 addRepaintBoundaries: false, // Added in the _SliverTreeState
991 addSemanticIndexes: false, // Added in the _SliverTreeState
992 ));
993
994 final Map<UniqueKey, TreeSliverNodesAnimation> activeAnimations;
995 final double indentation;
996
997 @override
998 RenderTreeSliver createRenderObject(BuildContext context) {
999 final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement;
1000 return RenderTreeSliver(
1001 itemExtentBuilder: itemExtentBuilder,
1002 activeAnimations: activeAnimations,
1003 indentation: indentation,
1004 childManager: element,
1005 );
1006 }
1007
1008 @override
1009 void updateRenderObject(BuildContext context, RenderTreeSliver renderObject) {
1010 renderObject
1011 ..itemExtentBuilder = itemExtentBuilder
1012 ..activeAnimations = activeAnimations
1013 ..indentation = indentation;
1014 }
1015}
1016