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 @protected
68 Size computeDryLayout(covariant BoxConstraints constraints) {
69 assert(_debugCheckConstraints(constraints));
70 double mainAxisExtent = 0.0;
71 RenderBox? child = firstChild;
72 switch (axisDirection) {
73 case AxisDirection.right:
74 case AxisDirection.left:
75 final BoxConstraints innerConstraints = BoxConstraints.tightFor(height: constraints.maxHeight);
76 while (child != null) {
77 final Size childSize = child.getDryLayout(innerConstraints);
78 mainAxisExtent += childSize.width;
79 child = childAfter(child);
80 }
81 return constraints.constrain(Size(mainAxisExtent, constraints.maxHeight));
82 case AxisDirection.up:
83 case AxisDirection.down:
84 final BoxConstraints innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth);
85 while (child != null) {
86 final Size childSize = child.getDryLayout(innerConstraints);
87 mainAxisExtent += childSize.height;
88 child = childAfter(child);
89 }
90 return constraints.constrain(Size(constraints.maxWidth, mainAxisExtent));
91 }
92 }
93
94 bool _debugCheckConstraints(BoxConstraints constraints) {
95 assert(() {
96 switch (mainAxis) {
97 case Axis.horizontal:
98 if (!constraints.hasBoundedWidth) {
99 return true;
100 }
101 case Axis.vertical:
102 if (!constraints.hasBoundedHeight) {
103 return true;
104 }
105 }
106 throw FlutterError.fromParts(<DiagnosticsNode>[
107 ErrorSummary('RenderListBody must have unlimited space along its main axis.'),
108 ErrorDescription(
109 'RenderListBody does not clip or resize its children, so it must be '
110 'placed in a parent that does not constrain the main '
111 'axis.',
112 ),
113 ErrorHint(
114 'You probably want to put the RenderListBody inside a '
115 'RenderViewport with a matching main axis.',
116 ),
117 ]);
118 }());
119 assert(() {
120 switch (mainAxis) {
121 case Axis.horizontal:
122 if (constraints.hasBoundedHeight) {
123 return true;
124 }
125 case Axis.vertical:
126 if (constraints.hasBoundedWidth) {
127 return true;
128 }
129 }
130 // TODO(ianh): Detect if we're actually nested blocks and say something
131 // more specific to the exact situation in that case, and don't mention
132 // nesting blocks in the negative case.
133 throw FlutterError.fromParts(<DiagnosticsNode>[
134 ErrorSummary('RenderListBody must have a bounded constraint for its cross axis.'),
135 ErrorDescription(
136 "RenderListBody forces its children to expand to fit the RenderListBody's container, "
137 'so it must be placed in a parent that constrains the cross '
138 'axis to a finite dimension.',
139 ),
140 // TODO(jacobr): this hint is a great candidate to promote to being an
141 // automated quick fix in the future.
142 ErrorHint(
143 'If you are attempting to nest a RenderListBody with '
144 'one direction inside one of another direction, you will want to '
145 'wrap the inner one inside a box that fixes the dimension in that direction, '
146 'for example, a RenderIntrinsicWidth or RenderIntrinsicHeight object. '
147 'This is relatively expensive, however.', // (that's why we don't do it automatically)
148 ),
149 ]);
150 }());
151 return true;
152 }
153
154 @override
155 void performLayout() {
156 final BoxConstraints constraints = this.constraints;
157 assert(_debugCheckConstraints(constraints));
158 double mainAxisExtent = 0.0;
159 RenderBox? child = firstChild;
160 switch (axisDirection) {
161 case AxisDirection.right:
162 final BoxConstraints innerConstraints = BoxConstraints.tightFor(height: constraints.maxHeight);
163 while (child != null) {
164 child.layout(innerConstraints, parentUsesSize: true);
165 final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
166 childParentData.offset = Offset(mainAxisExtent, 0.0);
167 mainAxisExtent += child.size.width;
168 assert(child.parentData == childParentData);
169 child = childParentData.nextSibling;
170 }
171 size = constraints.constrain(Size(mainAxisExtent, constraints.maxHeight));
172 case AxisDirection.left:
173 final BoxConstraints innerConstraints = BoxConstraints.tightFor(height: constraints.maxHeight);
174 while (child != null) {
175 child.layout(innerConstraints, parentUsesSize: true);
176 final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
177 mainAxisExtent += child.size.width;
178 assert(child.parentData == childParentData);
179 child = childParentData.nextSibling;
180 }
181 double position = 0.0;
182 child = firstChild;
183 while (child != null) {
184 final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
185 position += child.size.width;
186 childParentData.offset = Offset(mainAxisExtent - position, 0.0);
187 assert(child.parentData == childParentData);
188 child = childParentData.nextSibling;
189 }
190 size = constraints.constrain(Size(mainAxisExtent, constraints.maxHeight));
191 case AxisDirection.down:
192 final BoxConstraints innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth);
193 while (child != null) {
194 child.layout(innerConstraints, parentUsesSize: true);
195 final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
196 childParentData.offset = Offset(0.0, mainAxisExtent);
197 mainAxisExtent += child.size.height;
198 assert(child.parentData == childParentData);
199 child = childParentData.nextSibling;
200 }
201 size = constraints.constrain(Size(constraints.maxWidth, mainAxisExtent));
202 case AxisDirection.up:
203 final BoxConstraints innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth);
204 while (child != null) {
205 child.layout(innerConstraints, parentUsesSize: true);
206 final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
207 mainAxisExtent += child.size.height;
208 assert(child.parentData == childParentData);
209 child = childParentData.nextSibling;
210 }
211 double position = 0.0;
212 child = firstChild;
213 while (child != null) {
214 final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
215 position += child.size.height;
216 childParentData.offset = Offset(0.0, mainAxisExtent - position);
217 assert(child.parentData == childParentData);
218 child = childParentData.nextSibling;
219 }
220 size = constraints.constrain(Size(constraints.maxWidth, mainAxisExtent));
221 }
222 assert(size.isFinite);
223 }
224
225 @override
226 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
227 super.debugFillProperties(properties);
228 properties.add(EnumProperty<AxisDirection>('axisDirection', axisDirection));
229 }
230
231 double _getIntrinsicCrossAxis(_ChildSizingFunction childSize) {
232 double extent = 0.0;
233 RenderBox? child = firstChild;
234 while (child != null) {
235 extent = math.max(extent, childSize(child));
236 final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
237 child = childParentData.nextSibling;
238 }
239 return extent;
240 }
241
242 double _getIntrinsicMainAxis(_ChildSizingFunction childSize) {
243 double extent = 0.0;
244 RenderBox? child = firstChild;
245 while (child != null) {
246 extent += childSize(child);
247 final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
248 child = childParentData.nextSibling;
249 }
250 return extent;
251 }
252
253 @override
254 double computeMinIntrinsicWidth(double height) {
255 switch (mainAxis) {
256 case Axis.horizontal:
257 return _getIntrinsicMainAxis((RenderBox child) => child.getMinIntrinsicWidth(height));
258 case Axis.vertical:
259 return _getIntrinsicCrossAxis((RenderBox child) => child.getMinIntrinsicWidth(height));
260 }
261 }
262
263 @override
264 double computeMaxIntrinsicWidth(double height) {
265 switch (mainAxis) {
266 case Axis.horizontal:
267 return _getIntrinsicMainAxis((RenderBox child) => child.getMaxIntrinsicWidth(height));
268 case Axis.vertical:
269 return _getIntrinsicCrossAxis((RenderBox child) => child.getMaxIntrinsicWidth(height));
270 }
271 }
272
273 @override
274 double computeMinIntrinsicHeight(double width) {
275 switch (mainAxis) {
276 case Axis.horizontal:
277 return _getIntrinsicMainAxis((RenderBox child) => child.getMinIntrinsicHeight(width));
278 case Axis.vertical:
279 return _getIntrinsicCrossAxis((RenderBox child) => child.getMinIntrinsicHeight(width));
280 }
281 }
282
283 @override
284 double computeMaxIntrinsicHeight(double width) {
285 switch (mainAxis) {
286 case Axis.horizontal:
287 return _getIntrinsicMainAxis((RenderBox child) => child.getMaxIntrinsicHeight(width));
288 case Axis.vertical:
289 return _getIntrinsicCrossAxis((RenderBox child) => child.getMaxIntrinsicHeight(width));
290 }
291 }
292
293 @override
294 double? computeDistanceToActualBaseline(TextBaseline baseline) {
295 return defaultComputeDistanceToFirstActualBaseline(baseline);
296 }
297
298 @override
299 void paint(PaintingContext context, Offset offset) {
300 defaultPaint(context, offset);
301 }
302
303 @override
304 bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
305 return defaultHitTestChildren(result, position: position);
306 }
307
308}
309