custom_layout.dart 13.6 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 48
/// 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.
///
/// ## Sample code
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 68 69 70
///     if (hasChild(_Slot.leader)) {
///       leaderSize = layoutChild(_Slot.leader, new BoxConstraints.loose(size));
///       positionChild(_Slot.leader, Offset.zero);
///     }
71
///
72 73 74 75 76
///     if (hasChild(_Slot.follower)) {
///       layoutChild(_Slot.follower, new BoxConstraints.tight(leaderSize));
///       positionChild(_Slot.follower, new Offset(size.width - leaderSize.width,
///           size.height - leaderSize.height));
///     }
77
///   }
78
///
79 80
///   @override
///   bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
/// }
/// ```
///
/// 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.
96
abstract class MultiChildLayoutDelegate {
97
  Map<Object, RenderBox> _idToChild;
98
  Set<RenderBox> _debugChildrenNeedingLayout;
99

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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