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

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

Adam Barth's avatar
Adam Barth committed
12 13 14 15 16 17 18 19 20 21
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;
22
    }
Adam Barth's avatar
Adam Barth committed
23 24 25 26 27 28
    return true;
  });
  return result;
}

List<double> _generateRegularOffsets(int count, double size) {
29 30
  final int length = count + 1;
  final List<double> result = new Float64List(length);
Adam Barth's avatar
Adam Barth committed
31 32 33 34 35 36 37 38 39 40
  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,
41 42
    this.columnSpacing: 0.0,
    this.rowSpacing: 0.0,
43
    this.padding: EdgeInsets.zero
Adam Barth's avatar
Adam Barth committed
44 45 46
  }) {
    assert(_debugIsMonotonic(columnOffsets));
    assert(_debugIsMonotonic(rowOffsets));
47 48 49
    assert(columnSpacing != null && columnSpacing >= 0.0);
    assert(rowSpacing != null && rowSpacing >= 0.0);
    assert(padding != null && padding.isNonNegative);
Adam Barth's avatar
Adam Barth committed
50 51 52
  }

  /// Creates a grid specification containing a certain number of equally sized tiles.
53 54 55 56
  /// The tileWidth is the sum of the width of the child it will contain and
  /// columnSpacing (even if columnCount is 1). Similarly tileHeight is child's height
  /// plus rowSpacing. If the tiles are to completely fill the grid, then their size
  /// should be based on the grid's padded interior.
Adam Barth's avatar
Adam Barth committed
57 58 59 60 61
  GridSpecification.fromRegularTiles({
    double tileWidth,
    double tileHeight,
    int columnCount,
    int rowCount,
62 63
    this.rowSpacing: 0.0,
    this.columnSpacing: 0.0,
64
    this.padding: EdgeInsets.zero
Adam Barth's avatar
Adam Barth committed
65 66 67 68
  }) : columnOffsets = _generateRegularOffsets(columnCount, tileWidth),
       rowOffsets = _generateRegularOffsets(rowCount, tileHeight) {
    assert(_debugIsMonotonic(columnOffsets));
    assert(_debugIsMonotonic(rowOffsets));
69 70
    assert(columnSpacing != null && columnSpacing >= 0.0);
    assert(rowSpacing != null && rowSpacing >= 0.0);
71
    assert(padding != null && padding.isNonNegative);
Adam Barth's avatar
Adam Barth committed
72 73 74 75 76
  }

  /// The offsets of the column boundaries in the grid.
  ///
  /// The first offset is the offset of the left edge of the left-most column
77 78 79
  /// from the left edge of the interior of the grid's padding (0.0 if the padding
  /// is EdgeOffsets.zero). 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.
Adam Barth's avatar
Adam Barth committed
80 81
  ///
  /// If there are n columns in the grid, there should be n + 1 entries in this
82 83
  /// list. The right edge of the last column is defined as columnOffsets(n), i.e.
  /// the left edge of an extra column.
Adam Barth's avatar
Adam Barth committed
84 85 86 87 88
  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
89 90 91
  /// the top edge of the interior of the grid's padding (usually if the padding
  /// is EdgeOffsets.zero). 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.
Adam Barth's avatar
Adam Barth committed
92 93
  ///
  /// If there are n rows in the grid, there should be n + 1 entries in this
94 95
  /// list. The bottom edge of the last row is defined as rowOffsets(n), i.e.
  /// the top edge of an extra row.
Adam Barth's avatar
Adam Barth committed
96 97
  final List<double> rowOffsets;

98 99 100 101 102 103
  /// The vertical distance between rows.
  final double rowSpacing;

  /// The horizontal distance between columns.
  final double columnSpacing;

Adam Barth's avatar
Adam Barth committed
104 105
  /// The interior padding of the grid.
  ///
106 107
  /// The grid's size encloses the spaced rows and columns and is then inflated
  /// by the padding.
108
  final EdgeInsets padding;
Adam Barth's avatar
Adam Barth committed
109 110 111

  /// The size of the grid.
  Size get gridSize => new Size(columnOffsets.last + padding.horizontal, rowOffsets.last + padding.vertical);
112 113 114 115 116 117

  /// 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
118
}
119

