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 |
164 | with ContainerParentDataMixin<RenderBox>, KeepAliveParentDataMixin { |
165 | /// The index of this child according to the [RenderSliverBoxChildManager]. |
166 | int? index; |
167 | |
168 | @override |
169 | bool get keptAlive => _keptAlive; |
170 | bool _keptAlive = false; |
171 | |
172 | @override |
173 | String toString() => 'index=$index ;${keepAlive ? "keepAlive; ": ""} ${super.toString()}'; |
174 | } |
175 | |
176 | /// A sliver with multiple box children. |
177 | /// |
178 | /// [RenderSliverMultiBoxAdaptor] is a base class for slivers that have multiple |
179 | /// box children. The children are managed by a [RenderSliverBoxChildManager], |
180 | /// which lets subclasses create children lazily during layout. Typically |
181 | /// subclasses will create only those children that are actually needed to fill |
182 | /// the [SliverConstraints.remainingPaintExtent]. |
183 | /// |
184 | /// The contract for adding and removing children from this render object is |
185 | /// more strict than for normal render objects: |
186 | /// |
187 | /// * Children can be removed except during a layout pass if they have already |
188 | /// been laid out during that layout pass. |
189 | /// * Children cannot be added except during a call to [childManager], and |
190 | /// then only if there is no child corresponding to that index (or the child |
191 | /// corresponding to that index was first removed). |
192 | /// |
193 | /// See also: |
194 | /// |
195 | /// * [RenderSliverToBoxAdapter], which has a single box child. |
196 | /// * [RenderSliverList], which places its children in a linear |
197 | /// array. |
198 | /// * [RenderSliverFixedExtentList], which places its children in a linear |
199 | /// array with a fixed extent in the main axis. |
200 | /// * [RenderSliverGrid], which places its children in arbitrary positions. |
201 | abstract class RenderSliverMultiBoxAdaptor extends RenderSliver |
202 | with |
203 | ContainerRenderObjectMixin<RenderBox, SliverMultiBoxAdaptorParentData>, |
204 | RenderSliverHelpers, |
205 | RenderSliverWithKeepAliveMixin { |
206 | /// Creates a sliver with multiple box children. |
207 | RenderSliverMultiBoxAdaptor({required RenderSliverBoxChildManager childManager}) |
208 | : _childManager = childManager { |
209 | assert(() { |
210 | _debugDanglingKeepAlives = <RenderBox>[]; |
211 | return true; |
212 | }()); |
213 | } |
214 | |
215 | @override |
216 | void setupParentData(RenderObject child) { |
217 | if (child.parentData is! SliverMultiBoxAdaptorParentData) { |
218 | child.parentData = SliverMultiBoxAdaptorParentData(); |
219 | } |
220 | } |
221 | |
222 | /// The delegate that manages the children of this object. |
223 | /// |
224 | /// Rather than having a concrete list of children, a |
225 | /// [RenderSliverMultiBoxAdaptor] uses a [RenderSliverBoxChildManager] to |
226 | /// create children during layout in order to fill the |
227 | /// [SliverConstraints.remainingPaintExtent]. |
228 | @protected |
229 | RenderSliverBoxChildManager get childManager => _childManager; |
230 | final RenderSliverBoxChildManager _childManager; |
231 | |
232 | /// The nodes being kept alive despite not being visible. |
233 | final Map<int, RenderBox> _keepAliveBucket = <int, RenderBox>{}; |
234 | |
235 | late List<RenderBox> _debugDanglingKeepAlives; |
236 | |
237 | /// Indicates whether integrity check is enabled. |
238 | /// |
239 | /// Setting this property to true will immediately perform an integrity check. |
240 | /// |
241 | /// The integrity check consists of: |
242 | /// |
243 | /// 1. Verify that the children index in childList is in ascending order. |
244 | /// 2. Verify that there is no dangling keepalive child as the result of [move]. |
245 | bool get debugChildIntegrityEnabled => _debugChildIntegrityEnabled; |
246 | bool _debugChildIntegrityEnabled = true; |
247 | set debugChildIntegrityEnabled(bool enabled) { |
248 | assert(() { |
249 | _debugChildIntegrityEnabled = enabled; |
250 | return _debugVerifyChildOrder() && |
251 | (!_debugChildIntegrityEnabled || _debugDanglingKeepAlives.isEmpty); |
252 | }()); |
253 | } |
254 | |
255 | @override |
256 | void adoptChild(RenderObject child) { |
257 | super.adoptChild(child); |
258 | final SliverMultiBoxAdaptorParentData childParentData = |
259 | child.parentData! as SliverMultiBoxAdaptorParentData; |
260 | if (!childParentData._keptAlive) { |
261 | childManager.didAdoptChild(child as RenderBox); |
262 | } |
263 | } |
264 | |
265 | bool _debugAssertChildListLocked() => childManager.debugAssertChildListLocked(); |
266 | |
267 | /// Verify that the child list index is in strictly increasing order. |
268 | /// |
269 | /// This has no effect in release builds. |
270 | bool _debugVerifyChildOrder() { |
271 | if (_debugChildIntegrityEnabled) { |
272 | RenderBox? child = firstChild; |
273 | int index; |
274 | while (child != null) { |
275 | index = indexOf(child); |
276 | child = childAfter(child); |
277 | assert(child == null || indexOf(child) > index); |
278 | } |
279 | } |
280 | return true; |
281 | } |
282 | |
283 | @override |
284 | void insert(RenderBox child, {RenderBox? after}) { |
285 | assert(!_keepAliveBucket.containsValue(child)); |
286 | super.insert(child, after: after); |
287 | assert(firstChild != null); |
288 | assert(_debugVerifyChildOrder()); |
289 | } |
290 | |
291 | @override |
292 | void move(RenderBox child, {RenderBox? after}) { |
293 | // There are two scenarios: |
294 | // |
295 | // 1. The child is not keptAlive. |
296 | // The child is in the childList maintained by ContainerRenderObjectMixin. |
297 | // We can call super.move and update parentData with the new slot. |
298 | // |
299 | // 2. The child is keptAlive. |
300 | // In this case, the child is no longer in the childList but might be stored in |
301 | // [_keepAliveBucket]. We need to update the location of the child in the bucket. |
302 | final SliverMultiBoxAdaptorParentData childParentData = |
303 | child.parentData! as SliverMultiBoxAdaptorParentData; |
304 | if (!childParentData.keptAlive) { |
305 | super.move(child, after: after); |
306 | childManager.didAdoptChild(child); // updates the slot in the parentData |
307 | // Its slot may change even if super.move does not change the position. |
308 | // In this case, we still want to mark as needs layout. |
309 | markNeedsLayout(); |
310 | } else { |
311 | // If the child in the bucket is not current child, that means someone has |
312 | // already moved and replaced current child, and we cannot remove this child. |
313 | if (_keepAliveBucket[childParentData.index] == child) { |
314 | _keepAliveBucket.remove(childParentData.index); |
315 | } |
316 | assert(() { |
317 | _debugDanglingKeepAlives.remove(child); |
318 | return true; |
319 | }()); |
320 | // Update the slot and reinsert back to _keepAliveBucket in the new slot. |
321 | childManager.didAdoptChild(child); |
322 | // If there is an existing child in the new slot, that mean that child will |
323 | // be moved to other index. In other cases, the existing child should have been |
324 | // removed by updateChild. Thus, it is ok to overwrite it. |
325 | assert(() { |
326 | if (_keepAliveBucket.containsKey(childParentData.index)) { |
327 | _debugDanglingKeepAlives.add(_keepAliveBucket[childParentData.index]!); |
328 | } |
329 | return true; |
330 | }()); |
331 | _keepAliveBucket[childParentData.index!] = child; |
332 | } |
333 | } |
334 | |
335 | @override |
336 | void remove(RenderBox child) { |
337 | final SliverMultiBoxAdaptorParentData childParentData = |
338 | child.parentData! as SliverMultiBoxAdaptorParentData; |
339 | if (!childParentData._keptAlive) { |
340 | super.remove(child); |
341 | return; |
342 | } |
343 | assert(_keepAliveBucket[childParentData.index] == child); |
344 | assert(() { |
345 | _debugDanglingKeepAlives.remove(child); |
346 | return true; |
347 | }()); |
348 | _keepAliveBucket.remove(childParentData.index); |
349 | dropChild(child); |
350 | } |
351 | |
352 | @override |
353 | void removeAll() { |
354 | super.removeAll(); |
355 | _keepAliveBucket.values.forEach(dropChild); |
356 | _keepAliveBucket.clear(); |
357 | } |
358 | |
359 | void _createOrObtainChild(int index, {required RenderBox? after}) { |
360 | invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) { |
361 | assert(constraints == this.constraints); |
362 | if (_keepAliveBucket.containsKey(index)) { |
363 | final RenderBox child = _keepAliveBucket.remove(index)!; |
364 | final SliverMultiBoxAdaptorParentData childParentData = |
365 | child.parentData! as SliverMultiBoxAdaptorParentData; |
366 | assert(childParentData._keptAlive); |
367 | dropChild(child); |
368 | child.parentData = childParentData; |
369 | insert(child, after: after); |
370 | childParentData._keptAlive = false; |
371 | } else { |
372 | _childManager.createChild(index, after: after); |
373 | } |
374 | }); |
375 | } |
376 | |
377 | void _destroyOrCacheChild(RenderBox child) { |
378 | final SliverMultiBoxAdaptorParentData childParentData = |
379 | child.parentData! as SliverMultiBoxAdaptorParentData; |
380 | if (childParentData.keepAlive) { |
381 | assert(!childParentData._keptAlive); |
382 | remove(child); |
383 | _keepAliveBucket[childParentData.index!] = child; |
384 | child.parentData = childParentData; |
385 | super.adoptChild(child); |
386 | childParentData._keptAlive = true; |
387 | } else { |
388 | assert(child.parent == this); |
389 | _childManager.removeChild(child); |
390 | assert(child.parent == null); |
391 | } |
392 | } |
393 | |
394 | @override |
395 | void attach(PipelineOwner owner) { |
396 | super.attach(owner); |
397 | for (final RenderBox child in _keepAliveBucket.values) { |
398 | child.attach(owner); |
399 | } |
400 | } |
401 | |
402 | @override |
403 | void detach() { |
404 | super.detach(); |
405 | for (final RenderBox child in _keepAliveBucket.values) { |
406 | child.detach(); |
407 | } |
408 | } |
409 | |
410 | @override |
411 | void redepthChildren() { |
412 | super.redepthChildren(); |
413 | _keepAliveBucket.values.forEach(redepthChild); |
414 | } |
415 | |
416 | @override |
417 | void visitChildren(RenderObjectVisitor visitor) { |
418 | super.visitChildren(visitor); |
419 | _keepAliveBucket.values.forEach(visitor); |
420 | } |
421 | |
422 | @override |
423 | void visitChildrenForSemantics(RenderObjectVisitor visitor) { |
424 | super.visitChildren(visitor); |
425 | // Do not visit children in [_keepAliveBucket]. |
426 | } |
427 | |
428 | @override |
429 | Rect get semanticBounds { |
430 | // If we laid out the first child but this sliver is not visible, we report the |
431 | // semantic bounds of this sliver as the bounds of the first child. This is necessary |
432 | // for accessibility technologies to reach this sliver even when it is outside |
433 | // the current viewport and cache extent. |
434 | if (geometry != null && !geometry!.visible && firstChild != null && firstChild!.hasSize) { |
435 | return firstChild!.paintBounds; |
436 | } |
437 | return super.semanticBounds; |
438 | } |
439 | |
440 | /// Called during layout to create and add the child with the given index and |
441 | /// scroll offset. |
442 | /// |
443 | /// Calls [RenderSliverBoxChildManager.createChild] to actually create and add |
444 | /// the child if necessary. The child may instead be obtained from a cache; |
445 | /// see [SliverMultiBoxAdaptorParentData.keepAlive]. |
446 | /// |
447 | /// Returns false if there was no cached child and `createChild` did not add |
448 | /// any child, otherwise returns true. |
449 | /// |
450 | /// Does not layout the new child. |
451 | /// |
452 | /// When this is called, there are no visible children, so no children can be |
453 | /// removed during the call to `createChild`. No child should be added during |
454 | /// that call either, except for the one that is created and returned by |
455 | /// `createChild`. |
456 | @protected |
457 | bool addInitialChild({int index = 0, double layoutOffset = 0.0}) { |
458 | assert(_debugAssertChildListLocked()); |
459 | assert(firstChild == null); |
460 | _createOrObtainChild(index, after: null); |
461 | if (firstChild != null) { |
462 | assert(firstChild == lastChild); |
463 | assert(indexOf(firstChild!) == index); |
464 | final SliverMultiBoxAdaptorParentData firstChildParentData = |
465 | firstChild!.parentData! as SliverMultiBoxAdaptorParentData; |
466 | firstChildParentData.layoutOffset = layoutOffset; |
467 | return true; |
468 | } |
469 | childManager.setDidUnderflow(true); |
470 | return false; |
471 | } |
472 | |
473 | /// Called during layout to create, add, and layout the child before |
474 | /// [firstChild]. |
475 | /// |
476 | /// Calls [RenderSliverBoxChildManager.createChild] to actually create and add |
477 | /// the child if necessary. The child may instead be obtained from a cache; |
478 | /// see [SliverMultiBoxAdaptorParentData.keepAlive]. |
479 | /// |
480 | /// Returns the new child or null if no child was obtained. |
481 | /// |
482 | /// The child that was previously the first child, as well as any subsequent |
483 | /// children, may be removed by this call if they have not yet been laid out |
484 | /// during this layout pass. No child should be added during that call except |
485 | /// for the one that is created and returned by `createChild`. |
486 | @protected |
487 | RenderBox? insertAndLayoutLeadingChild( |
488 | BoxConstraints childConstraints, { |
489 | bool parentUsesSize = false, |
490 | }) { |
491 | assert(_debugAssertChildListLocked()); |
492 | final int index = indexOf(firstChild!) - 1; |
493 | _createOrObtainChild(index, after: null); |
494 | if (indexOf(firstChild!) == index) { |
495 | firstChild!.layout(childConstraints, parentUsesSize: parentUsesSize); |
496 | return firstChild; |
497 | } |
498 | childManager.setDidUnderflow(true); |
499 | return null; |
500 | } |
501 | |
502 | /// Called during layout to create, add, and layout the child after |
503 | /// the given child. |
504 | /// |
505 | /// Calls [RenderSliverBoxChildManager.createChild] to actually create and add |
506 | /// the child if necessary. The child may instead be obtained from a cache; |
507 | /// see [SliverMultiBoxAdaptorParentData.keepAlive]. |
508 | /// |
509 | /// Returns the new child. It is the responsibility of the caller to configure |
510 | /// the child's scroll offset. |
511 | /// |
512 | /// Children after the `after` child may be removed in the process. Only the |
513 | /// new child may be added. |
514 | @protected |
515 | RenderBox? insertAndLayoutChild( |
516 | BoxConstraints childConstraints, { |
517 | required RenderBox? after, |
518 | bool parentUsesSize = false, |
519 | }) { |
520 | assert(_debugAssertChildListLocked()); |
521 | assert(after != null); |
522 | final int index = indexOf(after!) + 1; |
523 | _createOrObtainChild(index, after: after); |
524 | final RenderBox? child = childAfter(after); |
525 | if (child != null && indexOf(child) == index) { |
526 | child.layout(childConstraints, parentUsesSize: parentUsesSize); |
527 | return child; |
528 | } |
529 | childManager.setDidUnderflow(true); |
530 | return null; |
531 | } |
532 | |
533 | /// Returns the number of children preceding the `firstIndex` that need to be |
534 | /// garbage collected. |
535 | /// |
536 | /// See also: |
537 | /// |
538 | /// * [collectGarbage], which takes the leading and trailing number of |
539 | /// children to be garbage collected. |
540 | /// * [calculateTrailingGarbage], which similarly returns the number of |
541 | /// trailing children to be garbage collected. |
542 | @visibleForTesting |
543 | @protected |
544 | int calculateLeadingGarbage({required int firstIndex}) { |
545 | RenderBox? walker = firstChild; |
546 | int leadingGarbage = 0; |
547 | while (walker != null && indexOf(walker) < firstIndex) { |
548 | leadingGarbage += 1; |
549 | walker = childAfter(walker); |
550 | } |
551 | return leadingGarbage; |
552 | } |
553 | |
554 | /// Returns the number of children following the `lastIndex` that need to be |
555 | /// garbage collected. |
556 | /// |
557 | /// See also: |
558 | /// |
559 | /// * [collectGarbage], which takes the leading and trailing number of |
560 | /// children to be garbage collected. |
561 | /// * [calculateLeadingGarbage], which similarly returns the number of |
562 | /// leading children to be garbage collected. |
563 | @visibleForTesting |
564 | @protected |
565 | int calculateTrailingGarbage({required int lastIndex}) { |
566 | RenderBox? walker = lastChild; |
567 | int trailingGarbage = 0; |
568 | while (walker != null && indexOf(walker) > lastIndex) { |
569 | trailingGarbage += 1; |
570 | walker = childBefore(walker); |
571 | } |
572 | return trailingGarbage; |
573 | } |
574 | |
575 | /// Called after layout with the number of children that can be garbage |
576 | /// collected at the head and tail of the child list. |
577 | /// |
578 | /// Children whose [SliverMultiBoxAdaptorParentData.keepAlive] property is |
579 | /// set to true will be removed to a cache instead of being dropped. |
580 | /// |
581 | /// This method also collects any children that were previously kept alive but |
582 | /// are now no longer necessary. As such, it should be called every time |
583 | /// [performLayout] is run, even if the arguments are both zero. |
584 | /// |
585 | /// See also: |
586 | /// |
587 | /// * [calculateLeadingGarbage], which can be used to determine |
588 | /// `leadingGarbage` here. |
589 | /// * [calculateTrailingGarbage], which can be used to determine |
590 | /// `trailingGarbage` here. |
591 | @protected |
592 | void collectGarbage(int leadingGarbage, int trailingGarbage) { |
593 | assert(_debugAssertChildListLocked()); |
594 | assert(childCount >= leadingGarbage + trailingGarbage); |
595 | invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) { |
596 | while (leadingGarbage > 0) { |
597 | _destroyOrCacheChild(firstChild!); |
598 | leadingGarbage -= 1; |
599 | } |
600 | while (trailingGarbage > 0) { |
601 | _destroyOrCacheChild(lastChild!); |
602 | trailingGarbage -= 1; |
603 | } |
604 | // Ask the child manager to remove the children that are no longer being |
605 | // kept alive. (This should cause _keepAliveBucket to change, so we have |
606 | // to prepare our list ahead of time.) |
607 | _keepAliveBucket.values |
608 | .where((RenderBox child) { |
609 | final SliverMultiBoxAdaptorParentData childParentData = |
610 | child.parentData! as SliverMultiBoxAdaptorParentData; |
611 | return !childParentData.keepAlive; |
612 | }) |
613 | .toList() |
614 | .forEach(_childManager.removeChild); |
615 | assert( |
616 | _keepAliveBucket.values.where((RenderBox child) { |
617 | final SliverMultiBoxAdaptorParentData childParentData = |
618 | child.parentData! as SliverMultiBoxAdaptorParentData; |
619 | return !childParentData.keepAlive; |
620 | }).isEmpty, |
621 | ); |
622 | }); |
623 | } |
624 | |
625 | /// Returns the index of the given child, as given by the |
626 | /// [SliverMultiBoxAdaptorParentData.index] field of the child's [parentData]. |
627 | int indexOf(RenderBox child) { |
628 | final SliverMultiBoxAdaptorParentData childParentData = |
629 | child.parentData! as SliverMultiBoxAdaptorParentData; |
630 | assert(childParentData.index != null); |
631 | return childParentData.index!; |
632 | } |
633 | |
634 | /// Returns the dimension of the given child in the main axis, as given by the |
635 | /// child's [RenderBox.size] property. This is only valid after layout. |
636 | @protected |
637 | double paintExtentOf(RenderBox child) { |
638 | assert(child.hasSize); |
639 | return switch (constraints.axis) { |
640 | Axis.horizontal => child.size.width, |
641 | Axis.vertical => child.size.height, |
642 | }; |
643 | } |
644 | |
645 | @override |
646 | bool hitTestChildren( |
647 | SliverHitTestResult result, { |
648 | required double mainAxisPosition, |
649 | required double crossAxisPosition, |
650 | }) { |
651 | RenderBox? child = lastChild; |
652 | final BoxHitTestResult boxResult = BoxHitTestResult.wrap(result); |
653 | while (child != null) { |
654 | if (hitTestBoxChild( |
655 | boxResult, |
656 | child, |
657 | mainAxisPosition: mainAxisPosition, |
658 | crossAxisPosition: crossAxisPosition, |
659 | )) { |
660 | return true; |
661 | } |
662 | child = childBefore(child); |
663 | } |
664 | return false; |
665 | } |
666 | |
667 | @override |
668 | double childMainAxisPosition(RenderBox child) { |
669 | return childScrollOffset(child)! - constraints.scrollOffset; |
670 | } |
671 | |
672 | @override |
673 | double? childScrollOffset(RenderObject child) { |
674 | assert(child.parent == this); |
675 | final SliverMultiBoxAdaptorParentData childParentData = |
676 | child.parentData! as SliverMultiBoxAdaptorParentData; |
677 | return childParentData.layoutOffset; |
678 | } |
679 | |
680 | @override |
681 | bool paintsChild(RenderBox child) { |
682 | final SliverMultiBoxAdaptorParentData? childParentData = |
683 | child.parentData as SliverMultiBoxAdaptorParentData?; |
684 | return childParentData?.index != null && !_keepAliveBucket.containsKey(childParentData!.index); |
685 | } |
686 | |
687 | @override |
688 | void applyPaintTransform(RenderBox child, Matrix4 transform) { |
689 | if (!paintsChild(child)) { |
690 | // This can happen if some child asks for the global transform even though |
691 | // they are not getting painted. In that case, the transform sets set to |
692 | // zero since [applyPaintTransformForBoxChild] would end up throwing due |
693 | // to the child not being configured correctly for applying a transform. |
694 | // There's no assert here because asking for the paint transform is a |
695 | // valid thing to do even if a child would not be painted, but there is no |
696 | // meaningful non-zero matrix to use in this case. |
697 | transform.setZero(); |
698 | } else { |
699 | applyPaintTransformForBoxChild(child, transform); |
700 | } |
701 | } |
702 | |
703 | @override |
704 | void paint(PaintingContext context, Offset offset) { |
705 | if (firstChild == null) { |
706 | return; |
707 | } |
708 | // offset is to the top-left corner, regardless of our axis direction. |
709 | // originOffset gives us the delta from the real origin to the origin in the axis direction. |
710 | final Offset mainAxisUnit, crossAxisUnit, originOffset; |
711 | final bool addExtent; |
712 | switch (applyGrowthDirectionToAxisDirection( |
713 | constraints.axisDirection, |
714 | constraints.growthDirection, |
715 | )) { |
716 | case AxisDirection.up: |
717 | mainAxisUnit = const Offset(0.0, -1.0); |
718 | crossAxisUnit = const Offset(1.0, 0.0); |
719 | originOffset = offset + Offset(0.0, geometry!.paintExtent); |
720 | addExtent = true; |
721 | case AxisDirection.right: |
722 | mainAxisUnit = const Offset(1.0, 0.0); |
723 | crossAxisUnit = const Offset(0.0, 1.0); |
724 | originOffset = offset; |
725 | addExtent = false; |
726 | case AxisDirection.down: |
727 | mainAxisUnit = const Offset(0.0, 1.0); |
728 | crossAxisUnit = const Offset(1.0, 0.0); |
729 | originOffset = offset; |
730 | addExtent = false; |
731 | case AxisDirection.left: |
732 | mainAxisUnit = const Offset(-1.0, 0.0); |
733 | crossAxisUnit = const Offset(0.0, 1.0); |
734 | originOffset = offset + Offset(geometry!.paintExtent, 0.0); |
735 | addExtent = true; |
736 | } |
737 | RenderBox? child = firstChild; |
738 | while (child != null) { |
739 | final double mainAxisDelta = childMainAxisPosition(child); |
740 | final double crossAxisDelta = childCrossAxisPosition(child); |
741 | Offset childOffset = Offset( |
742 | originOffset.dx + mainAxisUnit.dx * mainAxisDelta + crossAxisUnit.dx * crossAxisDelta, |
743 | originOffset.dy + mainAxisUnit.dy * mainAxisDelta + crossAxisUnit.dy * crossAxisDelta, |
744 | ); |
745 | if (addExtent) { |
746 | childOffset += mainAxisUnit * paintExtentOf(child); |
747 | } |
748 | |
749 | // If the child's visible interval (mainAxisDelta, mainAxisDelta + paintExtentOf(child)) |
750 | // does not intersect the paint extent interval (0, constraints.remainingPaintExtent), it's hidden. |
751 | if (mainAxisDelta < constraints.remainingPaintExtent && |
752 | mainAxisDelta + paintExtentOf(child) > 0) { |
753 | context.paintChild(child, childOffset); |
754 | } |
755 | |
756 | child = childAfter(child); |
757 | } |
758 | } |
759 | |
760 | @override |
761 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
762 | super.debugFillProperties(properties); |
763 | properties.add( |
764 | DiagnosticsNode.message( |
765 | firstChild != null |
766 | ?'currently live children:${indexOf(firstChild!)}to${indexOf(lastChild!)}' |
767 | :'no children current live', |
768 | ), |
769 | ); |
770 | } |
771 | |
772 | /// Asserts that the reified child list is not empty and has a contiguous |
773 | /// sequence of indices. |
774 | /// |
775 | /// Always returns true. |
776 | bool debugAssertChildListIsNonEmptyAndContiguous() { |
777 | assert(() { |
778 | assert(firstChild != null); |
779 | int index = indexOf(firstChild!); |
780 | RenderBox? child = childAfter(firstChild!); |
781 | while (child != null) { |
782 | index += 1; |
783 | assert(indexOf(child) == index); |
784 | child = childAfter(child); |
785 | } |
786 | return true; |
787 | }()); |
788 | return true; |
789 | } |
790 | |
791 | @override |
792 | List<DiagnosticsNode> debugDescribeChildren() { |
793 | final List<DiagnosticsNode> children = <DiagnosticsNode>[]; |
794 | if (firstChild != null) { |
795 | RenderBox? child = firstChild; |
796 | while (true) { |
797 | final SliverMultiBoxAdaptorParentData childParentData = |
798 | child!.parentData! as SliverMultiBoxAdaptorParentData; |
799 | children.add(child.toDiagnosticsNode(name:'child with index${childParentData.index}')); |
800 | if (child == lastChild) { |
801 | break; |
802 | } |
803 | child = childParentData.nextSibling; |
804 | } |
805 | } |
806 | if (_keepAliveBucket.isNotEmpty) { |
807 | final List<int> indices = _keepAliveBucket.keys.toList()..sort(); |
808 | for (final int index in indices) { |
809 | children.add( |
810 | _keepAliveBucket[index]!.toDiagnosticsNode( |
811 | name:'child with index$index(kept alive but not laid out)', |
812 | style: DiagnosticsTreeStyle.offstage, |
813 | ), |
814 | ); |
815 | } |
816 | } |
817 | return children; |
818 | } |
819 | } |
820 |
Definitions
- RenderSliverBoxChildManager
- createChild
- removeChild
- estimateMaxScrollOffset
- childCount
- estimatedChildCount
- didAdoptChild
- setDidUnderflow
- didStartLayout
- didFinishLayout
- debugAssertChildListLocked
- KeepAliveParentDataMixin
- keptAlive
- RenderSliverWithKeepAliveMixin
- setupParentData
- SliverMultiBoxAdaptorParentData
- keptAlive
- toString
- RenderSliverMultiBoxAdaptor
- RenderSliverMultiBoxAdaptor
- setupParentData
- childManager
- debugChildIntegrityEnabled
- debugChildIntegrityEnabled
- adoptChild
- _debugAssertChildListLocked
- _debugVerifyChildOrder
- insert
- move
- remove
- removeAll
- _createOrObtainChild
- _destroyOrCacheChild
- attach
- detach
- redepthChildren
- visitChildren
- visitChildrenForSemantics
- semanticBounds
- addInitialChild
- insertAndLayoutLeadingChild
- insertAndLayoutChild
- calculateLeadingGarbage
- calculateTrailingGarbage
- collectGarbage
- indexOf
- paintExtentOf
- hitTestChildren
- childMainAxisPosition
- childScrollOffset
- paintsChild
- applyPaintTransform
- paint
- debugFillProperties
- debugAssertChildListIsNonEmptyAndContiguous
Learn more about Flutter for embedded and desktop on industrialflutter.com