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 | import 'package:flutter/rendering.dart'; |
7 | |
8 | import 'debug.dart'; |
9 | import 'framework.dart'; |
10 | |
11 | /// The signature of the [LayoutBuilder] builder function. |
12 | typedef LayoutWidgetBuilder = Widget Function(BuildContext context, BoxConstraints constraints); |
13 | |
14 | /// An abstract superclass for widgets that defer their building until layout. |
15 | /// |
16 | /// Similar to the [Builder] widget except that the framework calls the [builder] |
17 | /// function at layout time and provides the constraints that this widget should |
18 | /// adhere to. This is useful when the parent constrains the child's size and layout, |
19 | /// and doesn't depend on the child's intrinsic size. |
20 | /// |
21 | /// {@template flutter.widgets.ConstrainedLayoutBuilder} |
22 | /// The [builder] function is called in the following situations: |
23 | /// |
24 | /// * The first time the widget is laid out. |
25 | /// * When the parent widget passes different layout constraints. |
26 | /// * When the parent widget updates this widget. |
27 | /// * When the dependencies that the [builder] function subscribes to change. |
28 | /// |
29 | /// The [builder] function is _not_ called during layout if the parent passes |
30 | /// the same constraints repeatedly. |
31 | /// {@endtemplate} |
32 | /// |
33 | /// Subclasses must return a [RenderObject] that mixes in |
34 | /// [RenderConstrainedLayoutBuilder]. |
35 | abstract class ConstrainedLayoutBuilder<ConstraintType extends Constraints> extends RenderObjectWidget { |
36 | /// Creates a widget that defers its building until layout. |
37 | const ConstrainedLayoutBuilder({ |
38 | super.key, |
39 | required this.builder, |
40 | }); |
41 | |
42 | @override |
43 | RenderObjectElement createElement() => _LayoutBuilderElement<ConstraintType>(this); |
44 | |
45 | /// Called at layout time to construct the widget tree. |
46 | /// |
47 | /// The builder must not return null. |
48 | final Widget Function(BuildContext context, ConstraintType constraints) builder; |
49 | |
50 | /// Whether [builder] needs to be called again even if the layout constraints |
51 | /// are the same. |
52 | /// |
53 | /// When this widget's configuration is updated, the [builder] callback most |
54 | /// likely needs to be called to build this widget's child. However, |
55 | /// subclasses may provide ways in which the widget can be updated without |
56 | /// needing to rebuild the child. Such subclasses can use this method to tell |
57 | /// the framework when the child widget should be rebuilt. |
58 | /// |
59 | /// When this method is called by the framework, the newly configured widget |
60 | /// is asked if it requires a rebuild, and it is passed the old widget as a |
61 | /// parameter. |
62 | /// |
63 | /// See also: |
64 | /// |
65 | /// * [State.setState] and [State.didUpdateWidget], which talk about widget |
66 | /// configuration changes and how they're triggered. |
67 | /// * [Element.update], the method that actually updates the widget's |
68 | /// configuration. |
69 | @protected |
70 | bool updateShouldRebuild(covariant ConstrainedLayoutBuilder<ConstraintType> oldWidget) => true; |
71 | |
72 | // updateRenderObject is redundant with the logic in the LayoutBuilderElement below. |
73 | } |
74 | |
75 | class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderObjectElement { |
76 | _LayoutBuilderElement(ConstrainedLayoutBuilder<ConstraintType> super.widget); |
77 | |
78 | @override |
79 | RenderConstrainedLayoutBuilder<ConstraintType, RenderObject> get renderObject => super.renderObject as RenderConstrainedLayoutBuilder<ConstraintType, RenderObject>; |
80 | |
81 | Element? _child; |
82 | |
83 | @override |
84 | void visitChildren(ElementVisitor visitor) { |
85 | if (_child != null) { |
86 | visitor(_child!); |
87 | } |
88 | } |
89 | |
90 | @override |
91 | void forgetChild(Element child) { |
92 | assert(child == _child); |
93 | _child = null; |
94 | super.forgetChild(child); |
95 | } |
96 | |
97 | @override |
98 | void mount(Element? parent, Object? newSlot) { |
99 | super.mount(parent, newSlot); // Creates the renderObject. |
100 | renderObject.updateCallback(_layout); |
101 | } |
102 | |
103 | @override |
104 | void update(ConstrainedLayoutBuilder<ConstraintType> newWidget) { |
105 | assert(widget != newWidget); |
106 | final ConstrainedLayoutBuilder<ConstraintType> oldWidget = widget as ConstrainedLayoutBuilder<ConstraintType>; |
107 | super.update(newWidget); |
108 | assert(widget == newWidget); |
109 | |
110 | renderObject.updateCallback(_layout); |
111 | if (newWidget.updateShouldRebuild(oldWidget)) { |
112 | renderObject.markNeedsBuild(); |
113 | } |
114 | } |
115 | |
116 | @override |
117 | void performRebuild() { |
118 | // This gets called if markNeedsBuild() is called on us. |
119 | // That might happen if, e.g., our builder uses Inherited widgets. |
120 | |
121 | // Force the callback to be called, even if the layout constraints are the |
122 | // same. This is because that callback may depend on the updated widget |
123 | // configuration, or an inherited widget. |
124 | renderObject.markNeedsBuild(); |
125 | super.performRebuild(); // Calls widget.updateRenderObject (a no-op in this case). |
126 | } |
127 | |
128 | @override |
129 | void unmount() { |
130 | renderObject.updateCallback(null); |
131 | super.unmount(); |
132 | } |
133 | |
134 | void _layout(ConstraintType constraints) { |
135 | @pragma('vm:notify-debugger-on-exception' ) |
136 | void layoutCallback() { |
137 | Widget built; |
138 | try { |
139 | built = (widget as ConstrainedLayoutBuilder<ConstraintType>).builder(this, constraints); |
140 | debugWidgetBuilderValue(widget, built); |
141 | } catch (e, stack) { |
142 | built = ErrorWidget.builder( |
143 | _reportException( |
144 | ErrorDescription('building $widget' ), |
145 | e, |
146 | stack, |
147 | informationCollector: () => <DiagnosticsNode>[ |
148 | if (kDebugMode) |
149 | DiagnosticsDebugCreator(DebugCreator(this)), |
150 | ], |
151 | ), |
152 | ); |
153 | } |
154 | try { |
155 | _child = updateChild(_child, built, null); |
156 | assert(_child != null); |
157 | } catch (e, stack) { |
158 | built = ErrorWidget.builder( |
159 | _reportException( |
160 | ErrorDescription('building $widget' ), |
161 | e, |
162 | stack, |
163 | informationCollector: () => <DiagnosticsNode>[ |
164 | if (kDebugMode) |
165 | DiagnosticsDebugCreator(DebugCreator(this)), |
166 | ], |
167 | ), |
168 | ); |
169 | _child = updateChild(null, built, slot); |
170 | } |
171 | } |
172 | |
173 | owner!.buildScope(this, layoutCallback); |
174 | } |
175 | |
176 | @override |
177 | void insertRenderObjectChild(RenderObject child, Object? slot) { |
178 | final RenderObjectWithChildMixin<RenderObject> renderObject = this.renderObject; |
179 | assert(slot == null); |
180 | assert(renderObject.debugValidateChild(child)); |
181 | renderObject.child = child; |
182 | assert(renderObject == this.renderObject); |
183 | } |
184 | |
185 | @override |
186 | void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object? newSlot) { |
187 | assert(false); |
188 | } |
189 | |
190 | @override |
191 | void removeRenderObjectChild(RenderObject child, Object? slot) { |
192 | final RenderConstrainedLayoutBuilder<ConstraintType, RenderObject> renderObject = this.renderObject; |
193 | assert(renderObject.child == child); |
194 | renderObject.child = null; |
195 | assert(renderObject == this.renderObject); |
196 | } |
197 | } |
198 | |
199 | /// Generic mixin for [RenderObject]s created by [ConstrainedLayoutBuilder]. |
200 | /// |
201 | /// Provides a callback that should be called at layout time, typically in |
202 | /// [RenderObject.performLayout]. |
203 | mixin RenderConstrainedLayoutBuilder<ConstraintType extends Constraints, ChildType extends RenderObject> on RenderObjectWithChildMixin<ChildType> { |
204 | LayoutCallback<ConstraintType>? _callback; |
205 | /// Change the layout callback. |
206 | void updateCallback(LayoutCallback<ConstraintType>? value) { |
207 | if (value == _callback) { |
208 | return; |
209 | } |
210 | _callback = value; |
211 | markNeedsLayout(); |
212 | } |
213 | |
214 | bool _needsBuild = true; |
215 | |
216 | /// Marks this layout builder as needing to rebuild. |
217 | /// |
218 | /// The layout build rebuilds automatically when layout constraints change. |
219 | /// However, we must also rebuild when the widget updates, e.g. after |
220 | /// [State.setState], or [State.didChangeDependencies], even when the layout |
221 | /// constraints remain unchanged. |
222 | /// |
223 | /// See also: |
224 | /// |
225 | /// * [ConstrainedLayoutBuilder.builder], which is called during the rebuild. |
226 | void markNeedsBuild() { |
227 | // Do not call the callback directly. It must be called during the layout |
228 | // phase, when parent constraints are available. Calling `markNeedsLayout` |
229 | // will cause it to be called at the right time. |
230 | _needsBuild = true; |
231 | markNeedsLayout(); |
232 | } |
233 | |
234 | // The constraints that were passed to this class last time it was laid out. |
235 | // These constraints are compared to the new constraints to determine whether |
236 | // [ConstrainedLayoutBuilder.builder] needs to be called. |
237 | Constraints? _previousConstraints; |
238 | |
239 | /// Invoke the callback supplied via [updateCallback]. |
240 | /// |
241 | /// Typically this results in [ConstrainedLayoutBuilder.builder] being called |
242 | /// during layout. |
243 | void rebuildIfNecessary() { |
244 | assert(_callback != null); |
245 | if (_needsBuild || constraints != _previousConstraints) { |
246 | _previousConstraints = constraints; |
247 | _needsBuild = false; |
248 | invokeLayoutCallback(_callback!); |
249 | } |
250 | } |
251 | } |
252 | |
253 | /// Builds a widget tree that can depend on the parent widget's size. |
254 | /// |
255 | /// Similar to the [Builder] widget except that the framework calls the [builder] |
256 | /// function at layout time and provides the parent widget's constraints. This |
257 | /// is useful when the parent constrains the child's size and doesn't depend on |
258 | /// the child's intrinsic size. The [LayoutBuilder]'s final size will match its |
259 | /// child's size. |
260 | /// |
261 | /// {@macro flutter.widgets.ConstrainedLayoutBuilder} |
262 | /// |
263 | /// {@youtube 560 315 https://www.youtube.com/watch?v=IYDVcriKjsw} |
264 | /// |
265 | /// If the child should be smaller than the parent, consider wrapping the child |
266 | /// in an [Align] widget. If the child might want to be bigger, consider |
267 | /// wrapping it in a [SingleChildScrollView] or [OverflowBox]. |
268 | /// |
269 | /// {@tool dartpad} |
270 | /// This example uses a [LayoutBuilder] to build a different widget depending on the available width. Resize the |
271 | /// DartPad window to see [LayoutBuilder] in action! |
272 | /// |
273 | /// ** See code in examples/api/lib/widgets/layout_builder/layout_builder.0.dart ** |
274 | /// {@end-tool} |
275 | /// |
276 | /// See also: |
277 | /// |
278 | /// * [SliverLayoutBuilder], the sliver counterpart of this widget. |
279 | /// * [Builder], which calls a `builder` function at build time. |
280 | /// * [StatefulBuilder], which passes its `builder` function a `setState` callback. |
281 | /// * [CustomSingleChildLayout], which positions its child during layout. |
282 | /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). |
283 | class LayoutBuilder extends ConstrainedLayoutBuilder<BoxConstraints> { |
284 | /// Creates a widget that defers its building until layout. |
285 | const LayoutBuilder({ |
286 | super.key, |
287 | required super.builder, |
288 | }); |
289 | |
290 | @override |
291 | RenderObject createRenderObject(BuildContext context) => _RenderLayoutBuilder(); |
292 | } |
293 | |
294 | class _RenderLayoutBuilder extends RenderBox with RenderObjectWithChildMixin<RenderBox>, RenderConstrainedLayoutBuilder<BoxConstraints, RenderBox> { |
295 | @override |
296 | double computeMinIntrinsicWidth(double height) { |
297 | assert(_debugThrowIfNotCheckingIntrinsics()); |
298 | return 0.0; |
299 | } |
300 | |
301 | @override |
302 | double computeMaxIntrinsicWidth(double height) { |
303 | assert(_debugThrowIfNotCheckingIntrinsics()); |
304 | return 0.0; |
305 | } |
306 | |
307 | @override |
308 | double computeMinIntrinsicHeight(double width) { |
309 | assert(_debugThrowIfNotCheckingIntrinsics()); |
310 | return 0.0; |
311 | } |
312 | |
313 | @override |
314 | double computeMaxIntrinsicHeight(double width) { |
315 | assert(_debugThrowIfNotCheckingIntrinsics()); |
316 | return 0.0; |
317 | } |
318 | |
319 | @override |
320 | Size computeDryLayout(BoxConstraints constraints) { |
321 | assert(debugCannotComputeDryLayout(reason: |
322 | 'Calculating the dry layout would require running the layout callback ' |
323 | 'speculatively, which might mutate the live render object tree.' , |
324 | )); |
325 | return Size.zero; |
326 | } |
327 | |
328 | @override |
329 | void performLayout() { |
330 | final BoxConstraints constraints = this.constraints; |
331 | rebuildIfNecessary(); |
332 | if (child != null) { |
333 | child!.layout(constraints, parentUsesSize: true); |
334 | size = constraints.constrain(child!.size); |
335 | } else { |
336 | size = constraints.biggest; |
337 | } |
338 | } |
339 | |
340 | @override |
341 | double? computeDistanceToActualBaseline(TextBaseline baseline) { |
342 | if (child != null) { |
343 | return child!.getDistanceToActualBaseline(baseline); |
344 | } |
345 | return super.computeDistanceToActualBaseline(baseline); |
346 | } |
347 | |
348 | @override |
349 | bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { |
350 | return child?.hitTest(result, position: position) ?? false; |
351 | } |
352 | |
353 | @override |
354 | void paint(PaintingContext context, Offset offset) { |
355 | if (child != null) { |
356 | context.paintChild(child!, offset); |
357 | } |
358 | } |
359 | |
360 | bool _debugThrowIfNotCheckingIntrinsics() { |
361 | assert(() { |
362 | if (!RenderObject.debugCheckingIntrinsics) { |
363 | throw FlutterError( |
364 | 'LayoutBuilder does not support returning intrinsic dimensions.\n' |
365 | 'Calculating the intrinsic dimensions would require running the layout ' |
366 | 'callback speculatively, which might mutate the live render object tree.' , |
367 | ); |
368 | } |
369 | return true; |
370 | }()); |
371 | |
372 | return true; |
373 | } |
374 | } |
375 | |
376 | FlutterErrorDetails _reportException( |
377 | DiagnosticsNode context, |
378 | Object exception, |
379 | StackTrace stack, { |
380 | InformationCollector? informationCollector, |
381 | }) { |
382 | final FlutterErrorDetails details = FlutterErrorDetails( |
383 | exception: exception, |
384 | stack: stack, |
385 | library: 'widgets library' , |
386 | context: context, |
387 | informationCollector: informationCollector, |
388 | ); |
389 | FlutterError.reportError(details); |
390 | return details; |
391 | } |
392 | |