layout_builder.dart 13.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/foundation.dart';
6 7
import 'package:flutter/rendering.dart';

8 9 10
import 'debug.dart';
import 'framework.dart';

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 18 19
/// function at layout time and provides the constraints that this widget should
/// adhere to. This is useful when the parent constrains the child's size and layout,
/// and doesn't depend on the child's intrinsic size.
20
///
21
/// {@template flutter.widgets.ConstrainedLayoutBuilder}
22 23 24 25 26
/// The [builder] function is called in the following situations:
///
/// * The first time the widget is laid out.
/// * When the parent widget passes different layout constraints.
/// * When the parent widget updates this widget.
27
/// * When the dependencies that the [builder] function subscribes to change.
28 29 30 31
///
/// The [builder] function is _not_ called during layout if the parent passes
/// the same constraints repeatedly.
/// {@endtemplate}
32 33 34
///
/// Subclasses must return a [RenderObject] that mixes in
/// [RenderConstrainedLayoutBuilder].
35
abstract class ConstrainedLayoutBuilder<ConstraintType extends Constraints> extends RenderObjectWidget {
36 37
  /// Creates a widget that defers its building until layout.
  ///
38 39 40
  /// The [builder] argument must not be null, and the returned widget should not
  /// be null.
  const ConstrainedLayoutBuilder({
41 42
    Key? key,
    required this.builder,
43 44
  }) : assert(builder != null),
       super(key: key);
45 46

  @override
47
  RenderObjectElement createElement() => _LayoutBuilderElement<ConstraintType>(this);
48

49 50 51 52
  /// Called at layout time to construct the widget tree.
  ///
  /// The builder must not return null.
  final Widget Function(BuildContext, ConstraintType) builder;
53 54

  // updateRenderObject is redundant with the logic in the LayoutBuilderElement below.
55 56
}

57 58
class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderObjectElement {
  _LayoutBuilderElement(ConstrainedLayoutBuilder<ConstraintType> widget) : super(widget);
59 60

  @override
61
  ConstrainedLayoutBuilder<ConstraintType> get widget => super.widget as ConstrainedLayoutBuilder<ConstraintType>;
62 63

  @override
64
  RenderConstrainedLayoutBuilder<ConstraintType, RenderObject> get renderObject => super.renderObject as RenderConstrainedLayoutBuilder<ConstraintType, RenderObject>;
65

66
  Element? _child;
67 68 69 70

  @override
  void visitChildren(ElementVisitor visitor) {
    if (_child != null)
71
      visitor(_child!);
72 73
  }

74
  @override
75
  void forgetChild(Element child) {
76 77
    assert(child == _child);
    _child = null;
78
    super.forgetChild(child);
79 80
  }

81
  @override
82
  void mount(Element? parent, Object? newSlot) {
83
    super.mount(parent, newSlot); // Creates the renderObject.
84
    renderObject.updateCallback(_layout);
85 86 87
  }

  @override
88
  void update(ConstrainedLayoutBuilder<ConstraintType> newWidget) {
89 90 91
    assert(widget != newWidget);
    super.update(newWidget);
    assert(widget == newWidget);
92

93
    renderObject.updateCallback(_layout);
94 95 96
    // Force the callback to be called, even if the layout constraints are the
    // same, because the logic in the callback might have changed.
    renderObject.markNeedsBuild();
97 98
  }

99 100 101 102
  @override
  void performRebuild() {
    // This gets called if markNeedsBuild() is called on us.
    // That might happen if, e.g., our builder uses Inherited widgets.
103 104 105 106 107

    // Force the callback to be called, even if the layout constraints are the
    // same. This is because that callback may depend on the updated widget
    // configuration, or an inherited widget.
    renderObject.markNeedsBuild();
108
    super.performRebuild(); // Calls widget.updateRenderObject (a no-op in this case).
109 110
  }

111 112
  @override
  void unmount() {
113
    renderObject.updateCallback(null);
114 115 116
    super.unmount();
  }

117
  void _layout(ConstraintType constraints) {
118 119
    @pragma('vm:notify-debugger-on-exception')
    void layoutCallback() {
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
      Widget built;
      try {
        built = widget.builder(this, constraints);
        debugWidgetBuilderValue(widget, built);
      } catch (e, stack) {
        built = ErrorWidget.builder(
          _debugReportException(
            ErrorDescription('building $widget'),
            e,
            stack,
            informationCollector: () sync* {
              yield DiagnosticsDebugCreator(DebugCreator(this));
            },
          ),
        );
135 136 137 138 139
      }
      try {
        _child = updateChild(_child, built, null);
        assert(_child != null);
      } catch (e, stack) {
140 141 142 143 144 145 146 147
        built = ErrorWidget.builder(
          _debugReportException(
            ErrorDescription('building $widget'),
            e,
            stack,
            informationCollector: () sync* {
              yield DiagnosticsDebugCreator(DebugCreator(this));
            },
148
          ),
149
        );
150 151
        _child = updateChild(null, built, slot);
      }
152 153 154
    }

    owner!.buildScope(this, layoutCallback);
155 156 157
  }