Adam Barth's avatar
Adam Barth committed
120 121 122 123 124 125
/// Where to place a child within a grid.
class GridChildPlacement {
  GridChildPlacement({
    this.column,
    this.row,
    this.columnSpan: 1,
126
    this.rowSpan: 1
Adam Barth's avatar
Adam Barth committed
127
  }) {
128 129 130 131
    assert(column != null && column >= 0);
    assert(row != null && row >= 0);
    assert(columnSpan != null && columnSpan > 0);
    assert(rowSpan != null && rowSpan > 0);
132 133
  }

Adam Barth's avatar
Adam Barth committed
134 135 136 137 138 139 140 141
  /// 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;
142

Adam Barth's avatar
Adam Barth committed
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
  /// How many rows the child should span.
  final int rowSpan;
}

/// 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
169
  /// decreases the preferred height.
Adam Barth's avatar
Adam Barth committed
170 171 172 173 174 175 176 177 178 179 180
  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
181
  /// decreases the preferred width.
Adam Barth's avatar
Adam Barth committed
182 183 184 185 186 187 188
  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 {
189 190 191
  GridDelegateWithInOrderChildPlacement({
    this.columnSpacing: 0.0,
    this.rowSpacing: 0.0,
192
    this.padding: EdgeInsets.zero
193 194 195 196 197 198 199 200 201 202 203
  }) {
    assert(columnSpacing != null && columnSpacing >= 0.0);
    assert(rowSpacing != null && rowSpacing >= 0.0);
    assert(padding != null && padding.isNonNegative);
  }

  /// The horizontal distance between columns.
  final double columnSpacing;

  /// The vertical distance between rows.
  final double rowSpacing;
Adam Barth's avatar
Adam Barth committed
204

205
  // Insets for the entire grid.
206
  final EdgeInsets padding;
Adam Barth's avatar
Adam Barth committed
207

208
  @override
Adam Barth's avatar
Adam Barth committed
209
  GridChildPlacement getChildPlacement(GridSpecification specification, int index, Object placementData) {
210
    final int columnCount = specification.columnOffsets.length - 1;
Adam Barth's avatar
Adam Barth committed
211 212
    return new GridChildPlacement(
      column: index % columnCount,
213
      row: index ~/ columnCount
Adam Barth's avatar
Adam Barth committed
214 215 216
    );
  }

217
  @override
Adam Barth's avatar
Adam Barth committed
218
  bool shouldRelayout(GridDelegateWithInOrderChildPlacement oldDelegate) {
219
    return columnSpacing != oldDelegate.columnSpacing
220 221
        || rowSpacing != oldDelegate.rowSpacing
        || padding != oldDelegate.padding;
Adam Barth's avatar
Adam Barth committed
222 223 224
  }
}

225

Adam Barth's avatar
Adam Barth committed
226 227 228 229
/// A [GridDelegate] that divides the grid's width evenly amount a fixed number of columns.
class FixedColumnCountGridDelegate extends GridDelegateWithInOrderChildPlacement {
  FixedColumnCountGridDelegate({
    this.columnCount,
230 231
    double columnSpacing: 0.0,
    double rowSpacing: 0.0,
232
    EdgeInsets padding: EdgeInsets.zero,
233 234 235 236 237
    this.tileAspectRatio: 1.0
  }) : super(columnSpacing: columnSpacing, rowSpacing: rowSpacing, padding: padding) {
    assert(columnCount != null && columnCount >= 0);
    assert(tileAspectRatio != null && tileAspectRatio > 0.0);
  }
Adam Barth's avatar
Adam Barth committed
238 239 240 241 242 243 244

  /// 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;

245
  @override
Adam Barth's avatar
Adam Barth committed
246 247 248
  GridSpecification getGridSpecification(BoxConstraints constraints, int childCount) {
    assert(constraints.maxWidth < double.INFINITY);
    int rowCount = (childCount / columnCount).ceil();
249
    double tileWidth = math.max(0.0, constraints.maxWidth - padding.horizontal + columnSpacing) / columnCount;
Adam Barth's avatar
Adam Barth committed
250 251 252 253 254 255
    double tileHeight = tileWidth / tileAspectRatio;
    return new GridSpecification.fromRegularTiles(
      tileWidth: tileWidth,
      tileHeight: tileHeight,
      columnCount: columnCount,
      rowCount: rowCount,
256 257 258
      columnSpacing: columnSpacing,
      rowSpacing: rowSpacing,
      padding: padding
Adam Barth's avatar
Adam Barth committed
259 260 261
    );
  }

262
  @override
Adam Barth's avatar
Adam Barth committed
263 264 265 266 267 268
  bool shouldRelayout(FixedColumnCountGridDelegate oldDelegate) {
    return columnCount != oldDelegate.columnCount
        || tileAspectRatio != oldDelegate.tileAspectRatio
        || super.shouldRelayout(oldDelegate);
  }

269
  @override
Adam Barth's avatar
Adam Barth committed
270 271 272 273
  double getMinIntrinsicWidth(BoxConstraints constraints, int childCount) {
    return constraints.constrainWidth(0.0);
  }

274
  @override
Adam Barth's avatar
Adam Barth committed
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
  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,
292 293
    double columnSpacing: 0.0,
    double rowSpacing: 0.0,
294
    EdgeInsets padding: EdgeInsets.zero
295 296 297 298
  }) : super(columnSpacing: columnSpacing, rowSpacing: rowSpacing, padding: padding) {
    assert(maxTileWidth != null && maxTileWidth >= 0.0);
    assert(tileAspectRatio != null && tileAspectRatio > 0.0);
  }
