
This PR changes the paint functions for SliverMainAxisGroup and SliverCrossAxisGroup so that only visible slivers are painted. Fixes https://github.com/flutter/flutter/issues/129214.
344 lines
13 KiB
Dart
344 lines
13 KiB
Dart
// 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: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) {
|
|
switch (constraints.axisDirection) {
|
|
case AxisDirection.up:
|
|
case AxisDirection.down:
|
|
return (child.parentData! as SliverPhysicalParentData).paintOffset.dx;
|
|
case AxisDirection.left:
|
|
case AxisDirection.right:
|
|
return (child.parentData! as SliverPhysicalParentData).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.
|
|
int totalFlex = 0;
|
|
double remainingExtent = crossAxisExtent;
|
|
RenderSliver? child = firstChild;
|
|
while (child != null) {
|
|
final SliverPhysicalParentData 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 SliverPhysicalParentData 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;
|
|
double offset = 0.0;
|
|
while (child != null) {
|
|
final SliverPhysicalParentData 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.
|
|
switch (constraints.axis) {
|
|
case Axis.vertical:
|
|
childParentData.paintOffset = Offset(offset, -paintCorrection);
|
|
case Axis.horizontal:
|
|
childParentData.paintOffset = 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 SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
|
|
context.paintChild(child, offset + childParentData.paintOffset);
|
|
}
|
|
child = childAfter(child);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void applyPaintTransform(RenderSliver child, Matrix4 transform) {
|
|
final SliverPhysicalParentData 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 bool isHit = result.addWithAxisOffset(
|
|
mainAxisPosition: mainAxisPosition,
|
|
crossAxisPosition: crossAxisPosition,
|
|
paintOffset: null,
|
|
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 childMainAxisPosition(RenderSliver child) {
|
|
switch (constraints.axisDirection) {
|
|
case AxisDirection.up:
|
|
case AxisDirection.down:
|
|
return (child.parentData! as SliverPhysicalParentData).paintOffset.dy;
|
|
case AxisDirection.left:
|
|
case AxisDirection.right:
|
|
return (child.parentData! as SliverPhysicalParentData).paintOffset.dx;
|
|
}
|
|
}
|
|
|
|
@override
|
|
double childCrossAxisPosition(RenderSliver child) => 0.0;
|
|
|
|
@override
|
|
void performLayout() {
|
|
double offset = 0;
|
|
double maxPaintExtent = 0;
|
|
|
|
RenderSliver? child = firstChild;
|
|
|
|
|
|
while (child != null) {
|
|
final double beforeOffsetPaintExtent = calculatePaintOffset(
|
|
constraints,
|
|
from: 0.0,
|
|
to: offset,
|
|
);
|
|
child.layout(
|
|
constraints.copyWith(
|
|
scrollOffset: math.max(0.0, constraints.scrollOffset - offset),
|
|
cacheOrigin: math.min(0.0, constraints.cacheOrigin + offset),
|
|
overlap: math.max(0.0, constraints.overlap - beforeOffsetPaintExtent),
|
|
remainingPaintExtent: constraints.remainingPaintExtent - beforeOffsetPaintExtent,
|
|
remainingCacheExtent: constraints.remainingCacheExtent - calculateCacheOffset(constraints, from: 0.0, to: offset),
|
|
precedingScrollExtent: offset + constraints.precedingScrollExtent,
|
|
),
|
|
parentUsesSize: true,
|
|
);
|
|
final SliverGeometry childLayoutGeometry = child.geometry!;
|
|
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
|
|
switch (constraints.axis) {
|
|
case Axis.vertical:
|
|
childParentData.paintOffset = Offset(0.0, beforeOffsetPaintExtent);
|
|
case Axis.horizontal:
|
|
childParentData.paintOffset = Offset(beforeOffsetPaintExtent, 0.0);
|
|
}
|
|
offset += childLayoutGeometry.scrollExtent;
|
|
maxPaintExtent += child.geometry!.maxPaintExtent;
|
|
child = childAfter(child);
|
|
}
|
|
|
|
final double totalScrollExtent = offset;
|
|
offset = 0.0;
|
|
child = firstChild;
|
|
// Second pass to correct out of bound paintOffsets.
|
|
while (child != null) {
|
|
final double beforeOffsetPaintExtent = calculatePaintOffset(
|
|
constraints,
|
|
from: 0.0,
|
|
to: offset,
|
|
);
|
|
final SliverGeometry childLayoutGeometry = child.geometry!;
|
|
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
|
|
final double remainingExtent = totalScrollExtent - constraints.scrollOffset;
|
|
if (childLayoutGeometry.paintExtent > remainingExtent) {
|
|
final double paintCorrection = childLayoutGeometry.paintExtent - remainingExtent;
|
|
switch (constraints.axis) {
|
|
case Axis.vertical:
|
|
childParentData.paintOffset = Offset(0.0, beforeOffsetPaintExtent - paintCorrection);
|
|
case Axis.horizontal:
|
|
childParentData.paintOffset = Offset(beforeOffsetPaintExtent - paintCorrection, 0.0);
|
|
}
|
|
}
|
|
offset += child.geometry!.scrollExtent;
|
|
child = childAfter(child);
|
|
}
|
|
geometry = SliverGeometry(
|
|
scrollExtent: totalScrollExtent,
|
|
paintExtent: calculatePaintOffset(constraints, from: 0, to: totalScrollExtent),
|
|
maxPaintExtent: maxPaintExtent,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
RenderSliver? child = lastChild;
|
|
|
|
while (child != null) {
|
|
if (child.geometry!.visible) {
|
|
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
|
|
context.paintChild(child, offset + childParentData.paintOffset);
|
|
}
|
|
child = childBefore(child);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void applyPaintTransform(RenderSliver child, Matrix4 transform) {
|
|
final SliverPhysicalParentData 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 bool isHit = result.addWithAxisOffset(
|
|
mainAxisPosition: mainAxisPosition,
|
|
crossAxisPosition: crossAxisPosition,
|
|
paintOffset: null,
|
|
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) {
|
|
visitor(child);
|
|
}
|
|
child = childAfter(child);
|
|
}
|
|
}
|
|
}
|