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 | /// |
7 | /// @docImport 'sliver_grid.dart'; |
8 | /// @docImport 'sliver_list.dart'; |
9 | library; |
10 | |
11 | import 'package:flutter/foundation.dart'; |
12 | import 'package:vector_math/vector_math_64.dart' ; |
13 | |
14 | import 'box.dart'; |
15 | import 'object.dart'; |
16 | import 'sliver.dart'; |
17 | import 'sliver_fixed_extent_list.dart'; |
18 | |
19 | /// A delegate used by [RenderSliverMultiBoxAdaptor] to manage its children. |
20 | /// |
21 | /// [RenderSliverMultiBoxAdaptor] objects reify their children lazily to avoid |
22 | /// spending resources on children that are not visible in the viewport. This |
23 | /// delegate lets these objects create and remove children as well as estimate |
24 | /// the total scroll offset extent occupied by the full child list. |
25 | abstract class RenderSliverBoxChildManager { |
26 | /// Called during layout when a new child is needed. The child should be |
27 | /// inserted into the child list in the appropriate position, after the |
28 | /// `after` child (at the start of the list if `after` is null). Its index and |
29 | /// scroll offsets will automatically be set appropriately. |
30 | /// |
31 | /// The `index` argument gives the index of the child to show. It is possible |
32 | /// for negative indices to be requested. For example: if the user scrolls |
33 | /// from child 0 to child 10, and then those children get much smaller, and |
34 | /// then the user scrolls back up again, this method will eventually be asked |
35 | /// to produce a child for index -1. |
36 | /// |
37 | /// If no child corresponds to `index`, then do nothing. |
38 | /// |
39 | /// Which child is indicated by index zero depends on the [GrowthDirection] |
40 | /// specified in the `constraints` of the [RenderSliverMultiBoxAdaptor]. For |
41 | /// example if the children are the alphabet, then if |
42 | /// [SliverConstraints.growthDirection] is [GrowthDirection.forward] then |
43 | /// index zero is A, and index 25 is Z. On the other hand if |
44 | /// [SliverConstraints.growthDirection] is [GrowthDirection.reverse] then |
45 | /// index zero is Z, and index 25 is A. |
46 | /// |
47 | /// During a call to [createChild] it is valid to remove other children from |
48 | /// the [RenderSliverMultiBoxAdaptor] object if they were not created during |
49 | /// this frame and have not yet been updated during this frame. It is not |
50 | /// valid to add any other children to this render object. |
51 | void createChild(int index, { required RenderBox? after }); |
52 | |
53 | /// Remove the given child from the child list. |
54 | /// |
55 | /// Called by [RenderSliverMultiBoxAdaptor.collectGarbage], which itself is |
56 | /// called from [RenderSliverMultiBoxAdaptor]'s `performLayout`. |
57 | /// |
58 | /// The index of the given child can be obtained using the |
59 | /// [RenderSliverMultiBoxAdaptor.indexOf] method, which reads it from the |
60 | /// [SliverMultiBoxAdaptorParentData.index] field of the child's |
61 | /// [RenderObject.parentData]. |
62 | void removeChild(RenderBox child); |
63 | |
64 | /// Called to estimate the total scrollable extents of this object. |
65 | /// |
66 | /// Must return the total distance from the start of the child with the |
67 | /// earliest possible index to the end of the child with the last possible |
68 | /// index. |
69 | double estimateMaxScrollOffset( |
70 | SliverConstraints constraints, { |
71 | int? firstIndex, |
72 | int? lastIndex, |
73 | double? leadingScrollOffset, |
74 | double? trailingScrollOffset, |
75 | }); |
76 | |
77 | /// Called to obtain a precise measure of the total number of children. |
78 | /// |
79 | /// Must return the number that is one greater than the greatest `index` for |
80 | /// which `createChild` will actually create a child. |
81 | /// |
82 | /// This is used when [createChild] cannot add a child for a positive `index`, |
83 | /// to determine the precise dimensions of the sliver. It must return an |
84 | /// accurate and precise non-null value. It will not be called if |
85 | /// [createChild] is always able to create a child (e.g. for an infinite |
86 | /// list). |
87 | int get childCount; |
88 | |
89 | /// The best available estimate of [childCount], or null if no estimate is available. |
90 | /// |
91 | /// This differs from [childCount] in that [childCount] never returns null (and must |
92 | /// not be accessed if the child count is not yet available, meaning the [createChild] |
93 | /// method has not been provided an index that does not create a child). |
94 | /// |
95 | /// See also: |
96 | /// |
97 | /// * [SliverChildDelegate.estimatedChildCount], to which this getter defers. |
98 | int? get estimatedChildCount => null; |
99 | |
100 | /// Called during [RenderSliverMultiBoxAdaptor.adoptChild] or |
101 | /// [RenderSliverMultiBoxAdaptor.move]. |
102 | /// |
103 | /// Subclasses must ensure that the [SliverMultiBoxAdaptorParentData.index] |
104 | /// field of the child's [RenderObject.parentData] accurately reflects the |
105 | /// child's index in the child list after this function returns. |
106 | void didAdoptChild(RenderBox child); |
107 | |
108 | /// Called during layout to indicate whether this object provided insufficient |
109 | /// children for the [RenderSliverMultiBoxAdaptor] to fill the |
110 | /// [SliverConstraints.remainingPaintExtent]. |
111 | /// |
112 | /// Typically called unconditionally at the start of layout with false and |
113 | /// then later called with true when the [RenderSliverMultiBoxAdaptor] |
114 | /// fails to create a child required to fill the |
115 | /// [SliverConstraints.remainingPaintExtent]. |
116 | /// |
117 | /// Useful for subclasses to determine whether newly added children could |
118 | /// affect the visible contents of the [RenderSliverMultiBoxAdaptor]. |
119 | void setDidUnderflow(bool value); |
120 | |
121 | /// Called at the beginning of layout to indicate that layout is about to |
122 | /// occur. |
123 | void didStartLayout() { } |
124 | |
125 | /// Called at the end of layout to indicate that layout is now complete. |
126 | void didFinishLayout() { } |
127 | |
128 | /// In debug mode, asserts that this manager is not expecting any |
129 | /// modifications to the [RenderSliverMultiBoxAdaptor]'s child list. |
130 | /// |
131 | /// This function always returns true. |
132 | /// |
133 | /// The manager is not required to track whether it is expecting modifications |
134 | /// to the [RenderSliverMultiBoxAdaptor]'s child list and can return |
135 | /// true without making any assertions. |
136 | bool debugAssertChildListLocked() => true; |
137 | } |
138 | |
139 | /// Parent data structure used by [RenderSliverWithKeepAliveMixin]. |
140 | mixin KeepAliveParentDataMixin implements ParentData { |
141 | /// Whether to keep the child alive even when it is no longer visible. |
142 | bool keepAlive = false; |
143 | |
144 | /// Whether the widget is currently being kept alive, i.e. has [keepAlive] set |
145 | /// to true and is offscreen. |
146 | bool get keptAlive; |
147 | } |
148 | |
149 | /// This class exists to dissociate [KeepAlive] from [RenderSliverMultiBoxAdaptor]. |
150 | /// |
151 | /// [RenderSliverWithKeepAliveMixin.setupParentData] must be implemented to use |
152 | /// a parentData class that uses the right mixin or whatever is appropriate. |
153 | mixin RenderSliverWithKeepAliveMixin implements RenderSliver { |
154 | /// Alerts the developer that the child's parentData needs to be of type |
155 | /// [KeepAliveParentDataMixin]. |
156 | @override |
157 | void setupParentData(RenderObject child) { |
158 | assert(child.parentData is KeepAliveParentDataMixin); |
159 | } |
160 | } |
161 | |
162 | /// Parent data structure used by [RenderSliverMultiBoxAdaptor]. |
163 | class SliverMultiBoxAdaptorParentData extends SliverLogicalParentData with ContainerParentDataMixin<RenderBox>, KeepAliveParentDataMixin { |
164 | /// The index of this child according to the [RenderSliverBoxChildManager]. |
165 | int? index; |
166 | |
167 | @override |
168 | bool get keptAlive => _keptAlive; |
169 | bool _keptAlive = false; |
170 | |
171 | @override |
172 | String toString() => 'index= $index; ${keepAlive ? "keepAlive; " : "" }${super.toString()}' ; |
173 | } |
174 | |
175 | /// A sliver with multiple box children. |
176 | /// |
177 | /// [RenderSliverMultiBoxAdaptor] is a base class for slivers that have multiple |
178 | /// box children. The children are managed by a [RenderSliverBoxChildManager], |
179 | /// which lets subclasses create children lazily during layout. Typically |
180 | /// subclasses will create only those children that are actually needed to fill |
181 | /// the [SliverConstraints.remainingPaintExtent]. |
182 | /// |
183 | /// The contract for adding and removing children from this render object is |
184 | /// more strict than for normal render objects: |
185 | /// |
186 | /// * Children can be removed except during a layout pass if they have already |
187 | /// been laid out during that layout pass. |
188 | /// * Children cannot be added except during a call to [childManager], and |
189 | /// then only if there is no child corresponding to that index (or the child |
190 | /// corresponding to that index was first removed). |
191 | /// |
192 | /// See also: |
193 | /// |
194 | /// * [RenderSliverToBoxAdapter], which has a single box child. |
195 | /// * [RenderSliverList], which places its children in a linear |
196 | /// array. |
197 | /// * [RenderSliverFixedExtentList], which places its children in a linear |
198 | /// array with a fixed extent in the main axis. |
199 | /// * [RenderSliverGrid], which places its children in arbitrary positions. |
200 | abstract class RenderSliverMultiBoxAdaptor extends RenderSliver |
201 | with ContainerRenderObjectMixin<RenderBox, SliverMultiBoxAdaptorParentData>, |
202 | RenderSliverHelpers, RenderSliverWithKeepAliveMixin { |
203 | |
204 | /// Creates a sliver with multiple box children. |
205 | RenderSliverMultiBoxAdaptor({ |
206 | required RenderSliverBoxChildManager childManager, |
207 | }) : _childManager = childManager { |
208 | assert(() { |
209 | _debugDanglingKeepAlives = <RenderBox>[]; |
210 | return true; |
211 | }()); |
212 | } |
213 | |
214 | @override |
215 | void setupParentData(RenderObject child) { |
216 | if (child.parentData is! SliverMultiBoxAdaptorParentData) { |
217 | child.parentData = SliverMultiBoxAdaptorParentData(); |
218 | } |
219 | } |
220 | |
221 | /// The delegate that manages the children of this object. |
222 | /// |
223 | /// Rather than having a concrete list of children, a |
224 | /// [RenderSliverMultiBoxAdaptor] uses a [RenderSliverBoxChildManager] to |
225 | /// create children during layout in order to fill the |
226 | /// [SliverConstraints.remainingPaintExtent]. |
227 | @protected |
228 | RenderSliverBoxChildManager get childManager => _childManager; |
229 | final RenderSliverBoxChildManager _childManager; |
230 | |
231 | /// The nodes being kept alive despite not being visible. |
232 | final Map<int, RenderBox> _keepAliveBucket = <int, RenderBox>{}; |
233 | |
234 | late List<RenderBox> _debugDanglingKeepAlives; |
235 | |
236 | /// Indicates whether integrity check is enabled. |
237 | /// |
238 | /// Setting this property to true will immediately perform an integrity check. |
239 | /// |
240 | /// The integrity check consists of: |
241 | /// |
242 | /// 1. Verify that the children index in childList is in ascending order. |
243 | /// 2. Verify that there is no dangling keepalive child as the result of [move]. |
244 | bool get debugChildIntegrityEnabled => _debugChildIntegrityEnabled; |
245 | bool _debugChildIntegrityEnabled = true; |
246 | set debugChildIntegrityEnabled(bool enabled) { |
247 | assert(() { |
248 | _debugChildIntegrityEnabled = enabled; |
249 | return _debugVerifyChildOrder() && |
250 | (!_debugChildIntegrityEnabled || _debugDanglingKeepAlives.isEmpty); |
251 | }()); |
252 | } |
253 | |
254 | @override |
255 | void adoptChild(RenderObject child) { |
256 | super.adoptChild(child); |
257 | final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData; |
258 | if (!childParentData._keptAlive) { |
259 | childManager.didAdoptChild(child as RenderBox); |
260 | } |
261 | } |
262 | |
263 | bool _debugAssertChildListLocked() => childManager.debugAssertChildListLocked(); |
264 | |
265 | /// Verify that the child list index is in strictly increasing order. |
266 | /// |
267 | /// This has no effect in release builds. |
268 | bool _debugVerifyChildOrder() { |
269 | if (_debugChildIntegrityEnabled) { |
270 | RenderBox? child = firstChild; |
271 | int index; |
272 | while (child != null) { |
273 | index = indexOf(child); |
274 | child = childAfter(child); |
275 | assert(child == null || indexOf(child) > index); |
276 | } |
277 | } |
278 | return true; |
279 | } |
280 | |
281 | @override |
282 | void insert(RenderBox child, { RenderBox? after }) { |
283 | assert(!_keepAliveBucket.containsValue(child)); |
284 | super.insert(child, after: after); |
285 | assert(firstChild != null); |
286 | assert(_debugVerifyChildOrder()); |
287 | } |
288 | |
289 | @override |
290 | void move(RenderBox child, { RenderBox? after }) { |
291 | // There are two scenarios: |
292 | // |
293 | // 1. The child is not keptAlive. |
294 | // The child is in the childList maintained by ContainerRenderObjectMixin. |
295 | // We can call super.move and update parentData with the new slot. |
296 | // |
297 | // 2. The child is keptAlive. |
298 | // In this case, the child is no longer in the childList but might be stored in |
299 | // [_keepAliveBucket]. We need to update the location of the child in the bucket. |
300 | final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData; |
301 | if (!childParentData.keptAlive) { |
302 | super.move(child, after: after); |
303 | childManager.didAdoptChild(child); // updates the slot in the parentData |
304 | // Its slot may change even if super.move does not change the position. |
305 | // In this case, we still want to mark as needs layout. |
306 | markNeedsLayout(); |
307 | } else { |
308 | // If the child in the bucket is not current child, that means someone has |
309 | // already moved and replaced current child, and we cannot remove this child. |
310 | if (_keepAliveBucket[childParentData.index] == child) { |
311 | _keepAliveBucket.remove(childParentData.index); |
312 | } |
313 | assert(() { |
314 | _debugDanglingKeepAlives.remove(child); |
315 | return true; |
316 | }()); |
317 | // Update the slot and reinsert back to _keepAliveBucket in the new slot. |
318 | childManager.didAdoptChild(child); |
319 | // If there is an existing child in the new slot, that mean that child will |
320 | // be moved to other index. In other cases, the existing child should have been |
321 | // removed by updateChild. Thus, it is ok to overwrite it. |
322 | assert(() { |
323 | if (_keepAliveBucket.containsKey(childParentData.index)) { |
324 | _debugDanglingKeepAlives.add(_keepAliveBucket[childParentData.index]!); |
325 | } |
326 | return true; |
327 | }()); |
328 | _keepAliveBucket[childParentData.index!] = child; |
329 | } |
330 | } |
331 | |
332 | @override |
333 | void remove(RenderBox child) { |
334 | final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData; |
335 | if (!childParentData._keptAlive) { |
336 | super.remove(child); |
337 | return; |
338 | } |
339 | assert(_keepAliveBucket[childParentData.index] == child); |
340 | assert(() { |
341 | _debugDanglingKeepAlives.remove(child); |
342 | return true; |
343 | }()); |
344 | _keepAliveBucket.remove(childParentData.index); |
345 | dropChild(child); |
346 | } |
347 | |
348 | @override |
349 | void removeAll() { |
350 | super.removeAll(); |
351 | _keepAliveBucket.values.forEach(dropChild); |
352 | _keepAliveBucket.clear(); |
353 | } |
354 | |
355 | void _createOrObtainChild(int index, { required RenderBox? after }) { |
356 | invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) { |
357 | assert(constraints == this.constraints); |
358 | if (_keepAliveBucket.containsKey(index)) { |
359 | final RenderBox child = _keepAliveBucket.remove(index)!; |
360 | final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData; |
361 | assert(childParentData._keptAlive); |
362 | dropChild(child); |
363 | child.parentData = childParentData; |
364 | insert(child, after: after); |
365 | childParentData._keptAlive = false; |
366 | } else { |
367 | _childManager.createChild(index, after: after); |
368 | } |
369 | }); |
370 | } |
371 | |
372 | void _destroyOrCacheChild(RenderBox child) { |
373 | final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData; |
374 | if (childParentData.keepAlive) { |
375 | assert(!childParentData._keptAlive); |
376 | remove(child); |
377 | _keepAliveBucket[childParentData.index!] = child; |
378 | child.parentData = childParentData; |
379 | super.adoptChild(child); |
380 | childParentData._keptAlive = true; |
381 | } else { |
382 | assert(child.parent == this); |
383 | _childManager.removeChild(child); |
384 | assert(child.parent == null); |
385 | } |
386 | } |
387 | |
388 | @override |
389 | void attach(PipelineOwner owner) { |
390 | super.attach(owner); |
391 | for (final RenderBox child in _keepAliveBucket.values) { |
392 | child.attach(owner); |
393 | } |
394 | } |
395 | |
396 | @override |
397 | void detach() { |
398 | super.detach(); |
399 | for (final RenderBox child in _keepAliveBucket.values) { |
400 | child.detach(); |
401 | } |
402 | } |
403 | |
404 | @override |
405 | void redepthChildren() { |
406 | super.redepthChildren(); |
407 | _keepAliveBucket.values.forEach(redepthChild); |
408 | } |
409 | |
410 | @override |
411 | void visitChildren(RenderObjectVisitor visitor) { |
412 | super.visitChildren(visitor); |
413 | _keepAliveBucket.values.forEach(visitor); |
414 | } |
415 | |
416 | @override |
417 | void visitChildrenForSemantics(RenderObjectVisitor visitor) { |
418 | super.visitChildren(visitor); |
419 | // Do not visit children in [_keepAliveBucket]. |
420 | } |
421 | |
422 | /// Called during layout to create and add the child with the given index and |
423 | /// scroll offset. |
424 | /// |
425 | /// Calls [RenderSliverBoxChildManager.createChild] to actually create and add |
426 | /// the child if necessary. The child may instead be obtained from a cache; |
427 | /// see [SliverMultiBoxAdaptorParentData.keepAlive]. |
428 | /// |
429 | /// Returns false if there was no cached child and `createChild` did not add |
430 | /// any child, otherwise returns true. |
431 | /// |
432 | /// Does not layout the new child. |
433 | /// |
434 | /// When this is called, there are no visible children, so no children can be |
435 | /// removed during the call to `createChild`. No child should be added during |
436 | /// that call either, except for the one that is created and returned by |
437 | /// `createChild`. |
438 | @protected |
439 | bool addInitialChild({ int index = 0, double layoutOffset = 0.0 }) { |
440 | assert(_debugAssertChildListLocked()); |
441 | assert(firstChild == null); |
442 | _createOrObtainChild(index, after: null); |
443 | if (firstChild != null) { |
444 | assert(firstChild == lastChild); |
445 | assert(indexOf(firstChild!) == index); |
446 | final SliverMultiBoxAdaptorParentData firstChildParentData = firstChild!.parentData! as SliverMultiBoxAdaptorParentData; |
447 | firstChildParentData.layoutOffset = layoutOffset; |
448 | return true; |
449 | } |
450 | childManager.setDidUnderflow(true); |
451 | return false; |
452 | } |
453 | |
454 | /// Called during layout to create, add, and layout the child before |
455 | /// [firstChild]. |
456 | /// |
457 | /// Calls [RenderSliverBoxChildManager.createChild] to actually create and add |
458 | /// the child if necessary. The child may instead be obtained from a cache; |
459 | /// see [SliverMultiBoxAdaptorParentData.keepAlive]. |
460 | /// |
461 | /// Returns the new child or null if no child was obtained. |
462 | /// |
463 | /// The child that was previously the first child, as well as any subsequent |
464 | /// children, may be removed by this call if they have not yet been laid out |
465 | /// during this layout pass. No child should be added during that call except |
466 | /// for the one that is created and returned by `createChild`. |
467 | @protected |
468 | RenderBox? insertAndLayoutLeadingChild( |
469 | BoxConstraints childConstraints, { |
470 | bool parentUsesSize = false, |
471 | }) { |
472 | assert(_debugAssertChildListLocked()); |
473 | final int index = indexOf(firstChild!) - 1; |
474 | _createOrObtainChild(index, after: null); |
475 | if (indexOf(firstChild!) == index) { |
476 | firstChild!.layout(childConstraints, parentUsesSize: parentUsesSize); |
477 | return firstChild; |
478 | } |
479 | childManager.setDidUnderflow(true); |
480 | return null; |
481 | } |
482 | |
483 | /// Called during layout to create, add, and layout the child after |
484 | /// the given child. |
485 | /// |
486 | /// Calls [RenderSliverBoxChildManager.createChild] to actually create and add |
487 | /// the child if necessary. The child may instead be obtained from a cache; |
488 | /// see [SliverMultiBoxAdaptorParentData.keepAlive]. |
489 | /// |
490 | /// Returns the new child. It is the responsibility of the caller to configure |
491 | /// the child's scroll offset. |
492 | /// |
493 | /// Children after the `after` child may be removed in the process. Only the |
494 | /// new child may be added. |
495 | @protected |
496 | RenderBox? insertAndLayoutChild( |
497 | BoxConstraints childConstraints, { |
498 | required RenderBox? after, |
499 | bool parentUsesSize = false, |
500 | }) { |
501 | assert(_debugAssertChildListLocked()); |
502 | assert(after != null); |
503 | final int index = indexOf(after!) + 1; |
504 | _createOrObtainChild(index, after: after); |
505 | final RenderBox? child = childAfter(after); |
506 | if (child != null && indexOf(child) == index) { |
507 | child.layout(childConstraints, parentUsesSize: parentUsesSize); |
508 | return child; |
509 | } |
510 | childManager.setDidUnderflow(true); |
511 | return null; |
512 | } |
513 | |
514 | /// Returns the number of children preceding the `firstIndex` that need to be |
515 | /// garbage collected. |
516 | /// |
517 | /// See also: |
518 | /// |
519 | /// * [collectGarbage], which takes the leading and trailing number of |
520 | /// children to be garbage collected. |
521 | /// * [calculateTrailingGarbage], which similarly returns the number of |
522 | /// trailing children to be garbage collected. |
523 | @visibleForTesting |
524 | @protected |
525 | int calculateLeadingGarbage({required int firstIndex}) { |
526 | RenderBox? walker = firstChild; |
527 | int leadingGarbage = 0; |
528 | while (walker != null && indexOf(walker) < firstIndex) { |
529 | leadingGarbage += 1; |
530 | walker = childAfter(walker); |
531 | } |
532 | return leadingGarbage; |
533 | } |
534 | |
535 | /// Returns the number of children following the `lastIndex` that need to be |
536 | /// garbage collected. |
537 | /// |
538 | /// See also: |
539 | /// |
540 | /// * [collectGarbage], which takes the leading and trailing number of |
541 | /// children to be garbage collected. |
542 | /// * [calculateLeadingGarbage], which similarly returns the number of |
543 | /// leading children to be garbage collected. |
544 | @visibleForTesting |
545 | @protected |
546 | int calculateTrailingGarbage({required int lastIndex}) { |
547 | RenderBox? walker = lastChild; |
548 | int trailingGarbage = 0; |
549 | while (walker != null && indexOf(walker) > lastIndex) { |
550 | trailingGarbage += 1; |
551 | walker = childBefore(walker); |
552 | } |
553 | return trailingGarbage; |
554 | } |
555 | |
556 | /// Called after layout with the number of children that can be garbage |
557 | /// collected at the head and tail of the child list. |
558 | /// |
559 | /// Children whose [SliverMultiBoxAdaptorParentData.keepAlive] property is |
560 | /// set to true will be removed to a cache instead of being dropped. |
561 | /// |
562 | /// This method also collects any children that were previously kept alive but |
563 | /// are now no longer necessary. As such, it should be called every time |
564 | /// [performLayout] is run, even if the arguments are both zero. |
565 | /// |
566 | /// See also: |
567 | /// |
568 | /// * [calculateLeadingGarbage], which can be used to determine |
569 | /// `leadingGarbage` here. |
570 | /// * [calculateTrailingGarbage], which can be used to determine |
571 | /// `trailingGarbage` here. |
572 | @protected |
573 | void collectGarbage(int leadingGarbage, int trailingGarbage) { |
574 | assert(_debugAssertChildListLocked()); |
575 | assert(childCount >= leadingGarbage + trailingGarbage); |
576 | invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) { |
577 | while (leadingGarbage > 0) { |
578 | _destroyOrCacheChild(firstChild!); |
579 | leadingGarbage -= 1; |
580 | } |
581 | while (trailingGarbage > 0) { |
582 | _destroyOrCacheChild(lastChild!); |
583 | trailingGarbage -= 1; |
584 | } |
585 | // Ask the child manager to remove the children that are no longer being |
586 | // kept alive. (This should cause _keepAliveBucket to change, so we have |
587 | // to prepare our list ahead of time.) |
588 | _keepAliveBucket.values.where((RenderBox child) { |
589 | final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData; |
590 | return !childParentData.keepAlive; |
591 | }).toList().forEach(_childManager.removeChild); |
592 | assert(_keepAliveBucket.values.where((RenderBox child) { |
593 | final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData; |
594 | return !childParentData.keepAlive; |
595 | }).isEmpty); |
596 | }); |
597 | } |
598 | |
599 | /// Returns the index of the given child, as given by the |
600 | /// [SliverMultiBoxAdaptorParentData.index] field of the child's [parentData]. |
601 | int indexOf(RenderBox child) { |
602 | final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData; |
603 | assert(childParentData.index != null); |
604 | return childParentData.index!; |
605 | } |
606 | |
607 | /// Returns the dimension of the given child in the main axis, as given by the |
608 | /// child's [RenderBox.size] property. This is only valid after layout. |
609 | @protected |
610 | double paintExtentOf(RenderBox child) { |
611 | assert(child.hasSize); |
612 | return switch (constraints.axis) { |
613 | Axis.horizontal => child.size.width, |
614 | Axis.vertical => child.size.height, |
615 | }; |
616 | } |
617 | |
618 | @override |
619 | bool hitTestChildren(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) { |
620 | RenderBox? child = lastChild; |
621 | final BoxHitTestResult boxResult = BoxHitTestResult.wrap(result); |
622 | while (child != null) { |
623 | if (hitTestBoxChild(boxResult, child, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition)) { |
624 | return true; |
625 | } |
626 | child = childBefore(child); |
627 | } |
628 | return false; |
629 | } |
630 | |
631 | @override |
632 | double childMainAxisPosition(RenderBox child) { |
633 | return childScrollOffset(child)! - constraints.scrollOffset; |
634 | } |
635 | |
636 | @override |
637 | double? childScrollOffset(RenderObject child) { |
638 | assert(child.parent == this); |
639 | final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData; |
640 | return childParentData.layoutOffset; |
641 | } |
642 | |
643 | @override |
644 | bool paintsChild(RenderBox child) { |
645 | final SliverMultiBoxAdaptorParentData? childParentData = child.parentData as SliverMultiBoxAdaptorParentData?; |
646 | return childParentData?.index != null && |
647 | !_keepAliveBucket.containsKey(childParentData!.index); |
648 | } |
649 | |
650 | @override |
651 | void applyPaintTransform(RenderBox child, Matrix4 transform) { |
652 | if (!paintsChild(child)) { |
653 | // This can happen if some child asks for the global transform even though |
654 | // they are not getting painted. In that case, the transform sets set to |
655 | // zero since [applyPaintTransformForBoxChild] would end up throwing due |
656 | // to the child not being configured correctly for applying a transform. |
657 | // There's no assert here because asking for the paint transform is a |
658 | // valid thing to do even if a child would not be painted, but there is no |
659 | // meaningful non-zero matrix to use in this case. |
660 | transform.setZero(); |
661 | } else { |
662 | applyPaintTransformForBoxChild(child, transform); |
663 | } |
664 | } |
665 | |
666 | @override |
667 | void paint(PaintingContext context, Offset offset) { |
668 | if (firstChild == null) { |
669 | return; |
670 | } |
671 | // offset is to the top-left corner, regardless of our axis direction. |
672 | // originOffset gives us the delta from the real origin to the origin in the axis direction. |
673 | final Offset mainAxisUnit, crossAxisUnit, originOffset; |
674 | final bool addExtent; |
675 | switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) { |
676 | case AxisDirection.up: |
677 | mainAxisUnit = const Offset(0.0, -1.0); |
678 | crossAxisUnit = const Offset(1.0, 0.0); |
679 | originOffset = offset + Offset(0.0, geometry!.paintExtent); |
680 | addExtent = true; |
681 | case AxisDirection.right: |
682 | mainAxisUnit = const Offset(1.0, 0.0); |
683 | crossAxisUnit = const Offset(0.0, 1.0); |
684 | originOffset = offset; |
685 | addExtent = false; |
686 | case AxisDirection.down: |
687 | mainAxisUnit = const Offset(0.0, 1.0); |
688 | crossAxisUnit = const Offset(1.0, 0.0); |
689 | originOffset = offset; |
690 | addExtent = false; |
691 | case AxisDirection.left: |
692 | mainAxisUnit = const Offset(-1.0, 0.0); |
693 | crossAxisUnit = const Offset(0.0, 1.0); |
694 | originOffset = offset + Offset(geometry!.paintExtent, 0.0); |
695 | addExtent = true; |
696 | } |
697 | RenderBox? child = firstChild; |
698 | while (child != null) { |
699 | final double mainAxisDelta = childMainAxisPosition(child); |
700 | final double crossAxisDelta = childCrossAxisPosition(child); |
701 | Offset childOffset = Offset( |
702 | originOffset.dx + mainAxisUnit.dx * mainAxisDelta + crossAxisUnit.dx * crossAxisDelta, |
703 | originOffset.dy + mainAxisUnit.dy * mainAxisDelta + crossAxisUnit.dy * crossAxisDelta, |
704 | ); |
705 | if (addExtent) { |
706 | childOffset += mainAxisUnit * paintExtentOf(child); |
707 | } |
708 | |
709 | // If the child's visible interval (mainAxisDelta, mainAxisDelta + paintExtentOf(child)) |
710 | // does not intersect the paint extent interval (0, constraints.remainingPaintExtent), it's hidden. |
711 | if (mainAxisDelta < constraints.remainingPaintExtent && mainAxisDelta + paintExtentOf(child) > 0) { |
712 | context.paintChild(child, childOffset); |
713 | } |
714 | |
715 | child = childAfter(child); |
716 | } |
717 | } |
718 | |
719 | @override |
720 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
721 | super.debugFillProperties(properties); |
722 | properties.add(DiagnosticsNode.message(firstChild != null ? 'currently live children: ${indexOf(firstChild!)} to ${indexOf(lastChild!)}' : 'no children current live' )); |
723 | } |
724 | |
725 | /// Asserts that the reified child list is not empty and has a contiguous |
726 | /// sequence of indices. |
727 | /// |
728 | /// Always returns true. |
729 | bool debugAssertChildListIsNonEmptyAndContiguous() { |
730 | assert(() { |
731 | assert(firstChild != null); |
732 | int index = indexOf(firstChild!); |
733 | RenderBox? child = childAfter(firstChild!); |
734 | while (child != null) { |
735 | index += 1; |
736 | assert(indexOf(child) == index); |
737 | child = childAfter(child); |
738 | } |
739 | return true; |
740 | }()); |
741 | return true; |
742 | } |
743 | |
744 | @override |
745 | List<DiagnosticsNode> debugDescribeChildren() { |
746 | final List<DiagnosticsNode> children = <DiagnosticsNode>[]; |
747 | if (firstChild != null) { |
748 | RenderBox? child = firstChild; |
749 | while (true) { |
750 | final SliverMultiBoxAdaptorParentData childParentData = child!.parentData! as SliverMultiBoxAdaptorParentData; |
751 | children.add(child.toDiagnosticsNode(name: 'child with index ${childParentData.index}' )); |
752 | if (child == lastChild) { |
753 | break; |
754 | } |
755 | child = childParentData.nextSibling; |
756 | } |
757 | } |
758 | if (_keepAliveBucket.isNotEmpty) { |
759 | final List<int> indices = _keepAliveBucket.keys.toList()..sort(); |
760 | for (final int index in indices) { |
761 | children.add(_keepAliveBucket[index]!.toDiagnosticsNode( |
762 | name: 'child with index $index (kept alive but not laid out)' , |
763 | style: DiagnosticsTreeStyle.offstage, |
764 | )); |
765 | } |
766 | } |
767 | return children; |
768 | } |
769 | } |
770 | |