list_body.dart 10.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:math' as math;

7 8
import 'box.dart';
import 'object.dart';
9

10
/// Parent data for use with [RenderListBody].
11
class ListBodyParentData extends ContainerBoxParentData<RenderBox> { }
12

13
typedef _ChildSizingFunction = double Function(RenderBox child);
14

15 16
/// Displays its children sequentially along a given axis, forcing them to the
/// dimensions of the parent in the other axis.
17
///
18 19 20 21 22 23 24 25 26 27 28 29
/// This layout algorithm arranges its children linearly along the main axis
/// (either horizontally or vertically). In the cross axis, children are
/// stretched to match the box's cross-axis extent. In the main axis, children
/// are given unlimited space and the box expands its main axis to contain all
/// its children. Because [RenderListBody] boxes expand in the main axis, they
/// must be given unlimited space in the main axis, typically by being contained
/// in a viewport with a scrolling direction that matches the box's main axis.
class RenderListBody extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, ListBodyParentData>,
         RenderBoxContainerDefaultsMixin<RenderBox, ListBodyParentData> {
  /// Creates a render object that arranges its children sequentially along a
  /// given axis.
30
  ///
31 32
  /// By default, children are arranged along the vertical axis.
  RenderListBody({
33
    List<RenderBox> children,
34
    AxisDirection axisDirection = AxisDirection.down,
35 36
  }) : assert(axisDirection != null),
       _axisDirection = axisDirection {
37 38 39
    addAll(children);
  }

40
  @override
41
  void setupParentData(RenderBox child) {
42
    if (child.parentData is! ListBodyParentData)
43
      child.parentData = ListBodyParentData();
44 45
  }

46 47 48 49
  /// The direction in which the children are laid out.
  ///
  /// For example, if the [axisDirection] is [AxisDirection.down], each child
  /// will be laid out below the next, vertically.
50 51 52 53 54 55 56 57
  AxisDirection get axisDirection => _axisDirection;
  AxisDirection _axisDirection;
  set axisDirection(AxisDirection value) {
    assert(value != null);
    if (_axisDirection == value)
      return;
    _axisDirection = value;
    markNeedsLayout();
58 59
  }

60 61
  /// The axis (horizontal or vertical) corresponding to the current
  /// [axisDirection].
62
  Axis get mainAxis => axisDirectionToAxis(axisDirection);
63

64
  @override
65
  void performLayout() {
66
    final BoxConstraints constraints = this.constraints;
67 68 69
    assert(() {
      switch (mainAxis) {
        case Axis.horizontal:
70
          if (!constraints.hasBoundedWidth)
71 72 73
            return true;
          break;
        case Axis.vertical:
74
          if (!constraints.hasBoundedHeight)
75 76 77
            return true;
          break;
      }
78 79 80 81 82 83 84 85 86 87 88 89
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('RenderListBody must have unlimited space along its main axis.'),
        ErrorDescription(
          'RenderListBody does not clip or resize its children, so it must be '
          'placed in a parent that does not constrain the main '
          'axis.'
        ),
        ErrorHint(
          'You probably want to put the RenderListBody inside a '
          'RenderViewport with a matching main axis.'
        )
      ]);
90
    }());
91 92 93
    assert(() {
      switch (mainAxis) {
        case Axis.horizontal:
94
          if (constraints.hasBoundedHeight)
95 96 97
            return true;
          break;
        case Axis.vertical:
98
          if (constraints.hasBoundedWidth)
99 100 101 102 103 104
            return true;
          break;
      }
      // TODO(ianh): Detect if we're actually nested blocks and say something
      // more specific to the exact situation in that case, and don't mention
      // nesting blocks in the negative case.
105 106 107
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('RenderListBody must have a bounded constraint for its cross axis.'),
        ErrorDescription(
108
          "RenderListBody forces its children to expand to fit the RenderListBody's container, "
109 110 111 112 113 114 115 116 117 118 119 120 121
          'so it must be placed in a parent that constrains the cross '
          'axis to a finite dimension.'
        ),
        // TODO(jacobr): this hint is a great candidate to promote to being an
        // automated quick fix in the future.
        ErrorHint(
          'If you are attempting to nest a RenderListBody with '
          'one direction inside one of another direction, you will want to '
          'wrap the inner one inside a box that fixes the dimension in that direction, '
          'for example, a RenderIntrinsicWidth or RenderIntrinsicHeight object. '
          'This is relatively expensive, however.' // (that's why we don't do it automatically)
        )
      ]);
122
    }());
123
    double mainAxisExtent = 0.0;
124
    RenderBox child = firstChild;
