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 'primary_scroll_controller.dart'; |
6 | /// @docImport 'scrollable.dart'; |
7 | /// @docImport 'two_dimensional_scroll_view.dart'; |
8 | /// @docImport 'viewport.dart'; |
9 | library; |
10 | |
11 | import 'dart:math' as math; |
12 | |
13 | import 'package:flutter/animation.dart'; |
14 | import 'package:flutter/rendering.dart'; |
15 | |
16 | import 'framework.dart'; |
17 | import 'scroll_delegate.dart'; |
18 | import 'scroll_notification.dart'; |
19 | import 'scroll_position.dart'; |
20 | |
21 | export 'package:flutter/rendering.dart' show AxisDirection; |
22 | |
23 | // Examples can assume: |
24 | // late final RenderBox child; |
25 | // late final BoxConstraints constraints; |
26 | // class RenderSimpleTwoDimensionalViewport extends RenderTwoDimensionalViewport { |
27 | // RenderSimpleTwoDimensionalViewport({ |
28 | // required super.horizontalOffset, |
29 | // required super.horizontalAxisDirection, |
30 | // required super.verticalOffset, |
31 | // required super.verticalAxisDirection, |
32 | // required super.delegate, |
33 | // required super.mainAxis, |
34 | // required super.childManager, |
35 | // super.cacheExtent, |
36 | // super.clipBehavior = Clip.hardEdge, |
37 | // }); |
38 | // @override |
39 | // void layoutChildSequence() { } |
40 | // } |
41 | |
42 | /// Signature for a function that creates a widget for a given [ChildVicinity], |
43 | /// e.g., in a [TwoDimensionalScrollView], but may return null. |
44 | /// |
45 | /// Used by [TwoDimensionalChildBuilderDelegate.builder] and other APIs that |
46 | /// use lazily-generated widgets where the child count may not be known |
47 | /// ahead of time. |
48 | /// |
49 | /// Unlike most builders, this callback can return null, indicating the |
50 | /// [ChildVicinity.xIndex] or [ChildVicinity.yIndex] is out of range. Whether |
51 | /// and when this is valid depends on the semantics of the builder. For example, |
52 | /// [TwoDimensionalChildBuilderDelegate.builder] returns |
53 | /// null when one or both of the indices is out of range, where the range is |
54 | /// defined by the [TwoDimensionalChildBuilderDelegate.maxXIndex] or |
55 | /// [TwoDimensionalChildBuilderDelegate.maxYIndex]; so in that case the |
56 | /// vicinity values may determine whether returning null is valid or not. |
57 | /// |
58 | /// See also: |
59 | /// |
60 | /// * [WidgetBuilder], which is similar but only takes a [BuildContext]. |
61 | /// * [NullableIndexedWidgetBuilder], which is similar but may return null. |
62 | /// * [IndexedWidgetBuilder], which is similar but not nullable. |
63 | typedef TwoDimensionalIndexedWidgetBuilder = Widget? Function(BuildContext context, ChildVicinity vicinity); |
64 | |
65 | /// A widget through which a portion of larger content can be viewed, typically |
66 | /// in combination with a [TwoDimensionalScrollable]. |
67 | /// |
68 | /// [TwoDimensionalViewport] is the visual workhorse of the two dimensional |
69 | /// scrolling machinery. It displays a subset of its children according to its |
70 | /// own dimensions and the given [horizontalOffset] an [verticalOffset]. As the |
71 | /// offsets vary, different children are visible through the viewport. |
72 | /// |
73 | /// Subclasses must implement [createRenderObject] and [updateRenderObject]. |
74 | /// Both of these methods require the render object to be a subclass of |
75 | /// [RenderTwoDimensionalViewport]. This class will create its own |
76 | /// [RenderObjectElement] which already implements the |
77 | /// [TwoDimensionalChildManager], which means subclasses should cast the |
78 | /// [BuildContext] to provide as the child manager to the |
79 | /// [RenderTwoDimensionalViewport]. |
80 | /// |
81 | /// {@tool snippet} |
82 | /// This is an example of a subclass implementation of [TwoDimensionalViewport], |
83 | /// `SimpleTwoDimensionalViewport`. The `RenderSimpleTwoDimensionalViewport` is |
84 | /// a subclass of [RenderTwoDimensionalViewport]. |
85 | /// |
86 | /// ```dart |
87 | /// class SimpleTwoDimensionalViewport extends TwoDimensionalViewport { |
88 | /// const SimpleTwoDimensionalViewport({ |
89 | /// super.key, |
90 | /// required super.verticalOffset, |
91 | /// required super.verticalAxisDirection, |
92 | /// required super.horizontalOffset, |
93 | /// required super.horizontalAxisDirection, |
94 | /// required super.delegate, |
95 | /// required super.mainAxis, |
96 | /// super.cacheExtent, |
97 | /// super.clipBehavior = Clip.hardEdge, |
98 | /// }); |
99 | /// |
100 | /// @override |
101 | /// RenderSimpleTwoDimensionalViewport createRenderObject(BuildContext context) { |
102 | /// return RenderSimpleTwoDimensionalViewport( |
103 | /// horizontalOffset: horizontalOffset, |
104 | /// horizontalAxisDirection: horizontalAxisDirection, |
105 | /// verticalOffset: verticalOffset, |
106 | /// verticalAxisDirection: verticalAxisDirection, |
107 | /// mainAxis: mainAxis, |
108 | /// delegate: delegate, |
109 | /// childManager: context as TwoDimensionalChildManager, |
110 | /// cacheExtent: cacheExtent, |
111 | /// clipBehavior: clipBehavior, |
112 | /// ); |
113 | /// } |
114 | /// |
115 | /// @override |
116 | /// void updateRenderObject(BuildContext context, RenderSimpleTwoDimensionalViewport renderObject) { |
117 | /// renderObject |
118 | /// ..horizontalOffset = horizontalOffset |
119 | /// ..horizontalAxisDirection = horizontalAxisDirection |
120 | /// ..verticalOffset = verticalOffset |
121 | /// ..verticalAxisDirection = verticalAxisDirection |
122 | /// ..mainAxis = mainAxis |
123 | /// ..delegate = delegate |
124 | /// ..cacheExtent = cacheExtent |
125 | /// ..clipBehavior = clipBehavior; |
126 | /// } |
127 | /// } |
128 | /// ``` |
129 | /// {@end-tool} |
130 | /// |
131 | /// See also: |
132 | /// |
133 | /// * [Viewport], the equivalent of this widget that scrolls in only one |
134 | /// dimension. |
135 | abstract class TwoDimensionalViewport extends RenderObjectWidget { |
136 | /// Creates a viewport for [RenderBox] objects that extend and scroll in both |
137 | /// horizontal and vertical dimensions. |
138 | /// |
139 | /// The viewport listens to the [horizontalOffset] and [verticalOffset], which |
140 | /// means this widget does not need to be rebuilt when the offsets change. |
141 | const TwoDimensionalViewport({ |
142 | super.key, |
143 | required this.verticalOffset, |
144 | required this.verticalAxisDirection, |
145 | required this.horizontalOffset, |
146 | required this.horizontalAxisDirection, |
147 | required this.delegate, |
148 | required this.mainAxis, |
149 | this.cacheExtent, |
150 | this.clipBehavior = Clip.hardEdge, |
151 | }) : assert( |
152 | verticalAxisDirection == AxisDirection.down || verticalAxisDirection == AxisDirection.up, |
153 | 'TwoDimensionalViewport.verticalAxisDirection is not Axis.vertical.' |
154 | ), |
155 | assert( |
156 | horizontalAxisDirection == AxisDirection.left || horizontalAxisDirection == AxisDirection.right, |
157 | 'TwoDimensionalViewport.horizontalAxisDirection is not Axis.horizontal.' |
158 | ); |
159 | |
160 | /// Which part of the content inside the viewport should be visible in the |
161 | /// vertical axis. |
162 | /// |
163 | /// The [ViewportOffset.pixels] value determines the scroll offset that the |
164 | /// viewport uses to select which part of its content to display. As the user |
165 | /// scrolls the viewport vertically, this value changes, which changes the |
166 | /// content that is displayed. |
167 | /// |
168 | /// Typically a [ScrollPosition]. |
169 | final ViewportOffset verticalOffset; |
170 | |
171 | /// The direction in which the [verticalOffset]'s [ViewportOffset.pixels] |
172 | /// increases. |
173 | /// |
174 | /// For example, if the axis direction is [AxisDirection.down], a scroll |
175 | /// offset of zero is at the top of the viewport and increases towards the |
176 | /// bottom of the viewport. |
177 | /// |
178 | /// Must be either [AxisDirection.down] or [AxisDirection.up] in correlation |
179 | /// with an [Axis.vertical]. |
180 | final AxisDirection verticalAxisDirection; |
181 | |
182 | /// Which part of the content inside the viewport should be visible in the |
183 | /// horizontal axis. |
184 | /// |
185 | /// The [ViewportOffset.pixels] value determines the scroll offset that the |
186 | /// viewport uses to select which part of its content to display. As the user |
187 | /// scrolls the viewport horizontally, this value changes, which changes the |
188 | /// content that is displayed. |
189 | /// |
190 | /// Typically a [ScrollPosition]. |
191 | final ViewportOffset horizontalOffset; |
192 | |
193 | /// The direction in which the [horizontalOffset]'s [ViewportOffset.pixels] |
194 | /// increases. |
195 | /// |
196 | /// For example, if the axis direction is [AxisDirection.right], a scroll |
197 | /// offset of zero is at the left of the viewport and increases towards the |
198 | /// right of the viewport. |
199 | /// |
200 | /// Must be either [AxisDirection.left] or [AxisDirection.right] in correlation |
201 | /// with an [Axis.horizontal]. |
202 | final AxisDirection horizontalAxisDirection; |
203 | |
204 | /// The main axis of the two. |
205 | /// |
206 | /// Used to determine the paint order of the children of the viewport. When |
207 | /// the main axis is [Axis.vertical], children will be painted in row major |
208 | /// order, according to their associated [ChildVicinity]. When the main axis |
209 | /// is [Axis.horizontal], the children will be painted in column major order. |
210 | final Axis mainAxis; |
211 | |
212 | /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} |
213 | final double? cacheExtent; |
214 | |
215 | /// {@macro flutter.material.Material.clipBehavior} |
216 | final Clip clipBehavior; |
217 | |
218 | /// A delegate that provides the children for the [TwoDimensionalViewport]. |
219 | final TwoDimensionalChildDelegate delegate; |
220 | |
221 | @override |
222 | RenderObjectElement createElement() => _TwoDimensionalViewportElement(this); |
223 | |
224 | @override |
225 | RenderTwoDimensionalViewport createRenderObject(BuildContext context); |
226 | |
227 | @override |
228 | void updateRenderObject(BuildContext context, RenderTwoDimensionalViewport renderObject); |
229 | } |
230 | |
231 | class _TwoDimensionalViewportElement extends RenderObjectElement |
232 | with NotifiableElementMixin, ViewportElementMixin implements TwoDimensionalChildManager { |
233 | _TwoDimensionalViewportElement(super.widget); |
234 | |
235 | @override |
236 | RenderTwoDimensionalViewport get renderObject => super.renderObject as RenderTwoDimensionalViewport; |
237 | |
238 | // Contains all children, including those that are keyed. |
239 | Map<ChildVicinity, Element> _vicinityToChild = <ChildVicinity, Element>{}; |
240 | Map<Key, Element> _keyToChild = <Key, Element>{}; |
241 | // Used between _startLayout() & _endLayout() to compute the new values for |
242 | // _vicinityToChild and _keyToChild. |
243 | Map<ChildVicinity, Element>? _newVicinityToChild; |
244 | Map<Key, Element>? _newKeyToChild; |
245 | |
246 | @override |
247 | void performRebuild() { |
248 | super.performRebuild(); |
249 | // Children list is updated during layout since we only know during layout |
250 | // which children will be visible. |
251 | renderObject.markNeedsLayout(withDelegateRebuild: true); |
252 | } |
253 | |
254 | @override |
255 | void forgetChild(Element child) { |
256 | assert(!_debugIsDoingLayout); |
257 | super.forgetChild(child); |
258 | _vicinityToChild.remove(child.slot); |
259 | if (child.widget.key != null) { |
260 | _keyToChild.remove(child.widget.key); |
261 | } |
262 | } |
263 | |
264 | @override |
265 | void insertRenderObjectChild(RenderBox child, ChildVicinity slot) { |
266 | renderObject._insertChild(child, slot); |
267 | } |
268 | |
269 | @override |
270 | void moveRenderObjectChild(RenderBox child, ChildVicinity oldSlot, ChildVicinity newSlot) { |
271 | renderObject._moveChild(child, from: oldSlot, to: newSlot); |
272 | } |
273 | |
274 | @override |
275 | void removeRenderObjectChild(RenderBox child, ChildVicinity slot) { |
276 | renderObject._removeChild(child, slot); |
277 | } |
278 | |
279 | @override |
280 | void visitChildren(ElementVisitor visitor) { |
281 | _vicinityToChild.values.forEach(visitor); |
282 | } |
283 | |
284 | @override |
285 | List<DiagnosticsNode> debugDescribeChildren() { |
286 | final List<Element> children = _vicinityToChild.values.toList()..sort(_compareChildren); |
287 | return <DiagnosticsNode>[ |
288 | for (final Element child in children) |
289 | child.toDiagnosticsNode(name: child.slot.toString()) |
290 | ]; |
291 | } |
292 | |
293 | static int _compareChildren(Element a, Element b) { |
294 | final ChildVicinity aSlot = a.slot! as ChildVicinity; |
295 | final ChildVicinity bSlot = b.slot! as ChildVicinity; |
296 | return aSlot.compareTo(bSlot); |
297 | } |
298 | |
299 | // ---- ChildManager implementation ---- |
300 | |
301 | bool get _debugIsDoingLayout => _newKeyToChild != null && _newVicinityToChild != null; |
302 | |
303 | @override |
304 | void _startLayout() { |
305 | assert(!_debugIsDoingLayout); |
306 | _newVicinityToChild = <ChildVicinity, Element>{}; |
307 | _newKeyToChild = <Key, Element>{}; |
308 | } |
309 | |
310 | @override |
311 | void _buildChild(ChildVicinity vicinity) { |
312 | assert(_debugIsDoingLayout); |
313 | owner!.buildScope(this, () { |
314 | final Widget? newWidget = (widget as TwoDimensionalViewport).delegate.build(this, vicinity); |
315 | if (newWidget == null) { |
316 | return; |
317 | } |
318 | final Element? oldElement = _retrieveOldElement(newWidget, vicinity); |
319 | final Element? newChild = updateChild(oldElement, newWidget, vicinity); |
320 | assert(newChild != null); |
321 | // Ensure we are not overwriting an existing child. |
322 | assert(_newVicinityToChild![vicinity] == null); |
323 | _newVicinityToChild![vicinity] = newChild!; |
324 | if (newWidget.key != null) { |
325 | // Ensure we are not overwriting an existing key |
326 | assert(_newKeyToChild![newWidget.key!] == null); |
327 | _newKeyToChild![newWidget.key!] = newChild; |
328 | } |
329 | }); |
330 | } |
331 | |
332 | Element? _retrieveOldElement(Widget newWidget, ChildVicinity vicinity) { |
333 | if (newWidget.key != null) { |
334 | final Element? result = _keyToChild.remove(newWidget.key); |
335 | if (result != null) { |
336 | _vicinityToChild.remove(result.slot); |
337 | } |
338 | return result; |
339 | } |
340 | final Element? potentialOldElement = _vicinityToChild[vicinity]; |
341 | if (potentialOldElement != null && potentialOldElement.widget.key == null) { |
342 | return _vicinityToChild.remove(vicinity); |
343 | } |
344 | return null; |
345 | } |
346 | |
347 | @override |
348 | void _reuseChild(ChildVicinity vicinity) { |
349 | assert(_debugIsDoingLayout); |
350 | final Element? elementToReuse = _vicinityToChild.remove(vicinity); |
351 | assert( |
352 | elementToReuse != null, |
353 | 'Expected to re-use an element at $vicinity, but none was found.' |
354 | ); |
355 | _newVicinityToChild![vicinity] = elementToReuse!; |
356 | if (elementToReuse.widget.key != null) { |
357 | assert(_keyToChild.containsKey(elementToReuse.widget.key)); |
358 | assert(_keyToChild[elementToReuse.widget.key] == elementToReuse); |
359 | _newKeyToChild![elementToReuse.widget.key!] = _keyToChild.remove(elementToReuse.widget.key)!; |
360 | } |
361 | } |
362 | |
363 | @override |
364 | void _endLayout() { |
365 | assert(_debugIsDoingLayout); |
366 | |
367 | // Unmount all elements that have not been reused in the layout cycle. |
368 | for (final Element element in _vicinityToChild.values) { |
369 | if (element.widget.key == null) { |
370 | // If it has a key, we handle it below. |
371 | updateChild(element, null, null); |
372 | } else { |
373 | assert(_keyToChild.containsValue(element)); |
374 | } |
375 | } |
376 | for (final Element element in _keyToChild.values) { |
377 | assert(element.widget.key != null); |
378 | updateChild(element, null, null); |
379 | } |
380 | |
381 | _vicinityToChild = _newVicinityToChild!; |
382 | _keyToChild = _newKeyToChild!; |
383 | _newVicinityToChild = null; |
384 | _newKeyToChild = null; |
385 | assert(!_debugIsDoingLayout); |
386 | } |
387 | } |
388 | |
389 | /// Parent data structure used by [RenderTwoDimensionalViewport]. |
390 | /// |
391 | /// The parent data primarily describes where a child is in the viewport. The |
392 | /// [layoutOffset] must be set by subclasses of [RenderTwoDimensionalViewport], |
393 | /// during [RenderTwoDimensionalViewport.layoutChildSequence] which represents |
394 | /// the position of the child in the viewport. |
395 | /// |
396 | /// The [paintOffset] is computed by [RenderTwoDimensionalViewport] after |
397 | /// [RenderTwoDimensionalViewport.layoutChildSequence]. If subclasses of |
398 | /// RenderTwoDimensionalViewport override the paint method, the [paintOffset] |
399 | /// should be used to position the child in the viewport in order to account for |
400 | /// a reversed [AxisDirection] in one or both dimensions. |
401 | class TwoDimensionalViewportParentData extends ParentData with KeepAliveParentDataMixin { |
402 | /// The offset at which to paint the child in the parent's coordinate system. |
403 | /// |
404 | /// This [Offset] represents the top left corner of the child of the |
405 | /// [TwoDimensionalViewport]. |
406 | /// |
407 | /// This value must be set by implementors during |
408 | /// [RenderTwoDimensionalViewport.layoutChildSequence]. After the method is |
409 | /// complete, the [RenderTwoDimensionalViewport] will compute the |
410 | /// [paintOffset] based on this value to account for the [AxisDirection]. |
411 | Offset? layoutOffset; |
412 | |
413 | /// The logical positioning of children in two dimensions. |
414 | /// |
415 | /// While children may not be strictly laid out in rows and columns, the |
416 | /// relative positioning determines traversal of |
417 | /// children in row or column major format. |
418 | /// |
419 | /// This is set in the [RenderTwoDimensionalViewport.buildOrObtainChildFor]. |
420 | ChildVicinity vicinity = ChildVicinity.invalid; |
421 | |
422 | /// Whether or not the child is actually visible within the viewport. |
423 | /// |
424 | /// For example, if a child is contained within the |
425 | /// [RenderTwoDimensionalViewport.cacheExtent] and out of view. |
426 | /// |
427 | /// This is used during [RenderTwoDimensionalViewport.paint] in order to skip |
428 | /// painting children that cannot be seen. |
429 | bool get isVisible { |
430 | assert(() { |
431 | if (_paintExtent == null) { |
432 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
433 | ErrorSummary('The paint extent of the child has not been determined yet.' ), |
434 | ErrorDescription( |
435 | 'The paint extent, and therefore the visibility, of a child of a ' |
436 | 'RenderTwoDimensionalViewport is computed after ' |
437 | 'RenderTwoDimensionalViewport.layoutChildSequence.' |
438 | ), |
439 | ]); |
440 | } |
441 | return true; |
442 | }()); |
443 | return _paintExtent != Size.zero || _paintExtent!.height != 0.0 || _paintExtent!.width != 0.0; |
444 | } |
445 | |
446 | /// Represents the extent in both dimensions of the child that is actually |
447 | /// visible. |
448 | /// |
449 | /// For example, if a child [RenderBox] had a height of 100 pixels, and a |
450 | /// width of 100 pixels, but was scrolled to positions such that only 50 |
451 | /// pixels of both width and height were visible, the paintExtent would be |
452 | /// represented as `Size(50.0, 50.0)`. |
453 | /// |
454 | /// This is set in [RenderTwoDimensionalViewport.updateChildPaintData]. |
455 | Size? _paintExtent; |
456 | |
457 | /// The previous sibling in the parent's child list according to the traversal |
458 | /// order specified by [RenderTwoDimensionalViewport.mainAxis]. |
459 | RenderBox? _previousSibling; |
460 | |
461 | /// The next sibling in the parent's child list according to the traversal |
462 | /// order specified by [RenderTwoDimensionalViewport.mainAxis]. |
463 | RenderBox? _nextSibling; |
464 | |
465 | /// The position of the child relative to the bounds and [AxisDirection] of |
466 | /// the viewport. |
467 | /// |
468 | /// This is the distance from the top left visible corner of the parent to the |
469 | /// top left visible corner of the child. When the [AxisDirection]s are |
470 | /// [AxisDirection.down] or [AxisDirection.right], this value is the same as |
471 | /// the [layoutOffset]. This value deviates when scrolling in the reverse |
472 | /// directions of [AxisDirection.up] and [AxisDirection.left] to reposition |
473 | /// the children correctly. |
474 | /// |
475 | /// This is set in [RenderTwoDimensionalViewport.updateChildPaintData], after |
476 | /// [RenderTwoDimensionalViewport.layoutChildSequence]. |
477 | /// |
478 | /// If overriding [RenderTwoDimensionalViewport.paint], use this value to |
479 | /// position the children instead of [layoutOffset]. |
480 | Offset? paintOffset; |
481 | |
482 | @override |
483 | bool get keptAlive => keepAlive && !isVisible; |
484 | |
485 | @override |
486 | String toString() { |
487 | return 'vicinity= $vicinity; ' |
488 | 'layoutOffset= $layoutOffset; ' |
489 | 'paintOffset= $paintOffset; ' |
490 | ' ${_paintExtent == null |
491 | ? 'not visible; ' |
492 | : ' ${!isVisible ? 'not ' : '' }visible - paintExtent= $_paintExtent; ' }' |
493 | ' ${keepAlive ? "keepAlive; " : "" }' ; |
494 | } |
495 | } |
496 | |
497 | /// A base class for viewing render objects that scroll in two dimensions. |
498 | /// |
499 | /// The viewport listens to two [ViewportOffset]s, which determines the |
500 | /// visible content. |
501 | /// |
502 | /// Subclasses must implement [layoutChildSequence], calling on |
503 | /// [buildOrObtainChildFor] to manage the children of the viewport. |
504 | /// |
505 | /// Subclasses should not override [performLayout], as it handles housekeeping |
506 | /// on either side of the call to [layoutChildSequence]. |
507 | abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderAbstractViewport { |
508 | /// Initializes fields for subclasses. |
509 | /// |
510 | /// The [cacheExtent], if null, defaults to |
511 | /// [RenderAbstractViewport.defaultCacheExtent]. |
512 | RenderTwoDimensionalViewport({ |
513 | required ViewportOffset horizontalOffset, |
514 | required AxisDirection horizontalAxisDirection, |
515 | required ViewportOffset verticalOffset, |
516 | required AxisDirection verticalAxisDirection, |
517 | required TwoDimensionalChildDelegate delegate, |
518 | required Axis mainAxis, |
519 | required TwoDimensionalChildManager childManager, |
520 | double? cacheExtent, |
521 | Clip clipBehavior = Clip.hardEdge, |
522 | }) : assert( |
523 | verticalAxisDirection == AxisDirection.down || verticalAxisDirection == AxisDirection.up, |
524 | 'TwoDimensionalViewport.verticalAxisDirection is not Axis.vertical.' |
525 | ), |
526 | assert( |
527 | horizontalAxisDirection == AxisDirection.left || horizontalAxisDirection == AxisDirection.right, |
528 | 'TwoDimensionalViewport.horizontalAxisDirection is not Axis.horizontal.' |
529 | ), |
530 | _childManager = childManager, |
531 | _horizontalOffset = horizontalOffset, |
532 | _horizontalAxisDirection = horizontalAxisDirection, |
533 | _verticalOffset = verticalOffset, |
534 | _verticalAxisDirection = verticalAxisDirection, |
535 | _delegate = delegate, |
536 | _mainAxis = mainAxis, |
537 | _cacheExtent = cacheExtent ?? RenderAbstractViewport.defaultCacheExtent, |
538 | _clipBehavior = clipBehavior { |
539 | assert(() { |
540 | _debugDanglingKeepAlives = <RenderBox>[]; |
541 | return true; |
542 | }()); |
543 | } |
544 | |
545 | /// Which part of the content inside the viewport should be visible in the |
546 | /// horizontal axis. |
547 | /// |
548 | /// The [ViewportOffset.pixels] value determines the scroll offset that the |
549 | /// viewport uses to select which part of its content to display. As the user |
550 | /// scrolls the viewport horizontally, this value changes, which changes the |
551 | /// content that is displayed. |
552 | /// |
553 | /// Typically a [ScrollPosition]. |
554 | ViewportOffset get horizontalOffset => _horizontalOffset; |
555 | ViewportOffset _horizontalOffset; |
556 | set horizontalOffset(ViewportOffset value) { |
557 | if (_horizontalOffset == value) { |
558 | return; |
559 | } |
560 | if (attached) { |
561 | _horizontalOffset.removeListener(markNeedsLayout); |
562 | } |
563 | _horizontalOffset = value; |
564 | if (attached) { |
565 | _horizontalOffset.addListener(markNeedsLayout); |
566 | } |
567 | markNeedsLayout(); |
568 | } |
569 | |
570 | /// The direction in which the [horizontalOffset] increases. |
571 | /// |
572 | /// For example, if the axis direction is [AxisDirection.right], a scroll |
573 | /// offset of zero is at the left of the viewport and increases towards the |
574 | /// right of the viewport. |
575 | AxisDirection get horizontalAxisDirection => _horizontalAxisDirection; |
576 | AxisDirection _horizontalAxisDirection; |
577 | set horizontalAxisDirection(AxisDirection value) { |
578 | if (_horizontalAxisDirection == value) { |
579 | return; |
580 | } |
581 | _horizontalAxisDirection = value; |
582 | markNeedsLayout(); |
583 | } |
584 | |
585 | /// Which part of the content inside the viewport should be visible in the |
586 | /// vertical axis. |
587 | /// |
588 | /// The [ViewportOffset.pixels] value determines the scroll offset that the |
589 | /// viewport uses to select which part of its content to display. As the user |
590 | /// scrolls the viewport vertically, this value changes, which changes the |
591 | /// content that is displayed. |
592 | /// |
593 | /// Typically a [ScrollPosition]. |
594 | ViewportOffset get verticalOffset => _verticalOffset; |
595 | ViewportOffset _verticalOffset; |
596 | set verticalOffset(ViewportOffset value) { |
597 | if (_verticalOffset == value) { |
598 | return; |
599 | } |
600 | if (attached) { |
601 | _verticalOffset.removeListener(markNeedsLayout); |
602 | } |
603 | _verticalOffset = value; |
604 | if (attached) { |
605 | _verticalOffset.addListener(markNeedsLayout); |
606 | } |
607 | markNeedsLayout(); |
608 | } |
609 | |
610 | /// The direction in which the [verticalOffset] increases. |
611 | /// |
612 | /// For example, if the axis direction is [AxisDirection.down], a scroll |
613 | /// offset of zero is at the top the viewport and increases towards the |
614 | /// bottom of the viewport. |
615 | AxisDirection get verticalAxisDirection => _verticalAxisDirection; |
616 | AxisDirection _verticalAxisDirection; |
617 | set verticalAxisDirection(AxisDirection value) { |
618 | if (_verticalAxisDirection == value) { |
619 | return; |
620 | } |
621 | _verticalAxisDirection = value; |
622 | markNeedsLayout(); |
623 | } |
624 | |
625 | /// Supplies children for layout in the viewport. |
626 | TwoDimensionalChildDelegate get delegate => _delegate; |
627 | TwoDimensionalChildDelegate _delegate; |
628 | set delegate(covariant TwoDimensionalChildDelegate value) { |
629 | if (_delegate == value) { |
630 | return; |
631 | } |
632 | if (attached) { |
633 | _delegate.removeListener(_handleDelegateNotification); |
634 | } |
635 | final TwoDimensionalChildDelegate oldDelegate = _delegate; |
636 | _delegate = value; |
637 | if (attached) { |
638 | _delegate.addListener(_handleDelegateNotification); |
639 | } |
640 | if (_delegate.runtimeType != oldDelegate.runtimeType || _delegate.shouldRebuild(oldDelegate)) { |
641 | _handleDelegateNotification(); |
642 | } |
643 | } |
644 | |
645 | /// The major axis of the two dimensions. |
646 | /// |
647 | /// This is can be used by subclasses to determine paint order, |
648 | /// visitor patterns like row and column major ordering, or hit test |
649 | /// precedence. |
650 | /// |
651 | /// See also: |
652 | /// |
653 | /// * [TwoDimensionalScrollView], which assigns the [PrimaryScrollController] |
654 | /// to the [TwoDimensionalScrollView.mainAxis] and shares this value. |
655 | Axis get mainAxis => _mainAxis; |
656 | Axis _mainAxis; |
657 | set mainAxis(Axis value) { |
658 | if (_mainAxis == value) { |
659 | return; |
660 | } |
661 | _mainAxis = value; |
662 | // Child order needs to be resorted, which happens in performLayout. |
663 | markNeedsLayout(); |
664 | } |
665 | |
666 | /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} |
667 | double get cacheExtent => _cacheExtent ?? RenderAbstractViewport.defaultCacheExtent; |
668 | double? _cacheExtent; |
669 | set cacheExtent(double? value) { |
670 | if (_cacheExtent == value) { |
671 | return; |
672 | } |
673 | _cacheExtent = value; |
674 | markNeedsLayout(); |
675 | } |
676 | |
677 | /// {@macro flutter.material.Material.clipBehavior} |
678 | Clip get clipBehavior => _clipBehavior; |
679 | Clip _clipBehavior; |
680 | set clipBehavior(Clip value) { |
681 | if (_clipBehavior == value) { |
682 | return; |
683 | } |
684 | _clipBehavior = value; |
685 | markNeedsPaint(); |
686 | markNeedsSemanticsUpdate(); |
687 | } |
688 | |
689 | final TwoDimensionalChildManager _childManager; |
690 | final Map<ChildVicinity, RenderBox> _children = <ChildVicinity, RenderBox>{}; |
691 | /// Children that have been laid out (or re-used) during the course of |
692 | /// performLayout, used to update the keep alive bucket at the end of |
693 | /// performLayout. |
694 | final Map<ChildVicinity, RenderBox> _activeChildrenForLayoutPass = <ChildVicinity, RenderBox>{}; |
695 | /// The nodes being kept alive despite not being visible. |
696 | final Map<ChildVicinity, RenderBox> _keepAliveBucket = <ChildVicinity, RenderBox>{}; |
697 | |
698 | late List<RenderBox> _debugDanglingKeepAlives; |
699 | |
700 | bool _hasVisualOverflow = false; |
701 | final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>(); |
702 | |
703 | @override |
704 | bool get isRepaintBoundary => true; |
705 | |
706 | @override |
707 | bool get sizedByParent => true; |
708 | |
709 | // Keeps track of the upper and lower bounds of ChildVicinity indices when |
710 | // subclasses call buildOrObtainChildFor during layoutChildSequence. These |
711 | // values are used to sort children in accordance with the mainAxis for |
712 | // paint order. |
713 | int? _leadingXIndex; |
714 | int? _trailingXIndex; |
715 | int? _leadingYIndex; |
716 | int? _trailingYIndex; |
717 | |
718 | /// The first child of the viewport according to the traversal order of the |
719 | /// [mainAxis]. |
720 | /// |
721 | /// {@template flutter.rendering.twoDimensionalViewport.paintOrder} |
722 | /// The [mainAxis] correlates with the [ChildVicinity] of each child to paint |
723 | /// the children in a row or column major order. |
724 | /// |
725 | /// By default, the [mainAxis] is [Axis.vertical], which would result in a |
726 | /// row major paint order, visiting children in the horizontal indices before |
727 | /// advancing to the next vertical index. |
728 | /// {@endtemplate} |
729 | /// |
730 | /// This value is null during [layoutChildSequence] as children are reified |
731 | /// into the correct order after layout is completed. This can be used when |
732 | /// overriding [paint] in order to paint the children in the correct order. |
733 | RenderBox? get firstChild => _firstChild; |
734 | RenderBox? _firstChild; |
735 | |
736 | /// The last child in the viewport according to the traversal order of the |
737 | /// [mainAxis]. |
738 | /// |
739 | /// {@macro flutter.rendering.twoDimensionalViewport.paintOrder} |
740 | /// |
741 | /// This value is null during [layoutChildSequence] as children are reified |
742 | /// into the correct order after layout is completed. This can be used when |
743 | /// overriding [paint] in order to paint the children in the correct order. |
744 | RenderBox? get lastChild => _lastChild; |
745 | RenderBox? _lastChild; |
746 | |
747 | /// The previous child before the given child in the child list according to |
748 | /// the traversal order of the [mainAxis]. |
749 | /// |
750 | /// {@macro flutter.rendering.twoDimensionalViewport.paintOrder} |
751 | /// |
752 | /// This method is useful when overriding [paint] in order to paint children |
753 | /// in the correct order. |
754 | RenderBox? childBefore(RenderBox child) { |
755 | assert(child.parent == this); |
756 | return parentDataOf(child)._previousSibling; |
757 | } |
758 | |
759 | /// The next child after the given child in the child list according to |
760 | /// the traversal order of the [mainAxis]. |
761 | /// |
762 | /// {@macro flutter.rendering.twoDimensionalViewport.paintOrder} |
763 | /// |
764 | /// This method is useful when overriding [paint] in order to paint children |
765 | /// in the correct order. |
766 | RenderBox? childAfter(RenderBox child) { |
767 | assert(child.parent == this); |
768 | return parentDataOf(child)._nextSibling; |
769 | } |
770 | |
771 | void _handleDelegateNotification() { |
772 | return markNeedsLayout(withDelegateRebuild: true); |
773 | } |
774 | |
775 | @override |
776 | void setupParentData(RenderBox child) { |
777 | if (child.parentData is! TwoDimensionalViewportParentData) { |
778 | child.parentData = TwoDimensionalViewportParentData(); |
779 | } |
780 | } |
781 | |
782 | /// Convenience method for retrieving and casting the [ParentData] of the |
783 | /// viewport's children. |
784 | /// |
785 | /// Children must have a [ParentData] of type |
786 | /// [TwoDimensionalViewportParentData], or a subclass thereof. |
787 | @protected |
788 | @mustCallSuper |
789 | TwoDimensionalViewportParentData parentDataOf(RenderBox child) { |
790 | assert(_children.containsValue(child) || |
791 | _keepAliveBucket.containsValue(child) || |
792 | _debugOrphans!.contains(child)); |
793 | return child.parentData! as TwoDimensionalViewportParentData; |
794 | } |
795 | |
796 | /// Returns the active child located at the provided [ChildVicinity], if there |
797 | /// is one. |
798 | /// |
799 | /// This can be used by subclasses to access currently active children to make |
800 | /// use of their size or [TwoDimensionalViewportParentData], such as when |
801 | /// overriding the [paint] method. |
802 | /// |
803 | /// Returns null if there is no active child for the given [ChildVicinity]. |
804 | @protected |
805 | RenderBox? getChildFor(covariant ChildVicinity vicinity) => _children[vicinity]; |
806 | |
807 | @override |
808 | void attach(PipelineOwner owner) { |
809 | super.attach(owner); |
810 | _horizontalOffset.addListener(markNeedsLayout); |
811 | _verticalOffset.addListener(markNeedsLayout); |
812 | _delegate.addListener(_handleDelegateNotification); |
813 | for (final RenderBox child in _children.values) { |
814 | child.attach(owner); |
815 | } |
816 | for (final RenderBox child in _keepAliveBucket.values) { |
817 | child.attach(owner); |
818 | } |
819 | } |
820 | |
821 | @override |
822 | void detach() { |
823 | super.detach(); |
824 | _horizontalOffset.removeListener(markNeedsLayout); |
825 | _verticalOffset.removeListener(markNeedsLayout); |
826 | _delegate.removeListener(_handleDelegateNotification); |
827 | for (final RenderBox child in _children.values) { |
828 | child.detach(); |
829 | } |
830 | for (final RenderBox child in _keepAliveBucket.values) { |
831 | child.detach(); |
832 | } |
833 | } |
834 | |
835 | @override |
836 | void redepthChildren() { |
837 | for (final RenderBox child in _children.values) { |
838 | child.redepthChildren(); |
839 | } |
840 | _keepAliveBucket.values.forEach(redepthChild); |
841 | } |
842 | |
843 | @override |
844 | void visitChildren(RenderObjectVisitor visitor) { |
845 | RenderBox? child = _firstChild; |
846 | while (child != null) { |
847 | visitor(child); |
848 | child = parentDataOf(child)._nextSibling; |
849 | } |
850 | _keepAliveBucket.values.forEach(visitor); |
851 | } |
852 | |
853 | @override |
854 | void visitChildrenForSemantics(RenderObjectVisitor visitor) { |
855 | // Only children that are visible should be visited, and they must be in |
856 | // paint order. |
857 | RenderBox? child = _firstChild; |
858 | while (child != null) { |
859 | final TwoDimensionalViewportParentData childParentData = parentDataOf(child); |
860 | visitor(child); |
861 | child = childParentData._nextSibling; |
862 | } |
863 | // Do not visit children in [_keepAliveBucket]. |
864 | } |
865 | |
866 | @override |
867 | List<DiagnosticsNode> debugDescribeChildren() { |
868 | final List<DiagnosticsNode> debugChildren = <DiagnosticsNode>[ |
869 | ..._children.keys.map<DiagnosticsNode>((ChildVicinity vicinity) { |
870 | return _children[vicinity]!.toDiagnosticsNode(name: vicinity.toString()); |
871 | }) |
872 | ]; |
873 | return debugChildren; |
874 | } |
875 | |
876 | @override |
877 | Size computeDryLayout(BoxConstraints constraints) { |
878 | assert(debugCheckHasBoundedAxis(Axis.vertical, constraints)); |
879 | assert(debugCheckHasBoundedAxis(Axis.horizontal, constraints)); |
880 | return constraints.biggest; |
881 | } |
882 | |
883 | @override |
884 | bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { |
885 | for (final RenderBox child in _children.values) { |
886 | final TwoDimensionalViewportParentData childParentData = parentDataOf(child); |
887 | if (!childParentData.isVisible) { |
888 | // Can't hit a child that is not visible. |
889 | continue; |
890 | } |
891 | final bool isHit = result.addWithPaintOffset( |
892 | offset: childParentData.paintOffset, |
893 | position: position, |
894 | hitTest: (BoxHitTestResult result, Offset transformed) { |
895 | assert(transformed == position - childParentData.paintOffset!); |
896 | return child.hitTest(result, position: transformed); |
897 | }, |
898 | ); |
899 | if (isHit) { |
900 | return true; |
901 | } |
902 | } |
903 | return false; |
904 | } |
905 | |
906 | /// The dimensions of the viewport. |
907 | /// |
908 | /// This [Size] represents the width and height of the visible area. |
909 | Size get viewportDimension { |
910 | assert(hasSize); |
911 | return size; |
912 | } |
913 | |
914 | @override |
915 | void performResize() { |
916 | final Size? oldSize = hasSize ? size : null; |
917 | super.performResize(); |
918 | // Ignoring return value since we are doing a layout either way |
919 | // (performLayout will be invoked next). |
920 | horizontalOffset.applyViewportDimension(size.width); |
921 | verticalOffset.applyViewportDimension(size.height); |
922 | if (oldSize != size) { |
923 | // Specs can depend on viewport size. |
924 | _didResize = true; |
925 | } |
926 | } |
927 | |
928 | @protected |
929 | @override |
930 | RevealedOffset getOffsetToReveal( |
931 | RenderObject target, |
932 | double alignment, { |
933 | Rect? rect, |
934 | Axis? axis, |
935 | }) { |
936 | // If an axis has not been specified, use the mainAxis. |
937 | axis ??= mainAxis; |
938 | |
939 | final (double offset, AxisDirection axisDirection) = switch (axis) { |
940 | Axis.vertical => (verticalOffset.pixels, verticalAxisDirection), |
941 | Axis.horizontal => (horizontalOffset.pixels, horizontalAxisDirection), |
942 | }; |
943 | |
944 | rect ??= target.paintBounds; |
945 | // `child` will be the last RenderObject before the viewport when walking |
946 | // up from `target`. |
947 | RenderObject child = target; |
948 | while (child.parent != this) { |
949 | child = child.parent!; |
950 | } |
951 | |
952 | assert(child.parent == this); |
953 | final RenderBox box = child as RenderBox; |
954 | final Rect rectLocal = MatrixUtils.transformRect(target.getTransformTo(child), rect); |
955 | |
956 | double leadingScrollOffset = offset; |
957 | |
958 | // The scroll offset of `rect` within `child`. |
959 | leadingScrollOffset += switch (axisDirection) { |
960 | AxisDirection.up => child.size.height - rectLocal.bottom, |
961 | AxisDirection.left => child.size.width - rectLocal.right, |
962 | AxisDirection.right => rectLocal.left, |
963 | AxisDirection.down => rectLocal.top, |
964 | }; |
965 | |
966 | // The scroll offset in the viewport to `rect`. |
967 | final Offset paintOffset = parentDataOf(box).paintOffset!; |
968 | leadingScrollOffset += switch (axisDirection) { |
969 | AxisDirection.up => viewportDimension.height - paintOffset.dy - box.size.height, |
970 | AxisDirection.left => viewportDimension.width - paintOffset.dx - box.size.width, |
971 | AxisDirection.right => paintOffset.dx, |
972 | AxisDirection.down => paintOffset.dy, |
973 | }; |
974 | |
975 | // This step assumes the viewport's layout is up-to-date, i.e., if |
976 | // the position is changed after the last performLayout, the new scroll |
977 | // position will not be accounted for. |
978 | final Matrix4 transform = target.getTransformTo(this); |
979 | Rect targetRect = MatrixUtils.transformRect(transform, rect); |
980 | |
981 | final double mainAxisExtentDifference = switch (axis) { |
982 | Axis.horizontal => viewportDimension.width - rectLocal.width, |
983 | Axis.vertical => viewportDimension.height - rectLocal.height, |
984 | }; |
985 | |
986 | final double targetOffset = leadingScrollOffset - mainAxisExtentDifference * alignment; |
987 | |
988 | final double offsetDifference = switch (axis) { |
989 | Axis.horizontal => horizontalOffset.pixels - targetOffset, |
990 | Axis.vertical => verticalOffset.pixels - targetOffset, |
991 | }; |
992 | |
993 | targetRect = switch (axisDirection) { |
994 | AxisDirection.up => targetRect.translate(0.0, -offsetDifference), |
995 | AxisDirection.down => targetRect.translate(0.0, offsetDifference), |
996 | AxisDirection.left => targetRect.translate(-offsetDifference, 0.0), |
997 | AxisDirection.right => targetRect.translate( offsetDifference, 0.0), |
998 | }; |
999 | |
1000 | final RevealedOffset revealedOffset = RevealedOffset( |
1001 | offset: targetOffset, |
1002 | rect: targetRect, |
1003 | ); |
1004 | return revealedOffset; |
1005 | } |
1006 | |
1007 | @override |
1008 | void showOnScreen({ |
1009 | RenderObject? descendant, |
1010 | Rect? rect, |
1011 | Duration duration = Duration.zero, |
1012 | Curve curve = Curves.ease, |
1013 | }) { |
1014 | // It is possible for one and not both axes to allow for implicit scrolling, |
1015 | // so handling is split between the options for allowed implicit scrolling. |
1016 | final bool allowHorizontal = horizontalOffset.allowImplicitScrolling; |
1017 | final bool allowVertical = verticalOffset.allowImplicitScrolling; |
1018 | AxisDirection? axisDirection; |
1019 | switch ((allowHorizontal, allowVertical)) { |
1020 | case (true, true): |
1021 | // Both allow implicit scrolling. |
1022 | break; |
1023 | case (false, true): |
1024 | // Only the vertical Axis allows implicit scrolling. |
1025 | axisDirection = verticalAxisDirection; |
1026 | case (true, false): |
1027 | // Only the horizontal Axis allows implicit scrolling. |
1028 | axisDirection = horizontalAxisDirection; |
1029 | case (false, false): |
1030 | // Neither axis allows for implicit scrolling. |
1031 | return super.showOnScreen( |
1032 | descendant: descendant, |
1033 | rect: rect, |
1034 | duration: duration, |
1035 | curve: curve, |
1036 | ); |
1037 | } |
1038 | |
1039 | final Rect? newRect = RenderTwoDimensionalViewport.showInViewport( |
1040 | descendant: descendant, |
1041 | viewport: this, |
1042 | axisDirection: axisDirection, |
1043 | rect: rect, |
1044 | duration: duration, |
1045 | curve: curve, |
1046 | ); |
1047 | |
1048 | super.showOnScreen( |
1049 | rect: newRect, |
1050 | duration: duration, |
1051 | curve: curve, |
1052 | ); |
1053 | } |
1054 | |
1055 | /// Make (a portion of) the given `descendant` of the given `viewport` fully |
1056 | /// visible in one or both dimensions of the `viewport` by manipulating the |
1057 | /// [ViewportOffset]s. |
1058 | /// |
1059 | /// The `axisDirection` determines from which axes the `descendant` will be |
1060 | /// revealed. When the `axisDirection` is null, both axes will be updated to |
1061 | /// reveal the descendant. |
1062 | /// |
1063 | /// The optional `rect` parameter describes which area of the `descendant` |
1064 | /// should be shown in the viewport. If `rect` is null, the entire |
1065 | /// `descendant` will be revealed. The `rect` parameter is interpreted |
1066 | /// relative to the coordinate system of `descendant`. |
1067 | /// |
1068 | /// The returned [Rect] describes the new location of `descendant` or `rect` |
1069 | /// in the viewport after it has been revealed. See [RevealedOffset.rect] |
1070 | /// for a full definition of this [Rect]. |
1071 | /// |
1072 | /// The parameter `viewport` is required and cannot be null. If `descendant` |
1073 | /// is null, this is a no-op and `rect` is returned. |
1074 | /// |
1075 | /// If both `descendant` and `rect` are null, null is returned because there |
1076 | /// is nothing to be shown in the viewport. |
1077 | /// |
1078 | /// The `duration` parameter can be set to a non-zero value to animate the |
1079 | /// target object into the viewport with an animation defined by `curve`. |
1080 | /// |
1081 | /// See also: |
1082 | /// |
1083 | /// * [RenderObject.showOnScreen], overridden by |
1084 | /// [RenderTwoDimensionalViewport] to delegate to this method. |
1085 | static Rect? showInViewport({ |
1086 | RenderObject? descendant, |
1087 | Rect? rect, |
1088 | required RenderTwoDimensionalViewport viewport, |
1089 | Duration duration = Duration.zero, |
1090 | Curve curve = Curves.ease, |
1091 | AxisDirection? axisDirection, |
1092 | }) { |
1093 | if (descendant == null) { |
1094 | return rect; |
1095 | } |
1096 | |
1097 | Rect? showVertical(Rect? rect) { |
1098 | return RenderTwoDimensionalViewport._showInViewportForAxisDirection( |
1099 | descendant: descendant, |
1100 | viewport: viewport, |
1101 | axis: Axis.vertical, |
1102 | rect: rect, |
1103 | duration: duration, |
1104 | curve: curve, |
1105 | ); |
1106 | } |
1107 | |
1108 | Rect? showHorizontal(Rect? rect) { |
1109 | return RenderTwoDimensionalViewport._showInViewportForAxisDirection( |
1110 | descendant: descendant, |
1111 | viewport: viewport, |
1112 | axis: Axis.horizontal, |
1113 | rect: rect, |
1114 | duration: duration, |
1115 | curve: curve, |
1116 | ); |
1117 | } |
1118 | |
1119 | switch (axisDirection) { |
1120 | case AxisDirection.left: |
1121 | case AxisDirection.right: |
1122 | return showHorizontal(rect); |
1123 | case AxisDirection.up: |
1124 | case AxisDirection.down: |
1125 | return showVertical(rect); |
1126 | case null: |
1127 | // Update rect after revealing in one axis before revealing in the next. |
1128 | rect = showHorizontal(rect) ?? rect; |
1129 | // We only return the final rect after both have been revealed. |
1130 | rect = showVertical(rect); |
1131 | if (rect == null) { |
1132 | // `descendant` is between leading and trailing edge and hence already |
1133 | // fully shown on screen. |
1134 | assert(viewport.parent != null); |
1135 | final Matrix4 transform = descendant.getTransformTo(viewport.parent); |
1136 | return MatrixUtils.transformRect( |
1137 | transform, |
1138 | rect ?? descendant.paintBounds, |
1139 | ); |
1140 | } |
1141 | return rect; |
1142 | } |
1143 | } |
1144 | |
1145 | static Rect? _showInViewportForAxisDirection({ |
1146 | required RenderObject descendant, |
1147 | Rect? rect, |
1148 | required RenderTwoDimensionalViewport viewport, |
1149 | required Axis axis, |
1150 | Duration duration = Duration.zero, |
1151 | Curve curve = Curves.ease, |
1152 | }) { |
1153 | final ViewportOffset offset = switch (axis) { |
1154 | Axis.vertical => viewport.verticalOffset, |
1155 | Axis.horizontal => viewport.horizontalOffset, |
1156 | }; |
1157 | |
1158 | final RevealedOffset leadingEdgeOffset = viewport.getOffsetToReveal( |
1159 | descendant, |
1160 | 0.0, |
1161 | rect: rect, |
1162 | axis: axis, |
1163 | ); |
1164 | final RevealedOffset trailingEdgeOffset = viewport.getOffsetToReveal( |
1165 | descendant, |
1166 | 1.0, |
1167 | rect: rect, |
1168 | axis: axis, |
1169 | ); |
1170 | final double currentOffset = offset.pixels; |
1171 | |
1172 | final RevealedOffset? targetOffset = RevealedOffset.clampOffset( |
1173 | leadingEdgeOffset: leadingEdgeOffset, |
1174 | trailingEdgeOffset: trailingEdgeOffset, |
1175 | currentOffset: currentOffset, |
1176 | ); |
1177 | if (targetOffset == null) { |
1178 | // Already visible in this axis. |
1179 | return null; |
1180 | } |
1181 | |
1182 | offset.moveTo(targetOffset.offset, duration: duration, curve: curve); |
1183 | return targetOffset.rect; |
1184 | } |
1185 | |
1186 | /// Should be used by subclasses to invalidate any cached metrics for the |
1187 | /// viewport. |
1188 | /// |
1189 | /// This is set to true when the viewport has been resized, indicating that |
1190 | /// any cached dimensions are invalid. |
1191 | /// |
1192 | /// After performLayout, the value is set to false until the viewport |
1193 | /// dimensions are changed again in [performResize]. |
1194 | /// |
1195 | /// Subclasses are not required to use this value, but it can be used to |
1196 | /// safely cache layout information in between layout calls. |
1197 | bool get didResize => _didResize; |
1198 | bool _didResize = true; |
1199 | |
1200 | /// Should be used by subclasses to invalidate any cached data from the |
1201 | /// [delegate]. |
1202 | /// |
1203 | /// This value is set to false after [layoutChildSequence]. If |
1204 | /// [markNeedsLayout] is called `withDelegateRebuild` set to true, then this |
1205 | /// value will be updated to true, signifying any cached delegate information |
1206 | /// needs to be updated in the next call to [layoutChildSequence]. |
1207 | /// |
1208 | /// Subclasses are not required to use this value, but it can be used to |
1209 | /// safely cache layout information in between layout calls. |
1210 | @protected |
1211 | bool get needsDelegateRebuild => _needsDelegateRebuild; |
1212 | bool _needsDelegateRebuild = true; |
1213 | |
1214 | @override |
1215 | void markNeedsLayout({ bool withDelegateRebuild = false }) { |
1216 | _needsDelegateRebuild = _needsDelegateRebuild || withDelegateRebuild; |
1217 | super.markNeedsLayout(); |
1218 | } |
1219 | |
1220 | /// Primary work horse of [performLayout]. |
1221 | /// |
1222 | /// Subclasses must implement this method to layout the children of the |
1223 | /// viewport. The [TwoDimensionalViewportParentData.layoutOffset] must be set |
1224 | /// during this method in order for the children to be positioned during paint. |
1225 | /// Further, children of the viewport must be laid out with the expectation |
1226 | /// that the parent (this viewport) will use their size. |
1227 | /// |
1228 | /// ```dart |
1229 | /// child.layout(constraints, parentUsesSize: true); |
1230 | /// ``` |
1231 | /// |
1232 | /// The primary methods used for creating and obtaining children is |
1233 | /// [buildOrObtainChildFor], which takes a [ChildVicinity] that is used by the |
1234 | /// [TwoDimensionalChildDelegate]. If a child is not provided by the delegate |
1235 | /// for the provided vicinity, the method will return null, otherwise, it will |
1236 | /// return the [RenderBox] of the child. |
1237 | /// |
1238 | /// After [layoutChildSequence] is completed, any remaining children that were |
1239 | /// not obtained will be disposed. |
1240 | void layoutChildSequence(); |
1241 | |
1242 | @override |
1243 | void performLayout() { |
1244 | _firstChild = null; |
1245 | _lastChild = null; |
1246 | _activeChildrenForLayoutPass.clear(); |
1247 | _childManager._startLayout(); |
1248 | |
1249 | // Subclass lays out children. |
1250 | layoutChildSequence(); |
1251 | |
1252 | assert(_debugCheckContentDimensions()); |
1253 | _didResize = false; |
1254 | _needsDelegateRebuild = false; |
1255 | _cacheKeepAlives(); |
1256 | invokeLayoutCallback<BoxConstraints>((BoxConstraints _) { |
1257 | _childManager._endLayout(); |
1258 | assert(_debugOrphans?.isEmpty ?? true); |
1259 | assert(_debugDanglingKeepAlives.isEmpty); |
1260 | // Ensure we are not keeping anything alive that should not be any longer. |
1261 | assert(_keepAliveBucket.values.where((RenderBox child) { |
1262 | return !parentDataOf(child).keepAlive; |
1263 | }).isEmpty); |
1264 | // Organize children in paint order and complete parent data after |
1265 | // un-used children are disposed of by the childManager. |
1266 | _reifyChildren(); |
1267 | }); |
1268 | } |
1269 | |
1270 | void _cacheKeepAlives() { |
1271 | final List<RenderBox> remainingChildren = _children.values.toSet().difference( |
1272 | _activeChildrenForLayoutPass.values.toSet() |
1273 | ).toList(); |
1274 | for (final RenderBox child in remainingChildren) { |
1275 | final TwoDimensionalViewportParentData childParentData = parentDataOf(child); |
1276 | if (childParentData.keepAlive) { |
1277 | _keepAliveBucket[childParentData.vicinity] = child; |
1278 | // Let the child manager know we intend to keep this. |
1279 | _childManager._reuseChild(childParentData.vicinity); |
1280 | } |
1281 | } |
1282 | } |
1283 | |
1284 | // Ensures all children have a layoutOffset, sets paintExtent & paintOffset, |
1285 | // and arranges children in paint order. |
1286 | void _reifyChildren() { |
1287 | assert(_leadingXIndex != null); |
1288 | assert(_trailingXIndex != null); |
1289 | assert(_leadingYIndex != null); |
1290 | assert(_trailingYIndex != null); |
1291 | assert(_firstChild == null); |
1292 | assert(_lastChild == null); |
1293 | RenderBox? previousChild; |
1294 | switch (mainAxis) { |
1295 | case Axis.vertical: |
1296 | // Row major traversal. |
1297 | // This seems backwards, but the vertical axis is the typical default |
1298 | // axis for scrolling in Flutter, while Row-major ordering is the |
1299 | // typical default for matrices, which is why the inverse follows |
1300 | // through in the horizontal case below. |
1301 | // Minor |
1302 | for (int minorIndex = _leadingYIndex!; minorIndex <= _trailingYIndex!; minorIndex++) { |
1303 | // Major |
1304 | for (int majorIndex = _leadingXIndex!; majorIndex <= _trailingXIndex!; majorIndex++) { |
1305 | final ChildVicinity vicinity = ChildVicinity(xIndex: majorIndex, yIndex: minorIndex); |
1306 | previousChild = _completeChildParentData( |
1307 | vicinity, |
1308 | previousChild: previousChild, |
1309 | ) ?? previousChild; |
1310 | } |
1311 | } |
1312 | case Axis.horizontal: |
1313 | // Column major traversal |
1314 | // Minor |
1315 | for (int minorIndex = _leadingXIndex!; minorIndex <= _trailingXIndex!; minorIndex++) { |
1316 | // Major |
1317 | for (int majorIndex = _leadingYIndex!; majorIndex <= _trailingYIndex!; majorIndex++) { |
1318 | final ChildVicinity vicinity = ChildVicinity(xIndex: minorIndex, yIndex: majorIndex); |
1319 | previousChild = _completeChildParentData( |
1320 | vicinity, |
1321 | previousChild: previousChild, |
1322 | ) ?? previousChild; |
1323 | } |
1324 | } |
1325 | } |
1326 | _lastChild = previousChild; |
1327 | if (_lastChild != null) { |
1328 | parentDataOf(_lastChild!)._nextSibling = null; |
1329 | } |
1330 | // Reset for next layout pass. |
1331 | _leadingXIndex = null; |
1332 | _trailingXIndex = null; |
1333 | _leadingYIndex = null; |
1334 | _trailingYIndex = null; |
1335 | } |
1336 | |
1337 | RenderBox? _completeChildParentData(ChildVicinity vicinity, { RenderBox? previousChild }) { |
1338 | assert(vicinity != ChildVicinity.invalid); |
1339 | // It is possible and valid for a vicinity to be skipped. |
1340 | // For example, a table can have merged cells, spanning multiple |
1341 | // indices, but only represented by one RenderBox and ChildVicinity. |
1342 | if (_children.containsKey(vicinity)) { |
1343 | final RenderBox child = _children[vicinity]!; |
1344 | assert(parentDataOf(child).vicinity == vicinity); |
1345 | updateChildPaintData(child); |
1346 | if (previousChild == null) { |
1347 | // _firstChild is only set once. |
1348 | assert(_firstChild == null); |
1349 | _firstChild = child; |
1350 | } else { |
1351 | parentDataOf(previousChild)._nextSibling = child; |
1352 | parentDataOf(child)._previousSibling = previousChild; |
1353 | } |
1354 | return child; |
1355 | } |
1356 | return null; |
1357 | } |
1358 | |
1359 | bool _debugCheckContentDimensions() { |
1360 | const String hint = 'Subclasses should call applyContentDimensions on the ' |
1361 | 'verticalOffset and horizontalOffset to set the min and max scroll offset. ' |
1362 | 'If the contents exceed one or both sides of the viewportDimension, ' |
1363 | 'ensure the viewportDimension height or width is subtracted in that axis ' |
1364 | 'for the correct extent.' ; |
1365 | assert(() { |
1366 | if (!(verticalOffset as ScrollPosition).hasContentDimensions) { |
1367 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
1368 | ErrorSummary( |
1369 | 'The verticalOffset was not given content dimensions during ' |
1370 | 'layoutChildSequence.' |
1371 | ), |
1372 | ErrorHint(hint), |
1373 | ]); |
1374 | } |
1375 | return true; |
1376 | }()); |
1377 | assert(() { |
1378 | if (!(horizontalOffset as ScrollPosition).hasContentDimensions) { |
1379 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
1380 | ErrorSummary( |
1381 | 'The horizontalOffset was not given content dimensions during ' |
1382 | 'layoutChildSequence.' |
1383 | ), |
1384 | ErrorHint(hint), |
1385 | ]); |
1386 | } |
1387 | return true; |
1388 | }()); |
1389 | return true; |
1390 | } |
1391 | |
1392 | /// Returns the child for a given [ChildVicinity], should be called during |
1393 | /// [layoutChildSequence] in order to instantiate or retrieve children. |
1394 | /// |
1395 | /// This method will build the child if it has not been already, or will reuse |
1396 | /// it if it already exists, whether it was part of the previous frame or kept |
1397 | /// alive. |
1398 | /// |
1399 | /// Children for the given [ChildVicinity] will be inserted into the active |
1400 | /// children list, and so should be visible, or contained within the |
1401 | /// [cacheExtent]. |
1402 | RenderBox? buildOrObtainChildFor(ChildVicinity vicinity) { |
1403 | assert(vicinity != ChildVicinity.invalid); |
1404 | // This should only be called during layout. |
1405 | assert(debugDoingThisLayout); |
1406 | if (_leadingXIndex == null || _trailingXIndex == null || _leadingXIndex == null || _trailingYIndex == null) { |
1407 | // First child of this layout pass. Set leading and trailing trackers. |
1408 | _leadingXIndex = vicinity.xIndex; |
1409 | _trailingXIndex = vicinity.xIndex; |
1410 | _leadingYIndex = vicinity.yIndex; |
1411 | _trailingYIndex = vicinity.yIndex; |
1412 | } else { |
1413 | // If any of these are still null, we missed a child. |
1414 | assert(_leadingXIndex != null); |
1415 | assert(_trailingXIndex != null); |
1416 | assert(_leadingYIndex != null); |
1417 | assert(_trailingYIndex != null); |
1418 | |
1419 | // Update as we go. |
1420 | _leadingXIndex = math.min(vicinity.xIndex, _leadingXIndex!); |
1421 | _trailingXIndex = math.max(vicinity.xIndex, _trailingXIndex!); |
1422 | _leadingYIndex = math.min(vicinity.yIndex, _leadingYIndex!); |
1423 | _trailingYIndex = math.max(vicinity.yIndex, _trailingYIndex!); |
1424 | } |
1425 | if (_needsDelegateRebuild || (!_children.containsKey(vicinity) && !_keepAliveBucket.containsKey(vicinity))) { |
1426 | invokeLayoutCallback<BoxConstraints>((BoxConstraints _) { |
1427 | _childManager._buildChild(vicinity); |
1428 | }); |
1429 | } else { |
1430 | _keepAliveBucket.remove(vicinity); |
1431 | _childManager._reuseChild(vicinity); |
1432 | } |
1433 | if (!_children.containsKey(vicinity)) { |
1434 | // There is no child for this vicinity, we may have reached the end of the |
1435 | // children in one or both of the x/y indices. |
1436 | return null; |
1437 | } |
1438 | |
1439 | assert(_children.containsKey(vicinity)); |
1440 | final RenderBox child = _children[vicinity]!; |
1441 | _activeChildrenForLayoutPass[vicinity] = child; |
1442 | parentDataOf(child).vicinity = vicinity; |
1443 | return child; |
1444 | } |
1445 | |
1446 | /// Called after [layoutChildSequence] to compute the |
1447 | /// [TwoDimensionalViewportParentData.paintOffset] and |
1448 | /// [TwoDimensionalViewportParentData._paintExtent] of the child. |
1449 | void updateChildPaintData(RenderBox child) { |
1450 | final TwoDimensionalViewportParentData childParentData = parentDataOf(child); |
1451 | assert( |
1452 | childParentData.layoutOffset != null, |
1453 | 'The child with ChildVicinity(xIndex: ${childParentData.vicinity.xIndex}, ' |
1454 | 'yIndex: ${childParentData.vicinity.yIndex}) was not provided a ' |
1455 | 'layoutOffset. This should be set during layoutChildSequence, ' |
1456 | 'representing the position of the child.' |
1457 | ); |
1458 | assert(child.hasSize); // Child must have been laid out by now. |
1459 | |
1460 | // Set paintExtent (and visibility) |
1461 | childParentData._paintExtent = computeChildPaintExtent( |
1462 | childParentData.layoutOffset!, |
1463 | child.size, |
1464 | ); |
1465 | // Set paintOffset |
1466 | childParentData.paintOffset = computeAbsolutePaintOffsetFor( |
1467 | child, |
1468 | layoutOffset: childParentData.layoutOffset!, |
1469 | ); |
1470 | // If the child is partially visible, or not visible at all, there is |
1471 | // visual overflow. |
1472 | _hasVisualOverflow = _hasVisualOverflow |
1473 | || childParentData.layoutOffset != childParentData._paintExtent |
1474 | || !childParentData.isVisible; |
1475 | } |
1476 | |
1477 | /// Computes the portion of the child that is visible, assuming that only the |
1478 | /// region from the [ViewportOffset.pixels] of both dimensions to the |
1479 | /// [cacheExtent] is visible, and that the relationship between scroll offsets |
1480 | /// and paint offsets is linear. |
1481 | /// |
1482 | /// For example, if the [ViewportOffset]s each have a scroll offset of 100 and |
1483 | /// the arguments to this method describe a child with [layoutOffset] of |
1484 | /// `Offset(50.0, 50.0)`, with a size of `Size(200.0, 200.0)`, then the |
1485 | /// returned value would be `Size(150.0, 150.0)`, representing the visible |
1486 | /// extent of the child. |
1487 | Size computeChildPaintExtent(Offset layoutOffset, Size childSize) { |
1488 | if (childSize == Size.zero || childSize.height == 0.0 || childSize.width == 0.0) { |
1489 | return Size.zero; |
1490 | } |
1491 | // Horizontal extent |
1492 | final double width; |
1493 | if (layoutOffset.dx < 0.0) { |
1494 | // The child is positioned beyond the leading edge of the viewport. |
1495 | if (layoutOffset.dx + childSize.width <= 0.0) { |
1496 | // The child does not extend into the viewable area, it is not visible. |
1497 | return Size.zero; |
1498 | } |
1499 | // If the child is positioned starting at -50, then the paint extent is |
1500 | // the width + (-50). |
1501 | width = layoutOffset.dx + childSize.width; |
1502 | } else if (layoutOffset.dx >= viewportDimension.width) { |
1503 | // The child is positioned after the trailing edge of the viewport, also |
1504 | // not visible. |
1505 | return Size.zero; |
1506 | } else { |
1507 | // The child is positioned within the viewport bounds, but may extend |
1508 | // beyond it. |
1509 | assert(layoutOffset.dx >= 0 && layoutOffset.dx < viewportDimension.width); |
1510 | if (layoutOffset.dx + childSize.width > viewportDimension.width) { |
1511 | width = viewportDimension.width - layoutOffset.dx; |
1512 | } else { |
1513 | assert(layoutOffset.dx + childSize.width <= viewportDimension.width); |
1514 | width = childSize.width; |
1515 | } |
1516 | } |
1517 | |
1518 | // Vertical extent |
1519 | final double height; |
1520 | if (layoutOffset.dy < 0.0) { |
1521 | // The child is positioned beyond the leading edge of the viewport. |
1522 | if (layoutOffset.dy + childSize.height <= 0.0) { |
1523 | // The child does not extend into the viewable area, it is not visible. |
1524 | return Size.zero; |
1525 | } |
1526 | // If the child is positioned starting at -50, then the paint extent is |
1527 | // the width + (-50). |
1528 | height = layoutOffset.dy + childSize.height; |
1529 | } else if (layoutOffset.dy >= viewportDimension.height) { |
1530 | // The child is positioned after the trailing edge of the viewport, also |
1531 | // not visible. |
1532 | return Size.zero; |
1533 | } else { |
1534 | // The child is positioned within the viewport bounds, but may extend |
1535 | // beyond it. |
1536 | assert(layoutOffset.dy >= 0 && layoutOffset.dy < viewportDimension.height); |
1537 | if (layoutOffset.dy + childSize.height > viewportDimension.height) { |
1538 | height = viewportDimension.height - layoutOffset.dy; |
1539 | } else { |
1540 | assert(layoutOffset.dy + childSize.height <= viewportDimension.height); |
1541 | height = childSize.height; |
1542 | } |
1543 | } |
1544 | |
1545 | return Size(width, height); |
1546 | } |
1547 | |
1548 | /// The offset at which the given `child` should be painted. |
1549 | /// |
1550 | /// The returned offset is from the top left corner of the inside of the |
1551 | /// viewport to the top left corner of the paint coordinate system of the |
1552 | /// `child`. |
1553 | /// |
1554 | /// This is useful when the one or both of the axes of the viewport are |
1555 | /// reversed. The normalized layout offset of the child is used to compute |
1556 | /// the paint offset in relation to the [verticalAxisDirection] and |
1557 | /// [horizontalAxisDirection]. |
1558 | @protected |
1559 | Offset computeAbsolutePaintOffsetFor( |
1560 | RenderBox child, { |
1561 | required Offset layoutOffset, |
1562 | }) { |
1563 | // This is only usable once we have sizes. |
1564 | assert(hasSize); |
1565 | assert(child.hasSize); |
1566 | final double xOffset = switch (horizontalAxisDirection) { |
1567 | AxisDirection.right => layoutOffset.dx, |
1568 | AxisDirection.left => viewportDimension.width - (layoutOffset.dx + child.size.width), |
1569 | AxisDirection.up || AxisDirection.down => throw Exception('This should not happen' ), |
1570 | }; |
1571 | final double yOffset = switch (verticalAxisDirection) { |
1572 | AxisDirection.up => viewportDimension.height - (layoutOffset.dy + child.size.height), |
1573 | AxisDirection.down => layoutOffset.dy, |
1574 | AxisDirection.right || AxisDirection.left => throw Exception('This should not happen' ), |
1575 | }; |
1576 | return Offset(xOffset, yOffset); |
1577 | } |
1578 | |
1579 | @override |
1580 | void paint(PaintingContext context, Offset offset) { |
1581 | if (_children.isEmpty) { |
1582 | return; |
1583 | } |
1584 | if (_hasVisualOverflow && clipBehavior != Clip.none) { |
1585 | _clipRectLayer.layer = context.pushClipRect( |
1586 | needsCompositing, |
1587 | offset, |
1588 | Offset.zero & viewportDimension, |
1589 | _paintChildren, |
1590 | clipBehavior: clipBehavior, |
1591 | oldLayer: _clipRectLayer.layer, |
1592 | ); |
1593 | } else { |
1594 | _clipRectLayer.layer = null; |
1595 | _paintChildren(context, offset); |
1596 | } |
1597 | } |
1598 | |
1599 | void _paintChildren(PaintingContext context, Offset offset) { |
1600 | RenderBox? child = _firstChild; |
1601 | while (child != null) { |
1602 | final TwoDimensionalViewportParentData childParentData = parentDataOf(child); |
1603 | if (childParentData.isVisible) { |
1604 | context.paintChild(child, offset + childParentData.paintOffset!); |
1605 | } |
1606 | child = childParentData._nextSibling; |
1607 | } |
1608 | } |
1609 | |
1610 | // ---- Called from _TwoDimensionalViewportElement ---- |
1611 | |
1612 | void _insertChild(RenderBox child, ChildVicinity slot) { |
1613 | assert(_debugTrackOrphans(newOrphan: _children[slot])); |
1614 | assert(!_keepAliveBucket.containsValue(child)); |
1615 | _children[slot] = child; |
1616 | adoptChild(child); |
1617 | } |
1618 | |
1619 | void _moveChild(RenderBox child, {required ChildVicinity from, required ChildVicinity to}) { |
1620 | final TwoDimensionalViewportParentData childParentData = parentDataOf(child); |
1621 | if (!childParentData.keptAlive) { |
1622 | if (_children[from] == child) { |
1623 | _children.remove(from); |
1624 | } |
1625 | assert(_debugTrackOrphans(newOrphan: _children[to], noLongerOrphan: child)); |
1626 | _children[to] = child; |
1627 | return; |
1628 | } |
1629 | // If the child in the bucket is not current child, that means someone has |
1630 | // already moved and replaced current child, and we cannot remove this |
1631 | // child. |
1632 | if (_keepAliveBucket[childParentData.vicinity] == child) { |
1633 | _keepAliveBucket.remove(childParentData.vicinity); |
1634 | } |
1635 | assert(() { |
1636 | _debugDanglingKeepAlives.remove(child); |
1637 | return true; |
1638 | }()); |
1639 | // If there is an existing child in the new slot, that mean that child |
1640 | // will be moved to other index. In other cases, the existing child should |
1641 | // have been removed by _removeChild. Thus, it is ok to overwrite it. |
1642 | assert(() { |
1643 | if (_keepAliveBucket.containsKey(childParentData.vicinity)) { |
1644 | _debugDanglingKeepAlives.add(_keepAliveBucket[childParentData.vicinity]!); |
1645 | } |
1646 | return true; |
1647 | }()); |
1648 | _keepAliveBucket[childParentData.vicinity] = child; |
1649 | } |
1650 | |
1651 | void _removeChild(RenderBox child, ChildVicinity slot) { |
1652 | final TwoDimensionalViewportParentData childParentData = parentDataOf(child); |
1653 | if (!childParentData.keptAlive) { |
1654 | if (_children[slot] == child) { |
1655 | _children.remove(slot); |
1656 | } |
1657 | assert(_debugTrackOrphans(noLongerOrphan: child)); |
1658 | if (_keepAliveBucket[childParentData.vicinity] == child) { |
1659 | _keepAliveBucket.remove(childParentData.vicinity); |
1660 | } |
1661 | assert(_keepAliveBucket[childParentData.vicinity] != child); |
1662 | dropChild(child); |
1663 | return; |
1664 | } |
1665 | assert(_keepAliveBucket[childParentData.vicinity] == child); |
1666 | assert(() { |
1667 | _debugDanglingKeepAlives.remove(child); |
1668 | return true; |
1669 | }()); |
1670 | _keepAliveBucket.remove(childParentData.vicinity); |
1671 | dropChild(child); |
1672 | } |
1673 | |
1674 | List<RenderBox>? _debugOrphans; |
1675 | |
1676 | // When a child is inserted into a slot currently occupied by another child, |
1677 | // it becomes an orphan until it is either moved to another slot or removed. |
1678 | bool _debugTrackOrphans({RenderBox? newOrphan, RenderBox? noLongerOrphan}) { |
1679 | assert(() { |
1680 | _debugOrphans ??= <RenderBox>[]; |
1681 | if (newOrphan != null) { |
1682 | _debugOrphans!.add(newOrphan); |
1683 | } |
1684 | if (noLongerOrphan != null) { |
1685 | _debugOrphans!.remove(noLongerOrphan); |
1686 | } |
1687 | return true; |
1688 | }()); |
1689 | return true; |
1690 | } |
1691 | |
1692 | /// Throws an exception saying that the object does not support returning |
1693 | /// intrinsic dimensions if, in debug mode, we are not in the |
1694 | /// [RenderObject.debugCheckingIntrinsics] mode. |
1695 | /// |
1696 | /// This is used by [computeMinIntrinsicWidth] et al because viewports do not |
1697 | /// generally support returning intrinsic dimensions. See the discussion at |
1698 | /// [computeMinIntrinsicWidth]. |
1699 | @protected |
1700 | bool debugThrowIfNotCheckingIntrinsics() { |
1701 | assert(() { |
1702 | if (!RenderObject.debugCheckingIntrinsics) { |
1703 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
1704 | ErrorSummary(' $runtimeType does not support returning intrinsic dimensions.' ), |
1705 | ErrorDescription( |
1706 | 'Calculating the intrinsic dimensions would require instantiating every child of ' |
1707 | 'the viewport, which defeats the point of viewports being lazy.' , |
1708 | ), |
1709 | ]); |
1710 | } |
1711 | return true; |
1712 | }()); |
1713 | return true; |
1714 | } |
1715 | |
1716 | @override |
1717 | double computeMinIntrinsicWidth(double height) { |
1718 | assert(debugThrowIfNotCheckingIntrinsics()); |
1719 | return 0.0; |
1720 | } |
1721 | |
1722 | @override |
1723 | double computeMaxIntrinsicWidth(double height) { |
1724 | assert(debugThrowIfNotCheckingIntrinsics()); |
1725 | return 0.0; |
1726 | } |
1727 | |
1728 | @override |
1729 | double computeMinIntrinsicHeight(double width) { |
1730 | assert(debugThrowIfNotCheckingIntrinsics()); |
1731 | return 0.0; |
1732 | } |
1733 | |
1734 | @override |
1735 | double computeMaxIntrinsicHeight(double width) { |
1736 | assert(debugThrowIfNotCheckingIntrinsics()); |
1737 | return 0.0; |
1738 | } |
1739 | |
1740 | @override |
1741 | void applyPaintTransform(RenderBox child, Matrix4 transform) { |
1742 | final Offset paintOffset = parentDataOf(child).paintOffset!; |
1743 | transform.translate(paintOffset.dx, paintOffset.dy); |
1744 | } |
1745 | |
1746 | @override |
1747 | void dispose() { |
1748 | _clipRectLayer.layer = null; |
1749 | super.dispose(); |
1750 | } |
1751 | } |
1752 | |
1753 | /// A delegate used by [RenderTwoDimensionalViewport] to manage its children. |
1754 | /// |
1755 | /// [RenderTwoDimensionalViewport] objects reify their children lazily to avoid |
1756 | /// spending resources on children that are not visible in the viewport. This |
1757 | /// delegate lets these objects create, reuse and remove children. |
1758 | abstract class TwoDimensionalChildManager { |
1759 | void _startLayout(); |
1760 | void _buildChild(ChildVicinity vicinity); |
1761 | void _reuseChild(ChildVicinity vicinity); |
1762 | void _endLayout(); |
1763 | } |
1764 | |
1765 | /// The relative position of a child in a [TwoDimensionalViewport] in relation |
1766 | /// to other children of the viewport. |
1767 | /// |
1768 | /// While children can be plotted arbitrarily in two dimensional space, the |
1769 | /// [ChildVicinity] is used to disambiguate their positions, determining how to |
1770 | /// traverse the children of the space. |
1771 | /// |
1772 | /// Combined with the [RenderTwoDimensionalViewport.mainAxis], each child's |
1773 | /// vicinity determines its paint order among all of the children. |
1774 | @immutable |
1775 | class ChildVicinity implements Comparable<ChildVicinity> { |
1776 | /// Creates a reference to a child in a two dimensional plane, with the |
1777 | /// [xIndex] and [yIndex] being relative to other children in the viewport. |
1778 | const ChildVicinity({ |
1779 | required this.xIndex, |
1780 | required this.yIndex, |
1781 | }) : assert(xIndex >= -1), |
1782 | assert(yIndex >= -1); |
1783 | |
1784 | /// Represents an unassigned child position. The given child may be in the |
1785 | /// process of moving from one position to another. |
1786 | static const ChildVicinity invalid = ChildVicinity(xIndex: -1, yIndex: -1); |
1787 | |
1788 | /// The index of the child in the horizontal axis, relative to neighboring |
1789 | /// children. |
1790 | /// |
1791 | /// While children's offset and positioning may not be strictly defined in |
1792 | /// terms of rows and columns, like a table, [ChildVicinity.xIndex] and |
1793 | /// [ChildVicinity.yIndex] represents order of traversal in row or column |
1794 | /// major format. |
1795 | final int xIndex; |
1796 | |
1797 | /// The index of the child in the vertical axis, relative to neighboring |
1798 | /// children. |
1799 | /// |
1800 | /// While children's offset and positioning may not be strictly defined in |
1801 | /// terms of rows and columns, like a table, [ChildVicinity.xIndex] and |
1802 | /// [ChildVicinity.yIndex] represents order of traversal in row or column |
1803 | /// major format. |
1804 | final int yIndex; |
1805 | |
1806 | @override |
1807 | bool operator ==(Object other) { |
1808 | return other is ChildVicinity |
1809 | && other.xIndex == xIndex |
1810 | && other.yIndex == yIndex; |
1811 | } |
1812 | |
1813 | @override |
1814 | int get hashCode => Object.hash(xIndex, yIndex); |
1815 | |
1816 | @override |
1817 | int compareTo(ChildVicinity other) { |
1818 | if (xIndex == other.xIndex) { |
1819 | return yIndex - other.yIndex; |
1820 | } |
1821 | return xIndex - other.xIndex; |
1822 | } |
1823 | |
1824 | @override |
1825 | String toString() { |
1826 | return '(xIndex: $xIndex, yIndex: $yIndex)' ; |
1827 | } |
1828 | } |
1829 | |