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