125
    switch (axisDirection) {
126 127 128 129
      case AxisDirection.right:
        final BoxConstraints innerConstraints = BoxConstraints.tightFor(height: constraints.maxHeight);
        while (child != null) {
          child.layout(innerConstraints, parentUsesSize: true);
130
          final ListBodyParentData childParentData = child.parentData as ListBodyParentData;
131 132 133 134 135 136 137 138 139 140 141
          childParentData.offset = Offset(mainAxisExtent, 0.0);
          mainAxisExtent += child.size.width;
          assert(child.parentData == childParentData);
          child = childParentData.nextSibling;
        }
        size = constraints.constrain(Size(mainAxisExtent, constraints.maxHeight));
        break;
      case AxisDirection.left:
        final BoxConstraints innerConstraints = BoxConstraints.tightFor(height: constraints.maxHeight);
        while (child != null) {
          child.layout(innerConstraints, parentUsesSize: true);
142
          final ListBodyParentData childParentData = child.parentData as ListBodyParentData;
143 144 145 146 147 148 149
          mainAxisExtent += child.size.width;
          assert(child.parentData == childParentData);
          child = childParentData.nextSibling;
        }
        double position = 0.0;
        child = firstChild;
        while (child != null) {
150
          final ListBodyParentData childParentData = child.parentData as ListBodyParentData;
151 152 153 154 155 156 157 158 159 160 161
          position += child.size.width;
          childParentData.offset = Offset(mainAxisExtent - position, 0.0);
          assert(child.parentData == childParentData);
          child = childParentData.nextSibling;
        }
        size = constraints.constrain(Size(mainAxisExtent, constraints.maxHeight));
        break;
      case AxisDirection.down:
        final BoxConstraints innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth);
        while (child != null) {
          child.layout(innerConstraints, parentUsesSize: true);
162
          final ListBodyParentData childParentData = child.parentData as ListBodyParentData;
163 164 165 166 167 168 169 170 171 172 173
          childParentData.offset = Offset(0.0, mainAxisExtent);
          mainAxisExtent += child.size.height;
          assert(child.parentData == childParentData);
          child = childParentData.nextSibling;
        }
        size = constraints.constrain(Size(constraints.maxWidth, mainAxisExtent));
        break;
      case AxisDirection.up:
        final BoxConstraints innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth);
        while (child != null) {
          child.layout(innerConstraints, parentUsesSize: true);
174
          final ListBodyParentData childParentData = child.parentData as ListBodyParentData;
175 176 177 178 179 180 181
          mainAxisExtent += child.size.height;
          assert(child.parentData == childParentData);
          child = childParentData.nextSibling;
        }
        double position = 0.0;
        child = firstChild;
        while (child != null) {
182
          final ListBodyParentData childParentData = child.parentData as ListBodyParentData;
183 184 185 186 187 188 189
          position += child.size.height;
          childParentData.offset = Offset(0.0, mainAxisExtent - position);
          assert(child.parentData == childParentData);
          child = childParentData.nextSibling;
        }
        size = constraints.constrain(Size(constraints.maxWidth, mainAxisExtent));
        break;
190
    }
191
    assert(size.isFinite);
192 193
  }

194
  @override
195 196
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
197
    properties.add(EnumProperty<AxisDirection>('axisDirection', axisDirection));
198
  }
199

200
  double _getIntrinsicCrossAxis(_ChildSizingFunction childSize) {
201
    double extent = 0.0;
202 203
    RenderBox child = firstChild;
    while (child != null) {
204
      extent = math.max(extent, childSize(child));
205
      final ListBodyParentData childParentData = child.parentData as ListBodyParentData;
Hixie's avatar
Hixie committed
206
      child = childParentData.nextSibling;
207
    }
208
    return extent;
209 210
  }

211
  double _getIntrinsicMainAxis(_ChildSizingFunction childSize) {
212
    double extent = 0.0;
213 214
    RenderBox child = firstChild;
    while (child != null) {
215
      extent += childSize(child);
216
      final ListBodyParentData childParentData = child.parentData as ListBodyParentData;
Hixie's avatar
Hixie committed
217
      child = childParentData.nextSibling;
218
    }
219
    return extent;
220 221
  }

222
  @override
223
  double computeMinIntrinsicWidth(double height) {
224
    assert(mainAxis != null);
225 226
    switch (mainAxis) {
      case Axis.horizontal:
227
        return _getIntrinsicMainAxis((RenderBox child) => child.getMinIntrinsicWidth(height));
228
      case Axis.vertical:
229
        return _getIntrinsicCrossAxis((RenderBox child) => child.getMinIntrinsicWidth(height));
230
    }
231
    return null;
232 233
  }

234
  @override
235
  double computeMaxIntrinsicWidth(double height) {
236
    assert(mainAxis != null);
237 238
    switch (mainAxis) {
      case Axis.horizontal:
239
        return _getIntrinsicMainAxis((RenderBox child) => child.getMaxIntrinsicWidth(height));
240
      case Axis.vertical:
241
        return _getIntrinsicCrossAxis((RenderBox child) => child.getMaxIntrinsicWidth(height));
242
    }
243
    return null;
244 245
  }

246
  @override
247
  double computeMinIntrinsicHeight(double width) {
248
    assert(mainAxis != null);
249 250
    switch (mainAxis) {
      case Axis.horizontal:
251
        return _getIntrinsicMainAxis((RenderBox child) => child.getMinIntrinsicHeight(width));
252
      case Axis.vertical:
253
        return _getIntrinsicCrossAxis((RenderBox child) => child.getMinIntrinsicHeight(width));
254
    }
255
    return null;
256 257
  }

258
  @override
259
  double computeMaxIntrinsicHeight(double width) {
260
    assert(mainAxis != null);
261 262
    switch (mainAxis) {
      case Axis.horizontal:
263
        return _getIntrinsicMainAxis((RenderBox child) => child.getMaxIntrinsicHeight(width));
264
      case Axis.vertical:
265
        return _getIntrinsicCrossAxis((RenderBox child) => child.getMaxIntrinsicHeight(width));
266
    }
267
    return null;
268 269
  }

270
  @override
271 272 273 274
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    return defaultComputeDistanceToFirstActualBaseline(baseline);
  }

275
  @override
276 277
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
278 279
  }

280
  @override
281
  bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
Adam Barth's avatar
Adam Barth committed
282
    return defaultHitTestChildren(result, position: position);
283 284 285
  }

}