grid.dart 15.3 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 6
import 'dart:typed_data';

7 8
import 'box.dart';
import 'object.dart';
Adam Barth's avatar
Adam Barth committed
9
import 'viewport.dart';
10

Adam Barth's avatar
Adam Barth committed
11 12 13 14 15 16 17 18 19 20
bool _debugIsMonotonic(List<double> offsets) {
  bool result = true;
  assert(() {
    double current = 0.0;
    for (double offset in offsets) {
      if (current > offset) {
        result = false;
        break;
      }
      current = offset;
21
    }
Adam Barth's avatar
Adam Barth committed
22 23 24 25 26 27 28
    return true;
  });
  return result;
}

List<double> _generateRegularOffsets(int count, double size) {
  int length = count + 1;
29
  List<double> result = new Float64List(length);
Adam Barth's avatar
Adam Barth committed
30 31 32 33 34 35 36 37 38 39 40 41 42 43 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 79 80 81 82 83 84 85 86 87 88 89 90 91 92
  for (int i = 0; i < length; ++i)
    result[i] = i * size;
  return result;
}

class GridSpecification {
  /// Creates a grid specification from an explicit list of offsets.
  GridSpecification.fromOffsets({
    this.columnOffsets,
    this.rowOffsets,
    this.padding: EdgeDims.zero
  }) {
    assert(_debugIsMonotonic(columnOffsets));
    assert(_debugIsMonotonic(rowOffsets));
    assert(padding != null);
  }

  /// Creates a grid specification containing a certain number of equally sized tiles.
  GridSpecification.fromRegularTiles({
    double tileWidth,
    double tileHeight,
    int columnCount,
    int rowCount,
    this.padding: EdgeDims.zero
  }) : columnOffsets = _generateRegularOffsets(columnCount, tileWidth),
       rowOffsets = _generateRegularOffsets(rowCount, tileHeight) {
    assert(_debugIsMonotonic(columnOffsets));
    assert(_debugIsMonotonic(rowOffsets));
    assert(padding != null);
  }

  /// The offsets of the column boundaries in the grid.
  ///
  /// The first offset is the offset of the left edge of the left-most column
  /// from the left edge of the interior of the grid's padding (usually 0.0).
  /// The last offset is the offset of the right edge of the right-most column
  /// from the left edge of the interior of the grid's padding.
  ///
  /// If there are n columns in the grid, there should be n + 1 entries in this
  /// list (because there's an entry before the first column and after the last
  /// column).
  final List<double> columnOffsets;

  /// The offsets of the row boundaries in the grid.
  ///
  /// The first offset is the offset of the top edge of the top-most row from
  /// the top edge of the interior of the grid's padding (usually 0.0). The
  /// last offset is the offset of the bottom edge of the bottom-most column
  /// from the top edge of the interior of the grid's padding.
  ///
  /// If there are n rows in the grid, there should be n + 1 entries in this
  /// list (because there's an entry before the first row and after the last
  /// row).
  final List<double> rowOffsets;

  /// The interior padding of the grid.
  ///
  /// The grid's size encloses the rows and columns and is then inflated by the
  /// padding.
  final EdgeDims padding;

  /// The size of the grid.
  Size get gridSize => new Size(columnOffsets.last + padding.horizontal, rowOffsets.last + padding.vertical);
93 94 95 96 97 98

  /// The number of columns in this grid.
  int get columnCount => columnOffsets.length - 1;

  /// The number of rows in this grid.
  int get rowCount => rowOffsets.length - 1;
Adam Barth's avatar
Adam Barth committed
99
}
100

Adam Barth's avatar
Adam Barth committed
101 102 103 104 105 106 107 108 109 110 111 112 113 114
/// Where to place a child within a grid.
class GridChildPlacement {
  GridChildPlacement({
    this.column,
    this.row,
    this.columnSpan: 1,
    this.rowSpan: 1,
    this.padding: EdgeDims.zero
  }) {
    assert(column != null);
    assert(row != null);
    assert(columnSpan != null);
    assert(rowSpan != null);
    assert(padding != null);
115 116
  }

Adam Barth's avatar
Adam Barth committed
117 118 119 120 121 122 123 124
  /// The column in which to place the child.
  final int column;

  /// The row in which to place the child.
  final int row;

  /// How many columns the child should span.
  final int columnSpan;
125

Adam Barth's avatar
Adam Barth committed
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
  /// How many rows the child should span.
  final int rowSpan;

