custom_layout.dart 10.7 KB
Newer Older
1 2 3 4 5 6 7
// 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.

import 'box.dart';
import 'object.dart';

8
// For SingleChildLayoutDelegate and RenderCustomSingleChildLayoutBox, see shifted_box.dart
Hixie's avatar
Hixie committed
9

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

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

19
/// A delegate that controls the layout of multiple children.
20 21 22 23 24 25 26
///
/// Used with [MultiChildCustomLayout], the widget for the
/// [RenderCustomMultiChildLayoutBox] render object.
///
/// Subclasses must override some or all of the methods
/// marked "Override this method to..." to provide the constraints and
/// positions of the children.
27
abstract class MultiChildLayoutDelegate {
28
  Map<Object, RenderBox> _idToChild;
29
  Set<RenderBox> _debugChildrenNeedingLayout;
30

31
  /// True if a non-null LayoutChild was provided for the specified id.
32 33 34 35
  ///
  /// Call this from the [performLayout] or [getSize] methods to
  /// determine which children are available, if the child list might
  /// vary.
36
  bool hasChild(Object childId) => _idToChild[childId] != null;
37

38 39
  /// Ask the child to update its layout within the limits specified by
  /// the constraints parameter. The child's size is returned.
40 41 42
  ///
  /// Call this from your [performLayout] function to lay out each
  /// child. Every child must be laid out using this function exactly
43
  /// once each time the [performLayout] function is called.
44 45
  Size layoutChild(Object childId, BoxConstraints constraints) {
    final RenderBox child = _idToChild[childId];
46
    assert(() {
Hixie's avatar
Hixie committed
47
      if (child == null) {
48
        throw new FlutterError(
49
          'The $this custom multichild layout delegate tried to lay out a non-existent child.\n'
Hixie's avatar
Hixie committed
50 51 52 53
          'There is no child with the id "$childId".'
        );
      }
      if (!_debugChildrenNeedingLayout.remove(child)) {
54
        throw new FlutterError(
Hixie's avatar
Hixie committed
55 56 57 58 59
          '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 {
60
        assert(constraints.debugAssertIsValid(isAppliedConstraint: true));
Hixie's avatar
Hixie committed
61
      } on AssertionError catch (exception) {
62
        throw new FlutterError(
63
          'The $this custom multichild layout delegate provided invalid box constraints for the child with id "$childId".\n'
Hixie's avatar
Hixie committed
64 65 66 67 68 69 70
          '$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;
71
    });
72 73 74 75 76
    child.layout(constraints, parentUsesSize: true);
    return child.size;
  }

  /// Specify the child's origin relative to this origin.
77 78 79 80 81
  ///
  /// 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].
82
  void positionChild(Object childId, Offset offset) {
83
    final RenderBox child = _idToChild[childId];
Hixie's avatar
Hixie committed
84 85
    assert(() {
      if (child == null) {
86
        throw new FlutterError(
Hixie's avatar
Hixie committed
87 88 89 90 91
          '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) {
92
        throw new FlutterError(
Hixie's avatar
Hixie committed
93 94 95 96 97
          'The $this custom multichild layout delegate provided a null position for the child with id "$childId".'
        );
      }
      return true;
    });
98
    final MultiChildLayoutParentData childParentData = child.parentData;
99
    childParentData.offset = offset;
100 101
  }

Hixie's avatar
Hixie committed
102 103 104 105 106
  String _debugDescribeChild(RenderBox child) {
    final MultiChildLayoutParentData childParentData = child.parentData;
    return '${childParentData.id}: $child';
  }

107
  void _callPerformLayout(Size size, RenderBox firstChild) {
108 109 110
    // 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.
111
    final Map<Object, RenderBox> previousIdToChild = _idToChild;
112 113 114 115 116 117 118 119

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

120 121 122 123 124
    try {
      _idToChild = new Map<Object, RenderBox>();
      RenderBox child = firstChild;
      while (child != null) {
        final MultiChildLayoutParentData childParentData = child.parentData;
Hixie's avatar
Hixie committed
125 126
        assert(() {
          if (childParentData.id == null) {
127
            throw new FlutterError(
Hixie's avatar
Hixie committed
128 129 130 131 132 133 134
              'The following child has no ID:\n'
              '  $child\n'
              'Every child of a RenderCustomMultiChildLayoutBox must have an ID in its parent data.'
            );
          }
          return true;
        });
135
        _idToChild[childParentData.id] = child;
136 137 138 139
        assert(() {
          _debugChildrenNeedingLayout.add(child);
          return true;
        });
140 141
        child = childParentData.nextSibling;
      }
142
      performLayout(size);
143
      assert(() {
Hixie's avatar
Hixie committed
144 145
        if (_debugChildrenNeedingLayout.isNotEmpty) {
          if (_debugChildrenNeedingLayout.length > 1) {
146
            throw new FlutterError(
Hixie's avatar
Hixie committed
147 148 149 150 151
              '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 {
152
            throw new FlutterError(
Hixie's avatar
Hixie committed
153 154 155 156 157 158 159
              '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;
160
      });
161 162
    } finally {
      _idToChild = previousIdToChild;
163 164 165 166
      assert(() {
        _debugChildrenNeedingLayout = debugPreviousChildrenNeedingLayout;
        return true;
      });
167 168 169
    }
  }

170 171 172 173 174 175 176 177 178
  /// Override this method to return the size of this object given the
  /// incoming constraints. The size cannot reflect the instrinsic
  /// sizes of the children. If this layout has a fixed width or
  /// height the returned size can reflect that; the size will be
  /// 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;
179

180 181 182 183
  /// Override this method to lay out and position all children given this
  /// widget's size. This method must call [layoutChild] for each child. It
  /// should also specify the final position of each child with [positionChild].
  void performLayout(Size size);
Hixie's avatar
Hixie committed
184

185 186 187 188 189 190 191 192 193 194
  /// Override this method to return true when the children need to be
  /// 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.
  bool shouldRelayout(MultiChildLayoutDelegate oldDelegate);

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

199 200 201 202 203 204
/// 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.
205
class RenderCustomMultiChildLayoutBox extends RenderBox
206 207
  with ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
       RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
208 209 210
  /// Creates a render object that customizes the layout of multiple children.
  ///
  /// The [delegate] argument must not be null.
211 212 213 214 215 216 217 218
  RenderCustomMultiChildLayoutBox({
    List<RenderBox> children,
    MultiChildLayoutDelegate delegate
  }) : _delegate = delegate {
    assert(delegate != null);
    addAll(children);
  }

219
  @override
220
  void setupParentData(RenderBox child) {
221 222
    if (child.parentData is! MultiChildLayoutParentData)
      child.parentData = new MultiChildLayoutParentData();
223 224
  }

225
  /// The delegate that controls the layout of the children.
226 227
  MultiChildLayoutDelegate get delegate => _delegate;
  MultiChildLayoutDelegate _delegate;
228
  set delegate (MultiChildLayoutDelegate newDelegate) {
229 230 231
    assert(newDelegate != null);
    if (_delegate == newDelegate)
      return;
232 233
    if (newDelegate.runtimeType != _delegate.runtimeType || newDelegate.shouldRelayout(_delegate))
      markNeedsLayout();
234 235 236 237
    _delegate = newDelegate;
  }

  Size _getSize(BoxConstraints constraints) {
238
    assert(constraints.debugAssertIsValid());
239 240 241
    return constraints.constrain(_delegate.getSize(constraints));
  }

242 243 244 245
  // 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.

246
  @override
247
  double computeMinIntrinsicWidth(double height) {
248 249 250 251
    final double width = _getSize(new BoxConstraints.tightForFinite(height: height)).width;
    if (width.isFinite)
      return width;
    return 0.0;
252 253
  }

254
  @override
255
  double computeMaxIntrinsicWidth(double height) {
256 257 258 259
    final double width = _getSize(new BoxConstraints.tightForFinite(height: height)).width;
    if (width.isFinite)
      return width;
    return 0.0;
260 261
  }

262
  @override
263
  double computeMinIntrinsicHeight(double width) {
264 265 266 267
    final double height = _getSize(new BoxConstraints.tightForFinite(width: width)).height;
    if (height.isFinite)
      return height;
    return 0.0;
268 269
  }

270
  @override
271
  double computeMaxIntrinsicHeight(double width) {
272 273 274 275
    final double height = _getSize(new BoxConstraints.tightForFinite(width: width)).height;
    if (height.isFinite)
      return height;
    return 0.0;
276 277
  }

278
  @override
279
  void performLayout() {
280
    size = _getSize(constraints);
281
    delegate._callPerformLayout(size, firstChild);
282 283
  }

284
  @override
285 286 287 288
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
  }

289
  @override
Adam Barth's avatar
Adam Barth committed
290 291
  bool hitTestChildren(HitTestResult result, { Point position }) {
    return defaultHitTestChildren(result, position: position);
292 293
  }
}