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 {
items.map((AdaptiveItem item) => item.toListItem()).toList()
);
} else {
return new Block(
<Widget>[
new MaxTileWidthGrid(
items.map((AdaptiveItem item) => item.toCard()).toList(),
maxTileWidth: _maxTileWidth
)
]
return new ScrollableGrid(
children: items.map((AdaptiveItem item) => item.toCard()).toList(),
delegate: new MaxTileWidthGridDelegate(maxTileWidth: _maxTileWidth)
);
}
}
......
......@@ -261,7 +261,7 @@ class RenderBlockViewport extends RenderBlockBase {
bool _inCallback = false;
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
/// example so the child list contains only visible children.
......@@ -396,7 +396,9 @@ class RenderBlockViewport extends RenderBlockBase {
// scroll the RenderBlockViewport, it would shift in its parent if
// the parent was baseline-aligned, which makes no sense.
// TODO(abarth): debugDoesLayoutWithCallback appears to be unreferenced.
bool get debugDoesLayoutWithCallback => true;
void performLayout() {
if (_callback != null) {
try {
......
......@@ -2,9 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:typed_data';
import 'box.dart';
import 'object.dart';
import 'package:vector_math/vector_math_64.dart';
bool _debugIsMonotonic(List<double> offsets) {
bool result = true;
assert(() {
......@@ -23,7 +27,7 @@ bool _debugIsMonotonic(List<double> offsets) {
List<double> _generateRegularOffsets(int count, double size) {
int length = count + 1;
List<double> result = new List<double>(length);
List<double> result = new Float64List(length);
for (int i = 0; i < length; ++i)
result[i] = i * size;
return result;
......@@ -87,6 +91,12 @@ class GridSpecification {
/// The size of the grid.
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.
......@@ -308,8 +318,16 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr
RenderBoxContainerDefaultsMixin<RenderBox, GridParentData> {
RenderGrid({
List<RenderBox> children,
GridDelegate delegate
}) : _delegate = delegate {
GridDelegate 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);
addAll(children);
}
......@@ -321,11 +339,70 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr
assert(newDelegate != null);
if (_delegate == newDelegate)
return;
if (newDelegate.runtimeType != _delegate.runtimeType || newDelegate.shouldRelayout(_delegate))
if (newDelegate.runtimeType != _delegate.runtimeType || newDelegate.shouldRelayout(_delegate)) {
_specification = null;
markNeedsLayout();
}
_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) {
if (child.parentData is! GridParentData)
child.parentData = new GridParentData();
......@@ -333,46 +410,63 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr
double getMinIntrinsicWidth(BoxConstraints constraints) {
assert(constraints.isNormalized);
return _delegate.getMinIntrinsicWidth(constraints, childCount);
return _delegate.getMinIntrinsicWidth(constraints, virtualChildCount);
}
double getMaxIntrinsicWidth(BoxConstraints constraints) {
assert(constraints.isNormalized);
return _delegate.getMaxIntrinsicWidth(constraints, childCount);
return _delegate.getMaxIntrinsicWidth(constraints, virtualChildCount);
}
double getMinIntrinsicHeight(BoxConstraints constraints) {
assert(constraints.isNormalized);
return _delegate.getMinIntrinsicHeight(constraints, childCount);
return _delegate.getMinIntrinsicHeight(constraints, virtualChildCount);
}
double getMaxIntrinsicHeight(BoxConstraints constraints) {
assert(constraints.isNormalized);
return _delegate.getMaxIntrinsicHeight(constraints, childCount);
return _delegate.getMaxIntrinsicHeight(constraints, virtualChildCount);
}
double computeDistanceToActualBaseline(TextBaseline baseline) {
return defaultComputeDistanceToHighestActualBaseline(baseline);
}
GridSpecification get specification => _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;
void performLayout() {
_specification = delegate.getGridSpecification(constraints, childCount);
_updateGridSpecification();
Size gridSize = _specification.gridSize;
size = constraints.constrain(gridSize);
if (gridSize.width > size.width || gridSize.height > size.height)
_hasVisualOverflow = true;
if (_callback != null)
invokeLayoutCallback(_callback);
double gridTopPadding = _specification.padding.top;
double gridLeftPadding = _specification.padding.left;
int index = 0;
int childIndex = virtualChildBase;
RenderBox child = firstChild;
while (child != null) {
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.row >= 0);
assert(placement.column + placement.columnSpan < _specification.columnOffsets.length);
......@@ -398,21 +492,29 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr
tileTop + placement.padding.top
);
++index;
++childIndex;
assert(child.parentData == childParentData);
child = childParentData.nextSibling;
}
}
void applyPaintTransform(RenderBox child, Matrix4 transform) {
super.applyPaintTransform(child, transform.translate(paintOffset));
}
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) {
if (_hasVisualOverflow)
context.pushClipRect(needsCompositing, offset, Point.origin & size, defaultPaint);
context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintContents);
else
defaultPaint(context, offset);
_paintContents(context, offset);
}
}
......@@ -29,6 +29,7 @@ export 'package:flutter/rendering.dart' show
FlexAlignItems,
FlexDirection,
FlexJustifyContent,
FixedColumnCountGridDelegate,
FontStyle,
FontWeight,
FractionalOffset,
......@@ -40,6 +41,7 @@ export 'package:flutter/rendering.dart' show
InputEvent,
LinearGradient,
Matrix4,
MaxTileWidthGridDelegate,
Offset,
OneChildLayoutDelegate,
Paint,
......
......@@ -79,6 +79,7 @@ abstract class _ViewportBaseElement<T extends _ViewportBase> extends RenderObjec
void update(T newWidget) {
bool needLayout = newWidget.isLayoutDifferentThan(widget);
super.update(newWidget);
// TODO(abarth): Don't we need to update overlayPainter here?
if (needLayout)
renderObject.markNeedsLayout();
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';
export 'src/widgets/placeholder.dart';
export 'src/widgets/routes.dart';
export 'src/widgets/scrollable.dart';
export 'src/widgets/scrollable_grid.dart';
export 'src/widgets/statistics_overlay.dart';
export 'src/widgets/status_transitions.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