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';
6library;
7
8import 'dart:math' as math;
9
10import 'package:flutter/foundation.dart';
11
12import 'box.dart';
13import 'layer.dart';
14import 'object.dart';
15import 'sliver.dart';
16import 'sliver_fixed_extent_list.dart';
17import '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.
30typedef TreeSliverNodesAnimation = ({
31 int fromIndex,
32 int toIndex,
33 double value,
34});
35
36/// Used to pass information down to [RenderTreeSliver].
37class 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}
69class 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.
102typedef _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.
113class 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