mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Generalize grid layout
This patch make grid layout much more flexible. The behavior is factored out into a GridDelegate that's modeled after the custom layout delegates. The patch includes a MaxTileWidthGridDelegate that implements the old behavior and a FixedColumnCountGridDelegate that implements a grid layout with a fixed number of columns. Fixes #1048
This commit is contained in:
parent
a5925149d6
commit
46a178dce6
@ -20,7 +20,10 @@ Color randomColor() {
|
|||||||
|
|
||||||
RenderBox buildGridExample() {
|
RenderBox buildGridExample() {
|
||||||
List<RenderBox> children = new List<RenderBox>.generate(30, (_) => new RenderSolidColorBox(randomColor()));
|
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());
|
main() => new RenderingFlutterBinding(root: buildGridExample());
|
||||||
|
@ -62,7 +62,7 @@ class AdaptiveItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MediaQueryExample extends StatelessComponent {
|
class MediaQueryExample extends StatelessComponent {
|
||||||
static const double _maxChildExtent = 150.0;
|
static const double _maxTileWidth = 150.0;
|
||||||
static const double _gridViewBreakpoint = 450.0;
|
static const double _gridViewBreakpoint = 450.0;
|
||||||
|
|
||||||
Widget _buildBody(BuildContext context) {
|
Widget _buildBody(BuildContext context) {
|
||||||
@ -78,9 +78,9 @@ class MediaQueryExample extends StatelessComponent {
|
|||||||
} else {
|
} else {
|
||||||
return new Block(
|
return new Block(
|
||||||
<Widget>[
|
<Widget>[
|
||||||
new Grid(
|
new MaxTileWidthGrid(
|
||||||
items.map((AdaptiveItem item) => item.toCard()).toList(),
|
items.map((AdaptiveItem item) => item.toCard()).toList(),
|
||||||
maxChildExtent: _maxChildExtent
|
maxTileWidth: _maxTileWidth
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -43,8 +43,17 @@ class EdgeDims {
|
|||||||
/// Whether every dimension is non-negative.
|
/// Whether every dimension is non-negative.
|
||||||
bool get isNonNegative => top >= 0.0 && right >= 0.0 && bottom >= 0.0 && left >= 0.0;
|
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.
|
/// The total offset in the vertical direction.
|
||||||
ui.Size get collapsedSize => new ui.Size(left + right, top + bottom);
|
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) {
|
ui.Rect inflateRect(ui.Rect rect) {
|
||||||
return new ui.Rect.fromLTRB(rect.left - left, rect.top - top, rect.right + right, rect.bottom + bottom);
|
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 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.
|
/// its contents within itself.
|
||||||
///
|
///
|
||||||
/// Override in subclasses that implement [performLayout].
|
/// Override in subclasses that implement [performLayout].
|
||||||
|
@ -5,67 +5,325 @@
|
|||||||
import 'box.dart';
|
import 'box.dart';
|
||||||
import 'object.dart';
|
import 'object.dart';
|
||||||
|
|
||||||
class _GridMetrics {
|
bool _debugIsMonotonic(List<double> offsets) {
|
||||||
// Grid is width-in, height-out. We fill the max width and adjust height
|
bool result = true;
|
||||||
// accordingly.
|
assert(() {
|
||||||
factory _GridMetrics({ double width, int childCount, double maxChildExtent }) {
|
double current = 0.0;
|
||||||
assert(width != null);
|
for (double offset in offsets) {
|
||||||
assert(childCount != null);
|
if (current > offset) {
|
||||||
assert(maxChildExtent != null);
|
result = false;
|
||||||
double childExtent = maxChildExtent;
|
break;
|
||||||
int childrenPerRow = (width / childExtent).floor();
|
}
|
||||||
// If the child extent divides evenly into the width use that, otherwise + 1
|
current = offset;
|
||||||
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);
|
return true;
|
||||||
int rowCount = (childCount / childrenPerRow).ceil();
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
double height = childPadding * (rowCount + 1) + (childExtent * rowCount);
|
List<double> _generateRegularOffsets(int count, double size) {
|
||||||
Size childSize = new Size(childExtent, childExtent);
|
int length = count + 1;
|
||||||
Size size = new Size(width, height);
|
List<double> result = new List<double>(length);
|
||||||
return new _GridMetrics._(size, childSize, childrenPerRow, childPadding, rowCount);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
const _GridMetrics._(this.size, this.childSize, this.childrenPerRow, this.childPadding, this.rowCount);
|
/// 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);
|
||||||
|
}
|
||||||
|
|
||||||
final Size size;
|
/// The offsets of the column boundaries in the grid.
|
||||||
final Size childSize;
|
///
|
||||||
final int childrenPerRow; // aka columnCount
|
/// The first offset is the offset of the left edge of the left-most column
|
||||||
final double childPadding;
|
/// from the left edge of the interior of the grid's padding (usually 0.0).
|
||||||
final int rowCount;
|
/// 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]
|
/// 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
|
/// Implements the grid layout algorithm
|
||||||
///
|
///
|
||||||
/// In grid layout, children are arranged into rows and collumns in on a two
|
/// In grid layout, children are arranged into rows and columns in on a two
|
||||||
/// dimensional grid. The grid determines how many children will be placed in
|
/// dimensional grid. The [GridDelegate] determines how to arrange the
|
||||||
/// each row by making the children as wide as possible while still respecting
|
/// children on the grid.
|
||||||
/// the given [maxChildExtent].
|
///
|
||||||
|
/// 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>,
|
class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, GridParentData>,
|
||||||
RenderBoxContainerDefaultsMixin<RenderBox, GridParentData> {
|
RenderBoxContainerDefaultsMixin<RenderBox, GridParentData> {
|
||||||
RenderGrid({ List<RenderBox> children, double maxChildExtent }) {
|
RenderGrid({
|
||||||
|
List<RenderBox> children,
|
||||||
|
GridDelegate delegate
|
||||||
|
}) : _delegate = delegate {
|
||||||
|
assert(delegate != null);
|
||||||
addAll(children);
|
addAll(children);
|
||||||
_maxChildExtent = maxChildExtent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double _maxChildExtent;
|
/// The delegate that controls the layout of the children.
|
||||||
bool _hasVisualOverflow = false;
|
GridDelegate get delegate => _delegate;
|
||||||
|
GridDelegate _delegate;
|
||||||
double get maxChildExtent => _maxChildExtent;
|
void set delegate (GridDelegate newDelegate) {
|
||||||
void set maxChildExtent (double value) {
|
assert(newDelegate != null);
|
||||||
if (_maxChildExtent != value) {
|
if (_delegate == newDelegate)
|
||||||
_maxChildExtent = value;
|
return;
|
||||||
|
if (newDelegate.runtimeType != _delegate.runtimeType || newDelegate.shouldRelayout(_delegate))
|
||||||
markNeedsLayout();
|
markNeedsLayout();
|
||||||
}
|
_delegate = newDelegate;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setupParentData(RenderBox child) {
|
void setupParentData(RenderBox child) {
|
||||||
@ -75,63 +333,72 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr
|
|||||||
|
|
||||||
double getMinIntrinsicWidth(BoxConstraints constraints) {
|
double getMinIntrinsicWidth(BoxConstraints constraints) {
|
||||||
assert(constraints.isNormalized);
|
assert(constraints.isNormalized);
|
||||||
// We can render at any width.
|
return _delegate.getMinIntrinsicWidth(constraints, childCount);
|
||||||
return constraints.constrainWidth(0.0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double getMaxIntrinsicWidth(BoxConstraints constraints) {
|
double getMaxIntrinsicWidth(BoxConstraints constraints) {
|
||||||
assert(constraints.isNormalized);
|
assert(constraints.isNormalized);
|
||||||
double maxWidth = childCount * _maxChildExtent;
|
return _delegate.getMaxIntrinsicWidth(constraints, childCount);
|
||||||
return constraints.constrainWidth(maxWidth);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double getMinIntrinsicHeight(BoxConstraints constraints) {
|
double getMinIntrinsicHeight(BoxConstraints constraints) {
|
||||||
assert(constraints.isNormalized);
|
assert(constraints.isNormalized);
|
||||||
double desiredHeight = _computeMetrics().size.height;
|
return _delegate.getMinIntrinsicHeight(constraints, childCount);
|
||||||
return constraints.constrainHeight(desiredHeight);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double getMaxIntrinsicHeight(BoxConstraints constraints) {
|
double getMaxIntrinsicHeight(BoxConstraints constraints) {
|
||||||
assert(constraints.isNormalized);
|
assert(constraints.isNormalized);
|
||||||
return getMinIntrinsicHeight(constraints);
|
return _delegate.getMaxIntrinsicHeight(constraints, childCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
double computeDistanceToActualBaseline(TextBaseline baseline) {
|
double computeDistanceToActualBaseline(TextBaseline baseline) {
|
||||||
return defaultComputeDistanceToHighestActualBaseline(baseline);
|
return defaultComputeDistanceToHighestActualBaseline(baseline);
|
||||||
}
|
}
|
||||||
|
|
||||||
_GridMetrics _computeMetrics() {
|
GridSpecification _specification;
|
||||||
return new _GridMetrics(
|
bool _hasVisualOverflow = false;
|
||||||
width: constraints.maxWidth,
|
|
||||||
childCount: childCount,
|
|
||||||
maxChildExtent: _maxChildExtent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void performLayout() {
|
void performLayout() {
|
||||||
// We could shrink-wrap our contents when infinite, but for now we don't.
|
_specification = delegate.getGridSpecification(constraints, childCount);
|
||||||
assert(constraints.maxWidth < double.INFINITY);
|
Size gridSize = _specification.gridSize;
|
||||||
_GridMetrics metrics = _computeMetrics();
|
size = constraints.constrain(gridSize);
|
||||||
size = constraints.constrain(metrics.size);
|
if (gridSize.width > size.width || gridSize.height > size.height)
|
||||||
if (constraints.maxHeight < size.height)
|
|
||||||
_hasVisualOverflow = true;
|
_hasVisualOverflow = true;
|
||||||
|
|
||||||
int row = 0;
|
double gridTopPadding = _specification.padding.top;
|
||||||
int column = 0;
|
double gridLeftPadding = _specification.padding.left;
|
||||||
|
int index = 0;
|
||||||
RenderBox child = firstChild;
|
RenderBox child = firstChild;
|
||||||
while (child != null) {
|
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;
|
final GridParentData childParentData = child.parentData;
|
||||||
childParentData.offset = new Offset(x, y);
|
|
||||||
|
|
||||||
column += 1;
|
GridChildPlacement placement = delegate.getChildPlacement(_specification, index, childParentData.placementData);
|
||||||
if (column >= metrics.childrenPerRow) {
|
assert(placement.column >= 0);
|
||||||
row += 1;
|
assert(placement.row >= 0);
|
||||||
column = 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);
|
assert(child.parentData == childParentData);
|
||||||
child = childParentData.nextSibling;
|
child = childParentData.nextSibling;
|
||||||
|
@ -33,6 +33,7 @@ export 'package:flutter/rendering.dart' show
|
|||||||
FontWeight,
|
FontWeight,
|
||||||
FractionalOffset,
|
FractionalOffset,
|
||||||
Gradient,
|
Gradient,
|
||||||
|
GridDelegate,
|
||||||
HitTestBehavior,
|
HitTestBehavior,
|
||||||
ImageFit,
|
ImageFit,
|
||||||
ImageRepeat,
|
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.
|
/// Uses the grid layout algorithm for its children.
|
||||||
///
|
///
|
||||||
/// For details about the grid layout algorithm, see [RenderGrid].
|
/// For details about the grid layout algorithm, see [RenderGrid].
|
||||||
class Grid extends MultiChildRenderObjectWidget {
|
class CustomGrid extends GridRenderObjectWidgetBase {
|
||||||
Grid(List<Widget> children, { Key key, this.maxChildExtent })
|
CustomGrid(List<Widget> children, { Key key, this.delegate })
|
||||||
: super(key: key, children: children) {
|
: 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;
|
||||||
|
}
|
||||||
|
|
||||||
void updateRenderObject(RenderGrid renderObject, Grid oldWidget) {
|
/// Uses a grid layout with a fixed column count.
|
||||||
renderObject.maxChildExtent = maxChildExtent;
|
///
|
||||||
|
/// 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;
|
||||||
|
|
||||||
|
/// 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())
|
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));
|
layout(grid, constraints: const BoxConstraints(maxWidth: 200.0));
|
||||||
|
|
||||||
children.forEach((RenderBox child) {
|
children.forEach((RenderBox child) {
|
||||||
@ -28,7 +31,7 @@ void main() {
|
|||||||
expect(grid.size.height, equals(200.0), reason: "grid height");
|
expect(grid.size.height, equals(200.0), reason: "grid height");
|
||||||
|
|
||||||
expect(grid.needsLayout, equals(false));
|
expect(grid.needsLayout, equals(false));
|
||||||
grid.maxChildExtent = 60.0;
|
grid.delegate = new MaxTileWidthGridDelegate(maxTileWidth: 60.0);
|
||||||
expect(grid.needsLayout, equals(true));
|
expect(grid.needsLayout, equals(true));
|
||||||
|
|
||||||
pumpFrame();
|
pumpFrame();
|
||||||
|
Loading…
Reference in New Issue
Block a user