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