From 37106ea603d47566f62033096c3d8ade472f91a1 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Mon, 4 Jan 2016 17:33:15 -0800 Subject: [PATCH 1/2] Complete features of ScrollableList2 This patch implements the remaining missing features of ScrollableList2. It should now be nearly a drop-in replacement for ScrollableList. The next patch will switch callers over to the new machinery. --- .../lib/src/material/scrollbar_painter.dart | 37 +++--- packages/flutter/lib/src/rendering/block.dart | 10 +- packages/flutter/lib/src/rendering/list.dart | 123 +++++++++++++++--- .../flutter/lib/src/rendering/viewport.dart | 39 +++++- .../flutter/lib/src/widgets/scrollable.dart | 17 ++- .../lib/src/widgets/scrollable_grid.dart | 3 + .../lib/src/widgets/scrollable_list.dart | 89 ++++++++++--- .../lib/src/widgets/virtual_viewport.dart | 24 +++- 8 files changed, 272 insertions(+), 70 deletions(-) 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..431acad188 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 RenderScrollable { 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..a857e3c64d 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 RenderScrollable { 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..d5240dae3d 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 RenderScrollable { + 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 RenderScrollable { 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 { + return (renderObject as RenderScrollable)?.scrollDirection; + } - Size get viewportSize => renderer.size; + Size get viewportSize => renderObject.size; double get contentExtent => _contentExtent; double _contentExtent = 0.0; @@ -463,7 +466,7 @@ abstract class ScrollableListPainter extends Painter { if (_contentExtent == value) return; _contentExtent = value; - renderer?.markNeedsPaint(); + renderObject?.markNeedsPaint(); } double get scrollOffset => _scrollOffset; @@ -473,7 +476,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 +678,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..93efa9e54d 100644 --- a/packages/flutter/lib/src/widgets/scrollable_grid.dart +++ b/packages/flutter/lib/src/widgets/scrollable_grid.dart @@ -79,6 +79,9 @@ class GridViewport extends VirtualViewport { final ExtentsChangedCallback onExtentsChanged; final List children; + // TODO(abarth): Support horizontal scrolling; + ScrollDirection get scrollDirection => ScrollDirection.vertical; + RenderGrid createRenderObject() => new RenderGrid(delegate: delegate); _GridViewportElement createElement() => new _GridViewportElement(this); diff --git a/packages/flutter/lib/src/widgets/scrollable_list.dart b/packages/flutter/lib/src/widgets/scrollable_list.dart index 753b46323d..6b4b30c7dc 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,22 +15,30 @@ 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 bool itemsWrap; + final EdgeDims padding; + final ScrollableListPainter scrollableListPainter; final List 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,15 +91,26 @@ 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 double startOffset; + final ScrollDirection scrollDirection; + final double itemExtent; + final bool itemsWrap; + final EdgeDims padding; + final Painter overlayPainter; final List 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; + 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) { + int length = widget.children.length; + _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..ed13bd0a00 100644 --- a/packages/flutter/lib/src/widgets/virtual_viewport.dart +++ b/packages/flutter/lib/src/widgets/virtual_viewport.dart @@ -11,6 +11,7 @@ typedef void ExtentsChangedCallback(double contentExtent, double containerExtent abstract class VirtualViewport extends RenderObjectWidget { double get startOffset; + ScrollDirection get scrollDirection; List get children; } @@ -52,8 +53,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 +83,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(); } } @@ -88,7 +104,7 @@ abstract class VirtualViewportElement extends RenderO List newWidgets = new List(count); for (int i = 0; i < count; ++i) { int childIndex = base + i; - Widget child = widget.children[childIndex]; + Widget child = widget.children[childIndex % widget.children.length]; Key key = new ValueKey(child.key ?? childIndex); newWidgets[i] = new RepaintBoundary(key: key, child: child); } From b1f9138f5233af80c07578bbb423220a34f5a9a0 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Mon, 4 Jan 2016 21:52:10 -0800 Subject: [PATCH 2/2] Switch clients of ScrollableList to ScrollableList2 This patch also changed ScrollableList2 to use an Iterable instead of an List for its children. This change lets clients map their underlying data lazily. If the clients actually have a concrete list, we skip the extra copy and grab the child list directly. --- examples/fitness/lib/feed.dart | 5 ++- examples/stocks/lib/stock_list.dart | 7 ++-- examples/widgets/card_collection.dart | 7 ++-- examples/widgets/media_query.dart | 4 +-- examples/widgets/scrollbar.dart | 28 ++++++++++------ .../lib/src/material/material_list.dart | 29 ++++++----------- packages/flutter/lib/src/rendering/block.dart | 2 +- packages/flutter/lib/src/rendering/list.dart | 2 +- .../flutter/lib/src/rendering/viewport.dart | 4 +-- .../flutter/lib/src/widgets/scrollable.dart | 5 +-- .../lib/src/widgets/scrollable_grid.dart | 8 ++--- .../lib/src/widgets/scrollable_list.dart | 8 ++--- .../lib/src/widgets/virtual_viewport.dart | 32 +++++++++++++++++-- .../flutter/test/widget/dismissable_test.dart | 11 ++++--- .../test/widget/reparent_state_test.dart | 11 +++---- .../scrollable_list_hit_testing_test.dart | 24 ++++++-------- .../scrollable_list_horizontal_test.dart | 12 +++---- .../widget/scrollable_list_vertical_test.dart | 12 +++---- .../test/widget/snap_scrolling_test.dart | 10 +++--- 19 files changed, 118 insertions(+), 103 deletions(-) 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/rendering/block.dart b/packages/flutter/lib/src/rendering/block.dart index 431acad188..259465d45c 100644 --- a/packages/flutter/lib/src/rendering/block.dart +++ b/packages/flutter/lib/src/rendering/block.dart @@ -36,7 +36,7 @@ typedef double _Constrainer(double value); abstract class RenderBlockBase extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin - implements RenderScrollable { + implements HasScrollDirection { RenderBlockBase({ List children, diff --git a/packages/flutter/lib/src/rendering/list.dart b/packages/flutter/lib/src/rendering/list.dart index a857e3c64d..b94284f222 100644 --- a/packages/flutter/lib/src/rendering/list.dart +++ b/packages/flutter/lib/src/rendering/list.dart @@ -11,7 +11,7 @@ import 'viewport.dart'; /// Parent data for use with [RenderList]. class ListParentData extends ContainerBoxParentDataMixin { } -class RenderList extends RenderVirtualViewport implements RenderScrollable { +class RenderList extends RenderVirtualViewport implements HasScrollDirection { RenderList({ List children, double itemExtent, diff --git a/packages/flutter/lib/src/rendering/viewport.dart b/packages/flutter/lib/src/rendering/viewport.dart index d5240dae3d..245107569f 100644 --- a/packages/flutter/lib/src/rendering/viewport.dart +++ b/packages/flutter/lib/src/rendering/viewport.dart @@ -18,7 +18,7 @@ enum ScrollDirection { vertical, } -abstract class RenderScrollable { +abstract class HasScrollDirection { ScrollDirection get scrollDirection; } @@ -32,7 +32,7 @@ abstract class RenderScrollable { /// Viewport is the core scrolling primitive in the system, but it can be used /// in other situations. class RenderViewport extends RenderBox with RenderObjectWithChildMixin - implements RenderScrollable { + implements HasScrollDirection { RenderViewport({ RenderBox child, diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index 5914e09be0..460248ed17 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -446,14 +446,15 @@ class Block extends StatelessComponent { abstract class ScrollableListPainter extends Painter { void attach(RenderObject renderObject) { assert(renderObject is RenderBox); - assert(renderObject is RenderScrollable); + assert(renderObject is HasScrollDirection); super.attach(renderObject); } RenderBox get renderObject => super.renderObject; ScrollDirection get scrollDirection { - return (renderObject as RenderScrollable)?.scrollDirection; + HasScrollDirection scrollable = renderObject as dynamic; + return scrollable?.scrollDirection; } Size get viewportSize => renderObject.size; diff --git a/packages/flutter/lib/src/widgets/scrollable_grid.dart b/packages/flutter/lib/src/widgets/scrollable_grid.dart index 93efa9e54d..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,7 @@ 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; @@ -139,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 6b4b30c7dc..e3a54a688f 100644 --- a/packages/flutter/lib/src/widgets/scrollable_list.dart +++ b/packages/flutter/lib/src/widgets/scrollable_list.dart @@ -39,7 +39,7 @@ class ScrollableList2 extends Scrollable { final bool itemsWrap; final EdgeDims padding; final ScrollableListPainter scrollableListPainter; - final List children; + final Iterable children; ScrollableState createState() => new _ScrollableList2State(); } @@ -111,7 +111,7 @@ class ListViewport extends VirtualViewport { final bool itemsWrap; final EdgeDims padding; final Painter overlayPainter; - final List children; + final Iterable children; RenderList createRenderObject() => new RenderList(itemExtent: itemExtent); @@ -156,14 +156,14 @@ class _ListViewportElement extends VirtualViewportElement { } void layout(BoxConstraints constraints) { - double contentExtent = widget.itemExtent * widget.children.length; + 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) { - int length = widget.children.length; _materializedChildBase = math.min(length, _materializedChildBase); materializedChildLimit = math.min(length, materializedChildLimit); } diff --git a/packages/flutter/lib/src/widgets/virtual_viewport.dart b/packages/flutter/lib/src/widgets/virtual_viewport.dart index ed13bd0a00..18ccbe5bc9 100644 --- a/packages/flutter/lib/src/widgets/virtual_viewport.dart +++ b/packages/flutter/lib/src/widgets/virtual_viewport.dart @@ -12,7 +12,7 @@ typedef void ExtentsChangedCallback(double contentExtent, double containerExtent abstract class VirtualViewport extends RenderObjectWidget { double get startOffset; ScrollDirection get scrollDirection; - List get children; + Iterable get children; } abstract class VirtualViewportElement extends RenderObjectElement { @@ -36,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(); } @@ -46,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) @@ -96,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.children.length]; + 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) ) ) );