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 'package:flutter/foundation.dart';
6
7import 'box.dart';
8import 'object.dart';
9
10// For SingleChildLayoutDelegate and RenderCustomSingleChildLayoutBox, see shifted_box.dart
11
12/// [ParentData] used by [RenderCustomMultiChildLayoutBox].
13class 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.
117abstract 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.
302class 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