custom_layout.dart 13.5 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
/// 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.
///
41
/// Used with [CustomMultiChildLayout], the widget for the
42 43
/// [RenderCustomMultiChildLayoutBox] render object.
///
44 45 46 47
/// 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.
///
48
/// {@tool sample}
49 50
///
/// Below is an example implementation of [performLayout] that causes one widget
51
/// (the follower) to be the same size as another (the leader):
52 53
///
/// ```dart
54 55 56 57 58 59 60
/// // 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,
/// }
61
///
62 63 64 65
/// class FollowTheLeader extends MultiChildLayoutDelegate {
///   @override
///   void performLayout(Size size) {
///     Size leaderSize = Size.zero;
66
///
67
///     if (hasChild(_Slot.leader)) {
68
///       leaderSize = layoutChild(_Slot.leader, BoxConstraints.loose(size));
69 70
///       positionChild(_Slot.leader, Offset.zero);
///     }
71
///
72
///     if (hasChild(_Slot.follower)) {
73 74
///       layoutChild(_Slot.follower, BoxConstraints.tight(leaderSize));
///       positionChild(_Slot.follower, Offset(size.width - leaderSize.width,
75 76
///           size.height - leaderSize.height));
///     }
77
///   }
78
///
79 80
///   @override
///   bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
81 82
/// }
/// ```
83
/// {@end-tool}
84 85 86 87 88 89 90 91 92 93 94 95 96
///
/// 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.
97
abstract class MultiChildLayoutDelegate {
98
  Map<Object, RenderBox> _idToChild;
99
  Set<RenderBox> _debugChildrenNeedingLayout;
100

101
  /// True if a non-null LayoutChild was provided for the specified id.
102 103 104 105
  ///
  /// Call this from the [performLayout] or [getSize] methods to
  /// determine which children are available, if the child list might
  /// vary.
106
  bool hasChild(Object childId) => _idToChild[childId] != null;
107

108 109
  /// Ask the child to update its layout within the limits specified by
  /// the constraints parameter. The child's size is returned.
110 111 112
  ///
  /// Call this from your [performLayout] function to lay out each
  /// child. Every child must be laid out using this function exactly
113
  /// once each time the [performLayout] function is called.
114 115
  Size layoutChild(Object childId, BoxConstraints constraints) {
    final RenderBox child = _idToChild[childId];
116
    assert(() {
Hixie's avatar
Hixie committed
117
      if (child == null) {
118
        throw FlutterError(
119
          'The $this custom multichild layout delegate tried to lay out a non-existent child.\n'
Hixie's avatar
Hixie committed
120 121 122 123
          'There is no child with the id "$childId".'
        );
      }
      if (!_debugChildrenNeedingLayout.remove(child)) {
124
        throw FlutterError(
Hixie's avatar
Hixie committed
125 126 127 128 129
          '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.'
        );
      }
      try {
130
        assert(constraints.debugAssertIsValid(isAppliedConstraint: true));
Hixie's avatar
Hixie committed
131
      } on AssertionError catch (exception) {
132
        throw FlutterError(
133
          'The $this custom multichild layout delegate provided invalid box constraints for the child with id "$childId".\n'
Hixie's avatar
Hixie committed
134 135 136 137 138 139 140
          '$exception\n'
          '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.'
        );
      }
      return true;
141
    }());
142 143 144 145 146
    child.layout(constraints, parentUsesSize: true);
    return child.size;
  }

