custom_layout.dart 15.6 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';
Adam Barth's avatar
Adam Barth committed
8

9 10 11
import 'box.dart';
import 'object.dart';

12
// For SingleChildLayoutDelegate and RenderCustomSingleChildLayoutBox, see shifted_box.dart
Hixie's avatar
Hixie committed
13

14
/// [ParentData] used by [RenderCustomMultiChildLayoutBox].
15
class MultiChildLayoutParentData extends ContainerBoxParentData<RenderBox> {
16
  /// An object representing the identity of this child.
17
  Object id;
Adam Barth's avatar
Adam Barth committed
18

19
  @override
Adam Barth's avatar
Adam Barth committed
20
  String toString() => '${super.toString()}; id=$id';
21
}
22

23
/// A delegate that controls the layout of multiple children.
24
///
25 26 27
/// Used with [CustomMultiChildLayout] (in the widgets library) and
/// [RenderCustomMultiChildLayoutBox] (in the rendering library).
///
28 29 30 31 32 33
/// Delegates must be idempotent. Specifically, if two delegates are equal, then
/// they must produce the same layout. To change the layout, replace the
/// delegate with a different instance whose [shouldRelayout] returns true when
/// given the previous instance.
///
/// Override [getSize] to control the overall size of the layout. The size of
34 35 36 37 38 39 40 41 42
/// the layout cannot depend on layout properties of the children. This was
/// a design decision to simplify the delegate implementations: This way,
/// the delegate implementations do not have to also handle various intrinsic
/// sizing functions if the parent's size depended on the children.
/// If you want to build a custom layout where you define the size of that widget
/// based on its children, then you will have to create a custom render object.
/// See [MultiChildRenderObjectWidget] with [ContainerRenderObjectMixin] and
/// [RenderBoxContainerDefaultsMixin] to get started or [RenderStack] for an
/// example implementation.
43 44 45 46 47 48 49 50 51 52 53
///
/// Override [performLayout] to size and position the children. An
/// implementation of [performLayout] must call [layoutChild] exactly once for
/// each child, but it may call [layoutChild] on children in an arbitrary order.
/// Typically a delegate will use the size returned from [layoutChild] on one
/// child to determine the constraints for [performLayout] on another child or
/// to determine the offset for [positionChild] for that child or another child.
///
/// Override [shouldRelayout] to determine when the layout of the children needs
/// to be recomputed when the delegate changes.
///
54 55 56 57 58
/// The most efficient way to trigger a relayout is to supply a `relayout`
/// argument to the constructor of the [MultiChildLayoutDelegate]. The custom
/// layout will listen to this value and relayout whenever the Listenable
/// notifies its listeners, such as when an [Animation] ticks. This allows
/// the custom layout to avoid the build phase of the pipeline.
59
///
60 61 62 63
/// Each child must be wrapped in a [LayoutId] widget to assign the id that
/// identifies it to the delegate. The [LayoutId.id] needs to be unique among
/// the children that the [CustomMultiChildLayout] manages.
///
64
/// {@tool snippet}
65 66
///
/// Below is an example implementation of [performLayout] that causes one widget
67
/// (the follower) to be the same size as another (the leader):
68 69
///
/// ```dart
70 71 72 73 74 75 76
/// // Define your own slot numbers, depending upon the id assigned by LayoutId.
/// // Typical usage is to define an enum like the one below, and use those
/// // values as the ids.
/// enum _Slot {
///   leader,
///   follower,
/// }
77
///
78 79 80 81
/// class FollowTheLeader extends MultiChildLayoutDelegate {
///   @override
///   void performLayout(Size size) {
///     Size leaderSize = Size.zero;
82
///
83
///     if (hasChild(_Slot.leader)) {
84
///       leaderSize = layoutChild(_Slot.leader, BoxConstraints.loose(size));
85 86
///       positionChild(_Slot.leader, Offset.zero);
///     }
87
///
88
///     if (hasChild(_Slot.follower)) {
89 90
///       layoutChild(_Slot.follower, BoxConstraints.tight(leaderSize));
///       positionChild(_Slot.follower, Offset(size.width - leaderSize.width,
91 92
///           size.height - leaderSize.height));
///     }
93
///   }
94
///
95 96
///   @override
///   bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
97 98
/// }
/// ```
99
/// {@end-tool}
100 101 102 103 104 105 106 107 108 109 110 111 112
///
/// The delegate gives the leader widget loose constraints, which means the
/// child determines what size to be (subject to fitting within the given size).
/// The delegate then remembers the size of that child and places it in the
/// upper left corner.
///
/// The delegate then gives the follower widget tight constraints, forcing it to
/// match the size of the leader widget. The delegate then places the follower
/// widget in the bottom right corner.
///
/// The leader and follower widget will paint in the order they appear in the
/// child list, regardless of the order in which [layoutChild] is called on
/// them.
113 114 115 116 117 118
///
/// See also:
///
///  * [CustomMultiChildLayout], the widget that uses this delegate.
///  * [RenderCustomMultiChildLayoutBox], render object that uses this
///    delegate.
119
abstract class MultiChildLayoutDelegate {
120 121 122 123 124 125 126
  /// Creates a layout delegate.
  ///
  /// The layout will update whenever [relayout] notifies its listeners.
  MultiChildLayoutDelegate({ Listenable relayout }) : _relayout = relayout;