Adam Barth's avatar
Adam Barth committed
299 300 301 302 303 304 305

  /// 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;

306
  @override
Adam Barth's avatar
Adam Barth committed
307 308
  GridSpecification getGridSpecification(BoxConstraints constraints, int childCount) {
    assert(constraints.maxWidth < double.INFINITY);
309
    final double gridWidth = math.max(0.0, constraints.maxWidth - padding.horizontal);
Adam Barth's avatar
Adam Barth committed
310 311 312 313 314 315 316 317 318
    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,
319 320 321
      columnSpacing: columnSpacing,
      rowSpacing: rowSpacing,
      padding: padding
Adam Barth's avatar
Adam Barth committed
322 323 324
    );
  }

325
  @override
Adam Barth's avatar
Adam Barth committed
326 327 328 329 330 331
  bool shouldRelayout(MaxTileWidthGridDelegate oldDelegate) {
    return maxTileWidth != oldDelegate.maxTileWidth
        || tileAspectRatio != oldDelegate.tileAspectRatio
        || super.shouldRelayout(oldDelegate);
  }

332
  @override
Adam Barth's avatar
Adam Barth committed
333 334 335 336
  double getMinIntrinsicWidth(BoxConstraints constraints, int childCount) {
    return constraints.constrainWidth(0.0);
  }

337
  @override
Adam Barth's avatar
Adam Barth committed
338 339 340
  double getMaxIntrinsicWidth(BoxConstraints constraints, int childCount) {
    return constraints.constrainWidth(maxTileWidth * childCount);
  }
341 342
}

343
/// Parent data for use with [RenderGrid]
Adam Barth's avatar
Adam Barth committed
344 345 346 347
class GridParentData extends ContainerBoxParentDataMixin<RenderBox> {
  /// Opaque data passed to the getChildPlacement method of the grid's [GridDelegate].
  Object placementData;

348
  @override
Adam Barth's avatar
Adam Barth committed
349 350
  String toString() => '${super.toString()}; placementData=$placementData';
}
351 352 353

