list_body.dart 12 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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
  Size computeDryLayout(BoxConstraints constraints) {
    assert(_debugCheckConstraints(constraints));
    double mainAxisExtent = 0.0;
    RenderBox? child = firstChild;
    switch (axisDirection) {
      case AxisDirection.right:
      case AxisDirection.left:
        final BoxConstraints innerConstraints = BoxConstraints.tightFor(height: constraints.maxHeight);
        while (child != null) {
          final Size childSize = child.getDryLayout(innerConstraints);
          mainAxisExtent += childSize.width;
          child = childAfter(child);
        }
        return constraints.constrain(Size(mainAxisExtent, constraints.maxHeight));
      case AxisDirection.up:
      case AxisDirection.down:
        final BoxConstraints innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth);
        while (child != null) {
          final Size childSize = child.getDryLayout(innerConstraints);
          mainAxisExtent += childSize.height;
          child = childAfter(child);
        }
        return constraints.constrain(Size(constraints.maxWidth, mainAxisExtent));
    }
  }

  bool _debugCheckConstraints(BoxConstraints constraints) {
92 93 94
    assert(() {
      switch (mainAxis) {
        case Axis.horizontal:
95
          if (!constraints.hasBoundedWidth)
96 97 98
            return true;
          break;
        case Axis.vertical:
99
          if (!constraints.hasBoundedHeight)
100 101 102
            return true;
          break;
      }
103 104 105 106 107
      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 '
108
          'axis.',
109 110 111
        ),
        ErrorHint(
          'You probably want to put the RenderListBody inside a '
112 113
          'RenderViewport with a matching main axis.',
        ),
114
      ]);
115
    }());
116 117 118
    assert(() {
      switch (mainAxis) {
        case Axis.horizontal:
119
          if (constraints.hasBoundedHeight)
120 121 122
            return true;
          break;
        case Axis.vertical:
123
          if (constraints.hasBoundedWidth)
124 125 126 127 128 129
            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.
130 131 132
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('RenderListBody must have a bounded constraint for its cross axis.'),
        ErrorDescription(
133
          "RenderListBody forces its children to expand to fit the RenderListBody's container, "
134
          'so it must be placed in a parent that constrains the cross '
135
          'axis to a finite dimension.',
136 137 138 139 140 141 142 143
        ),
        // 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. '
144 145
          'This is relatively expensive, however.', // (that's why we don't do it automatically)
        ),
146
      ]);
147
    }());
148 149 150 151 152 153 154
    return true;
  }

  @override
  void performLayout() {
    final BoxConstraints constraints = this.constraints;
    assert(_debugCheckConstraints(constraints));
155
    double mainAxisExtent = 0.0;
156
    RenderBox? child = firstChild;
157
    switch (axisDirection) {
158 159 160 161
      case AxisDirection.right:
        final BoxConstraints innerConstraints = BoxConstraints.tightFor(height: constraints.maxHeight);
        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(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);
174
          final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
175 176 177 178 179 180 181
          mainAxisExtent += child.size.width;
          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 190 191 192 193
          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);
194
          final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
195 196 197 198 199 200 201 202 203 204 205
          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);
206
          final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
207 208 209 210 211 212 213
          mainAxisExtent += child.size.height;
          assert(child.parentData == childParentData);
          child = childParentData.nextSibling;
        }
        double position = 0.0;
        child = firstChild;
        while (child != null) {
214
          final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
215 216 217 218 219 220 221
          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;
222
    }
223
    assert(size.isFinite);
224 225
  }

226
  @override
227 228
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
229
    properties.add(EnumProperty<AxisDirection>('axisDirection', axisDirection));
230
  }
231

232
  double _getIntrinsicCrossAxis(_ChildSizingFunction childSize) {
233
    double extent = 0.0;
234
    RenderBox? child = firstChild;
235
    while (child != null) {
236
      extent = math.max(extent, childSize(child));
237
      final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
Hixie's avatar
Hixie committed
238
      child = childParentData.nextSibling;
239
    }
240
    return extent;
241 242
  }

243
  double _getIntrinsicMainAxis(_ChildSizingFunction childSize) {
244
    double extent = 0.0;
245
    RenderBox? child = firstChild;
246
    while (child != null) {
247
      extent += childSize(child);
248
      final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
Hixie's avatar
Hixie committed
249
      child = childParentData.nextSibling;
250
    }
251
    return extent;
252 253
  }

254
  @override
255
  double computeMinIntrinsicWidth(double height) {
256
    assert(mainAxis != null);
257 258
    switch (mainAxis) {
      case Axis.horizontal:
259
        return _getIntrinsicMainAxis((RenderBox child) => child.getMinIntrinsicWidth(height));
260
      case Axis.vertical:
261
        return _getIntrinsicCrossAxis((RenderBox child) => child.getMinIntrinsicWidth(height));
262 263 264
    }
  }

265
  @override
266
  double computeMaxIntrinsicWidth(double height) {
267
    assert(mainAxis != null);
268 269
    switch (mainAxis) {
      case Axis.horizontal:
270
        return _getIntrinsicMainAxis((RenderBox child) => child.getMaxIntrinsicWidth(height));
271
      case Axis.vertical:
272
        return _getIntrinsicCrossAxis((RenderBox child) => child.getMaxIntrinsicWidth(height));
273 274 275
    }
  }

276
  @override
277
  double computeMinIntrinsicHeight(double width) {
278
    assert(mainAxis != null);
279 280
    switch (mainAxis) {
      case Axis.horizontal:
281
        return _getIntrinsicMainAxis((RenderBox child) => child.getMinIntrinsicHeight(width));
282
      case Axis.vertical:
283
        return _getIntrinsicCrossAxis((RenderBox child) => child.getMinIntrinsicHeight(width));
284
    }
285 286
  }

287
  @override
288
  double computeMaxIntrinsicHeight(double width) {
289
    assert(mainAxis != null);
290 291
    switch (mainAxis) {
      case Axis.horizontal:
292
        return _getIntrinsicMainAxis((RenderBox child) => child.getMaxIntrinsicHeight(width));
293
      case Axis.vertical:
294
        return _getIntrinsicCrossAxis((RenderBox child) => child.getMaxIntrinsicHeight(width));
295
    }
296 297
  }

298
  @override
299
  double? computeDistanceToActualBaseline(TextBaseline baseline) {
300 301 302
    return defaultComputeDistanceToFirstActualBaseline(baseline);
  }

303
  @override
304 305
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
306 307
  }

308
  @override
309
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
Adam Barth's avatar
Adam Barth committed
310
    return defaultHitTestChildren(result, position: position);
311 312 313
  }

}