Commit 6106fa9d authored by Adam Barth's avatar Adam Barth

Add support for scrollable grids

We now support (vertically) scrollable grids with viewporting. If the
scroll doesn't reveal any new rows, we execute the scroll with a repaint
(i.e., no layout). If the scroll reveals a new row, we trigger a layout
to change the set of materialized children in the viewport.
parent 35b18c3d
...@@ -76,13 +76,9 @@ class MediaQueryExample extends StatelessComponent { ...@@ -76,13 +76,9 @@ class MediaQueryExample extends StatelessComponent {
items.map((AdaptiveItem item) => item.toListItem()).toList() items.map((AdaptiveItem item) => item.toListItem()).toList()
); );
} else { } else {
return new Block( return new ScrollableGrid(
<Widget>[ children: items.map((AdaptiveItem item) => item.toCard()).toList(),
new MaxTileWidthGrid( delegate: new MaxTileWidthGridDelegate(maxTileWidth: _maxTileWidth)
items.map((AdaptiveItem item) => item.toCard()).toList(),
maxTileWidth: _maxTileWidth
)
]
); );
} }
} }
......
...@@ -261,7 +261,7 @@ class RenderBlockViewport extends RenderBlockBase { ...@@ -261,7 +261,7 @@ class RenderBlockViewport extends RenderBlockBase {
bool _inCallback = false; bool _inCallback = false;
bool get hasLayer => true; bool get hasLayer => true;
/// Called during [layout] to determine the blocks children. /// Called during [layout] to determine the block's children.
/// ///
/// Typically the callback will mutate the child list appropriately, for /// Typically the callback will mutate the child list appropriately, for
/// example so the child list contains only visible children. /// example so the child list contains only visible children.
...@@ -396,7 +396,9 @@ class RenderBlockViewport extends RenderBlockBase { ...@@ -396,7 +396,9 @@ class RenderBlockViewport extends RenderBlockBase {
// scroll the RenderBlockViewport, it would shift in its parent if // scroll the RenderBlockViewport, it would shift in its parent if
// the parent was baseline-aligned, which makes no sense. // the parent was baseline-aligned, which makes no sense.
// TODO(abarth): debugDoesLayoutWithCallback appears to be unreferenced.
bool get debugDoesLayoutWithCallback => true; bool get debugDoesLayoutWithCallback => true;
void performLayout() { void performLayout() {
if (_callback != null) { if (_callback != null) {
try { try {
......
...@@ -2,9 +2,13 @@ ...@@ -2,9 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:typed_data';
import 'box.dart'; import 'box.dart';
import 'object.dart'; import 'object.dart';
import 'package:vector_math/vector_math_64.dart';
bool _debugIsMonotonic(List<double> offsets) { bool _debugIsMonotonic(List<double> offsets) {
bool result = true; bool result = true;
assert(() { assert(() {
...@@ -23,7 +27,7 @@ bool _debugIsMonotonic(List<double> offsets) { ...@@ -23,7 +27,7 @@ bool _debugIsMonotonic(List<double> offsets) {
List<double> _generateRegularOffsets(int count, double size) { List<double> _generateRegularOffsets(int count, double size) {
int length = count + 1; int length = count + 1;
List<double> result = new List<double>(length); List<double> result = new Float64List(length);
for (int i = 0; i < length; ++i) for (int i = 0; i < length; ++i)
result[i] = i * size; result[i] = i * size;
return result; return result;
...@@ -87,6 +91,12 @@ class GridSpecification { ...@@ -87,6 +91,12 @@ class GridSpecification {
/// The size of the grid. /// The size of the grid.
Size get gridSize => new Size(columnOffsets.last + padding.horizontal, rowOffsets.last + padding.vertical); Size get gridSize => new Size(columnOffsets.last + padding.horizontal, rowOffsets.last + padding.vertical);
/// 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;
} }
/// Where to place a child within a grid. /// Where to place a child within a grid.
...@@ -308,8 +318,16 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr ...@@ -308,8 +318,16 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr
RenderBoxContainerDefaultsMixin<RenderBox, GridParentData> { RenderBoxContainerDefaultsMixin<RenderBox, GridParentData> {
RenderGrid({ RenderGrid({
List<RenderBox> children, List<RenderBox> children,
GridDelegate delegate GridDelegate delegate,
}) : _delegate = delegate { int virtualChildBase: 0,
int virtualChildCount,
Offset paintOffset: Offset.zero,
LayoutCallback callback
}) : _delegate = delegate,
_virtualChildBase = virtualChildBase,
_virtualChildCount = virtualChildCount,
_paintOffset = paintOffset,
_callback = callback {
assert(delegate != null); assert(delegate != null);
addAll(children); addAll(children);
} }
...@@ -321,11 +339,70 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr ...@@ -321,11 +339,70 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr
assert(newDelegate != null); assert(newDelegate != null);
if (_delegate == newDelegate) if (_delegate == newDelegate)
return; return;
if (newDelegate.runtimeType != _delegate.runtimeType || newDelegate.shouldRelayout(_delegate)) if (newDelegate.runtimeType != _delegate.runtimeType || newDelegate.shouldRelayout(_delegate)) {
_specification = null;
markNeedsLayout(); markNeedsLayout();
}
_delegate = newDelegate; _delegate = newDelegate;
} }
/// 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();
}
/// The total number of virtual children in the grid.
///
/// When asking the delegate for the grid specification, the grid will use
/// this number of children, which can be larger than the actual number of
/// children of this render object.
///
/// If the this value is null, the grid will use the actual child count of
/// this render object.
int get virtualChildCount => _virtualChildCount ?? childCount;
int _virtualChildCount;
void set virtualChildCount(int value) {
if (_virtualChildCount == value)
return;
_virtualChildCount = value;
markNeedsLayout();
}
/// The offset at which to paint the first tile.
///
/// Note: you can modify this property from within [callback], if necessary.
Offset get paintOffset => _paintOffset;
Offset _paintOffset;
void set paintOffset(Offset value) {
assert(value != null);
if (value == _paintOffset)
return;
_paintOffset = value;
markNeedsPaint();
}
/// Called during [layout] to determine the grid's children.
///
/// Typically the callback will mutate the child list appropriately, for
/// example so the child list contains only visible children.
LayoutCallback get callback => _callback;
LayoutCallback _callback;
void set callback(LayoutCallback value) {
if (value == _callback)
return;
_callback = value;
markNeedsLayout();
}
void setupParentData(RenderBox child) { void setupParentData(RenderBox child) {
if (child.parentData is! GridParentData) if (child.parentData is! GridParentData)
child.parentData = new GridParentData(); child.parentData = new GridParentData();
...@@ -333,46 +410,63 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr ...@@ -333,46 +410,63 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr
double getMinIntrinsicWidth(BoxConstraints constraints) { double getMinIntrinsicWidth(BoxConstraints constraints) {
assert(constraints.isNormalized); assert(constraints.isNormalized);
return _delegate.getMinIntrinsicWidth(constraints, childCount); return _delegate.getMinIntrinsicWidth(constraints, virtualChildCount);
} }
double getMaxIntrinsicWidth(BoxConstraints constraints) { double getMaxIntrinsicWidth(BoxConstraints constraints) {
assert(constraints.isNormalized); assert(constraints.isNormalized);
return _delegate.getMaxIntrinsicWidth(constraints, childCount); return _delegate.getMaxIntrinsicWidth(constraints, virtualChildCount);
} }
double getMinIntrinsicHeight(BoxConstraints constraints) { double getMinIntrinsicHeight(BoxConstraints constraints) {
assert(constraints.isNormalized); assert(constraints.isNormalized);
return _delegate.getMinIntrinsicHeight(constraints, childCount); return _delegate.getMinIntrinsicHeight(constraints, virtualChildCount);
} }
double getMaxIntrinsicHeight(BoxConstraints constraints) { double getMaxIntrinsicHeight(BoxConstraints constraints) {
assert(constraints.isNormalized); assert(constraints.isNormalized);
return _delegate.getMaxIntrinsicHeight(constraints, childCount); return _delegate.getMaxIntrinsicHeight(constraints, virtualChildCount);
} }
double computeDistanceToActualBaseline(TextBaseline baseline) { double computeDistanceToActualBaseline(TextBaseline baseline) {
return defaultComputeDistanceToHighestActualBaseline(baseline); return defaultComputeDistanceToHighestActualBaseline(baseline);
} }
GridSpecification get specification => _specification;
GridSpecification _specification; GridSpecification _specification;
int _specificationChildCount;
BoxConstraints _specificationConstraints;
void _updateGridSpecification() {
if (_specification == null
|| _specificationChildCount != virtualChildCount
|| _specificationConstraints != constraints) {
_specification = delegate.getGridSpecification(constraints, virtualChildCount);
_specificationChildCount = virtualChildCount;
_specificationConstraints = constraints;
}
}
bool _hasVisualOverflow = false; bool _hasVisualOverflow = false;
void performLayout() { void performLayout() {
_specification = delegate.getGridSpecification(constraints, childCount); _updateGridSpecification();
Size gridSize = _specification.gridSize; Size gridSize = _specification.gridSize;
size = constraints.constrain(gridSize); size = constraints.constrain(gridSize);
if (gridSize.width > size.width || gridSize.height > size.height) if (gridSize.width > size.width || gridSize.height > size.height)
_hasVisualOverflow = true; _hasVisualOverflow = true;
if (_callback != null)
invokeLayoutCallback(_callback);
double gridTopPadding = _specification.padding.top; double gridTopPadding = _specification.padding.top;
double gridLeftPadding = _specification.padding.left; double gridLeftPadding = _specification.padding.left;
int index = 0; int childIndex = virtualChildBase;
RenderBox child = firstChild; RenderBox child = firstChild;
while (child != null) { while (child != null) {
final GridParentData childParentData = child.parentData; final GridParentData childParentData = child.parentData;
GridChildPlacement placement = delegate.getChildPlacement(_specification, index, childParentData.placementData); GridChildPlacement placement = delegate.getChildPlacement(_specification, childIndex, childParentData.placementData);
assert(placement.column >= 0); assert(placement.column >= 0);
assert(placement.row >= 0); assert(placement.row >= 0);
assert(placement.column + placement.columnSpan < _specification.columnOffsets.length); assert(placement.column + placement.columnSpan < _specification.columnOffsets.length);
...@@ -398,21 +492,29 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr ...@@ -398,21 +492,29 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr
tileTop + placement.padding.top tileTop + placement.padding.top
); );
++index; ++childIndex;
assert(child.parentData == childParentData); assert(child.parentData == childParentData);
child = childParentData.nextSibling; child = childParentData.nextSibling;
} }
} }
void applyPaintTransform(RenderBox child, Matrix4 transform) {
super.applyPaintTransform(child, transform.translate(paintOffset));
}
bool hitTestChildren(HitTestResult result, { Point position }) { bool hitTestChildren(HitTestResult result, { Point position }) {
return defaultHitTestChildren(result, position: position); return defaultHitTestChildren(result, position: position + -paintOffset);
}
void _paintContents(PaintingContext context, Offset offset) {
defaultPaint(context, offset + paintOffset);
} }
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (_hasVisualOverflow) if (_hasVisualOverflow)
context.pushClipRect(needsCompositing, offset, Point.origin & size, defaultPaint); context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintContents);
else else
defaultPaint(context, offset); _paintContents(context, offset);
} }
} }
...@@ -29,6 +29,7 @@ export 'package:flutter/rendering.dart' show ...@@ -29,6 +29,7 @@ export 'package:flutter/rendering.dart' show
FlexAlignItems, FlexAlignItems,
FlexDirection, FlexDirection,
FlexJustifyContent, FlexJustifyContent,
FixedColumnCountGridDelegate,
FontStyle, FontStyle,
FontWeight, FontWeight,
FractionalOffset, FractionalOffset,
...@@ -40,6 +41,7 @@ export 'package:flutter/rendering.dart' show ...@@ -40,6 +41,7 @@ export 'package:flutter/rendering.dart' show
InputEvent, InputEvent,
LinearGradient, LinearGradient,
Matrix4, Matrix4,
MaxTileWidthGridDelegate,
Offset, Offset,
OneChildLayoutDelegate, OneChildLayoutDelegate,
Paint, Paint,
......
...@@ -79,6 +79,7 @@ abstract class _ViewportBaseElement<T extends _ViewportBase> extends RenderObjec ...@@ -79,6 +79,7 @@ abstract class _ViewportBaseElement<T extends _ViewportBase> extends RenderObjec
void update(T newWidget) { void update(T newWidget) {
bool needLayout = newWidget.isLayoutDifferentThan(widget); bool needLayout = newWidget.isLayoutDifferentThan(widget);
super.update(newWidget); super.update(newWidget);
// TODO(abarth): Don't we need to update overlayPainter here?
if (needLayout) if (needLayout)
renderObject.markNeedsLayout(); renderObject.markNeedsLayout();
else else
......
// 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.
import 'dart:math' as math;
import 'basic.dart';
import 'framework.dart';
import 'scrollable.dart';
import 'package:flutter/animation.dart';
import 'package:flutter/rendering.dart';
/// A vertically scrollable grid.
///
/// Requires that delegate places its children in row-major order.
class ScrollableGrid extends Scrollable {
ScrollableGrid({
Key key,
double initialScrollOffset,
ScrollListener onScroll,
SnapOffsetCallback snapOffsetCallback,
double snapAlignmentOffset: 0.0,
this.delegate,
this.children
}) : super(
key: key,
initialScrollOffset: initialScrollOffset,
// TODO(abarth): Support horizontal offsets. For horizontally scrolling
// grids. For horizontally scrolling grids, we'll probably need to use a
// delegate that places children in column-major order.
scrollDirection: ScrollDirection.vertical,
onScroll: onScroll,
snapOffsetCallback: snapOffsetCallback,
snapAlignmentOffset: snapAlignmentOffset
);
final GridDelegate delegate;
final List<Widget> children;
ScrollableState createState() => new _ScrollableGrid();
}
class _ScrollableGrid extends ScrollableState<ScrollableGrid> {
ScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior();
ExtentScrollBehavior get scrollBehavior => super.scrollBehavior;
void _handleExtentsChanged(double contentExtent, double containerExtent) {
setState(() {
scrollTo(scrollBehavior.updateExtents(
contentExtent: contentExtent,
containerExtent: containerExtent,
scrollOffset: scrollOffset
));
});
}
Widget buildContent(BuildContext context) {
return new GridViewport(
startOffset: scrollOffset,
delegate: config.delegate,
onExtentsChanged: _handleExtentsChanged,
children: config.children
);
}
}
typedef void ExtentsChangedCallback(double contentExtent, double containerExtent);
class GridViewport extends RenderObjectWidget {
GridViewport({
Key key,
this.startOffset,
this.delegate,
this.onExtentsChanged,
this.children
});
final double startOffset;
final GridDelegate delegate;
final ExtentsChangedCallback onExtentsChanged;
final List<Widget> children;
RenderGrid createRenderObject() => new RenderGrid(delegate: delegate);
_GridViewportElement createElement() => new _GridViewportElement(this);
}
// TODO(abarth): This function should go somewhere more general.
int _lowerBound(List sortedList, var value, { int begin: 0 }) {
int current = begin;
int count = sortedList.length - current;
while (count > 0) {
int step = count >> 1;
int test = current + step;
if (sortedList[test] < value) {
current = test + 1;
count -= step + 1;
} else {
count = step;
}
}
return current;
}
class _GridViewportElement extends RenderObjectElement<GridViewport> {
_GridViewportElement(GridViewport widget) : super(widget);
double _contentExtent;
double _containerExtent;
int _materializedChildBase;
int _materializedChildCount;
List<Element> _materializedChildren = const <Element>[];
GridSpecification _specification;
double _repaintOffsetBase;
double _repaintOffsetLimit;
RenderGrid get renderObject => super.renderObject;
void visitChildren(ElementVisitor visitor) {
if (_materializedChildren == null)
return;
for (Element child in _materializedChildren)
visitor(child);
}
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
renderObject.callback = layout;
_updateRenderObject();
}
void unmount() {
renderObject.callback = null;
super.unmount();
}
void update(GridViewport newWidget) {
super.update(newWidget);
_updateRenderObject();
if (!renderObject.needsLayout)
_materializeChildren();
}
void _updatePaintOffset() {
renderObject.paintOffset = new Offset(0.0, -(widget.startOffset - _repaintOffsetBase));
}
void _updateRenderObject() {
renderObject.delegate = widget.delegate;
renderObject.virtualChildCount = widget.children.length;
if (_specification != null) {
_updatePaintOffset();
// If we don't already need layout, we need to request a layout if the
// viewport has shifted to expose a new row.
if (!renderObject.needsLayout) {
if (_repaintOffsetBase != null && widget.startOffset < _repaintOffsetBase)
renderObject.markNeedsLayout();
else if (_repaintOffsetLimit != null && widget.startOffset + _containerExtent > _repaintOffsetLimit)
renderObject.markNeedsLayout();
}
}
}
void _materializeChildren() {
assert(_materializedChildBase != null);
assert(_materializedChildCount != null);
List<Widget> newWidgets = new List<Widget>(_materializedChildCount);
for (int i = 0; i < _materializedChildCount; ++i) {
int childIndex = _materializedChildBase + i;
Widget child = widget.children[childIndex];
Key key = new ValueKey(child.key ?? childIndex);
newWidgets[i] = new RepaintBoundary(key: key, child: child);
}
_materializedChildren = updateChildren(_materializedChildren, newWidgets);
}
void layout(BoxConstraints constraints) {
_specification = renderObject.specification;
double contentExtent = _specification.gridSize.height;
double containerExtent = renderObject.size.height;
int materializedRowBase = math.max(0, _lowerBound(_specification.rowOffsets, widget.startOffset) - 1);
int materializedRowLimit = math.min(_specification.rowCount, _lowerBound(_specification.rowOffsets, widget.startOffset + containerExtent));
_materializedChildBase = materializedRowBase * _specification.columnCount;
_materializedChildCount = math.min(widget.children.length, materializedRowLimit * _specification.columnCount) - _materializedChildBase;
_repaintOffsetBase = _specification.rowOffsets[materializedRowBase];
_repaintOffsetLimit = _specification.rowOffsets[materializedRowLimit];
_updatePaintOffset();
BuildableElement.lockState(_materializeChildren);
if (contentExtent != _contentExtent || containerExtent != _containerExtent) {
_contentExtent = contentExtent;
_containerExtent = containerExtent;
widget.onExtentsChanged(_contentExtent, _containerExtent);
}
}
void insertChildRenderObject(RenderObject child, Element slot) {
RenderObject nextSibling = slot?.renderObject;
renderObject.add(child, before: nextSibling);
}
void moveChildRenderObject(RenderObject child, Element slot) {
assert(child.parent == renderObject);
RenderObject nextSibling = slot?.renderObject;
renderObject.move(child, before: nextSibling);
}
void removeChildRenderObject(RenderObject child) {
assert(child.parent == renderObject);
renderObject.remove(child);
}
}
...@@ -32,6 +32,7 @@ export 'src/widgets/pageable_list.dart'; ...@@ -32,6 +32,7 @@ export 'src/widgets/pageable_list.dart';
export 'src/widgets/placeholder.dart'; export 'src/widgets/placeholder.dart';
export 'src/widgets/routes.dart'; export 'src/widgets/routes.dart';
export 'src/widgets/scrollable.dart'; export 'src/widgets/scrollable.dart';
export 'src/widgets/scrollable_grid.dart';
export 'src/widgets/statistics_overlay.dart'; export 'src/widgets/statistics_overlay.dart';
export 'src/widgets/status_transitions.dart'; export 'src/widgets/status_transitions.dart';
export 'src/widgets/title.dart'; export 'src/widgets/title.dart';
......
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