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 'package:flutter/widgets.dart'; |
6 | library; |
7 | |
8 | import 'dart:math' as math; |
9 | |
10 | import 'package:flutter/foundation.dart'; |
11 | |
12 | import 'box.dart'; |
13 | import 'layer.dart'; |
14 | import 'object.dart'; |
15 | import 'sliver.dart'; |
16 | import 'sliver_fixed_extent_list.dart'; |
17 | import 'sliver_multi_box_adaptor.dart'; |
18 | |
19 | /// Represents the animation of the children of a parent [TreeSliverNode] that |
20 | /// are animating into or out of view. |
21 | /// |
22 | /// The `fromIndex` and `toIndex` are identify the animating children following |
23 | /// the parent, with the `value` representing the status of the current |
24 | /// animation. The value of `toIndex` is inclusive, meaning the child at that |
25 | /// index is included in the animating segment. |
26 | /// |
27 | /// Provided to [RenderTreeSliver] as part of |
28 | /// [RenderTreeSliver.activeAnimations] by [TreeSliver] to properly offset |
29 | /// animating children. |
30 | typedef TreeSliverNodesAnimation = ({ |
31 | int fromIndex, |
32 | int toIndex, |
33 | double value, |
34 | }); |
35 | |
36 | /// Used to pass information down to [RenderTreeSliver]. |
37 | class TreeSliverNodeParentData extends SliverMultiBoxAdaptorParentData { |
38 | /// The depth of the node, used by [RenderTreeSliver] to offset children by |
39 | /// by the [TreeSliverIndentationType]. |
40 | int depth = 0; |
41 | } |
42 | |
43 | /// The style of indentation for [TreeSliverNode]s in a [TreeSliver], as |
44 | /// handled by [RenderTreeSliver]. |
45 | /// |
46 | /// {@template flutter.rendering.TreeSliverIndentationType} |
47 | /// By default, the indentation is handled by [RenderTreeSliver]. Child nodes |
48 | /// are offset by the indentation specified by |
49 | /// [TreeSliverIndentationType.value] in the cross axis of the viewport. This |
50 | /// means the space allotted to the indentation will not be part of the space |
51 | /// made available to the Widget returned by [TreeSliver.treeNodeBuilder]. |
52 | /// |
53 | /// Alternatively, the indentation can be implemented in |
54 | /// [TreeSliver.treeNodeBuilder], with the depth of the given tree row |
55 | /// accessed by [TreeSliverNode.depth]. This allows for more customization in |
56 | /// building tree rows, such as filling the indented area with decorations or |
57 | /// ink effects. |
58 | /// |
59 | /// {@tool dartpad} |
60 | /// This example shows a highly customized [TreeSliver] configured to |
61 | /// [TreeSliverIndentationType.none]. This allows the indentation to be handled |
62 | /// by the developer in [TreeSliver.treeNodeBuilder], where a decoration is |
63 | /// used to fill the indented space. |
64 | /// |
65 | /// ** See code in examples/api/lib/widgets/sliver/sliver_tree.1.dart ** |
66 | /// {@end-tool} |
67 | /// |
68 | /// {@endtemplate} |
69 | class TreeSliverIndentationType { |
70 | const TreeSliverIndentationType._internal(double value) : _value = value; |
71 | |
72 | /// The number of pixels by which [TreeSliverNode]s will be offset according |
73 | /// to their [TreeSliverNode.depth]. |
74 | double get value => _value; |
75 | final double _value; |
76 | |
77 | /// The default indentation of child [TreeSliverNode]s in a [TreeSliver]. |
78 | /// |
79 | /// Child nodes will be offset by 10 pixels for each level in the tree. |
80 | static const TreeSliverIndentationType standard = TreeSliverIndentationType._internal(10.0); |
81 | |
82 | /// Configures no offsetting of child nodes in a [TreeSliver]. |
83 | /// |
84 | /// Useful if the indentation is implemented in the |
85 | /// [TreeSliver.treeNodeBuilder] instead for more customization options. |
86 | /// |
87 | /// Child nodes will not be offset in the tree. |
88 | static const TreeSliverIndentationType none = TreeSliverIndentationType._internal(0.0); |
89 | |
90 | /// Configures a custom offset for indenting child nodes in a |
91 | /// [TreeSliver]. |
92 | /// |
93 | /// Child nodes will be offset by the provided number of pixels in the tree. |
94 | /// The [value] must be a non negative number. |
95 | static TreeSliverIndentationType custom(double value) { |
96 | assert(value >= 0.0); |
97 | return TreeSliverIndentationType._internal(value); |
98 | } |
99 | } |
100 | |
101 | // Used during paint to delineate animating portions of the tree. |
102 | typedef _PaintSegment = ({int leadingIndex, int trailingIndex}); |
103 | |
104 | /// A sliver that places multiple [TreeSliverNode]s in a linear array along the |
105 | /// main access, while staggering nodes that are animating into and out of view. |
106 | /// |
107 | /// The extent of each child node is determined by the [itemExtentBuilder]. |
108 | /// |
109 | /// See also: |
110 | /// |
111 | /// * [TreeSliver], the widget that creates and manages this render |
112 | /// object. |
113 | class RenderTreeSliver extends RenderSliverVariedExtentList { |
114 | /// Creates the render object that lays out the [TreeSliverNode]s of a |
115 | /// [TreeSliver]. |
116 | RenderTreeSliver({ |
117 | required super.childManager, |
118 | required super.itemExtentBuilder, |
119 | required Map<UniqueKey, TreeSliverNodesAnimation> activeAnimations, |
120 | required double indentation, |
121 | }) : _activeAnimations = activeAnimations, |
122 | _indentation = indentation; |
123 | |
124 | // TODO(Piinks): There are some opportunities to cache even further as far as |
125 | // extents and layout offsets when using itemExtentBuilder from the super |
126 | // class as we do here. I want to yak shave that in a separate change. |
127 | |
128 | /// The currently active [TreeSliverNode] animations. |
129 | /// |
130 | /// Since the index of animating nodes can change at any time, the unique key |
131 | /// is used to track an animation of nodes across frames. |
132 | Map<UniqueKey, TreeSliverNodesAnimation> get activeAnimations => _activeAnimations; |
133 | Map<UniqueKey, TreeSliverNodesAnimation> _activeAnimations; |
134 | set activeAnimations(Map<UniqueKey, TreeSliverNodesAnimation> value) { |
135 | if (_activeAnimations == value) { |
136 | return; |
137 | } |
138 | _activeAnimations = value; |
139 | markNeedsLayout(); |
140 | } |
141 | |
142 | /// The number of pixels by which child nodes will be offset in the cross axis |
143 | /// based on their [TreeSliverNodeParentData.depth]. |
144 | /// |
145 | /// If zero, can alternatively offset children in |
146 | /// [TreeSliver.treeNodeBuilder] for more options to customize the |
147 | /// indented space. |
148 | double get indentation => _indentation; |
149 | double _indentation; |
150 | set indentation(double value) { |
151 | if (_indentation == value) { |
152 | return; |
153 | } |
154 | assert(indentation >= 0.0); |
155 | _indentation = value; |
156 | markNeedsLayout(); |
157 | } |
158 | |
159 | // Maps the index of parents to the animation key of their children. |
160 | final Map<int, UniqueKey> _animationLeadingIndices = <int, UniqueKey>{}; |
161 | // Maps the key of child node animations to the fixed distance they are |
162 | // traversing during the animation. Determined at the start of the animation. |
163 | final Map<UniqueKey, double> _animationOffsets = <UniqueKey, double>{}; |
164 | void _updateAnimationCache() { |
165 | _animationLeadingIndices.clear(); |
166 | _activeAnimations.forEach((UniqueKey key, TreeSliverNodesAnimation animation) { |
167 | _animationLeadingIndices[animation.fromIndex - 1] = key; |
168 | }); |
169 | // Remove any stored offsets or clip layers that are no longer actively |
170 | // animating. |
171 | _animationOffsets.removeWhere((UniqueKey key, _) => !_activeAnimations.keys.contains(key)); |
172 | _clipHandles.removeWhere((UniqueKey key, LayerHandle<ClipRectLayer> handle) { |
173 | if (!_activeAnimations.keys.contains(key)) { |
174 | handle.layer = null; |
175 | return true; |
176 | } |
177 | return false; |
178 | }); |
179 | } |
180 | |
181 | @override |
182 | void setupParentData(RenderBox child) { |
183 | if (child.parentData is! TreeSliverNodeParentData) { |
184 | child.parentData = TreeSliverNodeParentData(); |
185 | } |
186 | } |
187 | |
188 | @override |
189 | void dispose() { |
190 | _clipHandles.removeWhere((UniqueKey key, LayerHandle<ClipRectLayer> handle) { |
191 | handle.layer = null; |
192 | return true; |
193 | }); |
194 | super.dispose(); |
195 | } |
196 | |
197 | // TODO(Piinks): This should be made a public getter on the super class. |
198 | // Multiple subclasses are making use of it now, yak shave that refactor |
199 | // separately. |
200 | late SliverLayoutDimensions _currentLayoutDimensions; |
201 | |
202 | @override |
203 | void performLayout() { |
204 | assert( |
205 | constraints.axisDirection == AxisDirection.down, |
206 | 'TreeSliver is only supported in Viewports with an AxisDirection.down. ' |
207 | 'The current axis direction is: ${constraints.axisDirection}.' , |
208 | ); |
209 | _updateAnimationCache(); |
210 | _currentLayoutDimensions = SliverLayoutDimensions( |
211 | scrollOffset: constraints.scrollOffset, |
212 | precedingScrollExtent: constraints.precedingScrollExtent, |
213 | viewportMainAxisExtent: constraints.viewportMainAxisExtent, |
214 | crossAxisExtent: constraints.crossAxisExtent, |
215 | ); |
216 | super.performLayout(); |
217 | } |
218 | |
219 | @override |
220 | int getMinChildIndexForScrollOffset(double scrollOffset, double itemExtent) { |
221 | // itemExtent is deprecated in the super class, we ignore it because we use |
222 | // the builder anyways. |
223 | return _getChildIndexForScrollOffset(scrollOffset); |
224 | } |
225 | |
226 | @override |
227 | int getMaxChildIndexForScrollOffset(double scrollOffset, double itemExtent) { |
228 | // itemExtent is deprecated in the super class, we ignore it because we use |
229 | // the builder anyways. |
230 | return _getChildIndexForScrollOffset(scrollOffset); |
231 | } |
232 | |
233 | int _getChildIndexForScrollOffset(double scrollOffset) { |
234 | if (scrollOffset == 0.0) { |
235 | return 0; |
236 | } |
237 | double position = 0.0; |
238 | int index = 0; |
239 | double totalAnimationOffset = 0.0; |
240 | double? itemExtent; |
241 | final int? childCount = childManager.estimatedChildCount; |
242 | while (position < scrollOffset) { |
243 | if (childCount != null && index > childCount - 1) { |
244 | break; |
245 | } |
246 | |
247 | itemExtent = itemExtentBuilder(index, _currentLayoutDimensions); |
248 | if (itemExtent == null) { |
249 | break; |
250 | } |
251 | if (_animationLeadingIndices.keys.contains(index)) { |
252 | final UniqueKey animationKey = _animationLeadingIndices[index]!; |
253 | if (_animationOffsets[animationKey] == null) { |
254 | // We have not computed the distance this block is traversing over the |
255 | // lifetime of the animation. |
256 | _computeAnimationOffsetFor(animationKey, position); |
257 | } |
258 | // We add the offset accounting for the animation value. |
259 | totalAnimationOffset += _animationOffsets[animationKey]! * (1 - _activeAnimations[animationKey]!.value); |
260 | } |
261 | position += itemExtent - totalAnimationOffset; |
262 | ++index; |
263 | } |
264 | return index - 1; |
265 | } |
266 | |
267 | void _computeAnimationOffsetFor(UniqueKey key, double position) { |
268 | assert(_activeAnimations[key] != null); |
269 | final double targetPosition = constraints.scrollOffset + constraints.remainingCacheExtent; |
270 | double currentPosition = position; |
271 | final int startingIndex = _activeAnimations[key]!.fromIndex; |
272 | final int lastIndex = _activeAnimations[key]!.toIndex; |
273 | int currentIndex = startingIndex; |
274 | double totalAnimatingOffset = 0.0; |
275 | // We animate only a portion of children that would be visible/in the cache |
276 | // extent, unless all children would fit on the screen. |
277 | while (currentIndex <= lastIndex && currentPosition < targetPosition) { |
278 | final double itemExtent = itemExtentBuilder(currentIndex, _currentLayoutDimensions)!; |
279 | totalAnimatingOffset += itemExtent; |
280 | currentPosition += itemExtent; |
281 | currentIndex++; |
282 | } |
283 | // For the life of this animation, which affects all children following |
284 | // startingIndex (regardless of if they are a child of the triggering |
285 | // parent), they will be offset by totalAnimatingOffset * the |
286 | // animation value. This is because even though more children can be |
287 | // scrolled into view, the same distance must be maintained for a smooth |
288 | // animation. |
289 | _animationOffsets[key] = totalAnimatingOffset; |
290 | } |
291 | |
292 | @override |
293 | double indexToLayoutOffset(double itemExtent, int index) { |
294 | // itemExtent is deprecated in the super class, we ignore it because we use |
295 | // the builder anyways. |
296 | double position = 0.0; |
297 | int currentIndex = 0; |
298 | double totalAnimationOffset = 0.0; |
299 | double? itemExtent; |
300 | final int? childCount = childManager.estimatedChildCount; |
301 | while (currentIndex < index) { |
302 | if (childCount != null && currentIndex > childCount - 1) { |
303 | break; |
304 | } |
305 | |
306 | itemExtent = itemExtentBuilder(currentIndex, _currentLayoutDimensions); |
307 | if (itemExtent == null) { |
308 | break; |
309 | } |
310 | if (_animationLeadingIndices.keys.contains(currentIndex)) { |
311 | final UniqueKey animationKey = _animationLeadingIndices[currentIndex]!; |
312 | assert(_animationOffsets[animationKey] != null); |
313 | // We add the offset accounting for the animation value. |
314 | totalAnimationOffset += _animationOffsets[animationKey]! * (1 - _activeAnimations[animationKey]!.value); |
315 | } |
316 | position += itemExtent; |
317 | currentIndex++; |
318 | } |
319 | return position - totalAnimationOffset; |
320 | } |
321 | |
322 | final Map<UniqueKey, LayerHandle<ClipRectLayer>> _clipHandles = <UniqueKey, LayerHandle<ClipRectLayer>>{}; |
323 | |
324 | @override |
325 | void paint(PaintingContext context, Offset offset) { |
326 | if (firstChild == null) { |
327 | return; |
328 | } |
329 | |
330 | RenderBox? nextChild = firstChild; |
331 | void paintUpTo( |
332 | int index, |
333 | RenderBox? startWith, |
334 | PaintingContext context, |
335 | Offset offset, |
336 | ) { |
337 | RenderBox? child = startWith; |
338 | while (child != null && indexOf(child) <= index) { |
339 | final double mainAxisDelta = childMainAxisPosition(child); |
340 | final TreeSliverNodeParentData parentData = child.parentData! as TreeSliverNodeParentData; |
341 | final Offset childOffset = Offset( |
342 | parentData.depth * indentation, |
343 | parentData.layoutOffset!, |
344 | ); |
345 | |
346 | // If the child's visible interval (mainAxisDelta, mainAxisDelta + paintExtentOf(child)) |
347 | // does not intersect the paint extent interval (0, constraints.remainingPaintExtent), it's hidden. |
348 | if (mainAxisDelta < constraints.remainingPaintExtent && mainAxisDelta + paintExtentOf(child) > 0) { |
349 | context.paintChild(child, childOffset); |
350 | } |
351 | child = childAfter(child); |
352 | } |
353 | nextChild = child; |
354 | } |
355 | if (_animationLeadingIndices.isEmpty) { |
356 | // There are no animations running. |
357 | paintUpTo(indexOf(lastChild!), firstChild, context, offset); |
358 | return; |
359 | } |
360 | |
361 | // We are animating. |
362 | // Separate animating segments to clip for any overlap. |
363 | int leadingIndex = indexOf(firstChild!); |
364 | final List<int> animationIndices = _animationLeadingIndices.keys.toList()..sort(); |
365 | final List<_PaintSegment> paintSegments = <_PaintSegment>[]; |
366 | while (animationIndices.isNotEmpty) { |
367 | final int trailingIndex = animationIndices.removeAt(0); |
368 | paintSegments.add((leadingIndex: leadingIndex, trailingIndex: trailingIndex)); |
369 | leadingIndex = trailingIndex + 1; |
370 | } |
371 | paintSegments.add((leadingIndex: leadingIndex, trailingIndex: indexOf(lastChild!))); |
372 | |
373 | // Paint, clipping for all but the first segment. |
374 | paintUpTo(paintSegments.removeAt(0).trailingIndex, nextChild, context, offset); |
375 | // Paint the rest with clip layers. |
376 | while (paintSegments.isNotEmpty) { |
377 | final _PaintSegment segment = paintSegments.removeAt(0); |
378 | |
379 | // Rect is calculated by the trailing edge of the parent (preceding |
380 | // leadingIndex), and the trailing edge of the trailing index. We cannot |
381 | // rely on the leading edge of the leading index, because it is currently |
382 | // moving. |
383 | final int parentIndex = math.max(segment.leadingIndex - 1, 0); |
384 | final double leadingOffset = indexToLayoutOffset(0.0, parentIndex) |
385 | + (parentIndex == 0 ? 0.0 : itemExtentBuilder(parentIndex, _currentLayoutDimensions)!); |
386 | final double trailingOffset = indexToLayoutOffset(0.0, segment.trailingIndex) |
387 | + itemExtentBuilder(segment.trailingIndex, _currentLayoutDimensions)!; |
388 | final Rect rect = Rect.fromPoints( |
389 | Offset(0.0, leadingOffset), |
390 | Offset(constraints.crossAxisExtent, trailingOffset), |
391 | ); |
392 | // We use the same animation key to keep track of the clip layer, unless |
393 | // this is the odd man out segment. |
394 | final UniqueKey key = _animationLeadingIndices[parentIndex]!; |
395 | _clipHandles[key] ??= LayerHandle<ClipRectLayer>(); |
396 | _clipHandles[key]!.layer = context.pushClipRect( |
397 | needsCompositing, |
398 | offset, |
399 | rect, |
400 | (PaintingContext context, Offset offset) { |
401 | paintUpTo(segment.trailingIndex, nextChild, context, offset); |
402 | }, |
403 | oldLayer: _clipHandles[key]!.layer, |
404 | ); |
405 | } |
406 | } |
407 | } |
408 | |