  final Listenable _relayout;

127
  Map<Object, RenderBox> _idToChild;
128
  Set<RenderBox> _debugChildrenNeedingLayout;
129

130
  /// True if a non-null LayoutChild was provided for the specified id.
131 132 133 134
  ///
  /// Call this from the [performLayout] or [getSize] methods to
  /// determine which children are available, if the child list might
  /// vary.
135
  bool hasChild(Object childId) => _idToChild[childId] != null;
136

137 138
  /// Ask the child to update its layout within the limits specified by
  /// the constraints parameter. The child's size is returned.
139 140 141
  ///
  /// Call this from your [performLayout] function to lay out each
  /// child. Every child must be laid out using this function exactly
142
  /// once each time the [performLayout] function is called.
143 144
  Size layoutChild(Object childId, BoxConstraints constraints) {
    final RenderBox child = _idToChild[childId];
145
    assert(() {
Hixie's avatar
Hixie committed
146
      if (child == null) {
147 148 149 150
        throw FlutterError(
          'The $this custom multichild layout delegate tried to lay out a non-existent child.\n'
          'There is no child with the id "$childId".'
        );
Hixie's avatar
Hixie committed
151 152
      }
      if (!_debugChildrenNeedingLayout.remove(child)) {
153 154 155 156
        throw FlutterError(
          'The $this custom multichild layout delegate tried to lay out the child with id "$childId" more than once.\n'
          'Each child must be laid out exactly once.'
        );
Hixie's avatar
Hixie committed
157 158
      }
      try {
159
        assert(constraints.debugAssertIsValid(isAppliedConstraint: true));
Hixie's avatar
Hixie committed
160
      } on AssertionError catch (exception) {
161 162 163 164 165 166 167 168 169
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('The $this custom multichild layout delegate provided invalid box constraints for the child with id "$childId".'),
          DiagnosticsProperty<AssertionError>('Exception', exception, showName: false),
          ErrorDescription(
            'The minimum width and height must be greater than or equal to zero.\n'
            'The maximum width must be greater than or equal to the minimum width.\n'
            'The maximum height must be greater than or equal to the minimum height.'
          )
        ]);
Hixie's avatar
Hixie committed
170 171
      }
      return true;
172
    }());
173 174 175 176 177
    child.layout(constraints, parentUsesSize: true);
    return child.size;
  }

  /// Specify the child's origin relative to this origin.
178 179 180 181 182
  ///
  /// Call this from your [performLayout] function to position each
  /// child. If you do not call this for a child, its position will
  /// remain unchanged. Children initially have their position set to
  /// (0,0), i.e. the top left of the [RenderCustomMultiChildLayoutBox].
