custom_layout.dart 13 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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
/// ## Example
///
/// Below is an example implementation of [performLayout] that causes one widget
/// to be the same size as another:
///
/// ```dart
/// @override
/// void performLayout(Size size) {
///   Size followerSize = Size.zero;
///
///   if (hasChild(_Slots.leader) {
///     followerSize = layoutChild(_Slots.leader, new BoxConstraints.loose(size));
///     positionChild(_Slots.leader, Offset.zero);
///   }
///
///   if (hasChild(_Slots.follower)) {
///     layoutChild(_Slots.follower, new BoxConstraints.tight(followerSize));
///     positionChild(_Slots.follower, new Offset(size.width - followerSize.width,
///                                               size.height - followerSize.height));
///   }
/// }
/// ```
///
/// 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.
79
abstract class MultiChildLayoutDelegate {
80
  Map<Object, RenderBox> _idToChild;
81
  Set<RenderBox> _debugChildrenNeedingLayout;
82

83
  /// True if a non-null LayoutChild was provided for the specified id.
84 85 86 87
  ///
  /// Call this from the [performLayout] or [getSize] methods to
  /// determine which children are available, if the child list might
  /// vary.
88
  bool hasChild(Object childId) => _idToChild[childId] != null;
89

90 91
  /// Ask the child to update its layout within the limits specified by
  /// the constraints parameter. The child's size is returned.
92 93 94
  ///
  /// Call this from your [performLayout] function to lay out each
  /// child. Every child must be laid out using this function exactly
95
  /// once each time the [performLayout] function is called.
96 97
  Size layoutChild(Object childId, BoxConstraints constraints) {
    final RenderBox child = _idToChild[childId];
98
    assert(() {
Hixie's avatar
Hixie committed
99
      if (child == null) {
100
        throw new FlutterError(
101
          'The $this custom multichild layout delegate tried to lay out a non-existent child.\n'
Hixie's avatar
Hixie committed
102 103 104 105
          'There is no child with the id "$childId".'
        );
      }
      if (!_debugChildrenNeedingLayout.remove(child)) {
106
        throw new FlutterError(
Hixie's avatar
Hixie committed
107 108 109 110 111
          '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 {
112
        assert(constraints.debugAssertIsValid(isAppliedConstraint: true));
Hixie's avatar
Hixie committed
113
      } on AssertionError catch (exception) {
114
        throw new FlutterError(
115
          'The $this custom multichild layout delegate provided invalid box constraints for the child with id "$childId".\n'
Hixie's avatar
Hixie committed
116 117 118 119 120 121 122
          '$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;
123
    });
124 125 126 127 128
    child.layout(constraints, parentUsesSize: true);
    return child.size;
  }

  /// Specify the child's origin relative to this origin.
129 130 131 132 133
  ///
  /// 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].
134
  void positionChild(Object childId, Offset offset) {
135
    final RenderBox child = _idToChild[childId];
Hixie's avatar
Hixie committed
136 137
    assert(() {
      if (child == null) {
138
        throw new FlutterError(
Hixie's avatar
Hixie committed
139 140 141 142 143
          '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) {
144
        throw new FlutterError(
Hixie's avatar
Hixie committed
145 146 147 148 149
          'The $this custom multichild layout delegate provided a null position for the child with id "$childId".'
        );
      }
      return true;
    });
150
    final MultiChildLayoutParentData childParentData = child.parentData;
151
    childParentData.offset = offset;
152 153
  }

Hixie's avatar
Hixie committed
154 155 156 157 158
  String _debugDescribeChild(RenderBox child) {
    final MultiChildLayoutParentData childParentData = child.parentData;
    return '${childParentData.id}: $child';
  }

159
  void _callPerformLayout(Size size, RenderBox firstChild) {
160 161 162
    // 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.
163
    final Map<Object, RenderBox> previousIdToChild = _idToChild;
164 165 166 167 168 169 170 171

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

172
    try {
173
      _idToChild = <Object, RenderBox>{};
174 175 176
      RenderBox child = firstChild;
      while (child != null) {
        final MultiChildLayoutParentData childParentData = child.parentData;
Hixie's avatar
Hixie committed
177 178
        assert(() {
          if (childParentData.id == null) {
179
            throw new FlutterError(
Hixie's avatar
Hixie committed
180 181 182 183 184 185 186
              'The following child has no ID:\n'
              '  $child\n'
              'Every child of a RenderCustomMultiChildLayoutBox must have an ID in its parent data.'
            );
          }
          return true;
        });
187
        _idToChild[childParentData.id] = child;
188 189 190 191
        assert(() {
          _debugChildrenNeedingLayout.add(child);
          return true;
        });
192 193
        child = childParentData.nextSibling;
      }
194
      performLayout(size);
195
      assert(() {
Hixie's avatar
Hixie committed
196 197
        if (_debugChildrenNeedingLayout.isNotEmpty) {
          if (_debugChildrenNeedingLayout.length > 1) {
198
            throw new FlutterError(
Hixie's avatar
Hixie committed
199 200 201 202 203
              '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 {
204
            throw new FlutterError(
Hixie's avatar
Hixie committed
205 206 207 208 209 210 211
              '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;
212
      });
213 214
    } finally {
      _idToChild = previousIdToChild;
215 216 217 218
      assert(() {
        _debugChildrenNeedingLayout = debugPreviousChildrenNeedingLayout;
        return true;
      });
219 220 221
    }
  }

222
  /// Override this method to return the size of this object given the
223 224 225 226
  /// 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
227 228 229 230 231
  /// 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;
232

233
  /// Override this method to lay out and position all children given this
234 235 236 237
  /// widget's size.
  ///
  /// This method must call [layoutChild] for each child. It should also specify
  /// the final position of each child with [positionChild].
238
  void performLayout(Size size);
Hixie's avatar
Hixie committed
239

240
  /// Override this method to return true when the children need to be
241 242 243 244 245
  /// 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.
246
  bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate);
247 248 249 250 251

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

256 257 258 259 260 261
/// 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.
262
class RenderCustomMultiChildLayoutBox extends RenderBox
263 264
  with ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
       RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
265 266 267
  /// Creates a render object that customizes the layout of multiple children.
  ///
  /// The [delegate] argument must not be null.
268 269
  RenderCustomMultiChildLayoutBox({
    List<RenderBox> children,
270
    @required MultiChildLayoutDelegate delegate
271 272
  }) : assert(delegate != null),
       _delegate = delegate {
273 274 275
    addAll(children);
  }

276
  @override
277
  void setupParentData(RenderBox child) {
278 279
    if (child.parentData is! MultiChildLayoutParentData)
      child.parentData = new MultiChildLayoutParentData();
280 281
  }

282
  /// The delegate that controls the layout of the children.
283 284
  MultiChildLayoutDelegate get delegate => _delegate;
  MultiChildLayoutDelegate _delegate;
285
  set delegate(MultiChildLayoutDelegate value) {
286 287
    assert(value != null);
    if (_delegate == value)
288
      return;
289
    if (value.runtimeType != _delegate.runtimeType || value.shouldRelayout(_delegate))
290
      markNeedsLayout();
291
    _delegate = value;
292 293 294
  }

  Size _getSize(BoxConstraints constraints) {
295
    assert(constraints.debugAssertIsValid());
296 297 298
    return constraints.constrain(_delegate.getSize(constraints));
  }

299 300 301 302
  // 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.

303
  @override
304
  double computeMinIntrinsicWidth(double height) {
305 306 307 308
    final double width = _getSize(new BoxConstraints.tightForFinite(height: height)).width;
    if (width.isFinite)
      return width;
    return 0.0;
309 310
  }

311
  @override
312
  double computeMaxIntrinsicWidth(double height) {
313 314 315 316
    final double width = _getSize(new BoxConstraints.tightForFinite(height: height)).width;
    if (width.isFinite)
      return width;
    return 0.0;
317 318
  }

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

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

335
  @override
336
  void performLayout() {
337
    size = _getSize(constraints);
338
    delegate._callPerformLayout(size, firstChild);
339 340
  }

341
  @override
342 343 344 345
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
  }

346
  @override
347
  bool hitTestChildren(HitTestResult result, { Offset position }) {
Adam Barth's avatar
Adam Barth committed
348
    return defaultHitTestChildren(result, position: position);
349 350
  }
}