  /// How much the child should be inset from the column and row boundaries.
  final EdgeDims padding;
}

/// An abstract interface to control the layout of a [RenderGrid].
abstract class GridDelegate {
  /// Override this function to control size of the columns and rows.
  GridSpecification getGridSpecification(BoxConstraints constraints, int childCount);

  /// Override this function to control where children are placed in the grid.
  GridChildPlacement getChildPlacement(GridSpecification specification, int index, Object placementData);

  /// Override this method to return true when the children need to be laid out.
  bool shouldRelayout(GridDelegate oldDelegate) => true;

  Size _getGridSize(BoxConstraints constraints, int childCount) {
    return getGridSpecification(constraints, childCount).gridSize;
  }

  /// Returns the minimum width that this grid could be without failing to paint
  /// its contents within itself.
  double getMinIntrinsicWidth(BoxConstraints constraints, int childCount) {
    return constraints.constrainWidth(_getGridSize(constraints, childCount).width);
  }

  /// Returns the smallest width beyond which increasing the width never
  /// decreases the height.
  double getMaxIntrinsicWidth(BoxConstraints constraints, int childCount) {
    return constraints.constrainWidth(_getGridSize(constraints, childCount).width);
  }

  /// Return the minimum height that this grid could be without failing to paint
  /// its contents within itself.
  double getMinIntrinsicHeight(BoxConstraints constraints, int childCount) {
    return constraints.constrainHeight(_getGridSize(constraints, childCount).height);
  }

  /// Returns the smallest height beyond which increasing the height never
  /// decreases the width.
  double getMaxIntrinsicHeight(BoxConstraints constraints, int childCount) {
    return constraints.constrainHeight(_getGridSize(constraints, childCount).height);
  }
}

/// A [GridDelegate] the places its children in order throughout the grid.
abstract class GridDelegateWithInOrderChildPlacement extends GridDelegate {
  GridDelegateWithInOrderChildPlacement({ this.padding: EdgeDims.zero });

  /// The amount of padding to apply to each child.
  final EdgeDims padding;

  GridChildPlacement getChildPlacement(GridSpecification specification, int index, Object placementData) {
    int columnCount = specification.columnOffsets.length - 1;
    return new GridChildPlacement(
      column: index % columnCount,
      row: index ~/ columnCount,
      padding: padding
    );
  }

  bool shouldRelayout(GridDelegateWithInOrderChildPlacement oldDelegate) {
    return padding != oldDelegate.padding;
  }
}

/// A [GridDelegate] that divides the grid's width evenly amount a fixed number of columns.
class FixedColumnCountGridDelegate extends GridDelegateWithInOrderChildPlacement {
  FixedColumnCountGridDelegate({
    this.columnCount,
    this.tileAspectRatio: 1.0,
    EdgeDims padding: EdgeDims.zero
  }) : super(padding: padding);

  /// The number of columns in the grid.
  final int columnCount;

  /// The ratio of the width to the height of each tile in the grid.
  final double tileAspectRatio;

  GridSpecification getGridSpecification(BoxConstraints constraints, int childCount) {
    assert(constraints.maxWidth < double.INFINITY);
    int rowCount = (childCount / columnCount).ceil();
    double tileWidth = constraints.maxWidth / columnCount;
    double tileHeight = tileWidth / tileAspectRatio;
    return new GridSpecification.fromRegularTiles(
      tileWidth: tileWidth,
      tileHeight: tileHeight,
      columnCount: columnCount,
      rowCount: rowCount,
      padding: padding.flipped
    );
  }

  bool shouldRelayout(FixedColumnCountGridDelegate oldDelegate) {
    return columnCount != oldDelegate.columnCount
        || tileAspectRatio != oldDelegate.tileAspectRatio
        || super.shouldRelayout(oldDelegate);
  }

  double getMinIntrinsicWidth(BoxConstraints constraints, int childCount) {
    return constraints.constrainWidth(0.0);
  }

  double getMaxIntrinsicWidth(BoxConstraints constraints, int childCount) {
    return constraints.constrainWidth(0.0);
  }
}

/// A [GridDelegate] that fills the width with a variable number of tiles.
///
/// This delegate will select a tile width that is as large as possible subject
/// to the following conditions:
///
///  - The tile width evenly divides the width of the grid.
///  - The tile width is at most [maxTileWidth].
///
class MaxTileWidthGridDelegate extends GridDelegateWithInOrderChildPlacement {
  MaxTileWidthGridDelegate({
    this.maxTileWidth,
    this.tileAspectRatio: 1.0,
    EdgeDims padding: EdgeDims.zero
  }) : super(padding: padding);

