blob: feedabb357628fea7f76bc513d0bf26fd51ef1d5 [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.
/// @docImport 'package:flutter/widgets.dart';
library;
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart';
import 'object.dart';
import 'sliver.dart';
/// A sliver that places multiple sliver children in a linear array along the cross
/// axis.
///
/// Since the extent of the viewport in the cross axis direction is finite,
/// this extent will be divided up and allocated to the children slivers.
///
/// The algorithm for dividing up the cross axis extent is as follows.
/// Every widget has a [SliverPhysicalParentData.crossAxisFlex] value associated with them.
/// First, lay out all of the slivers with flex of 0 or null, in which case the slivers themselves will
/// figure out how much cross axis extent to take up. For example, [SliverConstrainedCrossAxis]
/// is an example of a widget which sets its own flex to 0. Then [RenderSliverCrossAxisGroup] will
/// divide up the remaining space to all the remaining children proportionally
/// to each child's flex factor. By default, children of [SliverCrossAxisGroup]
/// are setup to have a flex factor of 1, but a different flex factor can be
/// specified via the [SliverCrossAxisExpanded] widgets.
class RenderSliverCrossAxisGroup extends RenderSliver
with ContainerRenderObjectMixin<RenderSliver, SliverPhysicalContainerParentData> {
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SliverPhysicalContainerParentData) {
child.parentData = SliverPhysicalContainerParentData();
(child.parentData! as SliverPhysicalParentData).crossAxisFlex = 1;
}
}
@override
double childMainAxisPosition(RenderSliver child) => 0.0;
@override
double childCrossAxisPosition(RenderSliver child) {
final Offset paintOffset = (child.parentData! as SliverPhysicalParentData).paintOffset;
return switch (constraints.axis) {
Axis.vertical => paintOffset.dx,
Axis.horizontal => paintOffset.dy,
};
}
@override
void performLayout() {
// Iterate through each sliver.
// Get the parent's dimensions.
final double crossAxisExtent = constraints.crossAxisExtent;
assert(crossAxisExtent.isFinite);
// First, layout each child with flex == 0 or null.
var totalFlex = 0;
var remainingExtent = crossAxisExtent;
RenderSliver? child = firstChild;
while (child != null) {
final childParentData = child.parentData! as SliverPhysicalParentData;
final int flex = childParentData.crossAxisFlex ?? 0;
if (flex == 0) {
// If flex is 0 or null, then the child sliver must provide their own crossAxisExtent.
assert(_assertOutOfExtent(remainingExtent));
child.layout(constraints.copyWith(crossAxisExtent: remainingExtent), parentUsesSize: true);
final double? childCrossAxisExtent = child.geometry!.crossAxisExtent;
assert(childCrossAxisExtent != null);
remainingExtent = math.max(0.0, remainingExtent - childCrossAxisExtent!);
} else {
totalFlex += flex;
}
child = childAfter(child);
}
final double extentPerFlexValue = remainingExtent / totalFlex;
child = firstChild;
// At this point, all slivers with constrained cross axis should already be laid out.
// Layout the rest and keep track of the child geometry with greatest scrollExtent.
geometry = SliverGeometry.zero;
while (child != null) {
final childParentData = child.parentData! as SliverPhysicalParentData;
final int flex = childParentData.crossAxisFlex ?? 0;
double childExtent;
if (flex != 0) {
childExtent = extentPerFlexValue * flex;
assert(_assertOutOfExtent(childExtent));
child.layout(
constraints.copyWith(crossAxisExtent: extentPerFlexValue * flex),
parentUsesSize: true,
);
} else {
childExtent = child.geometry!.crossAxisExtent!;
}
final SliverGeometry childLayoutGeometry = child.geometry!;
if (geometry!.scrollExtent < childLayoutGeometry.scrollExtent) {
geometry = childLayoutGeometry;
}
child = childAfter(child);
}
// Go back and correct any slivers using a negative paint offset if it tries
// to paint outside the bounds of the sliver group.
child = firstChild;
var offset = 0.0;
while (child != null) {
final childParentData = child.parentData! as SliverPhysicalParentData;
final SliverGeometry childLayoutGeometry = child.geometry!;
final double remainingExtent = geometry!.scrollExtent - constraints.scrollOffset;
final double paintCorrection = childLayoutGeometry.paintExtent > remainingExtent
? childLayoutGeometry.paintExtent - remainingExtent
: 0.0;
final double childExtent =
child.geometry!.crossAxisExtent ??
extentPerFlexValue * (childParentData.crossAxisFlex ?? 0);
// Set child parent data.
childParentData.paintOffset = switch (constraints.axis) {
Axis.vertical => Offset(offset, -paintCorrection),
Axis.horizontal => Offset(-paintCorrection, offset),
};
offset += childExtent;
child = childAfter(child);
}
}
@override
void paint(PaintingContext context, Offset offset) {
RenderSliver? child = firstChild;
while (child != null) {
if (child.geometry!.visible) {
final childParentData = child.parentData! as SliverPhysicalParentData;
context.paintChild(child, offset + childParentData.paintOffset);
}
child = childAfter(child);
}
}
@override
void applyPaintTransform(RenderSliver child, Matrix4 transform) {
final childParentData = child.parentData! as SliverPhysicalParentData;
childParentData.applyPaintTransform(transform);
}
@override
bool hitTestChildren(
SliverHitTestResult result, {
required double mainAxisPosition,
required double crossAxisPosition,
}) {
RenderSliver? child = lastChild;
while (child != null) {
final Offset paintOffset = (child.parentData! as SliverPhysicalParentData).paintOffset;
final bool isHit = result.addWithAxisOffset(
mainAxisPosition: mainAxisPosition,
crossAxisPosition: crossAxisPosition,
paintOffset: paintOffset,
mainAxisOffset: childMainAxisPosition(child),
crossAxisOffset: childCrossAxisPosition(child),
hitTest: child.hitTest,
);
if (isHit) {
return true;
}
child = childBefore(child);
}
return false;
}
}
bool _assertOutOfExtent(double extent) {
if (extent <= 0.0) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('SliverCrossAxisGroup ran out of extent before child could be laid out.'),
ErrorDescription(
'SliverCrossAxisGroup lays out any slivers with a constrained cross '
'axis before laying out those which expand. In this case, cross axis '
'extent was used up before the next sliver could be laid out.',
),
ErrorHint(
'Make sure that the total amount of extent allocated by constrained '
'child slivers does not exceed the cross axis extent that is available '
'for the SliverCrossAxisGroup.',
),
]);
}
return true;
}
/// A sliver that places multiple sliver children in a linear array along the
/// main axis.
///
/// The layout algorithm lays out slivers one by one. If the sliver is at the top
/// of the viewport or above the top, then we pass in a nonzero [SliverConstraints.scrollOffset]
/// to inform the sliver at what point along the main axis we should start layout.
/// For the slivers that come after it, we compute the amount of space taken up so
/// far to be used as the [SliverPhysicalParentData.paintOffset] and the
/// [SliverConstraints.remainingPaintExtent] to be passed in as a constraint.
///
/// Finally, this sliver will also ensure that all child slivers are painted within
/// the total scroll extent of the group by adjusting the child's
/// [SliverPhysicalParentData.paintOffset] as necessary. This can happen for
/// slivers such as [SliverPersistentHeader] which, when pinned, positions itself
/// at the top of the [Viewport] regardless of the scroll offset.
class RenderSliverMainAxisGroup extends RenderSliver
with ContainerRenderObjectMixin<RenderSliver, SliverPhysicalContainerParentData> {
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SliverPhysicalContainerParentData) {
child.parentData = SliverPhysicalContainerParentData();
}
}
@override
double? childScrollOffset(RenderObject child) {
assert(child.parent == this);
assert(child is RenderSliver);
final double extentOfPinnedSlivers = _maxScrollObstructionExtentBefore(child as RenderSliver);
final GrowthDirection growthDirection = constraints.growthDirection;
switch (growthDirection) {
case GrowthDirection.forward:
var childScrollOffset = 0.0;
RenderSliver? current = childBefore(child);
while (current != null) {
childScrollOffset += current.geometry!.scrollExtent;
current = childBefore(current);
}
return childScrollOffset - extentOfPinnedSlivers;
case GrowthDirection.reverse:
var childScrollOffset = 0.0;
RenderSliver? current = childAfter(child);
while (current != null) {
childScrollOffset -= current.geometry!.scrollExtent;
current = childAfter(current);
}
return childScrollOffset - extentOfPinnedSlivers;
}
}
double _maxScrollObstructionExtentBefore(RenderSliver child) {
final GrowthDirection growthDirection = child.constraints.growthDirection;
switch (growthDirection) {
case GrowthDirection.forward:
var pinnedExtent = 0.0;
RenderSliver? current = firstChild;
while (current != child) {
pinnedExtent += current!.geometry!.maxScrollObstructionExtent;
current = childAfter(current);
}
return pinnedExtent;
case GrowthDirection.reverse:
var pinnedExtent = 0.0;
RenderSliver? current = lastChild;
while (current != child) {
pinnedExtent += current!.geometry!.maxScrollObstructionExtent;
current = childBefore(current);
}
return pinnedExtent;
}
}
@override
double childMainAxisPosition(RenderSliver child) {
final childParentData = child.parentData! as SliverPhysicalParentData;
return switch (applyGrowthDirectionToAxisDirection(
child.constraints.axisDirection,
child.constraints.growthDirection,
)) {
AxisDirection.down => childParentData.paintOffset.dy,
AxisDirection.right => childParentData.paintOffset.dx,
AxisDirection.up =>
geometry!.paintExtent - child.geometry!.paintExtent - childParentData.paintOffset.dy,
AxisDirection.left =>
geometry!.paintExtent - child.geometry!.paintExtent - childParentData.paintOffset.dx,
};
}
@override
double childCrossAxisPosition(RenderSliver child) => 0.0;
@override
void performLayout() {
double scrollOffset = 0;
double layoutOffset = 0;
double maxPaintExtent = 0;
double paintOffset = constraints.overlap;
double maxScrollObstructionExtent = 0;
double cacheOrigin = constraints.cacheOrigin;
double remainingCacheExtent = constraints.remainingCacheExtent;
final (
RenderSliver? leadingChild,
RenderSliver? Function(RenderSliver child) advance,
) = switch (constraints.growthDirection) {
GrowthDirection.forward => (firstChild, childAfter),
GrowthDirection.reverse => (lastChild, childBefore),
};
var child = leadingChild;
while (child != null) {
final double beforeOffsetPaintExtent = calculatePaintOffset(
constraints,
from: 0.0,
to: scrollOffset,
);
final double childScrollOffset = math.max(0.0, constraints.scrollOffset - scrollOffset);
final double correctedCacheOrigin = math.max(cacheOrigin, -childScrollOffset);
final double cacheExtentCorrection = cacheOrigin - correctedCacheOrigin;
child.layout(
constraints.copyWith(
scrollOffset: childScrollOffset,
cacheOrigin: correctedCacheOrigin,
overlap: math.max(0.0, _fixPrecisionError(paintOffset - beforeOffsetPaintExtent)),
remainingPaintExtent: _fixPrecisionError(
constraints.remainingPaintExtent - beforeOffsetPaintExtent,
),
remainingCacheExtent: math.max(
0.0,
_fixPrecisionError(remainingCacheExtent + cacheExtentCorrection),
),
precedingScrollExtent: scrollOffset + constraints.precedingScrollExtent,
),
parentUsesSize: true,
);
final SliverGeometry childLayoutGeometry = child.geometry!;
final double? scrollOffsetCorrection = childLayoutGeometry.scrollOffsetCorrection;
if (scrollOffsetCorrection != null) {
geometry = SliverGeometry(scrollOffsetCorrection: scrollOffsetCorrection);
return;
}
assert(childLayoutGeometry.debugAssertIsValid());
final double childPaintOffset = layoutOffset + childLayoutGeometry.paintOrigin;
final childParentData = child.parentData! as SliverPhysicalParentData;
childParentData.paintOffset = switch (constraints.axis) {
Axis.vertical => Offset(0.0, childPaintOffset),
Axis.horizontal => Offset(childPaintOffset, 0.0),
};
scrollOffset += childLayoutGeometry.scrollExtent;
layoutOffset += childLayoutGeometry.layoutExtent;
maxPaintExtent += childLayoutGeometry.maxPaintExtent;
maxScrollObstructionExtent += childLayoutGeometry.maxScrollObstructionExtent;
paintOffset = math.max(childPaintOffset + childLayoutGeometry.paintExtent, paintOffset);
if (childLayoutGeometry.cacheExtent != 0.0) {
remainingCacheExtent = _fixPrecisionError(
remainingCacheExtent - childLayoutGeometry.cacheExtent - cacheExtentCorrection,
);
cacheOrigin = math.min(correctedCacheOrigin + childLayoutGeometry.cacheExtent, 0.0);
}
child = advance(child);
assert(() {
if (child != null && maxPaintExtent.isInfinite) {
throw FlutterError(
'Unreachable sliver found, you may have a sliver following '
'a sliver with an infinite extent. ',
);
}
return true;
}());
}
final double remainingExtent = math.max(0, scrollOffset - constraints.scrollOffset);
// If the children's paint extent exceeds the remaining scroll extent of the `RenderSliverMainAxisGroup`,
// they need to be corrected.
if (paintOffset > remainingExtent) {
// Whether the current remaining space can accommodate all pinned children.
final bool pinnedChildrenOverflow =
maxScrollObstructionExtent > remainingExtent - constraints.overlap;
final double paintCorrection = paintOffset - remainingExtent;
paintOffset = remainingExtent;
child = firstChild;
while (child != null) {
final SliverGeometry childLayoutGeometry = child.geometry!;
final childParentData = child.parentData! as SliverPhysicalParentData;
final double childMainAxisPaintOffset = switch (constraints.axis) {
Axis.vertical => childParentData.paintOffset.dy,
Axis.horizontal => childParentData.paintOffset.dx,
};
final double childPaintEnd = childMainAxisPaintOffset + childLayoutGeometry.paintExtent;
final bool childIsPinned = childLayoutGeometry.maxScrollObstructionExtent > 0;
if (childPaintEnd > remainingExtent || (pinnedChildrenOverflow && childIsPinned)) {
childParentData.paintOffset = switch (constraints.axis) {
Axis.vertical => Offset(0.0, childParentData.paintOffset.dy - paintCorrection),
Axis.horizontal => Offset(childParentData.paintOffset.dx - paintCorrection, 0.0),
};
}
child = childAfter(child);
}
}
final double cacheExtent = calculateCacheOffset(
constraints,
from: math.min(constraints.scrollOffset, 0),
to: scrollOffset,
);
final double paintExtent = clampDouble(paintOffset, 0, constraints.remainingPaintExtent);
geometry = SliverGeometry(
scrollExtent: scrollOffset,
paintExtent: paintExtent,
cacheExtent: cacheExtent,
maxPaintExtent: maxPaintExtent,
hasVisualOverflow:
scrollOffset > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
);
// Update the children's paintOffset based on the direction again, which
// must be done after obtaining the `paintExtent`.
child = leadingChild;
while (child != null) {
final childParentData = child.parentData! as SliverPhysicalParentData;
childParentData.paintOffset = switch (applyGrowthDirectionToAxisDirection(
constraints.axisDirection,
constraints.growthDirection,
)) {
AxisDirection.up => Offset(
0.0,
paintExtent - childParentData.paintOffset.dy - child.geometry!.paintExtent,
),
AxisDirection.left => Offset(
paintExtent - childParentData.paintOffset.dx - child.geometry!.paintExtent,
0.0,
),
AxisDirection.right || AxisDirection.down => childParentData.paintOffset,
};
child = advance(child);
}
}
@override
void paint(PaintingContext context, Offset offset) {
RenderSliver? child = lastChild;
while (child != null) {
if (child.geometry!.visible) {
final childParentData = child.parentData! as SliverPhysicalParentData;
context.paintChild(child, offset + childParentData.paintOffset);
}
child = childBefore(child);
}
}
@override
void applyPaintTransform(RenderSliver child, Matrix4 transform) {
final childParentData = child.parentData! as SliverPhysicalParentData;
childParentData.applyPaintTransform(transform);
}
@override
bool hitTestChildren(
SliverHitTestResult result, {
required double mainAxisPosition,
required double crossAxisPosition,
}) {
RenderSliver? child = firstChild;
while (child != null) {
final Offset paintOffset = (child.parentData! as SliverPhysicalParentData).paintOffset;
final bool isHit = result.addWithAxisOffset(
mainAxisPosition: mainAxisPosition,
crossAxisPosition: crossAxisPosition,
paintOffset: paintOffset,
mainAxisOffset: childMainAxisPosition(child),
crossAxisOffset: childCrossAxisPosition(child),
hitTest: child.hitTest,
);
if (isHit) {
return true;
}
child = childAfter(child);
}
return false;
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
RenderSliver? child = firstChild;
while (child != null) {
if (child.geometry!.visible || child.geometry!.cacheExtent > 0.0 || child.ensureSemantics) {
visitor(child);
}
child = childAfter(child);
}
}
static double _fixPrecisionError(double number) {
return number.abs() < precisionErrorTolerance ? 0.0 : number;
}
}