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