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'; |
6 | library; |
7 | |
8 | import 'dart:math' as math; |
9 | import 'dart:ui'; |
10 | |
11 | import 'package:flutter/foundation.dart'; |
12 | import 'package:vector_math/vector_math_64.dart'; |
13 | |
14 | import 'object.dart'; |
15 | import '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. |
32 | class 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 | |
181 | bool _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. |
215 | class 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 |
Definitions
- RenderSliverCrossAxisGroup
- setupParentData
- childMainAxisPosition
- childCrossAxisPosition
- performLayout
- paint
- applyPaintTransform
- hitTestChildren
- _assertOutOfExtent
- RenderSliverMainAxisGroup
- setupParentData
- childScrollOffset
- childMainAxisPosition
- childCrossAxisPosition
- performLayout
- paint
- applyPaintTransform
- hitTestChildren
- visitChildrenForSemantics
Learn more about Flutter for embedded and desktop on industrialflutter.com