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