/// Implements the grid layout algorithm
///
Adam Barth's avatar
Adam Barth committed
354 355 356 357 358 359 360 361 362 363 364
/// 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
365
class RenderGrid extends RenderVirtualViewport<GridParentData> {
Adam Barth's avatar
Adam Barth committed
366 367
  RenderGrid({
    List<RenderBox> children,
368 369 370 371
    GridDelegate delegate,
    int virtualChildBase: 0,
    int virtualChildCount,
    Offset paintOffset: Offset.zero,
372
    RenderObjectPainter overlayPainter,
373
    LayoutCallback callback
Adam Barth's avatar
Adam Barth committed
374 375 376
  }) : _delegate = delegate, _virtualChildBase = virtualChildBase, super(
    virtualChildCount: virtualChildCount,
    paintOffset: paintOffset,
377
    overlayPainter: overlayPainter,
Adam Barth's avatar
Adam Barth committed
378 379
    callback: callback
  ) {
Adam Barth's avatar
Adam Barth committed
380
    assert(delegate != null);
381 382 383
    addAll(children);
  }

Adam Barth's avatar
Adam Barth committed
384 385 386
  /// The delegate that controls the layout of the children.
  GridDelegate get delegate => _delegate;
  GridDelegate _delegate;
387
  set delegate (GridDelegate newDelegate) {
Adam Barth's avatar
Adam Barth committed
388 389 390
    assert(newDelegate != null);
    if (_delegate == newDelegate)
      return;
391 392
    if (newDelegate.runtimeType != _delegate.runtimeType || newDelegate.shouldRelayout(_delegate)) {
      _specification = null;
393
      markNeedsLayout();
394
    }
Adam Barth's avatar
Adam Barth committed
395
    _delegate = newDelegate;
396 397
  }

398
  @override
399
  set mainAxis(Axis value) {
400 401
    assert(() {
      if (value != Axis.vertical)
402
        throw new FlutterError('RenderGrid doesn\'t yet support horizontal scrolling.');
403 404
      return true;
    });
405
    super.mainAxis = value;
406 407
  }

408
  @override
409 410
  int get virtualChildCount => super.virtualChildCount ?? childCount;

411 412 413 414 415 416
  /// 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;
417
  set virtualChildBase(int value) {
418 419 420 421 422 423 424
    assert(value != null);
    if (_virtualChildBase == value)
      return;
    _virtualChildBase = value;
    markNeedsLayout();
  }

425
  @override
426 427 428 429 430
  void setupParentData(RenderBox child) {
    if (child.parentData is! GridParentData)
      child.parentData = new GridParentData();
  }

431
  @override
432
  double getMinIntrinsicWidth(BoxConstraints constraints) {
433
    assert(constraints.debugAssertIsValid());
434
    return _delegate.getMinIntrinsicWidth(constraints, virtualChildCount);
435 436
  }

437
  @override
438
  double getMaxIntrinsicWidth(BoxConstraints constraints) {
439
    assert(constraints.debugAssertIsValid());
440
    return _delegate.getMaxIntrinsicWidth(constraints, virtualChildCount);
441 442
  }

443
  @override
444
  double getMinIntrinsicHeight(BoxConstraints constraints) {
445
    assert(constraints.debugAssertIsValid());
446
    return _delegate.getMinIntrinsicHeight(constraints, virtualChildCount);
447 448
  }

449
  @override
450
  double getMaxIntrinsicHeight(BoxConstraints constraints) {
451
    assert(constraints.debugAssertIsValid());
452
    return _delegate.getMaxIntrinsicHeight(constraints, virtualChildCount);
453 454
  }

455
  @override
456 457 458 459
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    return defaultComputeDistanceToHighestActualBaseline(baseline);
  }

460
  GridSpecification get specification => _specification;
Adam Barth's avatar
Adam Barth committed
461
  GridSpecification _specification;
462 463 464 465 466 467 468 469 470 471 472 473 474
  int _specificationChildCount;
  BoxConstraints _specificationConstraints;

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

475
  @override
476
  void performLayout() {
477
    _updateGridSpecification();
478
    final Size gridSize = _specification.gridSize;
Adam Barth's avatar
Adam Barth committed
479
    size = constraints.constrain(gridSize);
480

Adam Barth's avatar
Adam Barth committed
481 482
    if (callback != null)
      invokeLayoutCallback(callback);
483

484 485
    final double gridTopPadding = _specification.padding.top;
    final double gridLeftPadding = _specification.padding.left;
486
    int childIndex = virtualChildBase;
487 488
    RenderBox child = firstChild;
    while (child != null) {
Hixie's avatar
Hixie committed
489
      final GridParentData childParentData = child.parentData;
490

491
      GridChildPlacement placement = delegate.getChildPlacement(_specification, childIndex, childParentData.placementData);
Adam Barth's avatar
Adam Barth committed
492 493 494 495 496
      assert(placement.column >= 0);
      assert(placement.row >= 0);
      assert(placement.column + placement.columnSpan < _specification.columnOffsets.length);
      assert(placement.row + placement.rowSpan < _specification.rowOffsets.length);

497 498 499 500
      double tileLeft = gridLeftPadding + _specification.columnOffsets[placement.column];
      double tileRight = gridLeftPadding + _specification.columnOffsets[placement.column + placement.columnSpan];
      double tileTop = gridTopPadding + _specification.rowOffsets[placement.row];
      double tileBottom =  gridTopPadding + _specification.rowOffsets[placement.row + placement.rowSpan];
Adam Barth's avatar
Adam Barth committed
501

502 503
      double childWidth = math.max(0.0, tileRight - tileLeft - _specification.columnSpacing);
      double childHeight = math.max(0.0, tileBottom - tileTop - _specification.rowSpacing);
Adam Barth's avatar
Adam Barth committed
504 505 506 507 508 509 510 511

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

512
      childParentData.offset = new Offset(tileLeft, tileTop);
Adam Barth's avatar
Adam Barth committed
513
      childIndex += 1;
Hixie's avatar
Hixie committed
514 515 516

      assert(child.parentData == childParentData);
      child = childParentData.nextSibling;
517 518 519
    }
  }
}