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 | @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 | |