Commit c4faad11 authored by Adam Barth's avatar Adam Barth

Merge pull request #1074 from abarth/scrollable_list_features

Complete features of ScrollableList2
parents f72c8f6d b1f9138f
...@@ -14,11 +14,10 @@ class FitnessItemList extends StatelessComponent { ...@@ -14,11 +14,10 @@ class FitnessItemList extends StatelessComponent {
final FitnessItemHandler onDismissed; final FitnessItemHandler onDismissed;
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new ScrollableList<FitnessItem>( return new ScrollableList2(
padding: const EdgeDims.all(4.0), padding: const EdgeDims.all(4.0),
items: items,
itemExtent: kFitnessItemHeight, itemExtent: kFitnessItemHeight,
itemBuilder: (BuildContext context, FitnessItem item, int index) => item.toRow(onDismissed: onDismissed) children: items.map((FitnessItem item) => item.toRow(onDismissed: onDismissed))
); );
} }
} }
......
...@@ -14,10 +14,9 @@ class StockList extends StatelessComponent { ...@@ -14,10 +14,9 @@ class StockList extends StatelessComponent {
final StockRowActionCallback onAction; final StockRowActionCallback onAction;
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new ScrollableList<Stock>( return new ScrollableList2(
items: stocks,
itemExtent: StockRow.kHeight, itemExtent: StockRow.kHeight,
itemBuilder: (BuildContext context, Stock stock, int index) { children: stocks.map((Stock stock) {
return new StockRow( return new StockRow(
keySalt: keySalt, keySalt: keySalt,
stock: stock, stock: stock,
...@@ -25,7 +24,7 @@ class StockList extends StatelessComponent { ...@@ -25,7 +24,7 @@ class StockList extends StatelessComponent {
onDoubleTap: onShow, onDoubleTap: onShow,
onLongPressed: onAction onLongPressed: onAction
); );
} })
); );
} }
} }
...@@ -393,12 +393,11 @@ class CardCollectionState extends State<CardCollection> { ...@@ -393,12 +393,11 @@ class CardCollectionState extends State<CardCollection> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget cardCollection; Widget cardCollection;
if (_fixedSizeCards) { if (_fixedSizeCards) {
cardCollection = new ScrollableList<CardModel> ( cardCollection = new ScrollableList2 (
snapOffsetCallback: _snapToCenter ? _toSnapOffset : null, snapOffsetCallback: _snapToCenter ? _toSnapOffset : null,
snapAlignmentOffset: _cardCollectionSize.height / 2.0, snapAlignmentOffset: _cardCollectionSize.height / 2.0,
items: _cardModels, itemExtent: _cardModels[0].height,
itemBuilder: (BuildContext context, CardModel card, int index) => _buildCard(context, card.value), children: _cardModels.map((CardModel card) => _buildCard(context, card.value))
itemExtent: _cardModels[0].height
); );
} else { } else {
cardCollection = new ScrollableMixedWidgetList( cardCollection = new ScrollableMixedWidgetList(
......
...@@ -74,12 +74,12 @@ class MediaQueryExample extends StatelessComponent { ...@@ -74,12 +74,12 @@ class MediaQueryExample extends StatelessComponent {
if (MediaQuery.of(context).size.width < _gridViewBreakpoint) { if (MediaQuery.of(context).size.width < _gridViewBreakpoint) {
return new ScrollableList2( return new ScrollableList2(
itemExtent: 50.0, itemExtent: 50.0,
children: items.map((AdaptiveItem item) => item.toListItem()).toList() children: items.map((AdaptiveItem item) => item.toListItem())
); );
} else { } else {
return new ScrollableGrid( return new ScrollableGrid(
delegate: new MaxTileWidthGridDelegate(maxTileWidth: _maxTileWidth), delegate: new MaxTileWidthGridDelegate(maxTileWidth: _maxTileWidth),
children: items.map((AdaptiveItem item) => item.toCard()).toList() children: items.map((AdaptiveItem item) => item.toCard())
); );
} }
} }
......
...@@ -5,6 +5,21 @@ ...@@ -5,6 +5,21 @@
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
final NumberFormat _dd = new NumberFormat("00", "en_US");
class _Item extends StatelessComponent {
_Item(this.index);
int index;
Widget build(BuildContext context) {
return new Text('Item ${_dd.format(index)}',
key: new ValueKey<int>(index),
style: Theme.of(context).text.title
);
}
}
class ScrollbarApp extends StatefulComponent { class ScrollbarApp extends StatefulComponent {
ScrollbarAppState createState() => new ScrollbarAppState(); ScrollbarAppState createState() => new ScrollbarAppState();
} }
...@@ -15,17 +30,10 @@ class ScrollbarAppState extends State<ScrollbarApp> { ...@@ -15,17 +30,10 @@ class ScrollbarAppState extends State<ScrollbarApp> {
final ScrollbarPainter _scrollbarPainter = new ScrollbarPainter(); final ScrollbarPainter _scrollbarPainter = new ScrollbarPainter();
Widget _buildMenu(BuildContext context) { Widget _buildMenu(BuildContext context) {
NumberFormat dd = new NumberFormat("00", "en_US"); return new ScrollableList2(
return new ScrollableList<int>(
items: new List<int>.generate(_itemCount, (int i) => i),
itemExtent: _itemExtent, itemExtent: _itemExtent,
itemBuilder: (_, __, int index) { scrollableListPainter: _scrollbarPainter,
return new Text('Item ${dd.format(index)}', children: new List<Widget>.generate(_itemCount, (int i) => new _Item(i))
key: new ValueKey<int>(index),
style: Theme.of(context).text.title
);
},
scrollableListPainter: _scrollbarPainter
); );
} }
......
...@@ -21,44 +21,35 @@ Map<MaterialListType, double> _kItemExtent = const <MaterialListType, double>{ ...@@ -21,44 +21,35 @@ Map<MaterialListType, double> _kItemExtent = const <MaterialListType, double>{
MaterialListType.threeLine: kThreeLineListItemHeight, MaterialListType.threeLine: kThreeLineListItemHeight,
}; };
class MaterialList<T> extends StatefulComponent { class MaterialList extends StatefulComponent {
MaterialList({ MaterialList({
Key key, Key key,
this.initialScrollOffset, this.initialScrollOffset,
this.onScroll, this.onScroll,
this.items, this.type: MaterialListType.twoLine,
this.itemBuilder, this.children
this.type: MaterialListType.twoLine
}) : super(key: key); }) : super(key: key);
final double initialScrollOffset; final double initialScrollOffset;
final ScrollListener onScroll; final ScrollListener onScroll;
final List<T> items;
final ItemBuilder<T> itemBuilder;
final MaterialListType type; final MaterialListType type;
final Iterable<Widget> children;
_MaterialListState<T> createState() => new _MaterialListState<T>(); _MaterialListState createState() => new _MaterialListState();
} }
class _MaterialListState<T> extends State<MaterialList<T>> { class _MaterialListState extends State<MaterialList> {
ScrollbarPainter _scrollbarPainter = new ScrollbarPainter();
void initState() {
super.initState();
_scrollbarPainter = new ScrollbarPainter();
}
ScrollbarPainter _scrollbarPainter;
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new ScrollableList<T>( return new ScrollableList2(
initialScrollOffset: config.initialScrollOffset, initialScrollOffset: config.initialScrollOffset,
scrollDirection: ScrollDirection.vertical, scrollDirection: ScrollDirection.vertical,
onScroll: config.onScroll, onScroll: config.onScroll,
items: config.items,
itemBuilder: config.itemBuilder,
itemExtent: _kItemExtent[config.type], itemExtent: _kItemExtent[config.type],
padding: const EdgeDims.symmetric(vertical: 8.0), padding: const EdgeDims.symmetric(vertical: 8.0),
scrollableListPainter: _scrollbarPainter scrollableListPainter: _scrollbarPainter,
children: config.children
); );
} }
} }
...@@ -30,22 +30,25 @@ class ScrollbarPainter extends ScrollableListPainter { ...@@ -30,22 +30,25 @@ class ScrollbarPainter extends ScrollableListPainter {
Point thumbOrigin; Point thumbOrigin;
Size thumbSize; Size thumbSize;
if (isVertical) { switch (scrollDirection) {
double thumbHeight = viewportBounds.height * viewportBounds.height / contentExtent; case ScrollDirection.vertical:
thumbHeight = thumbHeight.clamp(_kMinScrollbarThumbLength, viewportBounds.height); double thumbHeight = viewportBounds.height * viewportBounds.height / contentExtent;
final double maxThumbTop = viewportBounds.height - thumbHeight; thumbHeight = thumbHeight.clamp(_kMinScrollbarThumbLength, viewportBounds.height);
double thumbTop = (scrollOffset / (contentExtent - viewportBounds.height)) * maxThumbTop; final double maxThumbTop = viewportBounds.height - thumbHeight;
thumbTop = viewportBounds.top + thumbTop.clamp(0.0, maxThumbTop); double thumbTop = (scrollOffset / (contentExtent - viewportBounds.height)) * maxThumbTop;
thumbOrigin = new Point(viewportBounds.right - _kScrollbarThumbGirth, thumbTop); thumbTop = viewportBounds.top + thumbTop.clamp(0.0, maxThumbTop);
thumbSize = new Size(_kScrollbarThumbGirth, thumbHeight); thumbOrigin = new Point(viewportBounds.right - _kScrollbarThumbGirth, thumbTop);
} else { thumbSize = new Size(_kScrollbarThumbGirth, thumbHeight);
double thumbWidth = viewportBounds.width * viewportBounds.width / contentExtent; break;
thumbWidth = thumbWidth.clamp(_kMinScrollbarThumbLength, viewportBounds.width); case ScrollDirection.horizontal:
final double maxThumbLeft = viewportBounds.width - thumbWidth; double thumbWidth = viewportBounds.width * viewportBounds.width / contentExtent;
double thumbLeft = (scrollOffset / (contentExtent - viewportBounds.width)) * maxThumbLeft; thumbWidth = thumbWidth.clamp(_kMinScrollbarThumbLength, viewportBounds.width);
thumbLeft = viewportBounds.left + thumbLeft.clamp(0.0, maxThumbLeft); final double maxThumbLeft = viewportBounds.width - thumbWidth;
thumbOrigin = new Point(thumbLeft, viewportBounds.height - _kScrollbarThumbGirth); double thumbLeft = (scrollOffset / (contentExtent - viewportBounds.width)) * maxThumbLeft;
thumbSize = new Size(thumbWidth, _kScrollbarThumbGirth); thumbLeft = viewportBounds.left + thumbLeft.clamp(0.0, maxThumbLeft);
thumbOrigin = new Point(thumbLeft, viewportBounds.height - _kScrollbarThumbGirth);
thumbSize = new Size(thumbWidth, _kScrollbarThumbGirth);
break;
} }
paintThumb(context, thumbOrigin & thumbSize); paintThumb(context, thumbOrigin & thumbSize);
...@@ -65,7 +68,7 @@ class ScrollbarPainter extends ScrollableListPainter { ...@@ -65,7 +68,7 @@ class ScrollbarPainter extends ScrollableListPainter {
..variable = new AnimatedValue<double>(0.0, end: 1.0, curve: Curves.ease) ..variable = new AnimatedValue<double>(0.0, end: 1.0, curve: Curves.ease)
..addListener(() { ..addListener(() {
_opacity = _fade.value; _opacity = _fade.value;
renderer?.markNeedsPaint(); renderObject?.markNeedsPaint();
}); });
return _fade.forward(); return _fade.forward();
} }
......
...@@ -8,6 +8,7 @@ import 'package:vector_math/vector_math_64.dart'; ...@@ -8,6 +8,7 @@ import 'package:vector_math/vector_math_64.dart';
import 'box.dart'; import 'box.dart';
import 'object.dart'; import 'object.dart';
import 'viewport.dart';
/// Parent data for use with [RenderBlockBase]. /// Parent data for use with [RenderBlockBase].
class BlockParentData extends ContainerBoxParentDataMixin<RenderBox> { } class BlockParentData extends ContainerBoxParentDataMixin<RenderBox> { }
...@@ -32,8 +33,10 @@ typedef double _Constrainer(double value); ...@@ -32,8 +33,10 @@ typedef double _Constrainer(double value);
/// children. Because blocks expand in the main axis, blocks must be given /// children. Because blocks expand in the main axis, blocks must be given
/// unlimited space in the main axis, typically by being contained in a /// unlimited space in the main axis, typically by being contained in a
/// viewport with a scrolling direction that matches the block's main axis. /// viewport with a scrolling direction that matches the block's main axis.
abstract class RenderBlockBase extends RenderBox with ContainerRenderObjectMixin<RenderBox, BlockParentData>, abstract class RenderBlockBase extends RenderBox
RenderBoxContainerDefaultsMixin<RenderBox, BlockParentData> { with ContainerRenderObjectMixin<RenderBox, BlockParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, BlockParentData>
implements HasScrollDirection {
RenderBlockBase({ RenderBlockBase({
List<RenderBox> children, List<RenderBox> children,
...@@ -82,6 +85,9 @@ abstract class RenderBlockBase extends RenderBox with ContainerRenderObjectMixin ...@@ -82,6 +85,9 @@ abstract class RenderBlockBase extends RenderBox with ContainerRenderObjectMixin
/// Whether the main axis is vertical. /// Whether the main axis is vertical.
bool get isVertical => _direction == BlockDirection.vertical; bool get isVertical => _direction == BlockDirection.vertical;
// TODO(abarth): Remove BlockDirection in favor of ScrollDirection.
ScrollDirection get scrollDirection => isVertical ? ScrollDirection.vertical : ScrollDirection.horizontal;
BoxConstraints _getInnerConstraints(BoxConstraints constraints) { BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
if (isVertical) if (isVertical)
return new BoxConstraints.tightFor(width: constraints.constrainWidth(constraints.maxWidth), return new BoxConstraints.tightFor(width: constraints.constrainWidth(constraints.maxWidth),
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// 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:math' as math;
import 'box.dart'; import 'box.dart';
import 'object.dart'; import 'object.dart';
import 'viewport.dart'; import 'viewport.dart';
...@@ -9,18 +11,23 @@ import 'viewport.dart'; ...@@ -9,18 +11,23 @@ import 'viewport.dart';
/// Parent data for use with [RenderList]. /// Parent data for use with [RenderList].
class ListParentData extends ContainerBoxParentDataMixin<RenderBox> { } class ListParentData extends ContainerBoxParentDataMixin<RenderBox> { }
class RenderList extends RenderVirtualViewport<ListParentData> { class RenderList extends RenderVirtualViewport<ListParentData> implements HasScrollDirection {
RenderList({ RenderList({
List<RenderBox> children, List<RenderBox> children,
double itemExtent, double itemExtent,
EdgeDims padding,
int virtualChildCount, int virtualChildCount,
Offset paintOffset: Offset.zero, Offset paintOffset: Offset.zero,
ScrollDirection scrollDirection: ScrollDirection.vertical,
LayoutCallback callback LayoutCallback callback
}) : _itemExtent = itemExtent, super( }) : _itemExtent = itemExtent,
virtualChildCount: virtualChildCount, _padding = padding,
paintOffset: paintOffset, _scrollDirection = scrollDirection,
callback: callback super(
) { virtualChildCount: virtualChildCount,
paintOffset: paintOffset,
callback: callback
) {
assert(itemExtent != null); assert(itemExtent != null);
addAll(children); addAll(children);
} }
...@@ -35,50 +42,124 @@ class RenderList extends RenderVirtualViewport<ListParentData> { ...@@ -35,50 +42,124 @@ class RenderList extends RenderVirtualViewport<ListParentData> {
markNeedsLayout(); markNeedsLayout();
} }
EdgeDims get padding => _padding;
EdgeDims _padding;
void set padding (EdgeDims newValue) {
if (_padding == newValue)
return;
_padding = newValue;
markNeedsLayout();
}
ScrollDirection get scrollDirection => _scrollDirection;
ScrollDirection _scrollDirection;
void set scrollDirection (ScrollDirection newValue) {
if (_scrollDirection == newValue)
return;
_scrollDirection = newValue;
markNeedsLayout();
}
void setupParentData(RenderBox child) { void setupParentData(RenderBox child) {
if (child.parentData is! ListParentData) if (child.parentData is! ListParentData)
child.parentData = new ListParentData(); child.parentData = new ListParentData();
} }
double get _preferredMainAxisExtent => itemExtent * virtualChildCount; double get _scrollAxisPadding {
switch (scrollDirection) {
case ScrollDirection.vertical:
return padding.vertical;
case ScrollDirection.horizontal:
return padding.horizontal;
}
}
double getMinIntrinsicWidth(BoxConstraints constraints) { double get _preferredExtent {
double extent = itemExtent * virtualChildCount;
if (padding != null)
extent += _scrollAxisPadding;
return extent;
}
double _getIntrinsicWidth(BoxConstraints constraints) {
assert(constraints.isNormalized); assert(constraints.isNormalized);
return constraints.constrainWidth(0.0); switch (scrollDirection) {
case ScrollDirection.vertical:
return constraints.constrainWidth(0.0);
case ScrollDirection.horizontal:
return constraints.constrainWidth(_preferredExtent);
}
}
double getMinIntrinsicWidth(BoxConstraints constraints) {
return _getIntrinsicWidth(constraints);
} }
double getMaxIntrinsicWidth(BoxConstraints constraints) { double getMaxIntrinsicWidth(BoxConstraints constraints) {
return _getIntrinsicWidth(constraints);
}
double _getIntrinsicHeight(BoxConstraints constraints) {
assert(constraints.isNormalized); assert(constraints.isNormalized);
return constraints.constrainWidth(0.0); switch (scrollDirection) {
case ScrollDirection.vertical:
return constraints.constrainHeight(_preferredExtent);
case ScrollDirection.horizontal:
return constraints.constrainHeight(0.0);
}
} }
double getMinIntrinsicHeight(BoxConstraints constraints) { double getMinIntrinsicHeight(BoxConstraints constraints) {
assert(constraints.isNormalized); return _getIntrinsicHeight(constraints);
return constraints.constrainHeight(_preferredMainAxisExtent);
} }
double getMaxIntrinsicHeight(BoxConstraints constraints) { double getMaxIntrinsicHeight(BoxConstraints constraints) {
assert(constraints.isNormalized); return _getIntrinsicHeight(constraints);
return constraints.constrainHeight(_preferredMainAxisExtent);
} }
void performLayout() { void performLayout() {
double height = _preferredMainAxisExtent; size = new Size(constraints.maxWidth,
size = new Size(constraints.maxWidth, constraints.constrainHeight(height)); constraints.constrainHeight(_preferredExtent));
if (callback != null) if (callback != null)
invokeLayoutCallback(callback); invokeLayoutCallback(callback);
double itemWidth;
double itemHeight;
double x = 0.0;
double dx = 0.0;
double y = 0.0;
double dy = 0.0;
switch (scrollDirection) {
case ScrollDirection.vertical:
itemWidth = math.max(0, size.width - (padding == null ? 0.0 : padding.horizontal));
itemHeight = itemExtent;
y = padding != null ? padding.top : 0.0;
dy = itemExtent;
break;
case ScrollDirection.horizontal:
itemWidth = itemExtent;
itemHeight = math.max(0, size.height - (padding == null ? 0.0 : padding.vertical));
x = padding != null ? padding.left : 0.0;
dx = itemExtent;
break;
}
BoxConstraints innerConstraints = BoxConstraints innerConstraints =
new BoxConstraints.tightFor(width: size.width, height: itemExtent); new BoxConstraints.tightFor(width: itemWidth, height: itemHeight);
int childIndex = 0;
RenderBox child = firstChild; RenderBox child = firstChild;
while (child != null) { while (child != null) {
child.layout(innerConstraints); child.layout(innerConstraints);
final ListParentData childParentData = child.parentData; final ListParentData childParentData = child.parentData;
childParentData.offset = new Offset(0.0, childIndex * itemExtent); childParentData.offset = new Offset(x, y);
childIndex += 1; x += dx;
y += dy;
assert(child.parentData == childParentData); assert(child.parentData == childParentData);
child = childParentData.nextSibling; child = childParentData.nextSibling;
} }
......
...@@ -18,6 +18,10 @@ enum ScrollDirection { ...@@ -18,6 +18,10 @@ enum ScrollDirection {
vertical, vertical,
} }
abstract class HasScrollDirection {
ScrollDirection get scrollDirection;
}
/// A render object that's bigger on the inside. /// A render object that's bigger on the inside.
/// ///
/// The child of a viewport can layout to a larger size than the viewport /// The child of a viewport can layout to a larger size than the viewport
...@@ -27,7 +31,8 @@ enum ScrollDirection { ...@@ -27,7 +31,8 @@ enum ScrollDirection {
/// ///
/// Viewport is the core scrolling primitive in the system, but it can be used /// Viewport is the core scrolling primitive in the system, but it can be used
/// in other situations. /// in other situations.
class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox> { class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox>
implements HasScrollDirection {
RenderViewport({ RenderViewport({
RenderBox child, RenderBox child,
...@@ -177,10 +182,12 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende ...@@ -177,10 +182,12 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
RenderVirtualViewport({ RenderVirtualViewport({
int virtualChildCount, int virtualChildCount,
Offset paintOffset, Offset paintOffset,
LayoutCallback callback LayoutCallback callback,
Painter overlayPainter
}) : _virtualChildCount = virtualChildCount, }) : _virtualChildCount = virtualChildCount,
_paintOffset = paintOffset, _paintOffset = paintOffset,
_callback = callback; _callback = callback,
_overlayPainter = overlayPainter;
int get virtualChildCount => _virtualChildCount ?? childCount; int get virtualChildCount => _virtualChildCount ?? childCount;
int _virtualChildCount; int _virtualChildCount;
...@@ -217,8 +224,31 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende ...@@ -217,8 +224,31 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
markNeedsLayout(); markNeedsLayout();
} }
Painter get overlayPainter => _overlayPainter;
Painter _overlayPainter;
void set overlayPainter(Painter value) {
if (_overlayPainter == value)
return;
if (attached)
_overlayPainter?.detach();
_overlayPainter = value;
if (attached)
_overlayPainter?.attach(this);
markNeedsPaint();
}
void attach() {
super.attach();
_overlayPainter?.attach(this);
}
void detach() {
super.detach();
_overlayPainter?.detach();
}
void applyPaintTransform(RenderBox child, Matrix4 transform) { void applyPaintTransform(RenderBox child, Matrix4 transform) {
super.applyPaintTransform(child, transform.translate(paintOffset)); super.applyPaintTransform(child, transform.translate(paintOffset.dx, paintOffset.dy));
} }
bool hitTestChildren(HitTestResult result, { Point position }) { bool hitTestChildren(HitTestResult result, { Point position }) {
...@@ -227,6 +257,7 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende ...@@ -227,6 +257,7 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
void _paintContents(PaintingContext context, Offset offset) { void _paintContents(PaintingContext context, Offset offset) {
defaultPaint(context, offset + paintOffset); defaultPaint(context, offset + paintOffset);
_overlayPainter?.paint(context, offset);
} }
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
......
...@@ -445,15 +445,19 @@ class Block extends StatelessComponent { ...@@ -445,15 +445,19 @@ class Block extends StatelessComponent {
abstract class ScrollableListPainter extends Painter { abstract class ScrollableListPainter extends Painter {
void attach(RenderObject renderObject) { void attach(RenderObject renderObject) {
assert(renderObject is RenderBlockViewport); assert(renderObject is RenderBox);
assert(renderObject is HasScrollDirection);
super.attach(renderObject); super.attach(renderObject);
} }
RenderBlockViewport get renderer => renderObject; RenderBox get renderObject => super.renderObject;
bool get isVertical => renderer.isVertical; ScrollDirection get scrollDirection {
HasScrollDirection scrollable = renderObject as dynamic;
return scrollable?.scrollDirection;
}
Size get viewportSize => renderer.size; Size get viewportSize => renderObject.size;
double get contentExtent => _contentExtent; double get contentExtent => _contentExtent;
double _contentExtent = 0.0; double _contentExtent = 0.0;
...@@ -463,7 +467,7 @@ abstract class ScrollableListPainter extends Painter { ...@@ -463,7 +467,7 @@ abstract class ScrollableListPainter extends Painter {
if (_contentExtent == value) if (_contentExtent == value)
return; return;
_contentExtent = value; _contentExtent = value;
renderer?.markNeedsPaint(); renderObject?.markNeedsPaint();
} }
double get scrollOffset => _scrollOffset; double get scrollOffset => _scrollOffset;
...@@ -473,7 +477,7 @@ abstract class ScrollableListPainter extends Painter { ...@@ -473,7 +477,7 @@ abstract class ScrollableListPainter extends Painter {
if (_scrollOffset == value) if (_scrollOffset == value)
return; return;
_scrollOffset = value; _scrollOffset = value;
renderer?.markNeedsPaint(); renderObject?.markNeedsPaint();
} }
/// Called when a scroll starts. Subclasses may override this method to /// Called when a scroll starts. Subclasses may override this method to
...@@ -675,7 +679,7 @@ class ScrollableList<T> extends ScrollableWidgetList { ...@@ -675,7 +679,7 @@ class ScrollableList<T> extends ScrollableWidgetList {
double snapAlignmentOffset: 0.0, double snapAlignmentOffset: 0.0,
this.items, this.items,
this.itemBuilder, this.itemBuilder,
itemsWrap: false, bool itemsWrap: false,
double itemExtent, double itemExtent,
EdgeDims padding, EdgeDims padding,
ScrollableListPainter scrollableListPainter ScrollableListPainter scrollableListPainter
......
...@@ -36,7 +36,7 @@ class ScrollableGrid extends Scrollable { ...@@ -36,7 +36,7 @@ class ScrollableGrid extends Scrollable {
); );
final GridDelegate delegate; final GridDelegate delegate;
final List<Widget> children; final Iterable<Widget> children;
ScrollableState createState() => new _ScrollableGridState(); ScrollableState createState() => new _ScrollableGridState();
} }
...@@ -77,7 +77,10 @@ class GridViewport extends VirtualViewport { ...@@ -77,7 +77,10 @@ class GridViewport extends VirtualViewport {
final double startOffset; final double startOffset;
final GridDelegate delegate; final GridDelegate delegate;
final ExtentsChangedCallback onExtentsChanged; final ExtentsChangedCallback onExtentsChanged;
final List<Widget> children; final Iterable<Widget> children;
// TODO(abarth): Support horizontal scrolling;
ScrollDirection get scrollDirection => ScrollDirection.vertical;
RenderGrid createRenderObject() => new RenderGrid(delegate: delegate); RenderGrid createRenderObject() => new RenderGrid(delegate: delegate);
...@@ -136,8 +139,8 @@ class _GridViewportElement extends VirtualViewportElement<GridViewport> { ...@@ -136,8 +139,8 @@ class _GridViewportElement extends VirtualViewportElement<GridViewport> {
int materializedRowBase = math.max(0, _lowerBound(_specification.rowOffsets, widget.startOffset) - 1); int materializedRowBase = math.max(0, _lowerBound(_specification.rowOffsets, widget.startOffset) - 1);
int materializedRowLimit = math.min(_specification.rowCount, _lowerBound(_specification.rowOffsets, widget.startOffset + containerExtent)); int materializedRowLimit = math.min(_specification.rowCount, _lowerBound(_specification.rowOffsets, widget.startOffset + containerExtent));
_materializedChildBase = (materializedRowBase * _specification.columnCount).clamp(0, widget.children.length); _materializedChildBase = (materializedRowBase * _specification.columnCount).clamp(0, renderObject.virtualChildCount);
_materializedChildCount = (materializedRowLimit * _specification.columnCount).clamp(0, widget.children.length) - _materializedChildBase; _materializedChildCount = (materializedRowLimit * _specification.columnCount).clamp(0, renderObject.virtualChildCount) - _materializedChildBase;
_repaintOffsetBase = _specification.rowOffsets[materializedRowBase]; _repaintOffsetBase = _specification.rowOffsets[materializedRowBase];
_repaintOffsetLimit = _specification.rowOffsets[materializedRowLimit]; _repaintOffsetLimit = _specification.rowOffsets[materializedRowLimit];
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// 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:math' as math;
import 'framework.dart'; import 'framework.dart';
import 'scrollable.dart'; import 'scrollable.dart';
import 'virtual_viewport.dart'; import 'virtual_viewport.dart';
...@@ -13,23 +15,31 @@ class ScrollableList2 extends Scrollable { ...@@ -13,23 +15,31 @@ class ScrollableList2 extends Scrollable {
ScrollableList2({ ScrollableList2({
Key key, Key key,
double initialScrollOffset, double initialScrollOffset,
ScrollDirection scrollDirection: ScrollDirection.vertical,
ScrollListener onScroll, ScrollListener onScroll,
SnapOffsetCallback snapOffsetCallback, SnapOffsetCallback snapOffsetCallback,
double snapAlignmentOffset: 0.0, double snapAlignmentOffset: 0.0,
this.itemExtent, this.itemExtent,
this.itemsWrap: false,
this.padding,
this.scrollableListPainter,
this.children this.children
}) : super( }) : super(
key: key, key: key,
initialScrollOffset: initialScrollOffset, initialScrollOffset: initialScrollOffset,
// TODO(abarth): Support horizontal offsets. scrollDirection: scrollDirection,
scrollDirection: ScrollDirection.vertical,
onScroll: onScroll, onScroll: onScroll,
snapOffsetCallback: snapOffsetCallback, snapOffsetCallback: snapOffsetCallback,
snapAlignmentOffset: snapAlignmentOffset snapAlignmentOffset: snapAlignmentOffset
); ) {
assert(itemExtent != null);
}
final double itemExtent; final double itemExtent;
final List<Widget> children; final bool itemsWrap;
final EdgeDims padding;
final ScrollableListPainter scrollableListPainter;
final Iterable<Widget> children;
ScrollableState createState() => new _ScrollableList2State(); ScrollableState createState() => new _ScrollableList2State();
} }
...@@ -39,20 +49,40 @@ class _ScrollableList2State extends ScrollableState<ScrollableList2> { ...@@ -39,20 +49,40 @@ class _ScrollableList2State extends ScrollableState<ScrollableList2> {
ExtentScrollBehavior get scrollBehavior => super.scrollBehavior; ExtentScrollBehavior get scrollBehavior => super.scrollBehavior;
void _handleExtentsChanged(double contentExtent, double containerExtent) { void _handleExtentsChanged(double contentExtent, double containerExtent) {
config.scrollableListPainter?.contentExtent = contentExtent;
setState(() { setState(() {
scrollTo(scrollBehavior.updateExtents( scrollTo(scrollBehavior.updateExtents(
contentExtent: contentExtent, contentExtent: config.itemsWrap ? double.INFINITY : contentExtent,
containerExtent: containerExtent, containerExtent: containerExtent,
scrollOffset: scrollOffset scrollOffset: scrollOffset
)); ));
}); });
} }
void dispatchOnScrollStart() {
super.dispatchOnScrollStart();
config.scrollableListPainter?.scrollStarted();
}
void dispatchOnScroll() {
super.dispatchOnScroll();
config.scrollableListPainter?.scrollOffset = scrollOffset;
}
void dispatchOnScrollEnd() {
super.dispatchOnScrollEnd();
config.scrollableListPainter?.scrollEnded();
}
Widget buildContent(BuildContext context) { Widget buildContent(BuildContext context) {
return new ListViewport( return new ListViewport(
onExtentsChanged: _handleExtentsChanged,
startOffset: scrollOffset, startOffset: scrollOffset,
scrollDirection: config.scrollDirection,
itemExtent: config.itemExtent, itemExtent: config.itemExtent,
onExtentsChanged: _handleExtentsChanged, itemsWrap: config.itemsWrap,
padding: config.padding,
overlayPainter: config.scrollableListPainter,
children: config.children children: config.children
); );
} }
...@@ -61,16 +91,27 @@ class _ScrollableList2State extends ScrollableState<ScrollableList2> { ...@@ -61,16 +91,27 @@ class _ScrollableList2State extends ScrollableState<ScrollableList2> {
class ListViewport extends VirtualViewport { class ListViewport extends VirtualViewport {
ListViewport({ ListViewport({
Key key, Key key,
this.startOffset,
this.itemExtent,
this.onExtentsChanged, this.onExtentsChanged,
this.startOffset: 0.0,
this.scrollDirection: ScrollDirection.vertical,
this.itemExtent,
this.itemsWrap: false,
this.padding,
this.overlayPainter,
this.children this.children
}); }) {
assert(scrollDirection != null);
assert(itemExtent != null);
}
final ExtentsChangedCallback onExtentsChanged;
final double startOffset; final double startOffset;
final ScrollDirection scrollDirection;
final double itemExtent; final double itemExtent;
final ExtentsChangedCallback onExtentsChanged; final bool itemsWrap;
final List<Widget> children; final EdgeDims padding;
final Painter overlayPainter;
final Iterable<Widget> children;
RenderList createRenderObject() => new RenderList(itemExtent: itemExtent); RenderList createRenderObject() => new RenderList(itemExtent: itemExtent);
...@@ -95,21 +136,39 @@ class _ListViewportElement extends VirtualViewportElement<ListViewport> { ...@@ -95,21 +136,39 @@ class _ListViewportElement extends VirtualViewportElement<ListViewport> {
double _repaintOffsetLimit; double _repaintOffsetLimit;
void updateRenderObject() { void updateRenderObject() {
renderObject.scrollDirection = widget.scrollDirection;
renderObject.itemExtent = widget.itemExtent; renderObject.itemExtent = widget.itemExtent;
renderObject.padding = widget.padding;
renderObject.overlayPainter = widget.overlayPainter;
super.updateRenderObject(); super.updateRenderObject();
} }
double _contentExtent; double _contentExtent;
double _containerExtent; double _containerExtent;
double _getContainerExtentFromRenderObject() {
switch (widget.scrollDirection) {
case ScrollDirection.vertical:
return renderObject.size.height;
case ScrollDirection.horizontal:
return renderObject.size.width;
}
}
void layout(BoxConstraints constraints) { void layout(BoxConstraints constraints) {
double contentExtent = widget.itemExtent * widget.children.length; int length = renderObject.virtualChildCount;
double containerExtent = renderObject.size.height; double contentExtent = widget.itemExtent * length;
double containerExtent = _getContainerExtentFromRenderObject();
_materializedChildBase = (widget.startOffset ~/ widget.itemExtent).clamp(0, widget.children.length); _materializedChildBase = math.max(0, widget.startOffset ~/ widget.itemExtent);
int materializedChildLimit = ((widget.startOffset + containerExtent) / widget.itemExtent).ceil().clamp(0, widget.children.length); int materializedChildLimit = math.max(0, ((widget.startOffset + containerExtent) / widget.itemExtent).ceil());
_materializedChildCount = materializedChildLimit - _materializedChildBase;
if (!widget.itemsWrap) {
_materializedChildBase = math.min(length, _materializedChildBase);
materializedChildLimit = math.min(length, materializedChildLimit);
}
_materializedChildCount = materializedChildLimit - _materializedChildBase;
_repaintOffsetBase = _materializedChildBase * widget.itemExtent; _repaintOffsetBase = _materializedChildBase * widget.itemExtent;
_repaintOffsetLimit = materializedChildLimit * widget.itemExtent; _repaintOffsetLimit = materializedChildLimit * widget.itemExtent;
......
...@@ -11,7 +11,8 @@ typedef void ExtentsChangedCallback(double contentExtent, double containerExtent ...@@ -11,7 +11,8 @@ typedef void ExtentsChangedCallback(double contentExtent, double containerExtent
abstract class VirtualViewport extends RenderObjectWidget { abstract class VirtualViewport extends RenderObjectWidget {
double get startOffset; double get startOffset;
List<Widget> get children; ScrollDirection get scrollDirection;
Iterable<Widget> get children;
} }
abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderObjectElement<T> { abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderObjectElement<T> {
...@@ -35,6 +36,8 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO ...@@ -35,6 +36,8 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
void mount(Element parent, dynamic newSlot) { void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot); super.mount(parent, newSlot);
_iterator = null;
_widgets = <Widget>[];
renderObject.callback = layout; renderObject.callback = layout;
updateRenderObject(); updateRenderObject();
} }
...@@ -45,6 +48,10 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO ...@@ -45,6 +48,10 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
} }
void update(T newWidget) { void update(T newWidget) {
if (widget.children != newWidget.children) {
_iterator = null;
_widgets = <Widget>[];
}
super.update(newWidget); super.update(newWidget);
updateRenderObject(); updateRenderObject();
if (!renderObject.needsLayout) if (!renderObject.needsLayout)
...@@ -52,8 +59,23 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO ...@@ -52,8 +59,23 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
} }
void _updatePaintOffset() { void _updatePaintOffset() {
renderObject.paintOffset = switch (widget.scrollDirection) {
renderObject.paintOffset = new Offset(0.0, -(widget.startOffset - repaintOffsetBase)); case ScrollDirection.vertical:
renderObject.paintOffset = new Offset(0.0, -(widget.startOffset - repaintOffsetBase));
break;
case ScrollDirection.horizontal:
renderObject.paintOffset = new Offset(-(widget.startOffset - repaintOffsetBase), 0.0);
break;
}
}
double get _containerExtent {
switch (widget.scrollDirection) {
case ScrollDirection.vertical:
return renderObject.size.height;
case ScrollDirection.horizontal:
return renderObject.size.width;
}
} }
void updateRenderObject() { void updateRenderObject() {
...@@ -67,7 +89,7 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO ...@@ -67,7 +89,7 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
if (!renderObject.needsLayout) { if (!renderObject.needsLayout) {
if (repaintOffsetBase != null && widget.startOffset < repaintOffsetBase) if (repaintOffsetBase != null && widget.startOffset < repaintOffsetBase)
renderObject.markNeedsLayout(); renderObject.markNeedsLayout();
else if (repaintOffsetLimit != null && widget.startOffset + renderObject.size.height > repaintOffsetLimit) else if (repaintOffsetLimit != null && widget.startOffset + _containerExtent > repaintOffsetLimit)
renderObject.markNeedsLayout(); renderObject.markNeedsLayout();
} }
} }
...@@ -80,15 +102,37 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO ...@@ -80,15 +102,37 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
BuildableElement.lockState(_materializeChildren); BuildableElement.lockState(_materializeChildren);
} }
Iterator<Widget> _iterator;
List<Widget> _widgets;
void _populateWidgets(int limit) {
if (limit <= _widgets.length)
return;
if (widget.children is List<Widget>) {
_widgets = widget.children;
return;
}
_iterator ??= widget.children.iterator;
while (_widgets.length < limit) {
bool moved = _iterator.moveNext();
assert(moved);
Widget current = _iterator.current;
assert(current != null);
_widgets.add(current);
}
}
void _materializeChildren() { void _materializeChildren() {
int base = materializedChildBase; int base = materializedChildBase;
int count = materializedChildCount; int count = materializedChildCount;
int length = renderObject.virtualChildCount;
assert(base != null); assert(base != null);
assert(count != null); assert(count != null);
_populateWidgets(base + count);
List<Widget> newWidgets = new List<Widget>(count); List<Widget> newWidgets = new List<Widget>(count);
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
int childIndex = base + i; int childIndex = base + i;
Widget child = widget.children[childIndex]; Widget child = _widgets[childIndex % length];
Key key = new ValueKey(child.key ?? childIndex); Key key = new ValueKey(child.key ?? childIndex);
newWidgets[i] = new RepaintBoundary(key: key, child: child); newWidgets[i] = new RepaintBoundary(key: key, child: child);
} }
......
...@@ -21,7 +21,7 @@ void handleOnDismissed(int item) { ...@@ -21,7 +21,7 @@ void handleOnDismissed(int item) {
dismissedItems.add(item); dismissedItems.add(item);
} }
Widget buildDismissableItem(BuildContext context, int item, int index) { Widget buildDismissableItem(int item) {
return new Dismissable( return new Dismissable(
key: new ValueKey<int>(item), key: new ValueKey<int>(item),
direction: dismissDirection, direction: dismissDirection,
...@@ -38,11 +38,12 @@ Widget buildDismissableItem(BuildContext context, int item, int index) { ...@@ -38,11 +38,12 @@ Widget buildDismissableItem(BuildContext context, int item, int index) {
Widget widgetBuilder() { Widget widgetBuilder() {
return new Container( return new Container(
padding: const EdgeDims.all(10.0), padding: const EdgeDims.all(10.0),
child: new ScrollableList<int>( child: new ScrollableList2(
items: <int>[0, 1, 2, 3, 4].where((int i) => !dismissedItems.contains(i)).toList(),
itemBuilder: buildDismissableItem,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
itemExtent: itemExtent itemExtent: itemExtent,
children: <int>[0, 1, 2, 3, 4].where(
(int i) => !dismissedItems.contains(i)
).map(buildDismissableItem)
) )
); );
} }
......
...@@ -99,16 +99,15 @@ void main() { ...@@ -99,16 +99,15 @@ void main() {
(key.currentState as StateMarkerState).marker = "marked"; (key.currentState as StateMarkerState).marker = "marked";
tester.pumpWidget(new ScrollableList<int>( tester.pumpWidget(new ScrollableList2(
items: <int>[0],
itemExtent: 100.0, itemExtent: 100.0,
itemBuilder: (BuildContext context, int item, int index) { children: <Widget>[
return new Container( new Container(
key: new Key('container'), key: new Key('container'),
height: 100.0, height: 100.0,
child: new StateMarker(key: key) child: new StateMarker(key: key)
); )
} ]
)); ));
expect((key.currentState as StateMarkerState).marker, equals("marked")); expect((key.currentState as StateMarkerState).marker, equals("marked"));
......
...@@ -17,20 +17,18 @@ void main() { ...@@ -17,20 +17,18 @@ void main() {
tester.pumpWidget(new Center( tester.pumpWidget(new Center(
child: new Container( child: new Container(
height: 50.0, height: 50.0,
child: new ScrollableList<int>( child: new ScrollableList2(
key: new GlobalKey(), key: new GlobalKey(),
items: items, itemExtent: 290.0,
itemBuilder: (BuildContext context, int item, int index) { scrollDirection: ScrollDirection.horizontal,
children: items.map((int item) {
return new Container( return new Container(
key: new ValueKey<int>(item),
child: new GestureDetector( child: new GestureDetector(
onTap: () { tapped.add(item); }, onTap: () { tapped.add(item); },
child: new Text('$item') child: new Text('$item')
) )
); );
}, })
itemExtent: 290.0,
scrollDirection: ScrollDirection.horizontal
) )
) )
)); ));
...@@ -59,20 +57,18 @@ void main() { ...@@ -59,20 +57,18 @@ void main() {
tester.pumpWidget(new Center( tester.pumpWidget(new Center(
child: new Container( child: new Container(
width: 50.0, width: 50.0,
child: new ScrollableList<int>( child: new ScrollableList2(
key: new GlobalKey(), key: new GlobalKey(),
items: items, itemExtent: 290.0,
itemBuilder: (BuildContext context, int item, int index) { scrollDirection: ScrollDirection.vertical,
children: items.map((int item) {
return new Container( return new Container(
key: new ValueKey<int>(item),
child: new GestureDetector( child: new GestureDetector(
onTap: () { tapped.add(item); }, onTap: () { tapped.add(item); },
child: new Text('$item') child: new Text('$item')
) )
); );
}, })
itemExtent: 290.0,
scrollDirection: ScrollDirection.vertical
) )
) )
)); ));
......
...@@ -13,16 +13,14 @@ Widget buildFrame() { ...@@ -13,16 +13,14 @@ Widget buildFrame() {
return new Center( return new Center(
child: new Container( child: new Container(
height: 50.0, height: 50.0,
child: new ScrollableList<int>( child: new ScrollableList2(
items: items, itemExtent: 290.0,
itemBuilder: (BuildContext context, int item, int index) { scrollDirection: ScrollDirection.horizontal,
children: items.map((int item) {
return new Container( return new Container(
key: new ValueKey<int>(item),
child: new Text('$item') child: new Text('$item')
); );
}, })
itemExtent: 290.0,
scrollDirection: ScrollDirection.horizontal
) )
) )
); );
......
...@@ -9,16 +9,14 @@ import 'package:test/test.dart'; ...@@ -9,16 +9,14 @@ import 'package:test/test.dart';
const List<int> items = const <int>[0, 1, 2, 3, 4, 5]; const List<int> items = const <int>[0, 1, 2, 3, 4, 5];
Widget buildFrame() { Widget buildFrame() {
return new ScrollableList<int>( return new ScrollableList2(
items: items, itemExtent: 290.0,
itemBuilder: (BuildContext context, int item, int index) { scrollDirection: ScrollDirection.vertical,
children: items.map((int item) {
return new Container( return new Container(
key: new ValueKey<int>(item),
child: new Text('$item') child: new Text('$item')
); );
}, })
itemExtent: 290.0,
scrollDirection: ScrollDirection.vertical
); );
} }
......
...@@ -12,9 +12,8 @@ const double itemExtent = 200.0; ...@@ -12,9 +12,8 @@ const double itemExtent = 200.0;
ScrollDirection scrollDirection = ScrollDirection.vertical; ScrollDirection scrollDirection = ScrollDirection.vertical;
GlobalKey scrollableListKey; GlobalKey scrollableListKey;
Widget buildItem(BuildContext context, int item, int index) { Widget buildItem(int item) {
return new Container( return new Container(
key: new ValueKey<int>(item),
width: itemExtent, width: itemExtent,
height: itemExtent, height: itemExtent,
child: new Text(item.toString()) child: new Text(item.toString())
...@@ -30,13 +29,12 @@ Widget buildFrame() { ...@@ -30,13 +29,12 @@ Widget buildFrame() {
return new Center( return new Center(
child: new Container( child: new Container(
height: itemExtent * 2.0, height: itemExtent * 2.0,
child: new ScrollableList<int>( child: new ScrollableList2(
key: scrollableListKey, key: scrollableListKey,
snapOffsetCallback: snapOffsetCallback, snapOffsetCallback: snapOffsetCallback,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
items: <int>[0, 1, 2, 3, 4, 5, 7, 8, 9], itemExtent: itemExtent,
itemBuilder: buildItem, children: <int>[0, 1, 2, 3, 4, 5, 7, 8, 9].map(buildItem)
itemExtent: itemExtent
) )
) )
); );
......
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