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 | import 'dart:math' as math; |
6 | import 'package:vector_math/vector_math_64.dart' ; |
7 | |
8 | import 'object.dart'; |
9 | import '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. |
26 | class 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 | |
167 | bool _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. |
201 | class 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 | |