list_body.dart 11.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
  }) : _axisDirection = axisDirection {
36 37 38
    addAll(children);
  }

39
  @override
40
  void setupParentData(RenderBox child) {
41
    if (child.parentData is! ListBodyParentData) {
42
      child.parentData = ListBodyParentData();
43
    }
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
  AxisDirection get axisDirection => _axisDirection;
  AxisDirection _axisDirection;
  set axisDirection(AxisDirection value) {
53
    if (_axisDirection == value) {
54
      return;
55
    }
56 57
    _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
            return true;
97
          }
98 99
          break;
        case Axis.vertical:
100
          if (!constraints.hasBoundedHeight) {
101
            return true;
102
          }
103 104
          break;
      }
105 106 107 108 109
      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 '
110
          'axis.',
111 112 113
        ),
        ErrorHint(
          'You probably want to put the RenderListBody inside a '
114 115
          'RenderViewport with a matching main axis.',
        ),
116
      ]);
117
    }());
118 119 120
    assert(() {
      switch (mainAxis) {
        case Axis.horizontal:
121
          if (constraints.hasBoundedHeight) {
122
            return true;
123
          }
124 125
          break;
        case Axis.vertical:
126
          if (constraints.hasBoundedWidth) {
127
            return true;
128
          }
129 130 131 132 133
          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.
134 135 136
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('RenderListBody must have a bounded constraint for its cross axis.'),
        ErrorDescription(
137
          "RenderListBody forces its children to expand to fit the RenderListBody's container, "
138
          'so it must be placed in a parent that constrains the cross '
139
          'axis to a finite dimension.',
140 141 142 143 144 145 146 147
        ),
        // 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. '
148 149
          'This is relatively expensive, however.', // (that's why we don't do it automatically)
        ),
150
      ]);
151
    }());
152 153 154 155 156 157 158
    return true;
  }

  @override
  void performLayout() {
    final BoxConstraints constraints = this.constraints;
    assert(_debugCheckConstraints(constraints));
159
    double mainAxisExtent = 0.0;
160
    RenderBox? child = firstChild;
161
    switch (axisDirection) {
162 163 164 165
      case AxisDirection.right:
        final BoxConstraints innerConstraints = BoxConstraints.tightFor(height: constraints.maxHeight);
        while (child != null) {
          child.layout(innerConstraints, parentUsesSize: true);
166
          final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
167 168 169 170 171 172 173 174 175 176 177
          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);
178
          final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
179 180 181 182 183 184 185
          mainAxisExtent += child.size.width;
          assert(child.parentData == childParentData);
          child = childParentData.nextSibling;
        }
        double position = 0.0;
        child = firstChild;
        while (child != null) {
186
          final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
187 188 189 190 191 192 193 194 195 196 197
          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);
198
          final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
199 200 201 202 203 204 205 206 207 208 209
          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);
210
          final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
211 212 213 214 215 216 217
          mainAxisExtent += child.size.height;
          assert(child.parentData == childParentData);
          child = childParentData.nextSibling;
        }
        double position = 0.0;
        child = firstChild;
        while (child != null) {
218
          final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
219 220 221 222 223 224 225
          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;
226
    }
227
    assert(size.isFinite);
228 229
  }

230
  @override
231 232
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
233
    properties.add(EnumProperty<AxisDirection>('axisDirection', axisDirection));
234
  }
235

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

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

258
  @override
259
  double computeMinIntrinsicWidth(double height) {
260 261
    switch (mainAxis) {
      case Axis.horizontal:
262
        return _getIntrinsicMainAxis((RenderBox child) => child.getMinIntrinsicWidth(height));
263
      case Axis.vertical:
264
        return _getIntrinsicCrossAxis((RenderBox child) => child.getMinIntrinsicWidth(height));
265 266 267
    }
  }

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

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

288
  @override
289
  double computeMaxIntrinsicHeight(double width) {
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
  }

}