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