layout_builder.dart 11.5 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
abstract class ConstrainedLayoutBuilder<ConstraintType extends Constraints> extends RenderObjectWidget {
35 36
  /// Creates a widget that defers its building until layout.
  ///
37 38 39
  /// The [builder] argument must not be null, and the returned widget should not
  /// be null.
  const ConstrainedLayoutBuilder({
40
    Key key,
41
    @required this.builder,
42 43
  }) : assert(builder != null),
       super(key: key);
44 45

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

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

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

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

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

  @override
63
  RenderConstrainedLayoutBuilder<ConstraintType, RenderObject> get renderObject => super.renderObject as RenderConstrainedLayoutBuilder<ConstraintType, RenderObject>;
64 65 66 67 68 69 70 71 72

  Element _child;

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

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

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

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

92
    renderObject.updateCallback(_layout);
93 94 95
    // 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();
96 97
  }

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

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

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

116
  void _layout(ConstraintType constraints) {
117 118 119
    owner.buildScope(this, () {
      Widget built;
      if (widget.builder != null) {
120 121 122 123
        try {
          built = widget.builder(this, constraints);
          debugWidgetBuilderValue(widget, built);
        } catch (e, stack) {
124 125 126 127 128 129 130 131
          built = ErrorWidget.builder(
            _debugReportException(
              ErrorDescription('building $widget'),
              e,
              stack,
              informationCollector: () sync* {
                yield DiagnosticsDebugCreator(DebugCreator(this));
              },
132
            ),
133
          );
134
        }
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 155 156 157 158
  }

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

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

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

178 179 180 181 182 183 184 185
/// 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) {
186 187 188 189 190 191
    if (value == _callback)
      return;
    _callback = value;
    markNeedsLayout();
  }

192 193 194 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
  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() {
222
    assert(_callback != null);
223 224 225 226 227
    if (_needsBuild || constraints != _previousConstraints) {
      _previousConstraints = constraints;
      _needsBuild = false;
      invokeLayoutCallback(_callback);
    }
228
  }
229 230 231 232 233 234 235 236 237 238
}

/// 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.
///
239 240
/// {@macro flutter.widgets.layoutBuilder.builderFunctionInvocation}
///
241 242 243 244
/// {@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
245
/// wrapping it in a [SingleChildScrollView] or [OverflowBox].
246 247 248 249 250 251 252
///
/// 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.
253
///  * The [catalog of layout widgets](https://flutter.dev/widgets/layout/).
254 255 256 257 258 259
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,
260 261 262
    @required LayoutWidgetBuilder builder,
  }) : assert(builder != null),
       super(key: key, builder: builder);
263

264 265 266 267 268 269 270 271
  @override
  LayoutWidgetBuilder get builder => super.builder;

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

class _RenderLayoutBuilder extends RenderBox with RenderObjectWithChildMixin<RenderBox>, RenderConstrainedLayoutBuilder<BoxConstraints, RenderBox> {
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
  @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() {
298
    final BoxConstraints constraints = this.constraints;
299
    rebuildIfNecessary();
300 301 302 303 304 305 306 307 308
    if (child != null) {
      child.layout(constraints, parentUsesSize: true);
      size = constraints.constrain(child.size);
    } else {
      size = constraints.biggest;
    }
  }

  @override
309
  bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
310 311 312 313 314 315 316 317
    return child?.hitTest(result, position: position) ?? false;
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null)
      context.paintChild(child, offset);
  }
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332

  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;
  }
333 334
}

335
FlutterErrorDetails _debugReportException(
336
  DiagnosticsNode context,
337
  dynamic exception,
338 339 340
  StackTrace stack, {
  InformationCollector informationCollector,
}) {
341
  final FlutterErrorDetails details = FlutterErrorDetails(
342 343 344
    exception: exception,
    stack: stack,
    library: 'widgets library',
345
    context: context,
346
    informationCollector: informationCollector,
347 348 349
  );
  FlutterError.reportError(details);
  return details;
350
}