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;
6
7import 'package:flutter/foundation.dart';
8
9import 'box.dart';
10import 'object.dart';
11
12/// Parent data for use with [RenderListBody].
13class ListBodyParentData extends ContainerBoxParentData<RenderBox> { }
14
15typedef _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.
27class 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