  /// The maximum width of a tile in the grid.
  final double maxTileWidth;

  /// The ratio of the width to the height of each tile in the grid.
  final double tileAspectRatio;

  GridSpecification getGridSpecification(BoxConstraints constraints, int childCount) {
    assert(constraints.maxWidth < double.INFINITY);
    double gridWidth = constraints.maxWidth;
    int columnCount = (gridWidth / maxTileWidth).ceil();
    int rowCount = (childCount / columnCount).ceil();
    double tileWidth = gridWidth / columnCount;
    double tileHeight = tileWidth / tileAspectRatio;
    return new GridSpecification.fromRegularTiles(
      tileWidth: tileWidth,
      tileHeight: tileHeight,
      columnCount: columnCount,
      rowCount: rowCount,
      padding: padding.flipped
    );
  }

  bool shouldRelayout(MaxTileWidthGridDelegate oldDelegate) {
    return maxTileWidth != oldDelegate.maxTileWidth
        || tileAspectRatio != oldDelegate.tileAspectRatio
        || super.shouldRelayout(oldDelegate);
  }

  double getMinIntrinsicWidth(BoxConstraints constraints, int childCount) {
    return constraints.constrainWidth(0.0);
  }

  double getMaxIntrinsicWidth(BoxConstraints constraints, int childCount) {
    return constraints.constrainWidth(maxTileWidth * childCount);
  }
287 288
}

289
/// Parent data for use with [RenderGrid]
Adam Barth's avatar
Adam Barth committed
290 291 292 293 294 295 296 297 298 299 300 301
class GridParentData extends ContainerBoxParentDataMixin<RenderBox> {
  /// Opaque data passed to the getChildPlacement method of the grid's [GridDelegate].
  Object placementData;

  void merge(GridParentData other) {
    if (other.placementData != null)
      placementData = other.placementData;
    super.merge(other);
  }

  String toString() => '${super.toString()}; placementData=$placementData';
}
302 303 304

/// Implements the grid layout algorithm
///
Adam Barth's avatar
Adam Barth committed
305 306 307 308 309 310 311 312 313 314 315
/// In grid layout, children are arranged into rows and columns in on a two
/// dimensional grid. The [GridDelegate] determines how to arrange the
/// children on the grid.
///
/// The arrangment of rows and columns in the grid cannot depend on the contents
/// of the tiles in the grid, which makes grid layout most useful for images and
/// card-like layouts rather than for document-like layouts that adjust to the
/// amount of text contained in the tiles.
///
/// Additionally, grid layout materializes all of its children, which makes it
/// most useful for grids containing a moderate number of tiles.
Adam Barth's avatar
Adam Barth committed
316
class RenderGrid extends RenderVirtualViewport<GridParentData> {
Adam Barth's avatar
Adam Barth committed
317 318
  RenderGrid({
    List<RenderBox> children,
319 320 321 322 323
    GridDelegate delegate,
    int virtualChildBase: 0,
    int virtualChildCount,
    Offset paintOffset: Offset.zero,
    LayoutCallback callback
Adam Barth's avatar
Adam Barth committed
324 325 326 327 328
  }) : _delegate = delegate, _virtualChildBase = virtualChildBase, super(
    virtualChildCount: virtualChildCount,
    paintOffset: paintOffset,
    callback: callback
  ) {
Adam Barth's avatar
Adam Barth committed
329
    assert(delegate != null);
330 331 332
    addAll(children);
  }

Adam Barth's avatar
Adam Barth committed
333 334 335 336 337 338 339
  /// The delegate that controls the layout of the children.
  GridDelegate get delegate => _delegate;
  GridDelegate _delegate;
  void set delegate (GridDelegate newDelegate) {
    assert(newDelegate != null);
    if (_delegate == newDelegate)
      return;
340 341
    if (newDelegate.runtimeType != _delegate.runtimeType || newDelegate.shouldRelayout(_delegate)) {
      _specification = null;
342
      markNeedsLayout();
343
    }
Adam Barth's avatar
Adam Barth committed
344
    _delegate = newDelegate;
345 346
  }

347 348 349 350 351 352 353 354 355 356 357 358 359 360
  /// The virtual index of the first child.
  ///
  /// When asking the delegate for the position of each child, the grid will add
  /// the virtual child i to the indices of its children.
  int get virtualChildBase => _virtualChildBase;
  int _virtualChildBase;
  void set virtualChildBase(int value) {
    assert(value != null);
    if (_virtualChildBase == value)
      return;
    _virtualChildBase = value;
    markNeedsLayout();
  }

361 362 363 364 365 366
  void setupParentData(RenderBox child) {
    if (child.parentData is! GridParentData)
      child.parentData = new GridParentData();
  }

