layout_builder.dart 12.9 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 6
// @dart = 2.8

7
import 'package:flutter/foundation.dart';
8 9
import 'package:flutter/rendering.dart';

10 11 12
import 'debug.dart';
import 'framework.dart';

13
/// The signature of the [LayoutBuilder] builder function.
14
typedef LayoutWidgetBuilder = Widget Function(BuildContext context, BoxConstraints constraints);
15

16
/// An abstract superclass for widgets that defer their building until layout.
17 18
///
/// Similar to the [Builder] widget except that the framework calls the [builder]
19 20 21
/// 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.
22 23 24 25 26 27 28
///
/// {@template flutter.widgets.layoutBuilder.builderFunctionInvocation}
/// 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.
29
/// * When the dependencies that the [builder] function subscribes to change.
30 31 32 33
///
/// The [builder] function is _not_ called during layout if the parent passes
/// the same constraints repeatedly.
/// {@endtemplate}
34 35 36
///
/// Subclasses must return a [RenderObject] that mixes in
/// [RenderConstrainedLayoutBuilder].
37
abstract class ConstrainedLayoutBuilder<ConstraintType extends Constraints> extends RenderObjectWidget {
38 39
  /// Creates a widget that defers its building until layout.
  ///
40 41 42
  /// The [builder] argument must not be null, and the returned widget should not
  /// be null.
  const ConstrainedLayoutBuilder({
43
    Key key,
44
    @required this.builder,
45 46
  }) : assert(builder != null),
       super(key: key);
47 48

  @override
49
  _LayoutBuilderElement<ConstraintType> createElement() => _LayoutBuilderElement<ConstraintType>(this);
50

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

  // updateRenderObject is redundant with the logic in the LayoutBuilderElement below.
57 58
}

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

  @override
63
  ConstrainedLayoutBuilder<ConstraintType> get widget => super.widget as ConstrainedLayoutBuilder<ConstraintType>;
64 65

  @override
66
  RenderConstrainedLayoutBuilder<ConstraintType, RenderObject> get renderObject => super.renderObject as RenderConstrainedLayoutBuilder<ConstraintType, RenderObject>;
67 68 69 70 71 72 73 74 75

  Element _child;

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

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

83 84
  @override
  void mount(Element parent, dynamic newSlot) {
85
    super.mount(parent, newSlot); // Creates the renderObject.
86
    renderObject.updateCallback(_layout);
87 88 89
  }

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

95
    renderObject.updateCallback(_layout);
96 97 98
    // 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();
99 100
  }

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

    // 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();
110
    super.performRebuild(); // Calls widget.updateRenderObject (a no-op in this case).
111 112
  }

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

119
  void _layout(ConstraintType constraints) {
120 121 122
    owner.buildScope(this, () {
      Widget built;
      if (widget.builder != null) {
123 124 125 126
        try {
          built = widget.builder(this, constraints);
          debugWidgetBuilderValue(widget, built);
        } catch (e, stack) {
127 128 129 130 131 132 133 134
          built = ErrorWidget.builder(
            _debugReportException(
              ErrorDescription('building $widget'),
              e,
              stack,
              informationCollector: () sync* {
                yield DiagnosticsDebugCreator(DebugCreator(this));
              },
135
            ),
136
          );
137
        }
138 139 140 141 142
      }
      try {
        _child = updateChild(_child, built, null);
        assert(_child != null);
      } catch (e, stack) {
143 144 145 146 147 148 149 150
        built = ErrorWidget.builder(
          _debugReportException(
            ErrorDescription('building $widget'),
            e,
            stack,
            informationCollector: () sync* {
              yield DiagnosticsDebugCreator(DebugCreator(this));
            },
151
          ),
152
        );
153 154
        _child = updateChild(null, built, slot);
      }
155
    });
156 157 158 159 160 161
  }

  @override
  void insertChildRenderObject(RenderObject child, dynamic slot) {
    final RenderObjectWithChildMixin<RenderObject> renderObject = this.renderObject;
    assert(slot == null);
162
    assert(renderObject.debugValidateChild(child));
163 164 165 166 167 168 169 170 171 172 173
    renderObject.child = child;
    assert(renderObject == this.renderObject);
  }

  @override
  void moveChildRenderObject(RenderObject child, dynamic slot) {
    assert(false);
  }

  @override
  void removeChildRenderObject(RenderObject child) {
174
    final RenderConstrainedLayoutBuilder<ConstraintType, RenderObject> renderObject = this.renderObject;
175 176 177 178 179 180
    assert(renderObject.child == child);
    renderObject.child = null;
    assert(renderObject == this.renderObject);
  }
}

181 182 183 184 185 186 187 188
/// 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> {
  LayoutCallback<ConstraintType> _callback;
  /// Change the layout callback.
  void updateCallback(LayoutCallback<ConstraintType> value) {
189 190 191 192 193 194
    if (value == _callback)
      return;
    _callback = value;
    markNeedsLayout();
  }

195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
  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.
  Constraints _previousConstraints;

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

/// 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.
///
242 243
/// {@macro flutter.widgets.layoutBuilder.builderFunctionInvocation}
///
244 245 246 247
/// {@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
248
/// wrapping it in a [SingleChildScrollView] or [OverflowBox].
249
///
250 251 252 253 254 255 256 257 258 259 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 302
/// {@tool dartpad --template=stateless_widget_material}
///
/// 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(
///     appBar: AppBar(title: Text("LayoutBuilder Example")),
///     body: LayoutBuilder(
///       builder: (context, constraints) {
///         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}
///
303 304 305 306 307 308
/// 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.
309
///  * The [catalog of layout widgets](https://flutter.dev/widgets/layout/).
310 311 312 313 314 315
class LayoutBuilder extends ConstrainedLayoutBuilder<BoxConstraints> {
  /// Creates a widget that defers its building until layout.
  ///
  /// The [builder] argument must not be null.
  const LayoutBuilder({
    Key key,
316 317 318
    @required LayoutWidgetBuilder builder,
  }) : assert(builder != null),
       super(key: key, builder: builder);
319

320 321 322 323 324 325 326 327
  @override
  LayoutWidgetBuilder get builder => super.builder;

  @override
  _RenderLayoutBuilder createRenderObject(BuildContext context) => _RenderLayoutBuilder();
}

class _RenderLayoutBuilder extends RenderBox with RenderObjectWithChildMixin<RenderBox>, RenderConstrainedLayoutBuilder<BoxConstraints, RenderBox> {
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
  @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;
  }

  @override
  void performLayout() {
354
    final BoxConstraints constraints = this.constraints;
355
    rebuildIfNecessary();
356 357 358 359 360 361 362 363 364
    if (child != null) {
      child.layout(constraints, parentUsesSize: true);
      size = constraints.constrain(child.size);
    } else {
      size = constraints.biggest;
    }
  }

  @override
365
  bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
366 367 368 369 370 371 372 373
    return child?.hitTest(result, position: position) ?? false;
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null)
      context.paintChild(child, offset);
  }
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388

  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 '
          'callback speculatively, which might mutate the live render object tree.'
        );
      }
      return true;
    }());

    return true;
  }
389 390
}

391
FlutterErrorDetails _debugReportException(
392
  DiagnosticsNode context,
393
  dynamic exception,
394 395 396
  StackTrace stack, {
  InformationCollector informationCollector,
}) {
397
  final FlutterErrorDetails details = FlutterErrorDetails(
398 399 400
    exception: exception,
    stack: stack,
    library: 'widgets library',
401
    context: context,
402
    informationCollector: informationCollector,
403 404 405
  );
  FlutterError.reportError(details);
  return details;
406
}