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'; |
6 | library; |
7 | |
8 | import 'package:flutter/foundation.dart'; |
9 | import 'package:flutter/rendering.dart'; |
10 | |
11 | import 'basic.dart'; |
12 | import 'framework.dart'; |
13 | import 'gesture_detector.dart'; |
14 | import 'icon.dart'; |
15 | import 'icon_data.dart'; |
16 | import 'implicit_animations.dart'; |
17 | import 'scroll_delegate.dart'; |
18 | import 'sliver.dart'; |
19 | import 'text.dart'; |
20 | import 'ticker_provider.dart'; |
21 | |
22 | const 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. |
31 | class 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. |
80 | typedef 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. |
97 | typedef 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. |
109 | typedef 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]. |
122 | mixin 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. |
175 | class 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 | |
362 | int _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} |
402 | class 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. |
595 | typedef _AnimationRecord = ({ |
596 | AnimationController controller, |
597 | CurvedAnimation animation, |
598 | UniqueKey key, |
599 | }); |
600 | |
601 | class _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 | |
942 | class _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 | |
976 | class _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 | |