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
5import 'dart:math' as math;
6
7import 'package:flutter/animation.dart';
8import 'package:flutter/rendering.dart';
9
10import 'framework.dart';
11import 'scroll_delegate.dart';
12import 'scroll_notification.dart';
13import 'scroll_position.dart';
14
15export '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.
57typedef 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.
129abstract 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
225class _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.
395class 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].
501abstract 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.
1764abstract 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
1781class 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