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

7 8 9
import 'box.dart';
import 'object.dart';

10
// For SingleChildLayoutDelegate and RenderCustomSingleChildLayoutBox, see shifted_box.dart
Hixie's avatar
Hixie committed
11

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

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

21
/// A delegate that controls the layout of multiple children.
22
///
23 24 25
/// Used with [CustomMultiChildLayout] (in the widgets library) and
/// [RenderCustomMultiChildLayoutBox] (in the rendering library).
///
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
/// 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
/// the layout cannot depend on layout properties of the children.
///
/// 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.
///
44 45 46 47 48
/// 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.
49
///
50 51 52 53
/// 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.
///
54
/// {@tool sample}
55 56
///
/// Below is an example implementation of [performLayout] that causes one widget
57
/// (the follower) to be the same size as another (the leader):
58 59
///
/// ```dart
60 61 62 63 64 65 66
/// // 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,
/// }
67
///
68 69 70 71
/// class FollowTheLeader extends MultiChildLayoutDelegate {
///   @override
///   void performLayout(Size size) {
///     Size leaderSize = Size.zero;
72
///
73
///     if (hasChild(_Slot.leader)) {
74
///       leaderSize = layoutChild(_Slot.leader, BoxConstraints.loose(size));
75 76
///       positionChild(_Slot.leader, Offset.zero);
///     }
77
///
78
///     if (hasChild(_Slot.follower)) {
79 80
///       layoutChild(_Slot.follower, BoxConstraints.tight(leaderSize));
///       positionChild(_Slot.follower, Offset(size.width - leaderSize.width,
81 82
///           size.height - leaderSize.height));
///     }
83
///   }
84
///
85 86
///   @override
///   bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
87 88
/// }
/// ```
89
/// {@end-tool}
90 91 92 93 94 95 96 97 98 99 100 101 102
///
/// 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.
103 104 105 106 107 108
///
/// See also:
///
///  * [CustomMultiChildLayout], the widget that uses this delegate.
///  * [RenderCustomMultiChildLayoutBox], render object that uses this
///    delegate.
109
abstract class MultiChildLayoutDelegate {
110 111 112 113 114 115 116
  /// Creates a layout delegate.
  ///
  /// The layout will update whenever [relayout] notifies its listeners.
  MultiChildLayoutDelegate({ Listenable relayout }) : _relayout = relayout;

  final Listenable _relayout;

117
  Map<Object, RenderBox> _idToChild;
118
  Set<RenderBox> _debugChildrenNeedingLayout;
119

120
  /// True if a non-null LayoutChild was provided for the specified id.
121 122 123 124
  ///
  /// Call this from the [performLayout] or [getSize] methods to
  /// determine which children are available, if the child list might
  /// vary.
125
  bool hasChild(Object childId) => _idToChild[childId] != null;
126

127 128
  /// Ask the child to update its layout within the limits specified by
  /// the constraints parameter. The child's size is returned.
129 130 131
  ///
  /// Call this from your [performLayout] function to lay out each
  /// child. Every child must be laid out using this function exactly
132
  /// once each time the [performLayout] function is called.
133 134
  Size layoutChild(Object childId, BoxConstraints constraints) {
    final RenderBox child = _idToChild[childId];
135
    assert(() {
Hixie's avatar
Hixie committed
136
      if (child == null) {
137 138 139 140
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('The $this custom multichild layout delegate tried to lay out a non-existent child.'),
          ErrorDescription('There is no child with the id "$childId".')
        ]);
Hixie's avatar
Hixie committed
141 142
      }
      if (!_debugChildrenNeedingLayout.remove(child)) {
143 144 145 146
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('The $this custom multichild layout delegate tried to lay out the child with id "$childId" more than once.'),
          ErrorDescription('Each child must be laid out exactly once.')
        ]);
Hixie's avatar
Hixie committed
147 148
      }
      try {
149
        assert(constraints.debugAssertIsValid(isAppliedConstraint: true));
Hixie's avatar
Hixie committed
150
      } on AssertionError catch (exception) {
151 152 153 154 155 156 157 158 159
        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
160 161
      }
      return true;
162
    }());
163 164 165 166 167
    child.layout(constraints, parentUsesSize: true);
    return child.size;
  }

  /// Specify the child's origin relative to this origin.
168 169 170 171 172
  ///
  /// 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].
173
  void positionChild(Object childId, Offset offset) {
174
    final RenderBox child = _idToChild[childId];
Hixie's avatar
Hixie committed
175 176
    assert(() {
      if (child == null) {
177 178 179 180
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('The $this custom multichild layout delegate tried to position out a non-existent child:'),
          ErrorDescription('There is no child with the id "$childId".')
        ]);
Hixie's avatar
Hixie committed
181 182
      }
      if (offset == null) {
183 184 185
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('The $this custom multichild layout delegate provided a null position for the child with id "$childId".')
        ]);
Hixie's avatar
Hixie committed
186 187
      }
      return true;
188
    }());
