Commit 24fadf49 authored by Adam Barth's avatar Adam Barth

Merge pull request #1070 from abarth/scrollable_list2

Introduce ScrollableList2
parents c8ac09a5 99bca282
......@@ -72,13 +72,14 @@ class MediaQueryExample extends StatelessComponent {
items.add(new AdaptiveItem("Item $i"));
if (MediaQuery.of(context).size.width < _gridViewBreakpoint) {
return new Block(
items.map((AdaptiveItem item) => item.toListItem()).toList()
return new ScrollableList2(
itemExtent: 50.0,
children: items.map((AdaptiveItem item) => item.toListItem()).toList()
);
} else {
return new ScrollableGrid(
children: items.map((AdaptiveItem item) => item.toCard()).toList(),
delegate: new MaxTileWidthGridDelegate(maxTileWidth: _maxTileWidth)
delegate: new MaxTileWidthGridDelegate(maxTileWidth: _maxTileWidth),
children: items.map((AdaptiveItem item) => item.toCard()).toList()
);
}
}
......
......@@ -18,6 +18,7 @@ export 'src/rendering/flex.dart';
export 'src/rendering/grid.dart';
export 'src/rendering/image.dart';
export 'src/rendering/layer.dart';
export 'src/rendering/list.dart';
export 'src/rendering/node.dart';
export 'src/rendering/object.dart';
export 'src/rendering/overflow.dart';
......
......@@ -6,8 +6,7 @@ import 'dart:typed_data';
import 'box.dart';
import 'object.dart';
import 'package:vector_math/vector_math_64.dart';
import 'viewport.dart';
bool _debugIsMonotonic(List<double> offsets) {
bool result = true;
......@@ -314,8 +313,7 @@ class GridParentData extends ContainerBoxParentDataMixin<RenderBox> {
///
/// Additionally, grid layout materializes all of its children, which makes it
/// most useful for grids containing a moderate number of tiles.
class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, GridParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, GridParentData> {
class RenderGrid extends RenderVirtualViewport<GridParentData> {
RenderGrid({
List<RenderBox> children,
GridDelegate delegate,
......@@ -323,11 +321,11 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr
int virtualChildCount,
Offset paintOffset: Offset.zero,
LayoutCallback callback
}) : _delegate = delegate,
_virtualChildBase = virtualChildBase,
_virtualChildCount = virtualChildCount,
_paintOffset = paintOffset,
_callback = callback {
}) : _delegate = delegate, _virtualChildBase = virtualChildBase, super(
virtualChildCount: virtualChildCount,
paintOffset: paintOffset,
callback: callback
) {
assert(delegate != null);
addAll(children);
}
......@@ -360,49 +358,6 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr
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();
......@@ -447,17 +402,13 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr
}
}
bool _hasVisualOverflow = false;
void performLayout() {
_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);
if (callback != null)
invokeLayoutCallback(callback);
double gridTopPadding = _specification.padding.top;
double gridLeftPadding = _specification.padding.left;
......@@ -492,29 +443,10 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr
tileTop + placement.padding.top
);
++childIndex;
childIndex += 1;
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 + -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, _paintContents);
else
_paintContents(context, offset);
}
}
// 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 'box.dart';
import 'object.dart';
import 'viewport.dart';
/// Parent data for use with [RenderList].
class ListParentData extends ContainerBoxParentDataMixin<RenderBox> { }
class RenderList extends RenderVirtualViewport<ListParentData> {
RenderList({
List<RenderBox> children,
double itemExtent,
int virtualChildCount,
Offset paintOffset: Offset.zero,
LayoutCallback callback
}) : _itemExtent = itemExtent, super(
virtualChildCount: virtualChildCount,
paintOffset: paintOffset,
callback: callback
) {
assert(itemExtent != null);
addAll(children);
}
double get itemExtent => _itemExtent;
double _itemExtent;
void set itemExtent (double newValue) {
assert(newValue != null);
if (_itemExtent == newValue)
return;
_itemExtent = newValue;
markNeedsLayout();
}
void setupParentData(RenderBox child) {
if (child.parentData is! ListParentData)
child.parentData = new ListParentData();
}
double get _preferredMainAxisExtent => itemExtent * virtualChildCount;
double getMinIntrinsicWidth(BoxConstraints constraints) {
assert(constraints.isNormalized);
return constraints.constrainWidth(0.0);
}
double getMaxIntrinsicWidth(BoxConstraints constraints) {
assert(constraints.isNormalized);
return constraints.constrainWidth(0.0);
}
double getMinIntrinsicHeight(BoxConstraints constraints) {
assert(constraints.isNormalized);
return constraints.constrainHeight(_preferredMainAxisExtent);
}
double getMaxIntrinsicHeight(BoxConstraints constraints) {
assert(constraints.isNormalized);
return constraints.constrainHeight(_preferredMainAxisExtent);
}
void performLayout() {
double height = _preferredMainAxisExtent;
size = new Size(constraints.maxWidth, constraints.constrainHeight(height));
if (callback != null)
invokeLayoutCallback(callback);
BoxConstraints innerConstraints =
new BoxConstraints.tightFor(width: size.width, height: itemExtent);
int childIndex = 0;
RenderBox child = firstChild;
while (child != null) {
child.layout(innerConstraints);
final ListParentData childParentData = child.parentData;
childParentData.offset = new Offset(0.0, childIndex * itemExtent);
childIndex += 1;
assert(child.parentData == childParentData);
child = childParentData.nextSibling;
}
}
}
......@@ -178,3 +178,66 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox
return false;
}
}
abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<RenderBox>>
extends RenderBox with ContainerRenderObjectMixin<RenderBox, T>,
RenderBoxContainerDefaultsMixin<RenderBox, T> {
RenderVirtualViewport({
int virtualChildCount,
Offset paintOffset,
LayoutCallback callback
}) : _virtualChildCount = virtualChildCount,
_paintOffset = paintOffset,
_callback = callback;
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 item.
///
/// 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 applyPaintTransform(RenderBox child, Matrix4 transform) {
super.applyPaintTransform(child, transform.translate(paintOffset));
}
bool hitTestChildren(HitTestResult result, { Point position }) {
return defaultHitTestChildren(result, position: position + -paintOffset);
}
void _paintContents(PaintingContext context, Offset offset) {
defaultPaint(context, offset + paintOffset);
}
void paint(PaintingContext context, Offset offset) {
context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintContents);
}
}
......@@ -4,9 +4,9 @@
import 'dart:math' as math;
import 'basic.dart';
import 'framework.dart';
import 'scrollable.dart';
import 'virtual_viewport.dart';
import 'package:flutter/animation.dart';
import 'package:flutter/rendering.dart';
......@@ -38,11 +38,11 @@ class ScrollableGrid extends Scrollable {
final GridDelegate delegate;
final List<Widget> children;
ScrollableState createState() => new _ScrollableGrid();
ScrollableState createState() => new _ScrollableGridState();
}
class _ScrollableGrid extends ScrollableState<ScrollableGrid> {
ScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior();
class _ScrollableGridState extends ScrollableState<ScrollableGrid> {
ScrollBehavior createScrollBehavior() => new OverscrollBehavior();
ExtentScrollBehavior get scrollBehavior => super.scrollBehavior;
void _handleExtentsChanged(double contentExtent, double containerExtent) {
......@@ -65,9 +65,7 @@ class _ScrollableGrid extends ScrollableState<ScrollableGrid> {
}
}
typedef void ExtentsChangedCallback(double contentExtent, double containerExtent);
class GridViewport extends RenderObjectWidget {
class GridViewport extends VirtualViewport {
GridViewport({
Key key,
this.startOffset,
......@@ -87,6 +85,7 @@ class GridViewport extends RenderObjectWidget {
}
// TODO(abarth): This function should go somewhere more general.
// See https://github.com/dart-lang/collection/pull/16
int _lowerBound(List sortedList, var value, { int begin: 0 }) {
int current = begin;
int count = sortedList.length - current;
......@@ -103,82 +102,31 @@ int _lowerBound(List sortedList, var value, { int begin: 0 }) {
return current;
}
class _GridViewportElement extends RenderObjectElement<GridViewport> {
class _GridViewportElement extends VirtualViewportElement<GridViewport> {
_GridViewportElement(GridViewport widget) : super(widget);
double _contentExtent;
double _containerExtent;
RenderGrid get renderObject => super.renderObject;
int get materializedChildBase => _materializedChildBase;
int _materializedChildBase;
int _materializedChildCount;
List<Element> _materializedChildren = const <Element>[];
int get materializedChildCount => _materializedChildCount;
int _materializedChildCount;
GridSpecification _specification;
double get repaintOffsetBase => _repaintOffsetBase;
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));
}
double get repaintOffsetLimit =>_repaintOffsetLimit;
double _repaintOffsetLimit;
void _updateRenderObject() {
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();
}
}
super.updateRenderObject();
}
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);
}
double _contentExtent;
double _containerExtent;
GridSpecification _specification;
void layout(BoxConstraints constraints) {
_specification = renderObject.specification;
......@@ -188,13 +136,12 @@ class _GridViewportElement extends RenderObjectElement<GridViewport> {
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;
_materializedChildBase = (materializedRowBase * _specification.columnCount).clamp(0, widget.children.length);
_materializedChildCount = (materializedRowLimit * _specification.columnCount).clamp(0, widget.children.length) - _materializedChildBase;
_repaintOffsetBase = _specification.rowOffsets[materializedRowBase];
_repaintOffsetLimit = _specification.rowOffsets[materializedRowLimit];
_updatePaintOffset();
BuildableElement.lockState(_materializeChildren);
super.layout(constraints);
if (contentExtent != _contentExtent || containerExtent != _containerExtent) {
_contentExtent = contentExtent;
......@@ -202,20 +149,4 @@ class _GridViewportElement extends RenderObjectElement<GridViewport> {
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);
}
}
// 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 'framework.dart';
import 'scrollable.dart';
import 'virtual_viewport.dart';
import 'package:flutter/animation.dart';
import 'package:flutter/rendering.dart';
class ScrollableList2 extends Scrollable {
ScrollableList2({
Key key,
double initialScrollOffset,
ScrollListener onScroll,
SnapOffsetCallback snapOffsetCallback,
double snapAlignmentOffset: 0.0,
this.itemExtent,
this.children
}) : super(
key: key,
initialScrollOffset: initialScrollOffset,
// TODO(abarth): Support horizontal offsets.
scrollDirection: ScrollDirection.vertical,
onScroll: onScroll,
snapOffsetCallback: snapOffsetCallback,
snapAlignmentOffset: snapAlignmentOffset
);
final double itemExtent;
final List<Widget> children;
ScrollableState createState() => new _ScrollableList2State();
}
class _ScrollableList2State extends ScrollableState<ScrollableList2> {
ScrollBehavior createScrollBehavior() => new OverscrollBehavior();
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 ListViewport(
startOffset: scrollOffset,
itemExtent: config.itemExtent,
onExtentsChanged: _handleExtentsChanged,
children: config.children
);
}
}
class ListViewport extends VirtualViewport {
ListViewport({
Key key,
this.startOffset,
this.itemExtent,
this.onExtentsChanged,
this.children
});
final double startOffset;
final double itemExtent;
final ExtentsChangedCallback onExtentsChanged;
final List<Widget> children;
RenderList createRenderObject() => new RenderList(itemExtent: itemExtent);
_ListViewportElement createElement() => new _ListViewportElement(this);
}
class _ListViewportElement extends VirtualViewportElement<ListViewport> {
_ListViewportElement(ListViewport widget) : super(widget);
RenderList get renderObject => super.renderObject;
int get materializedChildBase => _materializedChildBase;
int _materializedChildBase;
int get materializedChildCount => _materializedChildCount;
int _materializedChildCount;
double get repaintOffsetBase => _repaintOffsetBase;
double _repaintOffsetBase;
double get repaintOffsetLimit =>_repaintOffsetLimit;
double _repaintOffsetLimit;
void updateRenderObject() {
renderObject.itemExtent = widget.itemExtent;
super.updateRenderObject();
}
double _contentExtent;
double _containerExtent;
void layout(BoxConstraints constraints) {
double contentExtent = widget.itemExtent * widget.children.length;
double containerExtent = renderObject.size.height;
_materializedChildBase = (widget.startOffset ~/ widget.itemExtent).clamp(0, widget.children.length);
int materializedChildLimit = ((widget.startOffset + containerExtent) / widget.itemExtent).ceil().clamp(0, widget.children.length);
_materializedChildCount = materializedChildLimit - _materializedChildBase;
_repaintOffsetBase = _materializedChildBase * widget.itemExtent;
_repaintOffsetLimit = materializedChildLimit * widget.itemExtent;
super.layout(constraints);
if (contentExtent != _contentExtent || containerExtent != _containerExtent) {
_contentExtent = contentExtent;
_containerExtent = containerExtent;
widget.onExtentsChanged(_contentExtent, _containerExtent);
}
}
}
// 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 'basic.dart';
import 'framework.dart';
import 'package:flutter/rendering.dart';
typedef void ExtentsChangedCallback(double contentExtent, double containerExtent);
abstract class VirtualViewport extends RenderObjectWidget {
double get startOffset;
List<Widget> get children;
}
abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderObjectElement<T> {
VirtualViewportElement(T widget) : super(widget);
int get materializedChildBase;
int get materializedChildCount;
double get repaintOffsetBase;
double get repaintOffsetLimit;
List<Element> _materializedChildren = const <Element>[];
RenderVirtualViewport 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(T newWidget) {
super.update(newWidget);
updateRenderObject();
if (!renderObject.needsLayout)
_materializeChildren();
}
void _updatePaintOffset() {
renderObject.paintOffset =
renderObject.paintOffset = new Offset(0.0, -(widget.startOffset - repaintOffsetBase));
}
void updateRenderObject() {
renderObject.virtualChildCount = widget.children.length;
if (repaintOffsetBase != null) {
_updatePaintOffset();
// If we don't already need layout, we need to request a layout if the
// viewport has shifted to expose new children.
if (!renderObject.needsLayout) {
if (repaintOffsetBase != null && widget.startOffset < repaintOffsetBase)
renderObject.markNeedsLayout();
else if (repaintOffsetLimit != null && widget.startOffset + renderObject.size.height > repaintOffsetLimit)
renderObject.markNeedsLayout();
}
}
}
void layout(BoxConstraints constraints) {
assert(repaintOffsetBase != null);
assert(repaintOffsetLimit != null);
_updatePaintOffset();
BuildableElement.lockState(_materializeChildren);
}
void _materializeChildren() {
int base = materializedChildBase;
int count = materializedChildCount;
assert(base != null);
assert(count != null);
List<Widget> newWidgets = new List<Widget>(count);
for (int i = 0; i < count; ++i) {
int childIndex = base + 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 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);
}
}
......@@ -33,10 +33,12 @@ export 'src/widgets/placeholder.dart';
export 'src/widgets/routes.dart';
export 'src/widgets/scrollable.dart';
export 'src/widgets/scrollable_grid.dart';
export 'src/widgets/scrollable_list.dart';
export 'src/widgets/statistics_overlay.dart';
export 'src/widgets/status_transitions.dart';
export 'src/widgets/title.dart';
export 'src/widgets/transitions.dart';
export 'src/widgets/unique_component.dart';
export 'src/widgets/virtual_viewport.dart';
export 'package:vector_math/vector_math_64.dart' show Matrix4;
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