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 | |
7 | import 'package:flutter/foundation.dart'; |
8 | |
9 | import 'box.dart'; |
10 | import 'object.dart'; |
11 | |
12 | /// Parent data for use with [RenderListBody]. |
13 | class ListBodyParentData extends ContainerBoxParentData<RenderBox> { } |
14 | |
15 | typedef _ChildSizingFunction = double Function(RenderBox child); |
16 | |
17 | /// Displays its children sequentially along a given axis, forcing them to the |
18 | /// dimensions of the parent in the other axis. |
19 | /// |
20 | /// This layout algorithm arranges its children linearly along the main axis |
21 | /// (either horizontally or vertically). In the cross axis, children are |
22 | /// stretched to match the box's cross-axis extent. In the main axis, children |
23 | /// are given unlimited space and the box expands its main axis to contain all |
24 | /// its children. Because [RenderListBody] boxes expand in the main axis, they |
25 | /// must be given unlimited space in the main axis, typically by being contained |
26 | /// in a viewport with a scrolling direction that matches the box's main axis. |
27 | class RenderListBody extends RenderBox |
28 | with ContainerRenderObjectMixin<RenderBox, ListBodyParentData>, |
29 | RenderBoxContainerDefaultsMixin<RenderBox, ListBodyParentData> { |
30 | /// Creates a render object that arranges its children sequentially along a |
31 | /// given axis. |
32 | /// |
33 | /// By default, children are arranged along the vertical axis. |
34 | RenderListBody({ |
35 | List<RenderBox>? children, |
36 | AxisDirection axisDirection = AxisDirection.down, |
37 | }) : _axisDirection = axisDirection { |
38 | addAll(children); |
39 | } |
40 | |
41 | @override |
42 | void setupParentData(RenderBox child) { |
43 | if (child.parentData is! ListBodyParentData) { |
44 | child.parentData = ListBodyParentData(); |
45 | } |
46 | } |
47 | |
48 | /// The direction in which the children are laid out. |
49 | /// |
50 | /// For example, if the [axisDirection] is [AxisDirection.down], each child |
51 | /// will be laid out below the next, vertically. |
52 | AxisDirection get axisDirection => _axisDirection; |
53 | AxisDirection _axisDirection; |
54 | set axisDirection(AxisDirection value) { |
55 | if (_axisDirection == value) { |
56 | return; |
57 | } |
58 | _axisDirection = value; |
59 | markNeedsLayout(); |
60 | } |
61 | |
62 | /// The axis (horizontal or vertical) corresponding to the current |
63 | /// [axisDirection]. |
64 | Axis get mainAxis => axisDirectionToAxis(axisDirection); |
65 | |
66 | @override |
67 | double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { |
68 | assert(_debugCheckConstraints(constraints)); |
69 | RenderBox? child; |
70 | final RenderBox? Function(RenderBox) nextChild; |
71 | switch (axisDirection) { |
72 | case AxisDirection.right: |
73 | case AxisDirection.left: |
74 | final BoxConstraints childConstraints = BoxConstraints.tightFor(height: constraints.maxHeight); |
75 | BaselineOffset baselineOffset = BaselineOffset.noBaseline; |
76 | for (child = firstChild; child != null; child = childAfter(child)) { |
77 | baselineOffset = baselineOffset.minOf(BaselineOffset(child.getDryBaseline(childConstraints, baseline))); |
78 | } |
79 | return baselineOffset.offset; |
80 | case AxisDirection.up: |
81 | child = lastChild; |
82 | nextChild = childBefore; |
83 | case AxisDirection.down: |
84 | child = firstChild; |
85 | nextChild = childAfter; |
86 | } |
87 | final BoxConstraints childConstraints = BoxConstraints.tightFor(width: constraints.maxWidth); |
88 | double mainAxisExtent = 0.0; |
89 | for (; child != null; child = nextChild(child)) { |
90 | final double? childBaseline = child.getDryBaseline(childConstraints, baseline); |
91 | if (childBaseline != null) { |
92 | return childBaseline + mainAxisExtent; |
93 | } |
94 | mainAxisExtent += child.getDryLayout(childConstraints).height; |
95 | } |
96 | return null; |
97 | } |
98 | |
99 | @override |
100 | @protected |
101 | Size computeDryLayout(covariant BoxConstraints constraints) { |
102 | assert(_debugCheckConstraints(constraints)); |
103 | double mainAxisExtent = 0.0; |
104 | RenderBox? child = firstChild; |
105 | switch (axisDirection) { |
106 | case AxisDirection.right: |
107 | case AxisDirection.left: |
108 | final BoxConstraints innerConstraints = BoxConstraints.tightFor(height: constraints.maxHeight); |
109 | while (child != null) { |
110 | final Size childSize = child.getDryLayout(innerConstraints); |
111 | mainAxisExtent += childSize.width; |
112 | child = childAfter(child); |
113 | } |
114 | return constraints.constrain(Size(mainAxisExtent, constraints.maxHeight)); |
115 | case AxisDirection.up: |
116 | case AxisDirection.down: |
117 | final BoxConstraints innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth); |
118 | while (child != null) { |
119 | final Size childSize = child.getDryLayout(innerConstraints); |
120 | mainAxisExtent += childSize.height; |
121 | child = childAfter(child); |
122 | } |
123 | return constraints.constrain(Size(constraints.maxWidth, mainAxisExtent)); |
124 | } |
125 | } |
126 | |
127 | bool _debugCheckConstraints(BoxConstraints constraints) { |
128 | assert(() { |
129 | switch (mainAxis) { |
130 | case Axis.horizontal: |
131 | if (!constraints.hasBoundedWidth) { |
132 | return true; |
133 | } |
134 | case Axis.vertical: |
135 | if (!constraints.hasBoundedHeight) { |
136 | return true; |
137 | } |
138 | } |
139 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
140 | ErrorSummary('RenderListBody must have unlimited space along its main axis.' ), |
141 | ErrorDescription( |
142 | 'RenderListBody does not clip or resize its children, so it must be ' |
143 | 'placed in a parent that does not constrain the main ' |
144 | 'axis.' , |
145 | ), |
146 | ErrorHint( |
147 | 'You probably want to put the RenderListBody inside a ' |
148 | 'RenderViewport with a matching main axis.' , |
149 | ), |
150 | ]); |
151 | }()); |
152 | assert(() { |
153 | switch (mainAxis) { |
154 | case Axis.horizontal: |
155 | if (constraints.hasBoundedHeight) { |
156 | return true; |
157 | } |
158 | case Axis.vertical: |
159 | if (constraints.hasBoundedWidth) { |
160 | return true; |
161 | } |
162 | } |
163 | // TODO(ianh): Detect if we're actually nested blocks and say something |
164 | // more specific to the exact situation in that case, and don't mention |
165 | // nesting blocks in the negative case. |
166 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
167 | ErrorSummary('RenderListBody must have a bounded constraint for its cross axis.' ), |
168 | ErrorDescription( |
169 | "RenderListBody forces its children to expand to fit the RenderListBody's container, " |
170 | 'so it must be placed in a parent that constrains the cross ' |
171 | 'axis to a finite dimension.' , |
172 | ), |
173 | // TODO(jacobr): this hint is a great candidate to promote to being an |
174 | // automated quick fix in the future. |
175 | ErrorHint( |
176 | 'If you are attempting to nest a RenderListBody with ' |
177 | 'one direction inside one of another direction, you will want to ' |
178 | 'wrap the inner one inside a box that fixes the dimension in that direction, ' |
179 | 'for example, a RenderIntrinsicWidth or RenderIntrinsicHeight object. ' |
180 | 'This is relatively expensive, however.' , // (that's why we don't do it automatically) |
181 | ), |
182 | ]); |
183 | }()); |
184 | return true; |
185 | } |
186 | |
187 | @override |
188 | void performLayout() { |
189 | final BoxConstraints constraints = this.constraints; |
190 | assert(_debugCheckConstraints(constraints)); |
191 | double mainAxisExtent = 0.0; |
192 | RenderBox? child = firstChild; |
193 | switch (axisDirection) { |
194 | case AxisDirection.right: |
195 | final BoxConstraints innerConstraints = BoxConstraints.tightFor(height: constraints.maxHeight); |
196 | while (child != null) { |
197 | child.layout(innerConstraints, parentUsesSize: true); |
198 | final ListBodyParentData childParentData = child.parentData! as ListBodyParentData; |
199 | childParentData.offset = Offset(mainAxisExtent, 0.0); |
200 | mainAxisExtent += child.size.width; |
201 | assert(child.parentData == childParentData); |
202 | child = childParentData.nextSibling; |
203 | } |
204 | size = constraints.constrain(Size(mainAxisExtent, constraints.maxHeight)); |
205 | case AxisDirection.left: |
206 | final BoxConstraints innerConstraints = BoxConstraints.tightFor(height: constraints.maxHeight); |
207 | while (child != null) { |
208 | child.layout(innerConstraints, parentUsesSize: true); |
209 | final ListBodyParentData childParentData = child.parentData! as ListBodyParentData; |
210 | mainAxisExtent += child.size.width; |
211 | assert(child.parentData == childParentData); |
212 | child = childParentData.nextSibling; |
213 | } |
214 | double position = 0.0; |
215 | child = firstChild; |
216 | while (child != null) { |
217 | final ListBodyParentData childParentData = child.parentData! as ListBodyParentData; |
218 | position += child.size.width; |
219 | childParentData.offset = Offset(mainAxisExtent - position, 0.0); |
220 | assert(child.parentData == childParentData); |
221 | child = childParentData.nextSibling; |
222 | } |
223 | size = constraints.constrain(Size(mainAxisExtent, constraints.maxHeight)); |
224 | case AxisDirection.down: |
225 | final BoxConstraints innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth); |
226 | while (child != null) { |
227 | child.layout(innerConstraints, parentUsesSize: true); |
228 | final ListBodyParentData childParentData = child.parentData! as ListBodyParentData; |
229 | childParentData.offset = Offset(0.0, mainAxisExtent); |
230 | mainAxisExtent += child.size.height; |
231 | assert(child.parentData == childParentData); |
232 | child = childParentData.nextSibling; |
233 | } |
234 | size = constraints.constrain(Size(constraints.maxWidth, mainAxisExtent)); |
235 | case AxisDirection.up: |
236 | final BoxConstraints innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth); |
237 | while (child != null) { |
238 | child.layout(innerConstraints, parentUsesSize: true); |
239 | final ListBodyParentData childParentData = child.parentData! as ListBodyParentData; |
240 | mainAxisExtent += child.size.height; |
241 | assert(child.parentData == childParentData); |
242 | child = childParentData.nextSibling; |
243 | } |
244 | double position = 0.0; |
245 | child = firstChild; |
246 | while (child != null) { |
247 | final ListBodyParentData childParentData = child.parentData! as ListBodyParentData; |
248 | position += child.size.height; |
249 | childParentData.offset = Offset(0.0, mainAxisExtent - position); |
250 | assert(child.parentData == childParentData); |
251 | child = childParentData.nextSibling; |
252 | } |
253 | size = constraints.constrain(Size(constraints.maxWidth, mainAxisExtent)); |
254 | } |
255 | assert(size.isFinite); |
256 | } |
257 | |
258 | @override |
259 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
260 | super.debugFillProperties(properties); |
261 | properties.add(EnumProperty<AxisDirection>('axisDirection' , axisDirection)); |
262 | } |
263 | |
264 | double _getIntrinsicCrossAxis(_ChildSizingFunction childSize) { |
265 | double extent = 0.0; |
266 | RenderBox? child = firstChild; |
267 | while (child != null) { |
268 | extent = math.max(extent, childSize(child)); |
269 | final ListBodyParentData childParentData = child.parentData! as ListBodyParentData; |
270 | child = childParentData.nextSibling; |
271 | } |
272 | return extent; |
273 | } |
274 | |
275 | double _getIntrinsicMainAxis(_ChildSizingFunction childSize) { |
276 | double extent = 0.0; |
277 | RenderBox? child = firstChild; |
278 | while (child != null) { |
279 | extent += childSize(child); |
280 | final ListBodyParentData childParentData = child.parentData! as ListBodyParentData; |
281 | child = childParentData.nextSibling; |
282 | } |
283 | return extent; |
284 | } |
285 | |
286 | @override |
287 | double computeMinIntrinsicWidth(double height) { |
288 | return switch (mainAxis) { |
289 | Axis.horizontal => _getIntrinsicMainAxis((RenderBox child) => child.getMinIntrinsicWidth(height)), |
290 | Axis.vertical => _getIntrinsicCrossAxis((RenderBox child) => child.getMinIntrinsicWidth(height)), |
291 | }; |
292 | } |
293 | |
294 | @override |
295 | double computeMaxIntrinsicWidth(double height) { |
296 | return switch (mainAxis) { |
297 | Axis.horizontal => _getIntrinsicMainAxis((RenderBox child) => child.getMaxIntrinsicWidth(height)), |
298 | Axis.vertical => _getIntrinsicCrossAxis((RenderBox child) => child.getMaxIntrinsicWidth(height)), |
299 | }; |
300 | } |
301 | |
302 | @override |
303 | double computeMinIntrinsicHeight(double width) { |
304 | return switch (mainAxis) { |
305 | Axis.horizontal => _getIntrinsicMainAxis((RenderBox child) => child.getMinIntrinsicHeight(width)), |
306 | Axis.vertical => _getIntrinsicCrossAxis((RenderBox child) => child.getMinIntrinsicHeight(width)), |
307 | }; |
308 | } |
309 | |
310 | @override |
311 | double computeMaxIntrinsicHeight(double width) { |
312 | return switch (mainAxis) { |
313 | Axis.horizontal => _getIntrinsicMainAxis((RenderBox child) => child.getMaxIntrinsicHeight(width)), |
314 | Axis.vertical => _getIntrinsicCrossAxis((RenderBox child) => child.getMaxIntrinsicHeight(width)), |
315 | }; |
316 | } |
317 | |
318 | @override |
319 | double? computeDistanceToActualBaseline(TextBaseline baseline) { |
320 | return defaultComputeDistanceToFirstActualBaseline(baseline); |
321 | } |
322 | |
323 | @override |
324 | void paint(PaintingContext context, Offset offset) { |
325 | defaultPaint(context, offset); |
326 | } |
327 | |
328 | @override |
329 | bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { |
330 | return defaultHitTestChildren(result, position: position); |
331 | } |
332 | |
333 | } |
334 | |