1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:math' as math;
6import 'package:vector_math/vector_math_64.dart';
7
8import 'object.dart';
9import 'sliver.dart';
10
11/// A sliver that places multiple sliver children in a linear array along the cross
12/// axis.
13///
14/// Since the extent of the viewport in the cross axis direction is finite,
15/// this extent will be divided up and allocated to the children slivers.
16///
17/// The algorithm for dividing up the cross axis extent is as follows.
18/// Every widget has a [SliverPhysicalParentData.crossAxisFlex] value associated with them.
19/// First, lay out all of the slivers with flex of 0 or null, in which case the slivers themselves will
20/// figure out how much cross axis extent to take up. For example, [SliverConstrainedCrossAxis]
21/// is an example of a widget which sets its own flex to 0. Then [RenderSliverCrossAxisGroup] will
22/// divide up the remaining space to all the remaining children proportionally
23/// to each child's flex factor. By default, children of [SliverCrossAxisGroup]
24/// are setup to have a flex factor of 1, but a different flex factor can be
25/// specified via the [SliverCrossAxisExpanded] widgets.
26class RenderSliverCrossAxisGroup extends RenderSliver with ContainerRenderObjectMixin<RenderSliver, SliverPhysicalContainerParentData> {
27 @override
28 void setupParentData(RenderObject child) {
29 if (child.parentData is! SliverPhysicalContainerParentData) {
30 child.parentData = SliverPhysicalContainerParentData();
31 (child.parentData! as SliverPhysicalParentData).crossAxisFlex = 1;
32 }
33 }
34
35 @override
36 double childMainAxisPosition(RenderSliver child) => 0.0;
37
38 @override
39 double childCrossAxisPosition(RenderSliver child) {
40 switch (constraints.axisDirection) {
41 case AxisDirection.up:
42 case AxisDirection.down:
43 return (child.parentData! as SliverPhysicalParentData).paintOffset.dx;
44 case AxisDirection.left:
45 case AxisDirection.right:
46 return (child.parentData! as SliverPhysicalParentData).paintOffset.dy;
47 }
48 }
49
50 @override
51 void performLayout() {
52 // Iterate through each sliver.
53 // Get the parent's dimensions.
54 final double crossAxisExtent = constraints.crossAxisExtent;
55 assert(crossAxisExtent.isFinite);
56
57 // First, layout each child with flex == 0 or null.
58 int totalFlex = 0;
59 double remainingExtent = crossAxisExtent;
60 RenderSliver? child = firstChild;
61 while (child != null) {
62 final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
63 final int flex = childParentData.crossAxisFlex ?? 0;
64 if (flex == 0) {
65 // If flex is 0 or null, then the child sliver must provide their own crossAxisExtent.
66 assert(_assertOutOfExtent(remainingExtent));
67 child.layout(constraints.copyWith(crossAxisExtent: remainingExtent), parentUsesSize: true);
68 final double? childCrossAxisExtent = child.geometry!.crossAxisExtent;
69 assert(childCrossAxisExtent != null);
70 remainingExtent = math.max(0.0, remainingExtent - childCrossAxisExtent!);
71 } else {
72 totalFlex += flex;
73 }
74 child = childAfter(child);
75 }
76 final double extentPerFlexValue = remainingExtent / totalFlex;
77
78 child = firstChild;
79
80 // At this point, all slivers with constrained cross axis should already be laid out.
81 // Layout the rest and keep track of the child geometry with greatest scrollExtent.
82 geometry = SliverGeometry.zero;
83 while (child != null) {
84 final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
85 final int flex = childParentData.crossAxisFlex ?? 0;
86 double childExtent;
87 if (flex != 0) {
88 childExtent = extentPerFlexValue * flex;
89 assert(_assertOutOfExtent(childExtent));
90 child.layout(constraints.copyWith(
91 crossAxisExtent: extentPerFlexValue * flex,
92 ), parentUsesSize: true);
93 } else {
94 childExtent = child.geometry!.crossAxisExtent!;
95 }
96 final SliverGeometry childLayoutGeometry = child.geometry!;
97 if (geometry!.scrollExtent < childLayoutGeometry.scrollExtent) {
98 geometry = childLayoutGeometry;
99 }
100 child = childAfter(child);
101 }
102
103 // Go back and correct any slivers using a negative paint offset if it tries
104 // to paint outside the bounds of the sliver group.
105 child = firstChild;
106 double offset = 0.0;
107 while (child != null) {
108 final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
109 final SliverGeometry childLayoutGeometry = child.geometry!;
110 final double remainingExtent = geometry!.scrollExtent - constraints.scrollOffset;
111 final double paintCorrection = childLayoutGeometry.paintExtent > remainingExtent
112 ? childLayoutGeometry.paintExtent - remainingExtent
113 : 0.0;
114 final double childExtent = child.geometry!.crossAxisExtent ?? extentPerFlexValue * (childParentData.crossAxisFlex ?? 0);
115 // Set child parent data.
116 switch (constraints.axis) {
117 case Axis.vertical:
118 childParentData.paintOffset = Offset(offset, -paintCorrection);
119 case Axis.horizontal:
120 childParentData.paintOffset = Offset(-paintCorrection, offset);
121 }
122 offset += childExtent;
123 child = childAfter(child);
124 }
125 }
126
127 @override
128 void paint(PaintingContext context, Offset offset) {
129 RenderSliver? child = firstChild;
130
131 while (child != null) {
132 if (child.geometry!.visible) {
133 final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
134 context.paintChild(child, offset + childParentData.paintOffset);
135 }
136 child = childAfter(child);
137 }
138 }
139
140 @override
141 void applyPaintTransform(RenderSliver child, Matrix4 transform) {
142 final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
143 childParentData.applyPaintTransform(transform);
144 }
145
146 @override
147 bool hitTestChildren(SliverHitTestResult result, {required double mainAxisPosition, required double crossAxisPosition}) {
148 RenderSliver? child = lastChild;
149 while (child != null) {
150 final bool isHit = result.addWithAxisOffset(
151 mainAxisPosition: mainAxisPosition,
152 crossAxisPosition: crossAxisPosition,
153 paintOffset: null,
154 mainAxisOffset: childMainAxisPosition(child),
155 crossAxisOffset: childCrossAxisPosition(child),
156 hitTest: child.hitTest,
157 );
158 if (isHit) {
159 return true;
160 }
161 child = childBefore(child);
162 }
163 return false;
164 }
165}
166
167bool _assertOutOfExtent(double extent) {
168 if (extent <= 0.0) {
169 throw FlutterError.fromParts(<DiagnosticsNode>[
170 ErrorSummary('SliverCrossAxisGroup ran out of extent before child could be laid out.'),
171 ErrorDescription(
172 'SliverCrossAxisGroup lays out any slivers with a constrained cross '
173 'axis before laying out those which expand. In this case, cross axis '
174 'extent was used up before the next sliver could be laid out.'
175 ),
176 ErrorHint(
177 'Make sure that the total amount of extent allocated by constrained '
178 'child slivers does not exceed the cross axis extent that is available '
179 'for the SliverCrossAxisGroup.'
180 ),
181 ]);
182 }
183 return true;
184}
185
186/// A sliver that places multiple sliver children in a linear array along the
187/// main axis.
188///
189/// The layout algorithm lays out slivers one by one. If the sliver is at the top
190/// of the viewport or above the top, then we pass in a nonzero [SliverConstraints.scrollOffset]
191/// to inform the sliver at what point along the main axis we should start layout.
192/// For the slivers that come after it, we compute the amount of space taken up so
193/// far to be used as the [SliverPhysicalParentData.paintOffset] and the
194/// [SliverConstraints.remainingPaintExtent] to be passed in as a constraint.
195///
196/// Finally, this sliver will also ensure that all child slivers are painted within
197/// the total scroll extent of the group by adjusting the child's
198/// [SliverPhysicalParentData.paintOffset] as necessary. This can happen for
199/// slivers such as [SliverPersistentHeader] which, when pinned, positions itself
200/// at the top of the [Viewport] regardless of the scroll offset.
201class RenderSliverMainAxisGroup extends RenderSliver with ContainerRenderObjectMixin<RenderSliver, SliverPhysicalContainerParentData> {
202 @override
203 void setupParentData(RenderObject child) {
204 if (child.parentData is! SliverPhysicalContainerParentData) {
205 child.parentData = SliverPhysicalContainerParentData();
206 }
207 }
208
209 @override
210 double childMainAxisPosition(RenderSliver child) {
211 switch (constraints.axisDirection) {
212 case AxisDirection.up:
213 case AxisDirection.down:
214 return (child.parentData! as SliverPhysicalParentData).paintOffset.dy;
215 case AxisDirection.left:
216 case AxisDirection.right:
217 return (child.parentData! as SliverPhysicalParentData).paintOffset.dx;
218 }
219 }
220
221 @override
222 double childCrossAxisPosition(RenderSliver child) => 0.0;
223
224 @override
225 void performLayout() {
226 double offset = 0;
227 double maxPaintExtent = 0;
228
229 RenderSliver? child = firstChild;
230
231
232 while (child != null) {
233 final double beforeOffsetPaintExtent = calculatePaintOffset(
234 constraints,
235 from: 0.0,
236 to: offset,
237 );
238 child.layout(
239 constraints.copyWith(
240 scrollOffset: math.max(0.0, constraints.scrollOffset - offset),
241 cacheOrigin: math.min(0.0, constraints.cacheOrigin + offset),
242 overlap: math.max(0.0, constraints.overlap - beforeOffsetPaintExtent),
243 remainingPaintExtent: constraints.remainingPaintExtent - beforeOffsetPaintExtent,
244 remainingCacheExtent: constraints.remainingCacheExtent - calculateCacheOffset(constraints, from: 0.0, to: offset),
245 precedingScrollExtent: offset + constraints.precedingScrollExtent,
246 ),
247 parentUsesSize: true,
248 );
249 final SliverGeometry childLayoutGeometry = child.geometry!;
250 final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
251 switch (constraints.axis) {
252 case Axis.vertical:
253 childParentData.paintOffset = Offset(0.0, beforeOffsetPaintExtent);
254 case Axis.horizontal:
255 childParentData.paintOffset = Offset(beforeOffsetPaintExtent, 0.0);
256 }
257 offset += childLayoutGeometry.scrollExtent;
258 maxPaintExtent += child.geometry!.maxPaintExtent;
259 child = childAfter(child);
260 }
261
262 final double totalScrollExtent = offset;
263 offset = 0.0;
264 child = firstChild;
265 // Second pass to correct out of bound paintOffsets.
266 while (child != null) {
267 final double beforeOffsetPaintExtent = calculatePaintOffset(
268 constraints,
269 from: 0.0,
270 to: offset,
271 );
272 final SliverGeometry childLayoutGeometry = child.geometry!;
273 final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
274 final double remainingExtent = totalScrollExtent - constraints.scrollOffset;
275 if (childLayoutGeometry.paintExtent > remainingExtent) {
276 final double paintCorrection = childLayoutGeometry.paintExtent - remainingExtent;
277 switch (constraints.axis) {
278 case Axis.vertical:
279 childParentData.paintOffset = Offset(0.0, beforeOffsetPaintExtent - paintCorrection);
280 case Axis.horizontal:
281 childParentData.paintOffset = Offset(beforeOffsetPaintExtent - paintCorrection, 0.0);
282 }
283 }
284 offset += child.geometry!.scrollExtent;
285 child = childAfter(child);
286 }
287 geometry = SliverGeometry(
288 scrollExtent: totalScrollExtent,
289 paintExtent: calculatePaintOffset(constraints, from: 0, to: totalScrollExtent),
290 maxPaintExtent: maxPaintExtent,
291 hasVisualOverflow: totalScrollExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
292 );
293 }
294
295 @override
296 void paint(PaintingContext context, Offset offset) {
297 RenderSliver? child = lastChild;
298
299 while (child != null) {
300 if (child.geometry!.visible) {
301 final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
302 context.paintChild(child, offset + childParentData.paintOffset);
303 }
304 child = childBefore(child);
305 }
306 }
307
308 @override
309 void applyPaintTransform(RenderSliver child, Matrix4 transform) {
310 final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
311 childParentData.applyPaintTransform(transform);
312 }
313
314 @override
315 bool hitTestChildren(SliverHitTestResult result, {required double mainAxisPosition, required double crossAxisPosition}) {
316 RenderSliver? child = firstChild;
317 while (child != null) {
318 final bool isHit = result.addWithAxisOffset(
319 mainAxisPosition: mainAxisPosition,
320 crossAxisPosition: crossAxisPosition,
321 paintOffset: null,
322 mainAxisOffset: childMainAxisPosition(child),
323 crossAxisOffset: childCrossAxisPosition(child),
324 hitTest: child.hitTest,
325 );
326 if (isHit) {
327 return true;
328 }
329 child = childAfter(child);
330 }
331 return false;
332 }
333
334 @override
335 void visitChildrenForSemantics(RenderObjectVisitor visitor) {
336 RenderSliver? child = firstChild;
337 while (child != null) {
338 if (child.geometry!.visible) {
339 visitor(child);
340 }
341 child = childAfter(child);
342 }
343 }
344}
345