  @override
158
  void insertRenderObjectChild(RenderObject child, Object? slot) {
159 160
    final RenderObjectWithChildMixin<RenderObject> renderObject = this.renderObject;
    assert(slot == null);
161
    assert(renderObject.debugValidateChild(child));
162 163 164 165 166
    renderObject.child = child;
    assert(renderObject == this.renderObject);
  }

  @override
167
  void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object? newSlot) {
168 169 170 171
    assert(false);
  }

  @override
172
  void removeRenderObjectChild(RenderObject child, Object? slot) {
173
    final RenderConstrainedLayoutBuilder<ConstraintType, RenderObject> renderObject = this.renderObject;
174 175 176 177 178 179
    assert(renderObject.child == child);
    renderObject.child = null;
    assert(renderObject == this.renderObject);
  }
}

180 181 182 183 184
/// Generic mixin for [RenderObject]s created by [ConstrainedLayoutBuilder].
///
/// Provides a callback that should be called at layout time, typically in
/// [RenderObject.performLayout].
mixin RenderConstrainedLayoutBuilder<ConstraintType extends Constraints, ChildType extends RenderObject> on RenderObjectWithChildMixin<ChildType> {
185
  LayoutCallback<ConstraintType>? _callback;
186
  /// Change the layout callback.
187
  void updateCallback(LayoutCallback<ConstraintType>? value) {
188 189 190 191 192 193
    if (value == _callback)
      return;
    _callback = value;
    markNeedsLayout();
  }

194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
  bool _needsBuild = true;

  /// Marks this layout builder as needing to rebuild.
  ///
  /// The layout build rebuilds automatically when layout constraints change.
  /// However, we must also rebuild when the widget updates, e.g. after
  /// [State.setState], or [State.didChangeDependencies], even when the layout
  /// constraints remain unchanged.
  ///
  /// See also:
  ///
  ///  * [ConstrainedLayoutBuilder.builder], which is called during the rebuild.
  void markNeedsBuild() {
    // Do not call the callback directly. It must be called during the layout
    // phase, when parent constraints are available. Calling `markNeedsLayout`
    // will cause it to be called at the right time.
    _needsBuild = true;
    markNeedsLayout();
  }

  // The constraints that were passed to this class last time it was laid out.
  // These constraints are compared to the new constraints to determine whether
  // [ConstrainedLayoutBuilder.builder] needs to be called.
217
  Constraints? _previousConstraints;
218 219 220 221 222 223

  /// Invoke the callback supplied via [updateCallback].
  ///
  /// Typically this results in [ConstrainedLayoutBuilder.builder] being called
  /// during layout.
  void rebuildIfNecessary() {
224
    assert(_callback != null);
225 226 227
    if (_needsBuild || constraints != _previousConstraints) {
      _previousConstraints = constraints;
      _needsBuild = false;
228
      invokeLayoutCallback(_callback!);
229
    }
230
  }
231 232 233 234 235 236 237 238 239 240
}

