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 'package:flutter/foundation.dart'; |
6 | |
7 | import 'box.dart'; |
8 | import 'object.dart'; |
9 | |
10 | // For SingleChildLayoutDelegate and RenderCustomSingleChildLayoutBox, see shifted_box.dart |
11 | |
12 | /// [ParentData] used by [RenderCustomMultiChildLayoutBox]. |
13 | class MultiChildLayoutParentData extends ContainerBoxParentData<RenderBox> { |
14 | /// An object representing the identity of this child. |
15 | Object? id; |
16 | |
17 | @override |
18 | String toString() => ' ${super.toString()}; id= $id' ; |
19 | } |
20 | |
21 | /// A delegate that controls the layout of multiple children. |
22 | /// |
23 | /// Used with [CustomMultiChildLayout] (in the widgets library) and |
24 | /// [RenderCustomMultiChildLayoutBox] (in the rendering library). |
25 | /// |
26 | /// Delegates must be idempotent. Specifically, if two delegates are equal, then |
27 | /// they must produce the same layout. To change the layout, replace the |
28 | /// delegate with a different instance whose [shouldRelayout] returns true when |
29 | /// given the previous instance. |
30 | /// |
31 | /// Override [getSize] to control the overall size of the layout. The size of |
32 | /// the layout cannot depend on layout properties of the children. This was |
33 | /// a design decision to simplify the delegate implementations: This way, |
34 | /// the delegate implementations do not have to also handle various intrinsic |
35 | /// sizing functions if the parent's size depended on the children. |
36 | /// If you want to build a custom layout where you define the size of that widget |
37 | /// based on its children, then you will have to create a custom render object. |
38 | /// See [MultiChildRenderObjectWidget] with [ContainerRenderObjectMixin] and |
39 | /// [RenderBoxContainerDefaultsMixin] to get started or [RenderStack] for an |
40 | /// example implementation. |
41 | /// |
42 | /// Override [performLayout] to size and position the children. An |
43 | /// implementation of [performLayout] must call [layoutChild] exactly once for |
44 | /// each child, but it may call [layoutChild] on children in an arbitrary order. |
45 | /// Typically a delegate will use the size returned from [layoutChild] on one |
46 | /// child to determine the constraints for [performLayout] on another child or |
47 | /// to determine the offset for [positionChild] for that child or another child. |
48 | /// |
49 | /// Override [shouldRelayout] to determine when the layout of the children needs |
50 | /// to be recomputed when the delegate changes. |
51 | /// |
52 | /// The most efficient way to trigger a relayout is to supply a `relayout` |
53 | /// argument to the constructor of the [MultiChildLayoutDelegate]. The custom |
54 | /// layout will listen to this value and relayout whenever the Listenable |
55 | /// notifies its listeners, such as when an [Animation] ticks. This allows |
56 | /// the custom layout to avoid the build phase of the pipeline. |
57 | /// |
58 | /// Each child must be wrapped in a [LayoutId] widget to assign the id that |
59 | /// identifies it to the delegate. The [LayoutId.id] needs to be unique among |
60 | /// the children that the [CustomMultiChildLayout] manages. |
61 | /// |
62 | /// {@tool snippet} |
63 | /// |
64 | /// Below is an example implementation of [performLayout] that causes one widget |
65 | /// (the follower) to be the same size as another (the leader): |
66 | /// |
67 | /// ```dart |
68 | /// // Define your own slot numbers, depending upon the id assigned by LayoutId. |
69 | /// // Typical usage is to define an enum like the one below, and use those |
70 | /// // values as the ids. |
71 | /// enum _Slot { |
72 | /// leader, |
73 | /// follower, |
74 | /// } |
75 | /// |
76 | /// class FollowTheLeader extends MultiChildLayoutDelegate { |
77 | /// @override |
78 | /// void performLayout(Size size) { |
79 | /// Size leaderSize = Size.zero; |
80 | /// |
81 | /// if (hasChild(_Slot.leader)) { |
82 | /// leaderSize = layoutChild(_Slot.leader, BoxConstraints.loose(size)); |
83 | /// positionChild(_Slot.leader, Offset.zero); |
84 | /// } |
85 | /// |
86 | /// if (hasChild(_Slot.follower)) { |
87 | /// layoutChild(_Slot.follower, BoxConstraints.tight(leaderSize)); |
88 | /// positionChild(_Slot.follower, Offset(size.width - leaderSize.width, |
89 | /// size.height - leaderSize.height)); |
90 | /// } |
91 | /// } |
92 | /// |
93 | /// @override |
94 | /// bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false; |
95 | /// } |
96 | /// ``` |
97 | /// {@end-tool} |
98 | /// |
99 | /// The delegate gives the leader widget loose constraints, which means the |
100 | /// child determines what size to be (subject to fitting within the given size). |
101 | /// The delegate then remembers the size of that child and places it in the |
102 | /// upper left corner. |
103 | /// |
104 | /// The delegate then gives the follower widget tight constraints, forcing it to |
105 | /// match the size of the leader widget. The delegate then places the follower |
106 | /// widget in the bottom right corner. |
107 | /// |
108 | /// The leader and follower widget will paint in the order they appear in the |
109 | /// child list, regardless of the order in which [layoutChild] is called on |
110 | /// them. |
111 | /// |
112 | /// See also: |
113 | /// |
114 | /// * [CustomMultiChildLayout], the widget that uses this delegate. |
115 | /// * [RenderCustomMultiChildLayoutBox], render object that uses this |
116 | /// delegate. |
117 | abstract class MultiChildLayoutDelegate { |
118 | /// Creates a layout delegate. |
119 | /// |
120 | /// The layout will update whenever [relayout] notifies its listeners. |
121 | MultiChildLayoutDelegate({ Listenable? relayout }) : _relayout = relayout; |
122 | |
123 | final Listenable? _relayout; |
124 | |
125 | Map<Object, RenderBox>? _idToChild; |
126 | Set<RenderBox>? _debugChildrenNeedingLayout; |
127 | |
128 | /// True if a non-null LayoutChild was provided for the specified id. |
129 | /// |
130 | /// Call this from the [performLayout] method to determine which children |
131 | /// are available, if the child list might vary. |
132 | /// |
133 | /// This method cannot be called from [getSize] as the size is not allowed |
134 | /// to depend on the children. |
135 | bool hasChild(Object childId) => _idToChild![childId] != null; |
136 | |
137 | /// Ask the child to update its layout within the limits specified by |
138 | /// the constraints parameter. The child's size is returned. |
139 | /// |
140 | /// Call this from your [performLayout] function to lay out each |
141 | /// child. Every child must be laid out using this function exactly |
142 | /// once each time the [performLayout] function is called. |
143 | Size layoutChild(Object childId, BoxConstraints constraints) { |
144 | final RenderBox? child = _idToChild![childId]; |
145 | assert(() { |
146 | if (child == null) { |
147 | throw FlutterError( |
148 | 'The $this custom multichild layout delegate tried to lay out a non-existent child.\n' |
149 | 'There is no child with the id " $childId".' , |
150 | ); |
151 | } |
152 | if (!_debugChildrenNeedingLayout!.remove(child)) { |
153 | throw FlutterError( |
154 | 'The $this custom multichild layout delegate tried to lay out the child with id " $childId" more than once.\n' |
155 | 'Each child must be laid out exactly once.' , |
156 | ); |
157 | } |
158 | try { |
159 | assert(constraints.debugAssertIsValid(isAppliedConstraint: true)); |
160 | } on AssertionError catch (exception) { |
161 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
162 | ErrorSummary('The $this custom multichild layout delegate provided invalid box constraints for the child with id " $childId".' ), |
163 | DiagnosticsProperty<AssertionError>('Exception' , exception, showName: false), |
164 | ErrorDescription( |
165 | 'The minimum width and height must be greater than or equal to zero.\n' |
166 | 'The maximum width must be greater than or equal to the minimum width.\n' |
167 | 'The maximum height must be greater than or equal to the minimum height.' , |
168 | ), |
169 | ]); |
170 | } |
171 | return true; |
172 | }()); |
173 | child!.layout(constraints, parentUsesSize: true); |
174 | return child.size; |
175 | } |
176 | |
177 | /// Specify the child's origin relative to this origin. |
178 | /// |
179 | /// Call this from your [performLayout] function to position each |
180 | /// child. If you do not call this for a child, its position will |
181 | /// remain unchanged. Children initially have their position set to |
182 | /// (0,0), i.e. the top left of the [RenderCustomMultiChildLayoutBox]. |
183 | void positionChild(Object childId, Offset offset) { |
184 | final RenderBox? child = _idToChild![childId]; |
185 | assert(() { |
186 | if (child == null) { |
187 | throw FlutterError( |
188 | 'The $this custom multichild layout delegate tried to position out a non-existent child:\n' |
189 | 'There is no child with the id " $childId".' , |
190 | ); |
191 | } |
192 | return true; |
193 | }()); |
194 | final MultiChildLayoutParentData childParentData = child!.parentData! as MultiChildLayoutParentData; |
195 | childParentData.offset = offset; |
196 | } |
197 | |
198 | DiagnosticsNode _debugDescribeChild(RenderBox child) { |
199 | final MultiChildLayoutParentData childParentData = child.parentData! as MultiChildLayoutParentData; |
200 | return DiagnosticsProperty<RenderBox>(' ${childParentData.id}' , child); |
201 | } |
202 | |
203 | void _callPerformLayout(Size size, RenderBox? firstChild) { |
204 | // A particular layout delegate could be called reentrantly, e.g. if it used |
205 | // by both a parent and a child. So, we must restore the _idToChild map when |
206 | // we return. |
207 | final Map<Object, RenderBox>? previousIdToChild = _idToChild; |
208 | |
209 | Set<RenderBox>? debugPreviousChildrenNeedingLayout; |
210 | assert(() { |
211 | debugPreviousChildrenNeedingLayout = _debugChildrenNeedingLayout; |
212 | _debugChildrenNeedingLayout = <RenderBox>{}; |
213 | return true; |
214 | }()); |
215 | |
216 | try { |
217 | _idToChild = <Object, RenderBox>{}; |
218 | RenderBox? child = firstChild; |
219 | while (child != null) { |
220 | final MultiChildLayoutParentData childParentData = child.parentData! as MultiChildLayoutParentData; |
221 | assert(() { |
222 | if (childParentData.id == null) { |
223 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
224 | ErrorSummary('Every child of a RenderCustomMultiChildLayoutBox must have an ID in its parent data.' ), |
225 | child!.describeForError('The following child has no ID' ), |
226 | ]); |
227 | } |
228 | return true; |
229 | }()); |
230 | _idToChild![childParentData.id!] = child; |
231 | assert(() { |
232 | _debugChildrenNeedingLayout!.add(child!); |
233 | return true; |
234 | }()); |
235 | child = childParentData.nextSibling; |
236 | } |
237 | performLayout(size); |
238 | assert(() { |
239 | if (_debugChildrenNeedingLayout!.isNotEmpty) { |
240 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
241 | ErrorSummary('Each child must be laid out exactly once.' ), |
242 | DiagnosticsBlock( |
243 | name: |
244 | 'The $this custom multichild layout delegate forgot ' |
245 | 'to lay out the following ' |
246 | ' ${_debugChildrenNeedingLayout!.length > 1 ? 'children' : 'child' }' , |
247 | properties: _debugChildrenNeedingLayout!.map<DiagnosticsNode>(_debugDescribeChild).toList(), |
248 | ), |
249 | ]); |
250 | } |
251 | return true; |
252 | }()); |
253 | } finally { |
254 | _idToChild = previousIdToChild; |
255 | assert(() { |
256 | _debugChildrenNeedingLayout = debugPreviousChildrenNeedingLayout; |
257 | return true; |
258 | }()); |
259 | } |
260 | } |
261 | |
262 | /// Override this method to return the size of this object given the |
263 | /// incoming constraints. |
264 | /// |
265 | /// The size cannot reflect the sizes of the children. If this layout has a |
266 | /// fixed width or height the returned size can reflect that; the size will be |
267 | /// constrained to the given constraints. |
268 | /// |
269 | /// By default, attempts to size the box to the biggest size |
270 | /// possible given the constraints. |
271 | Size getSize(BoxConstraints constraints) => constraints.biggest; |
272 | |
273 | /// Override this method to lay out and position all children given this |
274 | /// widget's size. |
275 | /// |
276 | /// This method must call [layoutChild] for each child. It should also specify |
277 | /// the final position of each child with [positionChild]. |
278 | void performLayout(Size size); |
279 | |
280 | /// Override this method to return true when the children need to be |
281 | /// laid out. |
282 | /// |
283 | /// This should compare the fields of the current delegate and the given |
284 | /// `oldDelegate` and return true if the fields are such that the layout would |
285 | /// be different. |
286 | bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate); |
287 | |
288 | /// Override this method to include additional information in the |
289 | /// debugging data printed by [debugDumpRenderTree] and friends. |
290 | /// |
291 | /// By default, returns the [runtimeType] of the class. |
292 | @override |
293 | String toString() => objectRuntimeType(this, 'MultiChildLayoutDelegate' ); |
294 | } |
295 | |
296 | /// Defers the layout of multiple children to a delegate. |
297 | /// |
298 | /// The delegate can determine the layout constraints for each child and can |
299 | /// decide where to position each child. The delegate can also determine the |
300 | /// size of the parent, but the size of the parent cannot depend on the sizes of |
301 | /// the children. |
302 | class RenderCustomMultiChildLayoutBox extends RenderBox |
303 | with ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>, |
304 | RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> { |
305 | /// Creates a render object that customizes the layout of multiple children. |
306 | RenderCustomMultiChildLayoutBox({ |
307 | List<RenderBox>? children, |
308 | required MultiChildLayoutDelegate delegate, |
309 | }) : _delegate = delegate { |
310 | addAll(children); |
311 | } |
312 | |
313 | @override |
314 | void setupParentData(RenderBox child) { |
315 | if (child.parentData is! MultiChildLayoutParentData) { |
316 | child.parentData = MultiChildLayoutParentData(); |
317 | } |
318 | } |
319 | |
320 | /// The delegate that controls the layout of the children. |
321 | MultiChildLayoutDelegate get delegate => _delegate; |
322 | MultiChildLayoutDelegate _delegate; |
323 | set delegate(MultiChildLayoutDelegate newDelegate) { |
324 | if (_delegate == newDelegate) { |
325 | return; |
326 | } |
327 | final MultiChildLayoutDelegate oldDelegate = _delegate; |
328 | if (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRelayout(oldDelegate)) { |
329 | markNeedsLayout(); |
330 | } |
331 | _delegate = newDelegate; |
332 | if (attached) { |
333 | oldDelegate._relayout?.removeListener(markNeedsLayout); |
334 | newDelegate._relayout?.addListener(markNeedsLayout); |
335 | } |
336 | } |
337 | |
338 | @override |
339 | void attach(PipelineOwner owner) { |
340 | super.attach(owner); |
341 | _delegate._relayout?.addListener(markNeedsLayout); |
342 | } |
343 | |
344 | @override |
345 | void detach() { |
346 | _delegate._relayout?.removeListener(markNeedsLayout); |
347 | super.detach(); |
348 | } |
349 | |
350 | Size _getSize(BoxConstraints constraints) { |
351 | assert(constraints.debugAssertIsValid()); |
352 | return constraints.constrain(_delegate.getSize(constraints)); |
353 | } |
354 | |
355 | // TODO(ianh): It's a bit dubious to be using the getSize function from the delegate to |
356 | // figure out the intrinsic dimensions. We really should either not support intrinsics, |
357 | // or we should expose intrinsic delegate callbacks and throw if they're not implemented. |
358 | |
359 | @override |
360 | double computeMinIntrinsicWidth(double height) { |
361 | final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width; |
362 | if (width.isFinite) { |
363 | return width; |
364 | } |
365 | return 0.0; |
366 | } |
367 | |
368 | @override |
369 | double computeMaxIntrinsicWidth(double height) { |
370 | final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width; |
371 | if (width.isFinite) { |
372 | return width; |
373 | } |
374 | return 0.0; |
375 | } |
376 | |
377 | @override |
378 | double computeMinIntrinsicHeight(double width) { |
379 | final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height; |
380 | if (height.isFinite) { |
381 | return height; |
382 | } |
383 | return 0.0; |
384 | } |
385 | |
386 | @override |
387 | double computeMaxIntrinsicHeight(double width) { |
388 | final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height; |
389 | if (height.isFinite) { |
390 | return height; |
391 | } |
392 | return 0.0; |
393 | } |
394 | |
395 | @override |
396 | @protected |
397 | Size computeDryLayout(covariant BoxConstraints constraints) { |
398 | return _getSize(constraints); |
399 | } |
400 | |
401 | @override |
402 | void performLayout() { |
403 | size = _getSize(constraints); |
404 | delegate._callPerformLayout(size, firstChild); |
405 | } |
406 | |
407 | @override |
408 | void paint(PaintingContext context, Offset offset) { |
409 | defaultPaint(context, offset); |
410 | } |
411 | |
412 | @override |
413 | bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { |
414 | return defaultHitTestChildren(result, position: position); |
415 | } |
416 | } |
417 | |