blob: 69976eb1e48c78704436528cae0dac601d3872e8 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/rendering.dart';
import 'framework.dart';
import 'scroll_delegate.dart';
import 'scroll_notification.dart';
import 'scroll_position.dart';
export 'package:flutter/rendering.dart' show AxisDirection;
// Examples can assume:
// late final RenderBox child;
// late final BoxConstraints constraints;
// class RenderSimpleTwoDimensionalViewport extends RenderTwoDimensionalViewport {
// RenderSimpleTwoDimensionalViewport({
// required super.horizontalOffset,
// required super.horizontalAxisDirection,
// required super.verticalOffset,
// required super.verticalAxisDirection,
// required super.delegate,
// required super.mainAxis,
// required super.childManager,
// super.cacheExtent,
// super.clipBehavior = Clip.hardEdge,
// });
// @override
// void layoutChildSequence() { }
// }
/// Signature for a function that creates a widget for a given [ChildVicinity],
/// e.g., in a [TwoDimensionalScrollView], but may return null.
///
/// Used by [TwoDimensionalChildBuilderDelegate.builder] and other APIs that
/// use lazily-generated widgets where the child count may not be known
/// ahead of time.
///
/// Unlike most builders, this callback can return null, indicating the
/// [ChildVicinity.xIndex] or [ChildVicinity.yIndex] is out of range. Whether
/// and when this is valid depends on the semantics of the builder. For example,
/// [TwoDimensionalChildBuilderDelegate.builder] returns
/// null when one or both of the indices is out of range, where the range is
/// defined by the [TwoDimensionalChildBuilderDelegate.maxXIndex] or
/// [TwoDimensionalChildBuilderDelegate.maxYIndex]; so in that case the
/// vicinity values may determine whether returning null is valid or not.
///
/// See also:
///
/// * [WidgetBuilder], which is similar but only takes a [BuildContext].
/// * [NullableIndexedWidgetBuilder], which is similar but may return null.
/// * [IndexedWidgetBuilder], which is similar but not nullable.
typedef TwoDimensionalIndexedWidgetBuilder = Widget? Function(BuildContext, ChildVicinity vicinity);
/// A widget through which a portion of larger content can be viewed, typically
/// in combination with a [TwoDimensionalScrollable].
///
/// [TwoDimensionalViewport] is the visual workhorse of the two dimensional
/// scrolling machinery. It displays a subset of its children according to its
/// own dimensions and the given [horizontalOffset] an [verticalOffset]. As the
/// offsets vary, different children are visible through the viewport.
///
/// Subclasses must implement [createRenderObject] and [updateRenderObject].
/// Both of these methods require the render object to be a subclass of
/// [RenderTwoDimensionalViewport]. This class will create its own
/// [RenderObjectElement] which already implements the
/// [TwoDimensionalChildManager], which means subclasses should cast the
/// [BuildContext] to provide as the child manager to the
/// [RenderTwoDimensionalViewport].
///
/// {@tool snippet}
/// This is an example of a subclass implementation of [TwoDimensionalViewport],
/// `SimpleTwoDimensionalViewport`. The `RenderSimpleTwoDimensionalViewport` is
/// a subclass of [RenderTwoDimensionalViewport].
///
/// ```dart
/// class SimpleTwoDimensionalViewport extends TwoDimensionalViewport {
/// const SimpleTwoDimensionalViewport({
/// super.key,
/// required super.verticalOffset,
/// required super.verticalAxisDirection,
/// required super.horizontalOffset,
/// required super.horizontalAxisDirection,
/// required super.delegate,
/// required super.mainAxis,
/// super.cacheExtent,
/// super.clipBehavior = Clip.hardEdge,
/// });
///
/// @override
/// RenderSimpleTwoDimensionalViewport createRenderObject(BuildContext context) {
/// return RenderSimpleTwoDimensionalViewport(
/// horizontalOffset: horizontalOffset,
/// horizontalAxisDirection: horizontalAxisDirection,
/// verticalOffset: verticalOffset,
/// verticalAxisDirection: verticalAxisDirection,
/// mainAxis: mainAxis,
/// delegate: delegate,
/// childManager: context as TwoDimensionalChildManager,
/// cacheExtent: cacheExtent,
/// clipBehavior: clipBehavior,
/// );
/// }
///
/// @override
/// void updateRenderObject(BuildContext context, RenderSimpleTwoDimensionalViewport renderObject) {
/// renderObject
/// ..horizontalOffset = horizontalOffset
/// ..horizontalAxisDirection = horizontalAxisDirection
/// ..verticalOffset = verticalOffset
/// ..verticalAxisDirection = verticalAxisDirection
/// ..mainAxis = mainAxis
/// ..delegate = delegate
/// ..cacheExtent = cacheExtent
/// ..clipBehavior = clipBehavior;
/// }
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [Viewport], the equivalent of this widget that scrolls in only one
/// dimension.
abstract class TwoDimensionalViewport extends RenderObjectWidget {
/// Creates a viewport for [RenderBox] objects that extend and scroll in both
/// horizontal and vertical dimensions.
///
/// The viewport listens to the [horizontalOffset] and [verticalOffset], which
/// means this widget does not need to be rebuilt when the offsets change.
const TwoDimensionalViewport({
super.key,
required this.verticalOffset,
required this.verticalAxisDirection,
required this.horizontalOffset,
required this.horizontalAxisDirection,
required this.delegate,
required this.mainAxis,
this.cacheExtent,
this.clipBehavior = Clip.hardEdge,
}) : assert(
verticalAxisDirection == AxisDirection.down || verticalAxisDirection == AxisDirection.up,
'TwoDimensionalViewport.verticalAxisDirection is not Axis.vertical.'
),
assert(
horizontalAxisDirection == AxisDirection.left || horizontalAxisDirection == AxisDirection.right,
'TwoDimensionalViewport.horizontalAxisDirection is not Axis.horizontal.'
);
/// Which part of the content inside the viewport should be visible in the
/// vertical axis.
///
/// The [ViewportOffset.pixels] value determines the scroll offset that the
/// viewport uses to select which part of its content to display. As the user
/// scrolls the viewport vertically, this value changes, which changes the
/// content that is displayed.
///
/// Typically a [ScrollPosition].
final ViewportOffset verticalOffset;
/// The direction in which the [verticalOffset]'s [ViewportOffset.pixels]
/// increases.
///
/// For example, if the axis direction is [AxisDirection.down], a scroll
/// offset of zero is at the top of the viewport and increases towards the
/// bottom of the viewport.
///
/// Must be either [AxisDirection.down] or [AxisDirection.up] in correlation
/// with an [Axis.vertical].
final AxisDirection verticalAxisDirection;
/// Which part of the content inside the viewport should be visible in the
/// horizontal axis.
///
/// The [ViewportOffset.pixels] value determines the scroll offset that the
/// viewport uses to select which part of its content to display. As the user
/// scrolls the viewport horizontally, this value changes, which changes the
/// content that is displayed.
///
/// Typically a [ScrollPosition].
final ViewportOffset horizontalOffset;
/// The direction in which the [horizontalOffset]'s [ViewportOffset.pixels]
/// increases.
///
/// For example, if the axis direction is [AxisDirection.right], a scroll
/// offset of zero is at the left of the viewport and increases towards the
/// right of the viewport.
///
/// Must be either [AxisDirection.left] or [AxisDirection.right] in correlation
/// with an [Axis.horizontal].
final AxisDirection horizontalAxisDirection;
/// The main axis of the two.
///
/// Used to determine the paint order of the children of the viewport. When
/// the main axis is [Axis.vertical], children will be painted in row major
/// order, according to their associated [ChildVicinity]. When the main axis
/// is [Axis.horizontal], the children will be painted in column major order.
final Axis mainAxis;
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
final double? cacheExtent;
/// {@macro flutter.material.Material.clipBehavior}
final Clip clipBehavior;
/// A delegate that provides the children for the [TwoDimensionalViewport].
final TwoDimensionalChildDelegate delegate;
@override
RenderObjectElement createElement() => _TwoDimensionalViewportElement(this);
@override
RenderTwoDimensionalViewport createRenderObject(BuildContext context);
@override
void updateRenderObject(BuildContext context, RenderTwoDimensionalViewport renderObject);
}
class _TwoDimensionalViewportElement extends RenderObjectElement
with NotifiableElementMixin, ViewportElementMixin implements TwoDimensionalChildManager {
_TwoDimensionalViewportElement(super.widget);
@override
RenderTwoDimensionalViewport get renderObject => super.renderObject as RenderTwoDimensionalViewport;
// Contains all children, including those that are keyed.
Map<ChildVicinity, Element> _vicinityToChild = <ChildVicinity, Element>{};
Map<Key, Element> _keyToChild = <Key, Element>{};
// Used between _startLayout() & _endLayout() to compute the new values for
// _vicinityToChild and _keyToChild.
Map<ChildVicinity, Element>? _newVicinityToChild;
Map<Key, Element>? _newKeyToChild;
@override
void performRebuild() {
super.performRebuild();
// Children list is updated during layout since we only know during layout
// which children will be visible.
renderObject.markNeedsLayout(withDelegateRebuild: true);
}
@override
void forgetChild(Element child) {
assert(!_debugIsDoingLayout);
super.forgetChild(child);
_vicinityToChild.remove(child.slot);
if (child.widget.key != null) {
_keyToChild.remove(child.widget.key);
}
}
@override
void insertRenderObjectChild(RenderBox child, ChildVicinity slot) {
renderObject._insertChild(child, slot);
}
@override
void moveRenderObjectChild(RenderBox child, ChildVicinity oldSlot, ChildVicinity newSlot) {
renderObject._moveChild(child, from: oldSlot, to: newSlot);
}
@override
void removeRenderObjectChild(RenderBox child, ChildVicinity slot) {
renderObject._removeChild(child, slot);
}
@override
void visitChildren(ElementVisitor visitor) {
_vicinityToChild.values.forEach(visitor);
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
final List<Element> children = _vicinityToChild.values.toList()..sort(_compareChildren);
return <DiagnosticsNode>[
for (final Element child in children)
child.toDiagnosticsNode(name: child.slot.toString())
];
}
static int _compareChildren(Element a, Element b) {
final ChildVicinity aSlot = a.slot! as ChildVicinity;
final ChildVicinity bSlot = b.slot! as ChildVicinity;
return aSlot.compareTo(bSlot);
}
// ---- ChildManager implementation ----
bool get _debugIsDoingLayout => _newKeyToChild != null && _newVicinityToChild != null;
@override
void _startLayout() {
assert(!_debugIsDoingLayout);
_newVicinityToChild = <ChildVicinity, Element>{};
_newKeyToChild = <Key, Element>{};
}
@override
void _buildChild(ChildVicinity vicinity) {
assert(_debugIsDoingLayout);
owner!.buildScope(this, () {
final Widget? newWidget = (widget as TwoDimensionalViewport).delegate.build(this, vicinity);
if (newWidget == null) {
return;
}
final Element? oldElement = _retrieveOldElement(newWidget, vicinity);
final Element? newChild = updateChild(oldElement, newWidget, vicinity);
assert(newChild != null);
// Ensure we are not overwriting an existing child.
assert(_newVicinityToChild![vicinity] == null);
_newVicinityToChild![vicinity] = newChild!;
if (newWidget.key != null) {
// Ensure we are not overwriting an existing key
assert(_newKeyToChild![newWidget.key!] == null);
_newKeyToChild![newWidget.key!] = newChild;
}
});
}
Element? _retrieveOldElement(Widget newWidget, ChildVicinity vicinity) {
if (newWidget.key != null) {
final Element? result = _keyToChild.remove(newWidget.key);
if (result != null) {
_vicinityToChild.remove(result.slot);
}
return result;
}
final Element? potentialOldElement = _vicinityToChild[vicinity];
if (potentialOldElement != null && potentialOldElement.widget.key == null) {
return _vicinityToChild.remove(vicinity);
}
return null;
}
@override
void _reuseChild(ChildVicinity vicinity) {
assert(_debugIsDoingLayout);
final Element? elementToReuse = _vicinityToChild.remove(vicinity);
assert(
elementToReuse != null,
'Expected to re-use an element at $vicinity, but none was found.'
);
_newVicinityToChild![vicinity] = elementToReuse!;
if (elementToReuse.widget.key != null) {
assert(_keyToChild.containsKey(elementToReuse.widget.key));
assert(_keyToChild[elementToReuse.widget.key] == elementToReuse);
_newKeyToChild![elementToReuse.widget.key!] = _keyToChild.remove(elementToReuse.widget.key)!;
}
}
@override
void _endLayout() {
assert(_debugIsDoingLayout);
// Unmount all elements that have not been reused in the layout cycle.
for (final Element element in _vicinityToChild.values) {
if (element.widget.key == null) {
// If it has a key, we handle it below.
updateChild(element, null, null);
} else {
assert(_keyToChild.containsValue(element));
}
}
for (final Element element in _keyToChild.values) {
assert(element.widget.key != null);
updateChild(element, null, null);
}
_vicinityToChild = _newVicinityToChild!;
_keyToChild = _newKeyToChild!;
_newVicinityToChild = null;
_newKeyToChild = null;
assert(!_debugIsDoingLayout);
}
}
/// Parent data structure used by [RenderTwoDimensionalViewport].
///
/// The parent data primarily describes where a child is in the viewport. The
/// [layoutOffset] must be set by subclasses of [RenderTwoDimensionalViewport],
/// during [RenderTwoDimensionalViewport.layoutChildSequence] which represents
/// the position of the child in the viewport.
///
/// The [paintOffset] is computed by [RenderTwoDimensionalViewport] after
/// [RenderTwoDimensionalViewport.layoutChildSequence]. If subclasses of
/// RenderTwoDimensionalViewport override the paint method, the [paintOffset]
/// should be used to position the child in the viewport in order to account for
/// a reversed [AxisDirection] in one or both dimensions.
class TwoDimensionalViewportParentData extends ParentData {
/// The offset at which to paint the child in the parent's coordinate system.
///
/// This [Offset] represents the top left corner of the child of the
/// [TwoDimensionalViewport].
///
/// This value must be set by implementors during
/// [RenderTwoDimensionalViewport.layoutChildSequence]. After the method is
/// complete, the [RenderTwoDimensionalViewport] will compute the
/// [paintOffset] based on this value to account for the [AxisDirection].
Offset? layoutOffset;
/// The logical positioning of children in two dimensions.
///
/// While children may not be strictly laid out in rows and columns, the
/// relative positioning determines traversal of
/// children in row or column major format.
///
/// This is set in the [RenderTwoDimensionalViewport.buildOrObtainChildFor].
ChildVicinity vicinity = ChildVicinity.invalid;
/// Whether or not the child is actually visible within the viewport.
///
/// For example, if a child is contained within the
/// [RenderTwoDimensionalViewport.cacheExtent] and out of view.
///
/// This is used during [RenderTwoDimensionalViewport.paint] in order to skip
/// painting children that cannot be seen.
bool get isVisible {
assert(() {
if (_paintExtent == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('The paint extent of the child has not been determined yet.'),
ErrorDescription(
'The paint extent, and therefore the visibility, of a child of a '
'RenderTwoDimensionalViewport is computed after '
'RenderTwoDimensionalViewport.layoutChildSequence.'
),
]);
}
return true;
}());
return _paintExtent != Size.zero || _paintExtent!.height != 0.0 || _paintExtent!.width != 0.0;
}
/// Represents the extent in both dimensions of the child that is actually
/// visible.
///
/// For example, if a child [RenderBox] had a height of 100 pixels, and a
/// width of 100 pixels, but was scrolled to positions such that only 50
/// pixels of both width and height were visible, the paintExtent would be
/// represented as `Size(50.0, 50.0)`.
///
/// This is set in [RenderTwoDimensionalViewport.updateChildPaintData].
Size? _paintExtent;
/// The previous sibling in the parent's child list according to the traversal
/// order specified by [RenderTwoDimensionalViewport.mainAxis].
RenderBox? _previousSibling;
/// The next sibling in the parent's child list according to the traversal
/// order specified by [RenderTwoDimensionalViewport.mainAxis].
RenderBox? _nextSibling;
/// The position of the child relative to the bounds and [AxisDirection] of
/// the viewport.
///
/// This is the distance from the top left visible corner of the parent to the
/// top left visible corner of the child. When the [AxisDirection]s are
/// [AxisDirection.down] or [AxisDirection.right], this value is the same as
/// the [layoutOffset]. This value deviates when scrolling in the reverse
/// directions of [AxisDirection.up] and [AxisDirection.left] to reposition
/// the children correctly.
///
/// This is set in [RenderTwoDimensionalViewport.updateChildPaintData], after
/// [RenderTwoDimensionalViewport.layoutChildSequence].
///
/// If overriding [RenderTwoDimensionalViewport.paint], use this value to
/// position the children instead of [layoutOffset].
Offset? paintOffset;
@override
String toString() {
return 'vicinity=$vicinity; '
'layoutOffset=$layoutOffset; '
'paintOffset=$paintOffset; '
'${_paintExtent == null
? 'not visible '
: '${!isVisible ? 'not ' : ''}visible - paintExtent=$_paintExtent'}';
}
}
/// A base class for viewing render objects that scroll in two dimensions.
///
/// The viewport listens to two [ViewportOffset]s, which determines the
/// visible content.
///
/// Subclasses must implement [layoutChildSequence], calling on
/// [buildOrObtainChildFor] to manage the children of the viewport.
///
/// Subclasses should not override [performLayout], as it handles housekeeping
/// on either side of the call to [layoutChildSequence].
// TODO(Piinks): Two follow up changes:
// - Keep alive https://github.com/flutter/flutter/issues/126297
// - ensureVisible https://github.com/flutter/flutter/issues/126299
abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderAbstractViewport {
/// Initializes fields for subclasses.
///
/// The [cacheExtent], if null, defaults to
/// [RenderAbstractViewport.defaultCacheExtent].
RenderTwoDimensionalViewport({
required ViewportOffset horizontalOffset,
required AxisDirection horizontalAxisDirection,
required ViewportOffset verticalOffset,
required AxisDirection verticalAxisDirection,
required TwoDimensionalChildDelegate delegate,
required Axis mainAxis,
required TwoDimensionalChildManager childManager,
double? cacheExtent,
Clip clipBehavior = Clip.hardEdge,
}) : assert(
verticalAxisDirection == AxisDirection.down || verticalAxisDirection == AxisDirection.up,
'TwoDimensionalViewport.verticalAxisDirection is not Axis.vertical.'
),
assert(
horizontalAxisDirection == AxisDirection.left || horizontalAxisDirection == AxisDirection.right,
'TwoDimensionalViewport.horizontalAxisDirection is not Axis.horizontal.'
),
_childManager = childManager,
_horizontalOffset = horizontalOffset,
_horizontalAxisDirection = horizontalAxisDirection,
_verticalOffset = verticalOffset,
_verticalAxisDirection = verticalAxisDirection,
_delegate = delegate,
_mainAxis = mainAxis,
_cacheExtent = cacheExtent ?? RenderAbstractViewport.defaultCacheExtent,
_clipBehavior = clipBehavior;
/// Which part of the content inside the viewport should be visible in the
/// horizontal axis.
///
/// The [ViewportOffset.pixels] value determines the scroll offset that the
/// viewport uses to select which part of its content to display. As the user
/// scrolls the viewport horizontally, this value changes, which changes the
/// content that is displayed.
///
/// Typically a [ScrollPosition].
ViewportOffset get horizontalOffset => _horizontalOffset;
ViewportOffset _horizontalOffset;
set horizontalOffset(ViewportOffset value) {
if (_horizontalOffset == value) {
return;
}
if (attached) {
_horizontalOffset.removeListener(markNeedsLayout);
}
_horizontalOffset = value;
if (attached) {
_horizontalOffset.addListener(markNeedsLayout);
}
markNeedsLayout();
}
/// The direction in which the [horizontalOffset] increases.
///
/// For example, if the axis direction is [AxisDirection.right], a scroll
/// offset of zero is at the left of the viewport and increases towards the
/// right of the viewport.
AxisDirection get horizontalAxisDirection => _horizontalAxisDirection;
AxisDirection _horizontalAxisDirection;
set horizontalAxisDirection(AxisDirection value) {
if (_horizontalAxisDirection == value) {
return;
}
_horizontalAxisDirection = value;
markNeedsLayout();
}
/// Which part of the content inside the viewport should be visible in the
/// vertical axis.
///
/// The [ViewportOffset.pixels] value determines the scroll offset that the
/// viewport uses to select which part of its content to display. As the user
/// scrolls the viewport vertically, this value changes, which changes the
/// content that is displayed.
///
/// Typically a [ScrollPosition].
ViewportOffset get verticalOffset => _verticalOffset;
ViewportOffset _verticalOffset;
set verticalOffset(ViewportOffset value) {
if (_verticalOffset == value) {
return;
}
if (attached) {
_verticalOffset.removeListener(markNeedsLayout);
}
_verticalOffset = value;
if (attached) {
_verticalOffset.addListener(markNeedsLayout);
}
markNeedsLayout();
}
/// The direction in which the [verticalOffset] increases.
///
/// For example, if the axis direction is [AxisDirection.down], a scroll
/// offset of zero is at the top the viewport and increases towards the
/// bottom of the viewport.
AxisDirection get verticalAxisDirection => _verticalAxisDirection;
AxisDirection _verticalAxisDirection;
set verticalAxisDirection(AxisDirection value) {
if (_verticalAxisDirection == value) {
return;
}
_verticalAxisDirection = value;
markNeedsLayout();
}
/// Supplies children for layout in the viewport.
TwoDimensionalChildDelegate get delegate => _delegate;
TwoDimensionalChildDelegate _delegate;
set delegate(TwoDimensionalChildDelegate value) {
if (_delegate == value) {
return;
}
if (attached) {
_delegate.removeListener(_handleDelegateNotification);
}
final TwoDimensionalChildDelegate oldDelegate = _delegate;
_delegate = value;
if (attached) {
_delegate.addListener(_handleDelegateNotification);
}
if (_delegate.runtimeType != oldDelegate.runtimeType || _delegate.shouldRebuild(oldDelegate)) {
_handleDelegateNotification();
}
}
/// The major axis of the two dimensions.
///
/// This is can be used by subclasses to determine paint order,
/// visitor patterns like row and column major ordering, or hit test
/// precedence.
///
/// See also:
///
/// * [TwoDimensionalScrollView], which assigns the [PrimaryScrollController]
/// to the [TwoDimensionalScrollView.mainAxis] and shares this value.
Axis get mainAxis => _mainAxis;
Axis _mainAxis;
set mainAxis(Axis value) {
if (_mainAxis == value) {
return;
}
_mainAxis = value;
// Child order needs to be resorted, which happens in performLayout.
markNeedsLayout();
}
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
double get cacheExtent => _cacheExtent ?? RenderAbstractViewport.defaultCacheExtent;
double? _cacheExtent;
set cacheExtent(double? value) {
if (_cacheExtent == value) {
return;
}
_cacheExtent = value;
markNeedsLayout();
}
/// {@macro flutter.material.Material.clipBehavior}
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior;
set clipBehavior(Clip value) {
if (_clipBehavior == value) {
return;
}
_clipBehavior = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
final TwoDimensionalChildManager _childManager;
bool _hasVisualOverflow = false;
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
bool get isRepaintBoundary => true;
@override
bool get sizedByParent => true;
final Map<ChildVicinity, RenderBox> _children = <ChildVicinity, RenderBox>{};
// Keeps track of the upper and lower bounds of ChildVicinity indices when
// subclasses call buildOrObtainChildFor during layoutChildSequence. These
// values are used to sort children in accordance with the mainAxis for
// paint order.
int? _leadingXIndex;
int? _trailingXIndex;
int? _leadingYIndex;
int? _trailingYIndex;
/// The first child of the viewport according to the traversal order of the
/// [mainAxis].
///
/// {@template flutter.rendering.twoDimensionalViewport.paintOrder}
/// The [mainAxis] correlates with the [ChildVicinity] of each child to paint
/// the children in a row or column major order.
///
/// By default, the [mainAxis] is [Axis.vertical], which would result in a
/// row major paint order, visiting children in the horizontal indices before
/// advancing to the next vertical index.
/// {@endtemplate}
///
/// This value is null during [layoutChildSequence] as children are reified
/// into the correct order after layout is completed. This can be used when
/// overriding [paint] in order to paint the children in the correct order.
RenderBox? get firstChild => _firstChild;
RenderBox? _firstChild;
/// The last child in the viewport according to the traversal order of the
/// [mainAxis].
///
/// {@macro flutter.rendering.twoDimensionalViewport.paintOrder}
///
/// This value is null during [layoutChildSequence] as children are reified
/// into the correct order after layout is completed. This can be used when
/// overriding [paint] in order to paint the children in the correct order.
RenderBox? get lastChild => _lastChild;
RenderBox? _lastChild;
/// The previous child before the given child in the child list according to
/// the traversal order of the [mainAxis].
///
/// {@macro flutter.rendering.twoDimensionalViewport.paintOrder}
///
/// This method is useful when overriding [paint] in order to paint children
/// in the correct order.
RenderBox? childBefore(RenderBox child) {
assert(child.parent == this);
return parentDataOf(child)._previousSibling;
}
/// The next child after the given child in the child list according to
/// the traversal order of the [mainAxis].
///
/// {@macro flutter.rendering.twoDimensionalViewport.paintOrder}
///
/// This method is useful when overriding [paint] in order to paint children
/// in the correct order.
RenderBox? childAfter(RenderBox child) {
assert(child.parent == this);
return parentDataOf(child)._nextSibling;
}
void _handleDelegateNotification() {
return markNeedsLayout(withDelegateRebuild: true);
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! TwoDimensionalViewportParentData) {
child.parentData = TwoDimensionalViewportParentData();
}
}
/// Convenience method for retrieving and casting the [ParentData] of the
/// viewport's children.
///
/// Children must have a [ParentData] of type
/// [TwoDimensionalViewportParentData], or a subclass thereof.
@protected
TwoDimensionalViewportParentData parentDataOf(RenderBox child) {
assert(_children.containsValue(child));
return child.parentData! as TwoDimensionalViewportParentData;
}
/// Returns the active child located at the provided [ChildVicinity], if there
/// is one.
///
/// This can be used by subclasses to access currently active children to make
/// use of their size or [TwoDimensionalViewportParentData], such as when
/// overriding the [paint] method.
///
/// Returns null if there is no active child for the given [ChildVicinity].
@protected
RenderBox? getChildFor(ChildVicinity vicinity) => _children[vicinity];
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_horizontalOffset.addListener(markNeedsLayout);
_verticalOffset.addListener(markNeedsLayout);
_delegate.addListener(_handleDelegateNotification);
for (final RenderBox child in _children.values) {
child.attach(owner);
}
}
@override
void detach() {
super.detach();
_horizontalOffset.removeListener(markNeedsLayout);
_verticalOffset.removeListener(markNeedsLayout);
_delegate.removeListener(_handleDelegateNotification);
for (final RenderBox child in _children.values) {
child.detach();
}
}
@override
void redepthChildren() {
for (final RenderBox child in _children.values) {
child.redepthChildren();
}
}
@override
void visitChildren(RenderObjectVisitor visitor) {
RenderBox? child = _firstChild;
while (child != null) {
visitor(child);
child = parentDataOf(child)._nextSibling;
}
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
// Only children that are visible should be visited, and they must be in
// paint order.
RenderBox? child = _firstChild;
while (child != null) {
final TwoDimensionalViewportParentData childParentData = parentDataOf(child);
if (childParentData.isVisible) {
visitor(child);
}
child = childParentData._nextSibling;
}
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
final List<DiagnosticsNode> debugChildren = <DiagnosticsNode>[
..._children.keys.map<DiagnosticsNode>((ChildVicinity vicinity) {
return _children[vicinity]!.toDiagnosticsNode(name: vicinity.toString());
})
];
return debugChildren;
}
@override
Size computeDryLayout(BoxConstraints constraints) {
assert(debugCheckHasBoundedAxis(Axis.vertical, constraints));
assert(debugCheckHasBoundedAxis(Axis.horizontal, constraints));
return constraints.biggest;
}
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
for (final RenderBox child in _children.values) {
final TwoDimensionalViewportParentData childParentData = parentDataOf(child);
if (!childParentData.isVisible) {
// Can't hit a child that is not visible.
continue;
}
final bool isHit = result.addWithPaintOffset(
offset: childParentData.paintOffset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.paintOffset!);
return child.hitTest(result, position: transformed);
},
);
if (isHit) {
return true;
}
}
return false;
}
/// The dimensions of the viewport.
///
/// This [Size] represents the width and height of the visible area.
Size get viewportDimension {
assert(hasSize);
return size;
}
@override
void performResize() {
final Size? oldSize = hasSize ? size : null;
super.performResize();
// Ignoring return value since we are doing a layout either way
// (performLayout will be invoked next).
horizontalOffset.applyViewportDimension(size.width);
verticalOffset.applyViewportDimension(size.height);
if (oldSize != size) {
// Specs can depend on viewport size.
_didResize = true;
}
}
@override
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect }) {
// TODO(Piinks): Add this back in follow up change (ensureVisible), https://github.com/flutter/flutter/issues/126299
return const RevealedOffset(offset: 0.0, rect: Rect.zero);
}
/// Should be used by subclasses to invalidate any cached metrics for the
/// viewport.
///
/// This is set to true when the viewport has been resized, indicating that
/// any cached dimensions are invalid.
///
/// After performLayout, the value is set to false until the viewport
/// dimensions are changed again in [performResize].
///
/// Subclasses are not required to use this value, but it can be used to
/// safely cache layout information in between layout calls.
bool get didResize => _didResize;
bool _didResize = true;
/// Should be used by subclasses to invalidate any cached data from the
/// [delegate].
///
/// This value is set to false after [layoutChildSequence]. If
/// [markNeedsLayout] is called `withDelegateRebuild` set to true, then this
/// value will be updated to true, signifying any cached delegate information
/// needs to be updated in the next call to [layoutChildSequence].
///
/// Subclasses are not required to use this value, but it can be used to
/// safely cache layout information in between layout calls.
@protected
bool get needsDelegateRebuild => _needsDelegateRebuild;
bool _needsDelegateRebuild = true;
@override
void markNeedsLayout({ bool withDelegateRebuild = false }) {
_needsDelegateRebuild = _needsDelegateRebuild || withDelegateRebuild;
super.markNeedsLayout();
}
/// Primary work horse of [performLayout].
///
/// Subclasses must implement this method to layout the children of the
/// viewport. The [TwoDimensionalViewportParentData.layoutOffset] must be set
/// during this method in order for the children to be positioned during paint.
/// Further, children of the viewport must be laid out with the expectation
/// that the parent (this viewport) will use their size.
///
/// ```dart
/// child.layout(constraints, parentUsesSize: true);
/// ```
///
/// The primary methods used for creating and obtaining children is
/// [buildOrObtainChildFor], which takes a [ChildVicinity] that is used by the
/// [TwoDimensionalChildDelegate]. If a child is not provided by the delegate
/// for the provided vicinity, the method will return null, otherwise, it will
/// return the [RenderBox] of the child.
///
/// After [layoutChildSequence] is completed, any remaining children that were
/// not obtained will be disposed.
void layoutChildSequence();
@override
void performLayout() {
_firstChild = null;
_lastChild = null;
_childManager._startLayout();
// Subclass lays out children.
layoutChildSequence();
assert(_debugCheckContentDimensions());
_didResize = false;
_needsDelegateRebuild = false;
invokeLayoutCallback<BoxConstraints>((BoxConstraints _) {
_childManager._endLayout();
assert(_debugOrphans?.isEmpty ?? true);
// Organize children in paint order and complete parent data after
// un-used children are disposed of by the childManager.
_reifyChildren();
});
}
// Ensures all children have a layoutOffset, sets paintExtent & paintOffset,
// and arranges children in paint order.
void _reifyChildren() {
assert(_leadingXIndex != null);
assert(_trailingXIndex != null);
assert(_leadingYIndex != null);
assert(_trailingYIndex != null);
assert(_firstChild == null);
assert(_lastChild == null);
RenderBox? previousChild;
switch (mainAxis) {
case Axis.vertical:
// Row major traversal.
// This seems backwards, but the vertical axis is the typical default
// axis for scrolling in Flutter, while Row-major ordering is the
// typical default for matrices, which is why the inverse follows
// through in the horizontal case below.
// Minor
for (int minorIndex = _leadingYIndex!; minorIndex <= _trailingYIndex!; minorIndex++) {
// Major
for (int majorIndex = _leadingXIndex!; majorIndex <= _trailingXIndex!; majorIndex++) {
final ChildVicinity vicinity = ChildVicinity(xIndex: majorIndex, yIndex: minorIndex);
previousChild = _completeChildParentData(
vicinity,
previousChild: previousChild,
) ?? previousChild;
}
}
case Axis.horizontal:
// Column major traversal
// Minor
for (int minorIndex = _leadingXIndex!; minorIndex <= _trailingXIndex!; minorIndex++) {
// Major
for (int majorIndex = _leadingYIndex!; majorIndex <= _trailingYIndex!; majorIndex++) {
final ChildVicinity vicinity = ChildVicinity(xIndex: minorIndex, yIndex: majorIndex);
previousChild = _completeChildParentData(
vicinity,
previousChild: previousChild,
) ?? previousChild;
}
}
}
_lastChild = previousChild;
parentDataOf(_lastChild!)._nextSibling = null;
// Reset for next layout pass.
_leadingXIndex = null;
_trailingXIndex = null;
_leadingYIndex = null;
_trailingYIndex = null;
}
RenderBox? _completeChildParentData(ChildVicinity vicinity, { RenderBox? previousChild }) {
assert(vicinity != ChildVicinity.invalid);
// It is possible and valid for a vicinity to be skipped.
// For example, a table can have merged cells, spanning multiple
// indices, but only represented by one RenderBox and ChildVicinity.
if (_children.containsKey(vicinity)) {
final RenderBox child = _children[vicinity]!;
assert(parentDataOf(child).vicinity == vicinity);
updateChildPaintData(child);
if (previousChild == null) {
// _firstChild is only set once.
assert(_firstChild == null);
_firstChild = child;
} else {
parentDataOf(previousChild)._nextSibling = child;
parentDataOf(child)._previousSibling = previousChild;
}
return child;
}
return null;
}
bool _debugCheckContentDimensions() {
const String hint = 'Subclasses should call applyContentDimensions on the '
'verticalOffset and horizontalOffset to set the min and max scroll offset. '
'If the contents exceed one or both sides of the viewportDimension, '
'ensure the viewportDimension height or width is subtracted in that axis '
'for the correct extent.';
assert(() {
if (!(verticalOffset as ScrollPosition).hasContentDimensions) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'The verticalOffset was not given content dimensions during '
'layoutChildSequence.'
),
ErrorHint(hint),
]);
}
return true;
}());
assert(() {
if (!(horizontalOffset as ScrollPosition).hasContentDimensions) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'The horizontalOffset was not given content dimensions during '
'layoutChildSequence.'
),
ErrorHint(hint),
]);
}
return true;
}());
return true;
}
/// Returns the child for a given [ChildVicinity].
///
/// This method will build the child if it has not been already, or will reuse
/// it if it already exists.
RenderBox? buildOrObtainChildFor(ChildVicinity vicinity) {
assert(vicinity != ChildVicinity.invalid);
if (_leadingXIndex == null || _trailingXIndex == null || _leadingXIndex == null || _trailingYIndex == null) {
// First child of this layout pass. Set leading and trailing trackers.
_leadingXIndex = vicinity.xIndex;
_trailingXIndex = vicinity.xIndex;
_leadingYIndex = vicinity.yIndex;
_trailingYIndex = vicinity.yIndex;
} else {
// If any of these are still null, we missed a child.
assert(_leadingXIndex != null);
assert(_trailingXIndex != null);
assert(_leadingYIndex != null);
assert(_trailingYIndex != null);
// Update as we go.
_leadingXIndex = math.min(vicinity.xIndex, _leadingXIndex!);
_trailingXIndex = math.max(vicinity.xIndex, _trailingXIndex!);
_leadingYIndex = math.min(vicinity.yIndex, _leadingYIndex!);
_trailingYIndex = math.max(vicinity.yIndex, _trailingYIndex!);
}
if (_needsDelegateRebuild || !_children.containsKey(vicinity)) {
invokeLayoutCallback<BoxConstraints>((BoxConstraints _) {
_childManager._buildChild(vicinity);
});
} else {
_childManager._reuseChild(vicinity);
}
if (!_children.containsKey(vicinity)) {
// There is no child for this vicinity, we may have reached the end of the
// children in one or both of the x/y indices.
return null;
}
assert(_children.containsKey(vicinity));
final RenderBox child = _children[vicinity]!;
parentDataOf(child).vicinity = vicinity;
return child;
}
/// Called after [layoutChildSequence] to compute the
/// [TwoDimensionalViewportParentData.paintOffset] and
/// [TwoDimensionalViewportParentData._paintExtent] of the child.
void updateChildPaintData(RenderBox child) {
final TwoDimensionalViewportParentData childParentData = parentDataOf(child);
assert(
childParentData.layoutOffset != null,
'The child with ChildVicinity(xIndex: ${childParentData.vicinity.xIndex}, '
'yIndex: ${childParentData.vicinity.yIndex}) was not provided a '
'layoutOffset. This should be set during layoutChildSequence, '
'representing the position of the child.'
);
assert(child.hasSize); // Child must have been laid out by now.
// Set paintExtent (and visibility)
childParentData._paintExtent = computeChildPaintExtent(
childParentData.layoutOffset!,
child.size,
);
// Set paintOffset
childParentData.paintOffset = computeAbsolutePaintOffsetFor(
child,
layoutOffset: childParentData.layoutOffset!,
);
// If the child is partially visible, or not visible at all, there is
// visual overflow.
_hasVisualOverflow = _hasVisualOverflow
|| childParentData.layoutOffset != childParentData._paintExtent
|| !childParentData.isVisible;
}
/// Computes the portion of the child that is visible, assuming that only the
/// region from the [ViewportOffset.pixels] of both dimensions to the
/// [cacheExtent] is visible, and that the relationship between scroll offsets
/// and paint offsets is linear.
///
/// For example, if the [ViewportOffset]s each have a scroll offset of 100 and
/// the arguments to this method describe a child with [layoutOffset] of
/// `Offset(50.0, 50.0)`, with a size of `Size(200.0, 200.0)`, then the
/// returned value would be `Size(150.0, 150.0)`, representing the visible
/// extent of the child.
Size computeChildPaintExtent(Offset layoutOffset, Size childSize) {
if (childSize == Size.zero || childSize.height == 0.0 || childSize.width == 0.0) {
return Size.zero;
}
// Horizontal extent
final double width;
if (layoutOffset.dx < 0.0) {
// The child is positioned beyond the leading edge of the viewport.
if (layoutOffset.dx + childSize.width <= 0.0) {
// The child does not extend into the viewable area, it is not visible.
return Size.zero;
}
// If the child is positioned starting at -50, then the paint extent is
// the width + (-50).
width = layoutOffset.dx + childSize.width;
} else if (layoutOffset.dx >= viewportDimension.width) {
// The child is positioned after the trailing edge of the viewport, also
// not visible.
return Size.zero;
} else {
// The child is positioned within the viewport bounds, but may extend
// beyond it.
assert(layoutOffset.dx >= 0 && layoutOffset.dx < viewportDimension.width);
if (layoutOffset.dx + childSize.width > viewportDimension.width) {
width = viewportDimension.width - layoutOffset.dx;
} else {
assert(layoutOffset.dx + childSize.width <= viewportDimension.width);
width = childSize.width;
}
}
// Vertical extent
final double height;
if (layoutOffset.dy < 0.0) {
// The child is positioned beyond the leading edge of the viewport.
if (layoutOffset.dy + childSize.height <= 0.0) {
// The child does not extend into the viewable area, it is not visible.
return Size.zero;
}
// If the child is positioned starting at -50, then the paint extent is
// the width + (-50).
height = layoutOffset.dy + childSize.height;
} else if (layoutOffset.dy >= viewportDimension.height) {
// The child is positioned after the trailing edge of the viewport, also
// not visible.
return Size.zero;
} else {
// The child is positioned within the viewport bounds, but may extend
// beyond it.
assert(layoutOffset.dy >= 0 && layoutOffset.dy < viewportDimension.height);
if (layoutOffset.dy + childSize.height > viewportDimension.height) {
height = viewportDimension.height - layoutOffset.dy;
} else {
assert(layoutOffset.dy + childSize.height <= viewportDimension.height);
height = childSize.height;
}
}
return Size(width, height);
}
/// The offset at which the given `child` should be painted.
///
/// The returned offset is from the top left corner of the inside of the
/// viewport to the top left corner of the paint coordinate system of the
/// `child`.
///
/// This is useful when the one or both of the axes of the viewport are
/// reversed. The normalized layout offset of the child is used to compute
/// the paint offset in relation to the [verticalAxisDirection] and
/// [horizontalAxisDirection].
@protected
Offset computeAbsolutePaintOffsetFor(
RenderBox child, {
required Offset layoutOffset,
}) {
// This is only usable once we have sizes.
assert(hasSize);
assert(child.hasSize);
final double xOffset;
final double yOffset;
switch (verticalAxisDirection) {
case AxisDirection.up:
yOffset = viewportDimension.height - (layoutOffset.dy + child.size.height);
case AxisDirection.down:
yOffset = layoutOffset.dy;
case AxisDirection.right:
case AxisDirection.left:
throw Exception('This should not happen');
}
switch (horizontalAxisDirection) {
case AxisDirection.right:
xOffset = layoutOffset.dx;
case AxisDirection.left:
xOffset = viewportDimension.width - (layoutOffset.dx + child.size.width);
case AxisDirection.up:
case AxisDirection.down:
throw Exception('This should not happen');
}
return Offset(xOffset, yOffset);
}
@override
void paint(PaintingContext context, Offset offset) {
if (_children.isEmpty) {
return;
}
if (_hasVisualOverflow && clipBehavior != Clip.none) {
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & viewportDimension,
_paintChildren,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer.layer = null;
_paintChildren(context, offset);
}
}
void _paintChildren(PaintingContext context, Offset offset) {
RenderBox? child = _firstChild;
while (child != null) {
final TwoDimensionalViewportParentData childParentData = parentDataOf(child);
if (childParentData.isVisible) {
context.paintChild(child, offset + childParentData.paintOffset!);
}
child = childParentData._nextSibling;
}
}
// ---- Called from _TwoDimensionalViewportElement ----
void _insertChild(RenderBox child, ChildVicinity slot) {
assert(_debugTrackOrphans(newOrphan: _children[slot]));
_children[slot] = child;
adoptChild(child);
}
void _moveChild(RenderBox child, {required ChildVicinity from, required ChildVicinity to}) {
if (_children[from] == child) {
_children.remove(from);
}
assert(_debugTrackOrphans(newOrphan: _children[to], noLongerOrphan: child));
_children[to] = child;
}
void _removeChild(RenderBox child, ChildVicinity slot) {
if (_children[slot] == child) {
_children.remove(slot);
}
assert(_debugTrackOrphans(noLongerOrphan: child));
dropChild(child);
}
List<RenderBox>? _debugOrphans;
// When a child is inserted into a slot currently occupied by another child,
// it becomes an orphan until it is either moved to another slot or removed.
bool _debugTrackOrphans({RenderBox? newOrphan, RenderBox? noLongerOrphan}) {
assert(() {
_debugOrphans ??= <RenderBox>[];
if (newOrphan != null) {
_debugOrphans!.add(newOrphan);
}
if (noLongerOrphan != null) {
_debugOrphans!.remove(noLongerOrphan);
}
return true;
}());
return true;
}
/// Throws an exception saying that the object does not support returning
/// intrinsic dimensions if, in debug mode, we are not in the
/// [RenderObject.debugCheckingIntrinsics] mode.
///
/// This is used by [computeMinIntrinsicWidth] et al because viewports do not
/// generally support returning intrinsic dimensions. See the discussion at
/// [computeMinIntrinsicWidth].
@protected
bool debugThrowIfNotCheckingIntrinsics() {
assert(() {
if (!RenderObject.debugCheckingIntrinsics) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('$runtimeType does not support returning intrinsic dimensions.'),
ErrorDescription(
'Calculating the intrinsic dimensions would require instantiating every child of '
'the viewport, which defeats the point of viewports being lazy.',
),
]);
}
return true;
}());
return true;
}
@override
double computeMinIntrinsicWidth(double height) {
assert(debugThrowIfNotCheckingIntrinsics());
return 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
assert(debugThrowIfNotCheckingIntrinsics());
return 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
assert(debugThrowIfNotCheckingIntrinsics());
return 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
assert(debugThrowIfNotCheckingIntrinsics());
return 0.0;
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
final Offset paintOffset = parentDataOf(child).paintOffset!;
transform.translate(paintOffset.dx, paintOffset.dy);
}
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
}
/// A delegate used by [RenderTwoDimensionalViewport] to manage its children.
///
/// [RenderTwoDimensionalViewport] objects reify their children lazily to avoid
/// spending resources on children that are not visible in the viewport. This
/// delegate lets these objects create, reuse and remove children.
abstract class TwoDimensionalChildManager {
void _startLayout();
void _buildChild(ChildVicinity vicinity);
void _reuseChild(ChildVicinity vicinity);
void _endLayout();
}
/// The relative position of a child in a [TwoDimensionalViewport] in relation
/// to other children of the viewport.
///
/// While children can be plotted arbitrarily in two dimensional space, the
/// [ChildVicinity] is used to disambiguate their positions, determining how to
/// traverse the children of the space.
///
/// Combined with the [RenderTwoDimensionalViewport.mainAxis], each child's
/// vicinity determines its paint order among all of the children.
@immutable
class ChildVicinity implements Comparable<ChildVicinity> {
/// Creates a reference to a child in a two dimensional plane, with the
/// [xIndex] and [yIndex] being relative to other children in the viewport.
const ChildVicinity({
required this.xIndex,
required this.yIndex,
}) : assert(xIndex >= -1),
assert(yIndex >= -1);
/// Represents an unassigned child position. The given child may be in the
/// process of moving from one position to another.
static const ChildVicinity invalid = ChildVicinity(xIndex: -1, yIndex: -1);
/// The index of the child in the horizontal axis, relative to neighboring
/// children.
///
/// While children's offset and positioning may not be strictly defined in
/// terms of rows and columns, like a table, [ChildVicinity.xIndex] and
/// [ChildVicinity.yIndex] represents order of traversal in row or column
/// major format.
final int xIndex;
/// The index of the child in the vertical axis, relative to neighboring
/// children.
///
/// While children's offset and positioning may not be strictly defined in
/// terms of rows and columns, like a table, [ChildVicinity.xIndex] and
/// [ChildVicinity.yIndex] represents order of traversal in row or column
/// major format.
final int yIndex;
@override
bool operator ==(Object other) {
return other is ChildVicinity
&& other.xIndex == xIndex
&& other.yIndex == yIndex;
}
@override
int get hashCode => Object.hash(xIndex, yIndex);
@override
int compareTo(ChildVicinity other) {
if (xIndex == other.xIndex) {
return yIndex - other.yIndex;
}
return xIndex - other.xIndex;
}
@override
String toString() {
return '(xIndex: $xIndex, yIndex: $yIndex)';
}
}