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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com