/// Builds a widget tree that can depend on the parent widget's size.
///
/// Similar to the [Builder] widget except that the framework calls the [builder]
/// function at layout time and provides the parent widget's constraints. This
/// is useful when the parent constrains the child's size and doesn't depend on
/// the child's intrinsic size. The [LayoutBuilder]'s final size will match its
/// child's size.
///
241
/// {@macro flutter.widgets.ConstrainedLayoutBuilder}
242
///
243 244 245 246
/// {@youtube 560 315 https://www.youtube.com/watch?v=IYDVcriKjsw}
///
/// If the child should be smaller than the parent, consider wrapping the child
/// in an [Align] widget. If the child might want to be bigger, consider
247
/// wrapping it in a [SingleChildScrollView] or [OverflowBox].
248
///
249
/// {@tool dartpad --template=stateless_widget_material}
250 251 252 253 254 255 256
///
/// This example uses a [LayoutBuilder] to build a different widget depending on the available width. Resize the
/// DartPad window to see [LayoutBuilder] in action!
///
/// ```dart
/// Widget build(BuildContext context) {
///   return Scaffold(
257
///     appBar: AppBar(title: const Text('LayoutBuilder Example')),
258
///     body: LayoutBuilder(
259
///       builder: (BuildContext context, BoxConstraints constraints) {
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
///         if (constraints.maxWidth > 600) {
///           return _buildWideContainers();
///         } else {
///           return _buildNormalContainer();
///         }
///       },
///     ),
///   );
/// }
///
/// Widget _buildNormalContainer() {
///   return Center(
///     child: Container(
///       height: 100.0,
///       width: 100.0,
///       color: Colors.red,
///     ),
///   );
/// }
///
/// Widget _buildWideContainers() {
///   return Center(
///     child: Row(
///       mainAxisAlignment: MainAxisAlignment.spaceEvenly,
///       children: <Widget>[
///         Container(
///           height: 100.0,
///           width: 100.0,
///           color: Colors.red,
///         ),
///         Container(
///           height: 100.0,
///           width: 100.0,
///           color: Colors.yellow,
///         ),
///       ],
///     ),
///   );
/// }
/// ```
/// {@end-tool}
///
302 303 304 305 306 307
/// See also:
///
///  * [SliverLayoutBuilder], the sliver counterpart of this widget.
///  * [Builder], which calls a `builder` function at build time.
///  * [StatefulBuilder], which passes its `builder` function a `setState` callback.
///  * [CustomSingleChildLayout], which positions its child during layout.
308
///  * The [catalog of layout widgets](https://flutter.dev/widgets/layout/).
309 310 311 312 313
class LayoutBuilder extends ConstrainedLayoutBuilder<BoxConstraints> {
  /// Creates a widget that defers its building until layout.
  ///
  /// The [builder] argument must not be null.
  const LayoutBuilder({
314 315
    Key? key,
    required LayoutWidgetBuilder builder,
316 317
  }) : assert(builder != null),
       super(key: key, builder: builder);
318

319 320 321 322
  @override
  LayoutWidgetBuilder get builder => super.builder;

  @override
323
  RenderObject createRenderObject(BuildContext context) => _RenderLayoutBuilder();
324 325 326
}

class _RenderLayoutBuilder extends RenderBox with RenderObjectWithChildMixin<RenderBox>, RenderConstrainedLayoutBuilder<BoxConstraints, RenderBox> {
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
  @override
  double computeMinIntrinsicWidth(double height) {
    assert(_debugThrowIfNotCheckingIntrinsics());
    return 0.0;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    assert(_debugThrowIfNotCheckingIntrinsics());
    return 0.0;
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    assert(_debugThrowIfNotCheckingIntrinsics());
    return 0.0;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    assert(_debugThrowIfNotCheckingIntrinsics());
    return 0.0;
  }

351 352 353 354 355 356
  @override
  Size computeDryLayout(BoxConstraints constraints) {
    assert(debugCannotComputeDryLayout(reason:
      'Calculating the dry layout would require running the layout callback '
      'speculatively, which might mutate the live render object tree.',
    ));
357
    return Size.zero;
358 359
  }

360 361
  @override
  void performLayout() {
362
    final BoxConstraints constraints = this.constraints;
363
    rebuildIfNecessary();
364
    if (child != null) {
365 366
      child!.layout(constraints, parentUsesSize: true);
      size = constraints.constrain(child!.size);
367 368 369 370 371
    } else {
      size = constraints.biggest;
    }
  }

372
  @override
373
  double? computeDistanceToActualBaseline(TextBaseline baseline) {
374
    if (child != null)
375
      return child!.getDistanceToActualBaseline(baseline);
376 377 378
    return super.computeDistanceToActualBaseline(baseline);
  }

379
  @override
380
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
381 382 383 384 385 386
    return child?.hitTest(result, position: position) ?? false;
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null)
387
      context.paintChild(child!, offset);
388
  }
389 390 391 392 393 394 395

  bool _debugThrowIfNotCheckingIntrinsics() {
    assert(() {
      if (!RenderObject.debugCheckingIntrinsics) {
        throw FlutterError(
          'LayoutBuilder does not support returning intrinsic dimensions.\n'
          'Calculating the intrinsic dimensions would require running the layout '
396
          'callback speculatively, which might mutate the live render object tree.',
397 398 399 400 401 402 403
        );
      }
      return true;
    }());

    return true;
  }
404 405
}

406
FlutterErrorDetails _debugReportException(
407
  DiagnosticsNode context,
408
  Object exception,
409
  StackTrace stack, {
410
  InformationCollector? informationCollector,
411
}) {
412
  final FlutterErrorDetails details = FlutterErrorDetails(
413 414 415
    exception: exception,
    stack: stack,
    library: 'widgets library',
416
    context: context,
417
    informationCollector: informationCollector,
418 419 420
  );
  FlutterError.reportError(details);
  return details;
421
}