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';
8library;
9
10import 'package:flutter/foundation.dart';
11
12import 'box.dart';
13import 'object.dart';
14
15// For SingleChildLayoutDelegate and RenderCustomSingleChildLayoutBox, see shifted_box.dart
16
17/// [ParentData] used by [RenderCustomMultiChildLayoutBox].
18class 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.
122abstract 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.
307class 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