  /// Specify the child's origin relative to this origin.
147 148 149 150 151
  ///
  /// 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].
152
  void positionChild(Object childId, Offset offset) {
153
    final RenderBox child = _idToChild[childId];
Hixie's avatar
Hixie committed
154 155
    assert(() {
      if (child == null) {
156
        throw FlutterError(
Hixie's avatar
Hixie committed
157 158 159 160 161
          'The $this custom multichild layout delegate tried to position out a non-existent child:\n'
          'There is no child with the id "$childId".'
        );
      }
      if (offset == null) {
162
        throw FlutterError(
Hixie's avatar
Hixie committed
163 164 165 166
          'The $this custom multichild layout delegate provided a null position for the child with id "$childId".'
        );
      }
      return true;
167
    }());
168
    final MultiChildLayoutParentData childParentData = child.parentData;
169
    childParentData.offset = offset;
170 171
  }

Hixie's avatar
Hixie committed
172 173 174 175 176
  String _debugDescribeChild(RenderBox child) {
    final MultiChildLayoutParentData childParentData = child.parentData;
    return '${childParentData.id}: $child';
  }

177
  void _callPerformLayout(Size size, RenderBox firstChild) {
178 179 180
    // 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.
181
    final Map<Object, RenderBox> previousIdToChild = _idToChild;
182 183 184 185

    Set<RenderBox> debugPreviousChildrenNeedingLayout;
    assert(() {
      debugPreviousChildrenNeedingLayout = _debugChildrenNeedingLayout;
186
      _debugChildrenNeedingLayout = <RenderBox>{};
187
      return true;
188
    }());
189

190
    try {
191
      _idToChild = <Object, RenderBox>{};
192 193 194
      RenderBox child = firstChild;
      while (child != null) {
        final MultiChildLayoutParentData childParentData = child.parentData;
Hixie's avatar
Hixie committed
195 196
        assert(() {
          if (childParentData.id == null) {
197
            throw FlutterError(
Hixie's avatar
Hixie committed
198 199 200 201 202 203
              'The following child has no ID:\n'
              '  $child\n'
              'Every child of a RenderCustomMultiChildLayoutBox must have an ID in its parent data.'
            );
          }
          return true;
204
        }());
205
        _idToChild[childParentData.id] = child;
206 207 208
        assert(() {
          _debugChildrenNeedingLayout.add(child);
          return true;
209
        }());
210 211
        child = childParentData.nextSibling;
      }
212
      performLayout(size);
213
      assert(() {
Hixie's avatar
Hixie committed
214 215
        if (_debugChildrenNeedingLayout.isNotEmpty) {
          if (_debugChildrenNeedingLayout.length > 1) {
216
            throw FlutterError(
Hixie's avatar
Hixie committed
217
              'The $this custom multichild layout delegate forgot to lay out the following children:\n'
218
              '  ${_debugChildrenNeedingLayout.map<String>(_debugDescribeChild).join("\n  ")}\n'
Hixie's avatar
Hixie committed
219 220 221
              'Each child must be laid out exactly once.'
            );
          } else {
222
            throw FlutterError(
Hixie's avatar
Hixie committed
223 224 225 226 227 228 229
              'The $this custom multichild layout delegate forgot to lay out the following child:\n'
              '  ${_debugDescribeChild(_debugChildrenNeedingLayout.single)}\n'
              'Each child must be laid out exactly once.'
            );
          }
        }
        return true;
230
      }());
231 232
    } finally {
      _idToChild = previousIdToChild;
233 234 235
      assert(() {
        _debugChildrenNeedingLayout = debugPreviousChildrenNeedingLayout;
        return true;
236
      }());
237 238 239
    }
  }

240
  /// Override this method to return the size of this object given the
241 242 243 244
  /// 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
245 246 247 248 249
  /// 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;
250

251
  /// Override this method to lay out and position all children given this
252 253 254 255
  /// widget's size.
  ///
  /// This method must call [layoutChild] for each child. It should also specify
  /// the final position of each child with [positionChild].
256
  void performLayout(Size size);
Hixie's avatar
Hixie committed
257

258
  /// Override this method to return true when the children need to be
259 260 261 262 263
  /// 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.
264
  bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate);
265 266 267 268 269

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

274 275 276 277 278 279
/// 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.
280
class RenderCustomMultiChildLayoutBox extends RenderBox
281 282
  with ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
       RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
283 284 285
  /// Creates a render object that customizes the layout of multiple children.
  ///
  /// The [delegate] argument must not be null.
286 287
  RenderCustomMultiChildLayoutBox({
    List<RenderBox> children,
288
    @required MultiChildLayoutDelegate delegate,
289 290
  }) : assert(delegate != null),
       _delegate = delegate {
291 292 293
    addAll(children);
  }

294
  @override
295
  void setupParentData(RenderBox child) {
296
    if (child.parentData is! MultiChildLayoutParentData)
297
      child.parentData = MultiChildLayoutParentData();
298 299
  }

300
  /// The delegate that controls the layout of the children.
301 302
  MultiChildLayoutDelegate get delegate => _delegate;
  MultiChildLayoutDelegate _delegate;
303
  set delegate(MultiChildLayoutDelegate value) {
304 305
    assert(value != null);
    if (_delegate == value)
306
      return;
307
    if (value.runtimeType != _delegate.runtimeType || value.shouldRelayout(_delegate))
308
      markNeedsLayout();
309
    _delegate = value;
310 311 312
  }

  Size _getSize(BoxConstraints constraints) {
313
    assert(constraints.debugAssertIsValid());
314 315 316
    return constraints.constrain(_delegate.getSize(constraints));
  }

317 318 319 320
  // 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.

321
  @override
322
  double computeMinIntrinsicWidth(double height) {
323
    final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width;
324 325 326
    if (width.isFinite)
      return width;
    return 0.0;
327 328
  }

329
  @override
330
  double computeMaxIntrinsicWidth(double height) {
331
    final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width;
332 333 334
    if (width.isFinite)
      return width;
    return 0.0;
335 336
  }

337
  @override
338
  double computeMinIntrinsicHeight(double width) {
339
    final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height;
340 341 342
    if (height.isFinite)
      return height;
    return 0.0;
343 344
  }

345
  @override
346
  double computeMaxIntrinsicHeight(double width) {
347
    final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height;
348 349 350
    if (height.isFinite)
      return height;
    return 0.0;
351 352
  }

353
  @override
354
  void performLayout() {
355
    size = _getSize(constraints);
356
    delegate._callPerformLayout(size, firstChild);
357 358
  }

359
  @override
360 361 362 363
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
  }

364
  @override
365
  bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
Adam Barth's avatar
Adam Barth committed
366
    return defaultHitTestChildren(result, position: position);
367 368
  }
}