list_body.dart 11.7 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 'package:flutter/foundation.dart';

9 10
import 'box.dart';
import 'object.dart';
11

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

15
typedef _ChildSizingFunction = double Function(RenderBox child);
16

17 18
/// Displays its children sequentially along a given axis, forcing them to the
/// dimensions of the parent in the other axis.
19
///
20 21 22 23 24 25 26 27 28 29 30 31
/// 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.
32
  ///
33 34
  /// By default, children are arranged along the vertical axis.
  RenderListBody({
35
    List<RenderBox>? children,
36
    AxisDirection axisDirection = AxisDirection.down,
37
  }) : _axisDirection = axisDirection {
38 39 40
    addAll(children);
  }

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

48 49 50 51
  /// 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.
52 53 54
  AxisDirection get axisDirection => _axisDirection;
  AxisDirection _axisDirection;
  set axisDirection(AxisDirection value) {
55
    if (_axisDirection == value) {
56
      return;
57
    }
58 59
    _axisDirection = value;
    markNeedsLayout();
60 61
  }

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

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

  @override
  void performLayout() {
    final BoxConstraints constraints = this.constraints;
    assert(_debugCheckConstraints(constraints));
158
    double mainAxisExtent = 0.0;
159
    RenderBox? child = firstChild;
160
    switch (axisDirection) {
161 162 163 164
      case AxisDirection.right:
        final BoxConstraints innerConstraints = BoxConstraints.tightFor(height: constraints.maxHeight);
        while (child != null) {
          child.layout(innerConstraints, parentUsesSize: true);
165
          final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
166 167 168 169 170 171 172 173 174 175
          childParentData.offset = Offset(mainAxisExtent, 0.0);
          mainAxisExtent += child.size.width;
          assert(child.parentData == childParentData);
          child = childParentData.nextSibling;
        }
        size = constraints.constrain(Size(mainAxisExtent, constraints.maxHeight));
      case AxisDirection.left:
        final BoxConstraints innerConstraints = BoxConstraints.tightFor(height: constraints.maxHeight);
        while (child != null) {
          child.layout(innerConstraints, parentUsesSize: true);
176
          final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
177 178 179 180 181 182 183
          mainAxisExtent += child.size.width;
          assert(child.parentData == childParentData);
          child = childParentData.nextSibling;
        }
        double position = 0.0;
        child = firstChild;
        while (child != null) {
184
          final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
185 186 187 188 189 190 191 192 193 194
          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));
      case AxisDirection.down:
        final BoxConstraints innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth);
        while (child != null) {
          child.layout(innerConstraints, parentUsesSize: true);
195
          final ListBodyParentData childParentData = child.parentData! as ListBodyParentData;
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));
      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
          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));
221
    }
222
    assert(size.isFinite);
223 224
  }

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

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

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

253
  @override
254
  double computeMinIntrinsicWidth(double height) {
255 256 257 258
    return switch (mainAxis) {
      Axis.horizontal => _getIntrinsicMainAxis((RenderBox child) => child.getMinIntrinsicWidth(height)),
      Axis.vertical  => _getIntrinsicCrossAxis((RenderBox child) => child.getMinIntrinsicWidth(height)),
    };
259 260
  }

261
  @override
262
  double computeMaxIntrinsicWidth(double height) {
263 264 265 266
    return switch (mainAxis) {
      Axis.horizontal => _getIntrinsicMainAxis((RenderBox child) => child.getMaxIntrinsicWidth(height)),
      Axis.vertical  => _getIntrinsicCrossAxis((RenderBox child) => child.getMaxIntrinsicWidth(height)),
    };
267 268
  }

269
  @override
270
  double computeMinIntrinsicHeight(double width) {
271 272 273 274
    return switch (mainAxis) {
      Axis.horizontal => _getIntrinsicMainAxis((RenderBox child) => child.getMinIntrinsicHeight(width)),
      Axis.vertical  => _getIntrinsicCrossAxis((RenderBox child) => child.getMinIntrinsicHeight(width)),
    };
275 276
  }

277
  @override
278
  double computeMaxIntrinsicHeight(double width) {
279 280 281 282
    return switch (mainAxis) {
      Axis.horizontal => _getIntrinsicMainAxis((RenderBox child) => child.getMaxIntrinsicHeight(width)),
      Axis.vertical  => _getIntrinsicCrossAxis((RenderBox child) => child.getMaxIntrinsicHeight(width)),
    };
283 284
  }

285
  @override
286
  double? computeDistanceToActualBaseline(TextBaseline baseline) {
287 288 289
    return defaultComputeDistanceToFirstActualBaseline(baseline);
  }

290
  @override
291 292
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
293 294
  }

295
  @override
296
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
Adam Barth's avatar
Adam Barth committed
297
    return defaultHitTestChildren(result, position: position);
298 299 300
  }

}