189
    final MultiChildLayoutParentData childParentData = child.parentData;
190
    childParentData.offset = offset;
191 192
  }

193
  DiagnosticsNode _debugDescribeChild(RenderBox child) {
Hixie's avatar
Hixie committed
194
    final MultiChildLayoutParentData childParentData = child.parentData;
195
    return DiagnosticsProperty<RenderBox>('${childParentData.id}', child);
Hixie's avatar
Hixie committed
196 197
  }

198
  void _callPerformLayout(Size size, RenderBox firstChild) {
199 200 201
    // 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.
202
    final Map<Object, RenderBox> previousIdToChild = _idToChild;
203 204 205 206

    Set<RenderBox> debugPreviousChildrenNeedingLayout;
    assert(() {
      debugPreviousChildrenNeedingLayout = _debugChildrenNeedingLayout;
207
      _debugChildrenNeedingLayout = <RenderBox>{};
208
      return true;
209
    }());
210

211
    try {
212
      _idToChild = <Object, RenderBox>{};
213 214 215
      RenderBox child = firstChild;
      while (child != null) {
        final MultiChildLayoutParentData childParentData = child.parentData;
Hixie's avatar
Hixie committed
216 217
        assert(() {
          if (childParentData.id == null) {
218 219 220 221
            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
222 223
          }
          return true;
224
        }());
225
        _idToChild[childParentData.id] = child;
226 227 228
        assert(() {
          _debugChildrenNeedingLayout.add(child);
          return true;
229
        }());
230 231
        child = childParentData.nextSibling;
      }
232
      performLayout(size);
233
      assert(() {
Hixie's avatar
Hixie committed
234
        if (_debugChildrenNeedingLayout.isNotEmpty) {
235 236 237 238 239 240 241 242 243 244 245
          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
246 247
        }
        return true;
248
      }());
249 250
    } finally {
      _idToChild = previousIdToChild;
251 252 253
      assert(() {
        _debugChildrenNeedingLayout = debugPreviousChildrenNeedingLayout;
        return true;
254
      }());
255 256 257
    }
  }

258
  /// Override this method to return the size of this object given the
259 260 261 262
  /// 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
263 264 265 266 267
  /// 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;
268

269
  /// Override this method to lay out and position all children given this
270 271 272 273
  /// widget's size.
  ///
  /// This method must call [layoutChild] for each child. It should also specify
  /// the final position of each child with [positionChild].
274
  void performLayout(Size size);
Hixie's avatar
Hixie committed
275

276
  /// Override this method to return true when the children need to be
277 278 279 280 281
  /// 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.
282
  bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate);
283 284 285 286 287

  /// Override this method to include additional information in the
  /// debugging data printed by [debugDumpRenderTree] and friends.
  ///
  /// By default, returns the [runtimeType] of the class.
288
  @override
Hixie's avatar
Hixie committed
289
  String toString() => '$runtimeType';
290 291
}

292 293 294 295 296 297
/// 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.
298
class RenderCustomMultiChildLayoutBox extends RenderBox
299 300
  with ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
       RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
301 302 303
  /// Creates a render object that customizes the layout of multiple children.
  ///
  /// The [delegate] argument must not be null.
304 305
  RenderCustomMultiChildLayoutBox({
    List<RenderBox> children,
306
    @required MultiChildLayoutDelegate delegate,
307 308
  }) : assert(delegate != null),
       _delegate = delegate {
309 310 311
    addAll(children);
  }

312
  @override
313
  void setupParentData(RenderBox child) {
314
    if (child.parentData is! MultiChildLayoutParentData)
315
      child.parentData = MultiChildLayoutParentData();
316 317
  }

318
  /// The delegate that controls the layout of the children.
319 320
  MultiChildLayoutDelegate get delegate => _delegate;
  MultiChildLayoutDelegate _delegate;
321 322 323
  set delegate(MultiChildLayoutDelegate newDelegate) {
    assert(newDelegate != null);
    if (_delegate == newDelegate)
324
      return;
325 326
    final MultiChildLayoutDelegate oldDelegate = _delegate;
    if (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRelayout(oldDelegate))
327
      markNeedsLayout();
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
    _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();
345 346 347
  }

  Size _getSize(BoxConstraints constraints) {
348
    assert(constraints.debugAssertIsValid());
349 350 351
    return constraints.constrain(_delegate.getSize(constraints));
  }

352 353 354 355
  // 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.

356
  @override
357
  double computeMinIntrinsicWidth(double height) {
358
    final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width;
359 360 361
    if (width.isFinite)
      return width;
    return 0.0;
362 363
  }

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

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

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

388
  @override
389
  void performLayout() {
390
    size = _getSize(constraints);
391
    delegate._callPerformLayout(size, firstChild);
392 393
  }

394
  @override
395 396 397 398
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
  }

399
  @override
400
  bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
Adam Barth's avatar
Adam Barth committed
401
    return defaultHitTestChildren(result, position: position);
402 403
  }
}