custom_layout.dart 10.8 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.

Adam Barth's avatar
Adam Barth committed
5 6
import 'package:meta/meta.dart';

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 ContainerBoxParentDataMixin<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
///
/// 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.
29
abstract class MultiChildLayoutDelegate {
30
  Map<Object, RenderBox> _idToChild;
31
  Set<RenderBox> _debugChildrenNeedingLayout;
32

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

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

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

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

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

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

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

172 173 174 175 176 177 178 179 180
  /// 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;
181

182 183 184 185
  /// 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
186

187 188 189 190
  /// 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.
Adam Barth's avatar
Adam Barth committed
191
  bool shouldRelayout(@checked MultiChildLayoutDelegate oldDelegate);
192 193 194 195 196

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

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

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

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

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

244 245 246 247
  // 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.

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

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

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

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

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

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

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