  double getMinIntrinsicWidth(BoxConstraints constraints) {
367
    assert(constraints.isNormalized);
368
    return _delegate.getMinIntrinsicWidth(constraints, virtualChildCount);
369 370 371
  }

  double getMaxIntrinsicWidth(BoxConstraints constraints) {
372
    assert(constraints.isNormalized);
373
    return _delegate.getMaxIntrinsicWidth(constraints, virtualChildCount);
374 375 376
  }

  double getMinIntrinsicHeight(BoxConstraints constraints) {
377
    assert(constraints.isNormalized);
378
    return _delegate.getMinIntrinsicHeight(constraints, virtualChildCount);
379 380 381
  }

  double getMaxIntrinsicHeight(BoxConstraints constraints) {
382
    assert(constraints.isNormalized);
383
    return _delegate.getMaxIntrinsicHeight(constraints, virtualChildCount);
384 385 386 387 388 389
  }

  double computeDistanceToActualBaseline(TextBaseline baseline) {
    return defaultComputeDistanceToHighestActualBaseline(baseline);
  }

390
  GridSpecification get specification => _specification;
Adam Barth's avatar
Adam Barth committed
391
  GridSpecification _specification;
392 393 394 395 396 397 398 399 400 401 402 403 404
  int _specificationChildCount;
  BoxConstraints _specificationConstraints;

  void _updateGridSpecification() {
    if (_specification == null
        || _specificationChildCount != virtualChildCount
        || _specificationConstraints != constraints) {
      _specification = delegate.getGridSpecification(constraints, virtualChildCount);
      _specificationChildCount = virtualChildCount;
      _specificationConstraints = constraints;
    }
  }

405
  void performLayout() {
406
    _updateGridSpecification();
Adam Barth's avatar
Adam Barth committed
407 408
    Size gridSize = _specification.gridSize;
    size = constraints.constrain(gridSize);
409

Adam Barth's avatar
Adam Barth committed
410 411
    if (callback != null)
      invokeLayoutCallback(callback);
412

Adam Barth's avatar
Adam Barth committed
413 414
    double gridTopPadding = _specification.padding.top;
    double gridLeftPadding = _specification.padding.left;
415
    int childIndex = virtualChildBase;
416 417
    RenderBox child = firstChild;
    while (child != null) {
Hixie's avatar
Hixie committed
418
      final GridParentData childParentData = child.parentData;
419

420
      GridChildPlacement placement = delegate.getChildPlacement(_specification, childIndex, childParentData.placementData);
Adam Barth's avatar
Adam Barth committed
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
      assert(placement.column >= 0);
      assert(placement.row >= 0);
      assert(placement.column + placement.columnSpan < _specification.columnOffsets.length);
      assert(placement.row + placement.rowSpan < _specification.rowOffsets.length);

      double tileLeft = _specification.columnOffsets[placement.column] + gridLeftPadding;
      double tileRight = _specification.columnOffsets[placement.column + placement.columnSpan] + gridLeftPadding;
      double tileTop = _specification.rowOffsets[placement.row] + gridTopPadding;
      double tileBottom = _specification.rowOffsets[placement.row + placement.rowSpan] + gridTopPadding;

      double childWidth = tileRight - tileLeft - placement.padding.horizontal;
      double childHeight = tileBottom - tileTop - placement.padding.vertical;

      child.layout(new BoxConstraints(
        minWidth: childWidth,
        maxWidth: childWidth,
        minHeight: childHeight,
        maxHeight: childHeight
      ));

      childParentData.offset = new Offset(
        tileLeft + placement.padding.left,
        tileTop + placement.padding.top
      );

Adam Barth's avatar
Adam Barth committed
446
      childIndex += 1;
Hixie's avatar
Hixie committed
447 448 449

      assert(child.parentData == childParentData);
      child = childParentData.nextSibling;
450 451 452
    }
  }
}