| // 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; |
| } |
| } |