183
  void positionChild(Object childId, Offset offset) {
184
    final RenderBox child = _idToChild[childId];
Hixie's avatar
Hixie committed
185 186
    assert(() {
      if (child == null) {
187 188 189 190
        throw FlutterError(
          'The $this custom multichild layout delegate tried to position out a non-existent child:\n'
          'There is no child with the id "$childId".'
        );
Hixie's avatar
Hixie committed
191 192
      }
      if (offset == null) {
193 194 195
        throw FlutterError(
          'The $this custom multichild layout delegate provided a null position for the child with id "$childId".'
        );
Hixie's avatar
Hixie committed
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);
Hixie's avatar
Hixie committed
206 207
  }

208
  void _callPerformLayout(Size size, RenderBox firstChild) {
209 210 211
    // A particular layout delegate could be called reentrantly, e.g. if it used
    // by both a parent and a child. So, we must restore the _idToChild map when
    // we return.
212
    final Map<Object, RenderBox> previousIdToChild = _idToChild;
213 214 215 216

    Set<RenderBox> debugPreviousChildrenNeedingLayout;
    assert(() {
      debugPreviousChildrenNeedingLayout = _debugChildrenNeedingLayout;
217
      _debugChildrenNeedingLayout = <RenderBox>{};
218
      return true;
219
    }());
220

221
    try {
222
      _idToChild = <Object, RenderBox>{};
223 224
      RenderBox child = firstChild;
      while (child != null) {
225
        final MultiChildLayoutParentData childParentData = child.parentData as MultiChildLayoutParentData;
Hixie's avatar
Hixie committed
226 227
        assert(() {
          if (childParentData.id == null) {
228 229 230 231
            throw FlutterError.fromParts(<DiagnosticsNode>[
              ErrorSummary('Every child of a RenderCustomMultiChildLayoutBox must have an ID in its parent data.'),
              child.describeForError('The following child has no ID'),
            ]);
Hixie's avatar
Hixie committed
232 233
          }
          return true;
234
        }());
235
        _idToChild[childParentData.id] = child;
236 237 238
        assert(() {
          _debugChildrenNeedingLayout.add(child);
          return true;
239
        }());
240 241
        child = childParentData.nextSibling;
      }
242
      performLayout(size);
243
      assert(() {
Hixie's avatar
Hixie committed
244
        if (_debugChildrenNeedingLayout.isNotEmpty) {
245 246 247 248 249 250 251 252 253 254 255
          throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('Each child must be laid out exactly once.'),
            DiagnosticsBlock(
              name:
                'The $this custom multichild layout delegate forgot '
                'to lay out the following '
                '${_debugChildrenNeedingLayout.length > 1 ? 'children' : 'child'}',
              properties: _debugChildrenNeedingLayout.map<DiagnosticsNode>(_debugDescribeChild).toList(),
              style: DiagnosticsTreeStyle.whitespace,
            ),
          ]);
Hixie's avatar
Hixie committed
256 257
        }
        return true;
258
      }());
259 260
    } finally {
      _idToChild = previousIdToChild;
261 262 263
      assert(() {
        _debugChildrenNeedingLayout = debugPreviousChildrenNeedingLayout;
        return true;
264
      }());
265 266 267
    }
  }

268
  /// Override this method to return the size of this object given the
269 270 271 272
  /// incoming constraints.
  ///
  /// The size cannot reflect the sizes of the children. If this layout has a
  /// fixed width or height the returned size can reflect that; the size will be
273 274 275 276 277
  /// constrained to the given constraints.
  ///
  /// By default, attempts to size the box to the biggest size
  /// possible given the constraints.
  Size getSize(BoxConstraints constraints) => constraints.biggest;
278

279
  /// Override this method to lay out and position all children given this
280 281 282 283
  /// widget's size.
  ///
  /// This method must call [layoutChild] for each child. It should also specify
  /// the final position of each child with [positionChild].
284
  void performLayout(Size size);
Hixie's avatar
Hixie committed
285

