Commit 7bf3cb41 authored by Adam Barth's avatar Adam Barth

Merge pull request #1061 from abarth/generalize_grid_layout

Generalize grid layout
parents a5925149 46a178dc
......@@ -20,7 +20,10 @@ Color randomColor() {
RenderBox buildGridExample() {
List<RenderBox> children = new List<RenderBox>.generate(30, (_) => new RenderSolidColorBox(randomColor()));
return new RenderGrid(children: children, maxChildExtent: 100.0);
return new RenderGrid(
children: children,
delegate: new MaxTileWidthGridDelegate(maxTileWidth: 100.0)
);
}
main() => new RenderingFlutterBinding(root: buildGridExample());
......@@ -62,7 +62,7 @@ class AdaptiveItem {
}
class MediaQueryExample extends StatelessComponent {
static const double _maxChildExtent = 150.0;
static const double _maxTileWidth = 150.0;
static const double _gridViewBreakpoint = 450.0;
Widget _buildBody(BuildContext context) {
......@@ -78,9 +78,9 @@ class MediaQueryExample extends StatelessComponent {
} else {
return new Block(
<Widget>[
new Grid(
new MaxTileWidthGrid(
items.map((AdaptiveItem item) => item.toCard()).toList(),
maxChildExtent: _maxChildExtent
maxTileWidth: _maxTileWidth
)
]
);
......
......@@ -43,8 +43,17 @@ class EdgeDims {
/// Whether every dimension is non-negative.
bool get isNonNegative => top >= 0.0 && right >= 0.0 && bottom >= 0.0 && left >= 0.0;
/// The size that this edge dims would occupy with an empty interior.
ui.Size get collapsedSize => new ui.Size(left + right, top + bottom);
/// The total offset in the vertical direction.
double get horizontal => left + right;
/// The total offset in the horizontal direction.
double get vertical => top + bottom;
/// The size that this EdgeDims would occupy with an empty interior.
ui.Size get collapsedSize => new ui.Size(horizontal, vertical);
/// An EdgeDims with top and bottom as well as left and right flipped.
EdgeDims get flipped => new EdgeDims.TRBL(bottom, left, top, right);
ui.Rect inflateRect(ui.Rect rect) {
return new ui.Rect.fromLTRB(rect.left - left, rect.top - top, rect.right + right, rect.bottom + bottom);
......
......@@ -374,7 +374,7 @@ abstract class RenderBox extends RenderObject {
return constraints.constrainWidth(0.0);
}
/// Return the minimum height that this box could be without failing to render
/// Return the minimum height that this box could be without failing to paint
/// its contents within itself.
///
/// Override in subclasses that implement [performLayout].
......
......@@ -5,67 +5,325 @@
import 'box.dart';
import 'object.dart';
class _GridMetrics {
// Grid is width-in, height-out. We fill the max width and adjust height
// accordingly.
factory _GridMetrics({ double width, int childCount, double maxChildExtent }) {
assert(width != null);
assert(childCount != null);
assert(maxChildExtent != null);
double childExtent = maxChildExtent;
int childrenPerRow = (width / childExtent).floor();
// If the child extent divides evenly into the width use that, otherwise + 1
if (width / childExtent != childrenPerRow.toDouble()) childrenPerRow += 1;
double totalPadding = 0.0;
if (childrenPerRow * childExtent > width) {
// TODO(eseidel): We should snap to pixel bounderies.
childExtent = width / childrenPerRow;
} else {
totalPadding = width - (childrenPerRow * childExtent);
}
double childPadding = totalPadding / (childrenPerRow + 1.0);
int rowCount = (childCount / childrenPerRow).ceil();
double height = childPadding * (rowCount + 1) + (childExtent * rowCount);
Size childSize = new Size(childExtent, childExtent);
Size size = new Size(width, height);
return new _GridMetrics._(size, childSize, childrenPerRow, childPadding, rowCount);
}
const _GridMetrics._(this.size, this.childSize, this.childrenPerRow, this.childPadding, this.rowCount);
final Size size;
final Size childSize;
final int childrenPerRow; // aka columnCount
final double childPadding;
final int rowCount;
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;
}
return true;
});
return result;
}
List<double> _generateRegularOffsets(int count, double size) {
int length = count + 1;
List<double> result = new List<double>(length);
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);
}
/// 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);
}
/// 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;
/// 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);
}
}
/// Parent data for use with [RenderGrid]
class GridParentData extends ContainerBoxParentDataMixin<RenderBox> {}
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';
}
/// Implements the grid layout algorithm
///
/// In grid layout, children are arranged into rows and collumns in on a two
/// dimensional grid. The grid determines how many children will be placed in
/// each row by making the children as wide as possible while still respecting
/// the given [maxChildExtent].
/// 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.
class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, GridParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, GridParentData> {
RenderGrid({ List<RenderBox> children, double maxChildExtent }) {
RenderGrid({
List<RenderBox> children,
GridDelegate delegate
}) : _delegate = delegate {
assert(delegate != null);
addAll(children);
_maxChildExtent = maxChildExtent;
}
double _maxChildExtent;
bool _hasVisualOverflow = false;
double get maxChildExtent => _maxChildExtent;
void set maxChildExtent (double value) {
if (_maxChildExtent != value) {
_maxChildExtent = value;
/// 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;
if (newDelegate.runtimeType != _delegate.runtimeType || newDelegate.shouldRelayout(_delegate))
markNeedsLayout();
}
_delegate = newDelegate;
}
void setupParentData(RenderBox child) {
......@@ -75,63 +333,72 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr
double getMinIntrinsicWidth(BoxConstraints constraints) {
assert(constraints.isNormalized);
// We can render at any width.
return constraints.constrainWidth(0.0);
return _delegate.getMinIntrinsicWidth(constraints, childCount);
}
double getMaxIntrinsicWidth(BoxConstraints constraints) {
assert(constraints.isNormalized);
double maxWidth = childCount * _maxChildExtent;
return constraints.constrainWidth(maxWidth);
return _delegate.getMaxIntrinsicWidth(constraints, childCount);
}
double getMinIntrinsicHeight(BoxConstraints constraints) {
assert(constraints.isNormalized);
double desiredHeight = _computeMetrics().size.height;
return constraints.constrainHeight(desiredHeight);
return _delegate.getMinIntrinsicHeight(constraints, childCount);
}
double getMaxIntrinsicHeight(BoxConstraints constraints) {
assert(constraints.isNormalized);
return getMinIntrinsicHeight(constraints);
return _delegate.getMaxIntrinsicHeight(constraints, childCount);
}
double computeDistanceToActualBaseline(TextBaseline baseline) {
return defaultComputeDistanceToHighestActualBaseline(baseline);
}
_GridMetrics _computeMetrics() {
return new _GridMetrics(
width: constraints.maxWidth,
childCount: childCount,
maxChildExtent: _maxChildExtent
);
}
GridSpecification _specification;
bool _hasVisualOverflow = false;
void performLayout() {
// We could shrink-wrap our contents when infinite, but for now we don't.
assert(constraints.maxWidth < double.INFINITY);
_GridMetrics metrics = _computeMetrics();
size = constraints.constrain(metrics.size);
if (constraints.maxHeight < size.height)
_specification = delegate.getGridSpecification(constraints, childCount);
Size gridSize = _specification.gridSize;
size = constraints.constrain(gridSize);
if (gridSize.width > size.width || gridSize.height > size.height)
_hasVisualOverflow = true;
int row = 0;
int column = 0;
double gridTopPadding = _specification.padding.top;
double gridLeftPadding = _specification.padding.left;
int index = 0;
RenderBox child = firstChild;
while (child != null) {
child.layout(new BoxConstraints.tight(metrics.childSize));
double x = (column + 1) * metrics.childPadding + (column * metrics.childSize.width);
double y = (row + 1) * metrics.childPadding + (row * metrics.childSize.height);
final GridParentData childParentData = child.parentData;
childParentData.offset = new Offset(x, y);
column += 1;
if (column >= metrics.childrenPerRow) {
row += 1;
column = 0;
}
GridChildPlacement placement = delegate.getChildPlacement(_specification, index, childParentData.placementData);
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
);
++index;
assert(child.parentData == childParentData);
child = childParentData.nextSibling;
......
......@@ -33,6 +33,7 @@ export 'package:flutter/rendering.dart' show
FontWeight,
FractionalOffset,
Gradient,
GridDelegate,
HitTestBehavior,
ImageFit,
ImageRepeat,
......@@ -1125,21 +1126,125 @@ class Positioned extends ParentDataWidget<StackRenderObjectWidgetBase> {
}
}
abstract class GridRenderObjectWidgetBase extends MultiChildRenderObjectWidget {
GridRenderObjectWidgetBase({
List<Widget> children,
Key key
}) : super(key: key, children: children) {
_delegate = createDelegate();
}
GridDelegate _delegate;
/// The delegate that controls the layout of the children.
GridDelegate createDelegate();
RenderGrid createRenderObject() => new RenderGrid(delegate: _delegate);
void updateRenderObject(RenderGrid renderObject, GridRenderObjectWidgetBase oldWidget) {
renderObject.delegate = _delegate;
}
}
/// Uses the grid layout algorithm for its children.
///
/// For details about the grid layout algorithm, see [RenderGrid].
class Grid extends MultiChildRenderObjectWidget {
Grid(List<Widget> children, { Key key, this.maxChildExtent })
class CustomGrid extends GridRenderObjectWidgetBase {
CustomGrid(List<Widget> children, { Key key, this.delegate })
: super(key: key, children: children) {
assert(maxChildExtent != null);
assert(delegate != null);
}
final double maxChildExtent;
/// The delegate that controls the layout of the children.
final GridDelegate delegate;
RenderGrid createRenderObject() => new RenderGrid(maxChildExtent: maxChildExtent);
GridDelegate createDelegate() => delegate;
}
/// Uses a grid layout with a fixed column count.
///
/// For details about the grid layout algorithm, see [MaxTileWidthGridDelegate].
class FixedColumnCountGrid extends GridRenderObjectWidgetBase {
FixedColumnCountGrid(List<Widget> children, {
Key key,
this.columnCount,
this.tileAspectRatio: 1.0,
this.padding: EdgeDims.zero
}) : super(key: key, children: children) {
assert(columnCount != null);
}
/// The number of columns in the grid.
final int columnCount;
void updateRenderObject(RenderGrid renderObject, Grid oldWidget) {
renderObject.maxChildExtent = maxChildExtent;
/// The ratio of the width to the height of each tile in the grid.
final double tileAspectRatio;
/// The amount of padding to apply to each child.
final EdgeDims padding;
FixedColumnCountGridDelegate createDelegate() {
return new FixedColumnCountGridDelegate(
columnCount: columnCount,
tileAspectRatio: tileAspectRatio,
padding: padding
);
}
}
/// Uses a grid layout with a max tile width.
///
/// For details about the grid layout algorithm, see [MaxTileWidthGridDelegate].
class MaxTileWidthGrid extends GridRenderObjectWidgetBase {
MaxTileWidthGrid(List<Widget> children, {
Key key,
this.maxTileWidth,
this.tileAspectRatio: 1.0,
this.padding: EdgeDims.zero
}) : super(key: key, children: children) {
assert(maxTileWidth != null);
}
/// 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;
/// The amount of padding to apply to each child.
final EdgeDims padding;
MaxTileWidthGridDelegate createDelegate() {
return new MaxTileWidthGridDelegate(
maxTileWidth: maxTileWidth,
tileAspectRatio: tileAspectRatio,
padding: padding
);
}
}
/// Supplies per-child data to the grid's [GridDelegate].
class GridPlacementData<DataType, WidgetType extends RenderObjectWidget> extends ParentDataWidget<WidgetType> {
GridPlacementData({ Key key, this.placementData, Widget child })
: super(key: key, child: child);
/// Opaque data passed to the getChildPlacement method of the grid's [GridDelegate].
final DataType placementData;
void applyParentData(RenderObject renderObject) {
assert(renderObject.parentData is GridParentData);
final GridParentData parentData = renderObject.parentData;
if (parentData.placementData != placementData) {
parentData.placementData = placementData;
AbstractNode targetParent = renderObject.parent;
if (targetParent is RenderObject)
targetParent.markNeedsLayout();
}
}
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('placementData: $placementData');
}
}
......
......@@ -16,7 +16,10 @@ void main() {
new RenderDecoratedBox(decoration: new BoxDecoration())
];
RenderGrid grid = new RenderGrid(children: children, maxChildExtent: 100.0);
RenderGrid grid = new RenderGrid(
children: children,
delegate: new MaxTileWidthGridDelegate(maxTileWidth: 100.0)
);
layout(grid, constraints: const BoxConstraints(maxWidth: 200.0));
children.forEach((RenderBox child) {
......@@ -28,7 +31,7 @@ void main() {
expect(grid.size.height, equals(200.0), reason: "grid height");
expect(grid.needsLayout, equals(false));
grid.maxChildExtent = 60.0;
grid.delegate = new MaxTileWidthGridDelegate(maxTileWidth: 60.0);
expect(grid.needsLayout, equals(true));
pumpFrame();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment