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