Commit 888dc770 authored by Adam Barth's avatar Adam Barth

Merge pull request #1063 from abarth/scrollable_grid

Add support for scrollable grids
parents 35b18c3d 6106fa9d
...@@ -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