286
  /// Override this method to return true when the children need to be
287 288 289 290 291
  /// laid out.
  ///
  /// This should compare the fields of the current delegate and the given
  /// `oldDelegate` and return true if the fields are such that the layout would
  /// be different.
292
  bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate);
293 294 295 296 297

  /// Override this method to include additional information in the
  /// debugging data printed by [debugDumpRenderTree] and friends.
  ///
  /// By default, returns the [runtimeType] of the class.
298
  @override
299
  String toString() => objectRuntimeType(this, 'MultiChildLayoutDelegate');
300 301
}

302 303 304 305 306 307
/// Defers the layout of multiple children to a delegate.
///
/// The delegate can determine the layout constraints for each child and can
/// decide where to position each child. The delegate can also determine the
/// size of the parent, but the size of the parent cannot depend on the sizes of
/// the children.
308
class RenderCustomMultiChildLayoutBox extends RenderBox
309 310
  with ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
       RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
311 312 313
  /// Creates a render object that customizes the layout of multiple children.
  ///
  /// The [delegate] argument must not be null.
314 315
  RenderCustomMultiChildLayoutBox({
    List<RenderBox> children,
316
    @required MultiChildLayoutDelegate delegate,
317 318
  }) : assert(delegate != null),
       _delegate = delegate {
319 320 321
    addAll(children);
  }

322
  @override
323
  void setupParentData(RenderBox child) {
324
    if (child.parentData is! MultiChildLayoutParentData)
325
      child.parentData = MultiChildLayoutParentData();
326 327
  }

328
  /// The delegate that controls the layout of the children.
329 330
  MultiChildLayoutDelegate get delegate => _delegate;
  MultiChildLayoutDelegate _delegate;
331 332 333
  set delegate(MultiChildLayoutDelegate newDelegate) {
    assert(newDelegate != null);
    if (_delegate == newDelegate)
334
      return;
335 336
    final MultiChildLayoutDelegate oldDelegate = _delegate;
    if (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRelayout(oldDelegate))
337
      markNeedsLayout();
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
    _delegate = newDelegate;
    if (attached) {
      oldDelegate?._relayout?.removeListener(markNeedsLayout);
      newDelegate?._relayout?.addListener(markNeedsLayout);
    }
  }

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    _delegate?._relayout?.addListener(markNeedsLayout);
  }

  @override
  void detach() {
    _delegate?._relayout?.removeListener(markNeedsLayout);
    super.detach();
355 356 357
  }

  Size _getSize(BoxConstraints constraints) {
358
    assert(constraints.debugAssertIsValid());
359 360 361
    return constraints.constrain(_delegate.getSize(constraints));
  }

362 363 364 365
  // TODO(ianh): It's a bit dubious to be using the getSize function from the delegate to
  // figure out the intrinsic dimensions. We really should either not support intrinsics,
  // or we should expose intrinsic delegate callbacks and throw if they're not implemented.

366
  @override
367
  double computeMinIntrinsicWidth(double height) {
368
    final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width;
369 370 371
    if (width.isFinite)
      return width;
    return 0.0;
372 373
  }

374
  @override
375
  double computeMaxIntrinsicWidth(double height) {
376
    final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width;
377 378 379
    if (width.isFinite)
      return width;
    return 0.0;
380 381
  }

382
  @override
383
  double computeMinIntrinsicHeight(double width) {
384
    final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height;
385 386 387
    if (height.isFinite)
      return height;
    return 0.0;
388 389
  }

390
  @override
391
  double computeMaxIntrinsicHeight(double width) {
392
    final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height;
393 394 395
    if (height.isFinite)
      return height;
    return 0.0;
396 397
  }

398
  @override
399
  void performLayout() {
400
    size = _getSize(constraints);
401
    delegate._callPerformLayout(size, firstChild);
402 403
  }

404
  @override
405 406 407 408
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
  }

409
  @override
410
  bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
Adam Barth's avatar
Adam Barth committed
411
    return defaultHitTestChildren(result, position: position);
412 413
  }
}