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