From 5b8966942426924dde9c049b13f1524109eb88ed Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 5 Feb 2016 13:47:38 -0800 Subject: [PATCH] Remove HomogeneousViewport The virtual viewport machinery now handles all of these use cases. Previous clients of ScrollableWidgetList can use ScrollableLazyList instead. --- .../flutter/lib/src/material/date_picker.dart | 40 +++- packages/flutter/lib/src/rendering/grid.dart | 2 + packages/flutter/lib/src/rendering/list.dart | 5 +- .../flutter/lib/src/rendering/viewport.dart | 2 +- .../lib/src/widgets/homogeneous_viewport.dart | 220 ------------------ .../lib/src/widgets/pageable_list.dart | 3 +- .../flutter/lib/src/widgets/scrollable.dart | 173 -------------- .../lib/src/widgets/scrollable_grid.dart | 3 +- .../lib/src/widgets/scrollable_list.dart | 168 +++++++++++-- .../lib/src/widgets/virtual_viewport.dart | 129 +++++++--- packages/flutter/lib/widgets.dart | 1 - .../widget/remember_scroll_position_test.dart | 32 ++- ...st.dart => scrollable_lazy_list_test.dart} | 73 +++--- 13 files changed, 329 insertions(+), 522 deletions(-) delete mode 100644 packages/flutter/lib/src/widgets/homogeneous_viewport.dart rename packages/flutter/test/widget/{homogeneous_viewport_test.dart => scrollable_lazy_list_test.dart} (71%) diff --git a/packages/flutter/lib/src/material/date_picker.dart b/packages/flutter/lib/src/material/date_picker.dart index 7204f2b1ef..21d9aabf52 100644 --- a/packages/flutter/lib/src/material/date_picker.dart +++ b/packages/flutter/lib/src/material/date_picker.dart @@ -268,15 +268,15 @@ class DayPicker extends StatelessComponent { } } -// Scrollable list of DayPickers to allow choosing a month -class MonthPicker extends ScrollableWidgetList { +class MonthPicker extends StatefulComponent { MonthPicker({ + Key key, this.selectedDate, this.onChanged, this.firstDate, this.lastDate, - double itemExtent - }) : super(itemExtent: itemExtent) { + this.itemExtent + }) : super(key: key) { assert(selectedDate != null); assert(onChanged != null); assert(lastDate.isAfter(firstDate)); @@ -286,11 +286,12 @@ class MonthPicker extends ScrollableWidgetList { final ValueChanged onChanged; final DateTime firstDate; final DateTime lastDate; + final double itemExtent; _MonthPickerState createState() => new _MonthPickerState(); } -class _MonthPickerState extends ScrollableWidgetListState { +class _MonthPickerState extends State { void initState() { super.initState(); _updateCurrentDate(); @@ -313,8 +314,6 @@ class _MonthPickerState extends ScrollableWidgetListState { }); } - int get itemCount => (config.lastDate.year - config.firstDate.year) * 12 + config.lastDate.month - config.firstDate.month + 1; - List buildItems(BuildContext context, int start, int count) { List result = new List(); DateTime startDate = new DateTime(config.firstDate.year + start ~/ 12, config.firstDate.month + start % 12); @@ -335,6 +334,14 @@ class _MonthPickerState extends ScrollableWidgetListState { return result; } + Widget build(BuildContext context) { + return new ScrollableLazyList( + itemExtent: config.itemExtent, + itemCount: (config.lastDate.year - config.firstDate.year) * 12 + config.lastDate.month - config.firstDate.month + 1, + itemBuilder: buildItems + ); + } + void dispose() { if (_timer != null) _timer.cancel(); @@ -343,13 +350,14 @@ class _MonthPickerState extends ScrollableWidgetListState { } // Scrollable list of years to allow picking a year -class YearPicker extends ScrollableWidgetList { +class YearPicker extends StatefulComponent { YearPicker({ + Key key, this.selectedDate, this.onChanged, this.firstDate, this.lastDate - }) : super(itemExtent: 50.0) { + }) : super(key: key) { assert(selectedDate != null); assert(onChanged != null); assert(lastDate.isAfter(firstDate)); @@ -363,8 +371,8 @@ class YearPicker extends ScrollableWidgetList { _YearPickerState createState() => new _YearPickerState(); } -class _YearPickerState extends ScrollableWidgetListState { - int get itemCount => config.lastDate.year - config.firstDate.year + 1; +class _YearPickerState extends State { + static const double _itemExtent = 50.0; List buildItems(BuildContext context, int start, int count) { TextStyle style = Theme.of(context).text.body1.copyWith(color: Colors.black54); @@ -379,7 +387,7 @@ class _YearPickerState extends ScrollableWidgetListState { config.onChanged(result); }, child: new Container( - height: config.itemExtent, + height: _itemExtent, decoration: year == config.selectedDate.year ? new BoxDecoration( backgroundColor: Theme.of(context).primarySwatch[100], shape: BoxShape.circle @@ -393,4 +401,12 @@ class _YearPickerState extends ScrollableWidgetListState { } return items; } + + Widget build(BuildContext context) { + return new ScrollableLazyList( + itemExtent: _itemExtent, + itemCount: config.lastDate.year - config.firstDate.year + 1, + itemBuilder: buildItems + ); + } } diff --git a/packages/flutter/lib/src/rendering/grid.dart b/packages/flutter/lib/src/rendering/grid.dart index 45388658e1..792e416ef3 100644 --- a/packages/flutter/lib/src/rendering/grid.dart +++ b/packages/flutter/lib/src/rendering/grid.dart @@ -344,6 +344,8 @@ class RenderGrid extends RenderVirtualViewport { _delegate = newDelegate; } + int get virtualChildCount => super.virtualChildCount ?? childCount; + /// The virtual index of the first child. /// /// When asking the delegate for the position of each child, the grid will add diff --git a/packages/flutter/lib/src/rendering/list.dart b/packages/flutter/lib/src/rendering/list.dart index af9522b1b5..dcfcfce1d0 100644 --- a/packages/flutter/lib/src/rendering/list.dart +++ b/packages/flutter/lib/src/rendering/list.dart @@ -76,7 +76,10 @@ class RenderList extends RenderVirtualViewport implements HasScr double get _preferredExtent { if (itemExtent == null) return double.INFINITY; - double extent = itemExtent * virtualChildCount; + int count = virtualChildCount; + if (count == null) + return double.INFINITY; + double extent = itemExtent * count; if (padding != null) extent += _scrollAxisPadding; return extent; diff --git a/packages/flutter/lib/src/rendering/viewport.dart b/packages/flutter/lib/src/rendering/viewport.dart index 06392f7717..ae281b9357 100644 --- a/packages/flutter/lib/src/rendering/viewport.dart +++ b/packages/flutter/lib/src/rendering/viewport.dart @@ -192,7 +192,7 @@ abstract class RenderVirtualViewport _virtualChildCount ?? childCount; + int get virtualChildCount => _virtualChildCount; int _virtualChildCount; void set virtualChildCount(int value) { if (_virtualChildCount == value) diff --git a/packages/flutter/lib/src/widgets/homogeneous_viewport.dart b/packages/flutter/lib/src/widgets/homogeneous_viewport.dart deleted file mode 100644 index a5ef83a6cd..0000000000 --- a/packages/flutter/lib/src/widgets/homogeneous_viewport.dart +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright 2015 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:math' as math; - -import 'package:flutter/rendering.dart'; - -import 'framework.dart'; -import 'basic.dart'; - -typedef List ListBuilder(BuildContext context, int startIndex, int count); - -abstract class _ViewportBase extends RenderObjectWidget { - _ViewportBase({ - Key key, - this.builder, - this.itemsWrap: false, - this.itemCount, - this.direction: Axis.vertical, - this.startOffset: 0.0, - this.overlayPainter - }) : super(key: key); - - final ListBuilder builder; - final bool itemsWrap; - final int itemCount; - final Axis direction; - final double startOffset; - final Painter overlayPainter; - - // we don't pass constructor arguments to the RenderBlockViewport() because until - // we know our children, the constructor arguments we could give have no effect - RenderBlockViewport createRenderObject() => new RenderBlockViewport(); - - bool isLayoutDifferentThan(_ViewportBase oldWidget) { - // changing the builder doesn't imply the layout changed - return itemsWrap != oldWidget.itemsWrap || - itemCount != oldWidget.itemCount || - direction != oldWidget.direction || - startOffset != oldWidget.startOffset; - } -} - -abstract class _ViewportBaseElement extends RenderObjectElement { - _ViewportBaseElement(T widget) : super(widget); - - List _children = const []; - int _layoutFirstIndex; - int _layoutItemCount; - - RenderBlockViewport get renderObject => super.renderObject; - - void visitChildren(ElementVisitor visitor) { - if (_children == null) - return; - for (Element child in _children) - visitor(child); - } - - void mount(Element parent, dynamic newSlot) { - super.mount(parent, newSlot); - renderObject.callback = layout; - renderObject.totalExtentCallback = getTotalExtent; - renderObject.minCrossAxisExtentCallback = getMinCrossAxisExtent; - renderObject.maxCrossAxisExtentCallback = getMaxCrossAxisExtent; - renderObject.overlayPainter = widget.overlayPainter; - } - - void unmount() { - renderObject.callback = null; - renderObject.totalExtentCallback = null; - renderObject.minCrossAxisExtentCallback = null; - renderObject.maxCrossAxisExtentCallback = null; - renderObject.overlayPainter = null; - super.unmount(); - } - - void update(T newWidget) { - bool needLayout = newWidget.isLayoutDifferentThan(widget); - super.update(newWidget); - // TODO(abarth): Don't we need to update overlayPainter here? - if (needLayout) - renderObject.markNeedsLayout(); - else - _updateChildren(); - } - - void reinvokeBuilders() { - _updateChildren(); - } - - void layout(BoxConstraints constraints); - - void _updateChildren() { - assert(_layoutFirstIndex != null); - assert(_layoutItemCount != null); - List newWidgets; - if (_layoutItemCount > 0) - newWidgets = widget.builder(this, _layoutFirstIndex, _layoutItemCount).map((Widget widget) { - return new RepaintBoundary(key: new ValueKey(widget.key), child: widget); - }).toList(); - else - newWidgets = []; - _children = updateChildren(_children, newWidgets); - } - - double getTotalExtent(BoxConstraints constraints); - - double getMinCrossAxisExtent(BoxConstraints constraints) { - return 0.0; - } - - double getMaxCrossAxisExtent(BoxConstraints constraints) { - if (widget.direction == Axis.vertical) - return constraints.maxWidth; - return constraints.maxHeight; - } - - void insertChildRenderObject(RenderObject child, Element slot) { - renderObject.insert(child, after: slot?.renderObject); - } - - void moveChildRenderObject(RenderObject child, Element slot) { - assert(child.parent == renderObject); - renderObject.move(child, after: slot?.renderObject); - } - - void removeChildRenderObject(RenderObject child) { - assert(child.parent == renderObject); - renderObject.remove(child); - } - -} - -class HomogeneousViewport extends _ViewportBase { - HomogeneousViewport({ - Key key, - ListBuilder builder, - bool itemsWrap: false, - int itemCount, // optional, but you cannot shrink-wrap this class or otherwise use its intrinsic dimensions if you don't specify it - Axis direction: Axis.vertical, - double startOffset: 0.0, - Painter overlayPainter, - this.itemExtent // required, must be non-zero - }) : super( - key: key, - builder: builder, - itemsWrap: itemsWrap, - itemCount: itemCount, - direction: direction, - startOffset: startOffset, - overlayPainter: overlayPainter - ) { - assert(itemExtent != null); - assert(itemExtent > 0); - } - - final double itemExtent; - - _HomogeneousViewportElement createElement() => new _HomogeneousViewportElement(this); - - bool isLayoutDifferentThan(HomogeneousViewport oldWidget) { - return itemExtent != oldWidget.itemExtent || super.isLayoutDifferentThan(oldWidget); - } -} - -class _HomogeneousViewportElement extends _ViewportBaseElement { - _HomogeneousViewportElement(HomogeneousViewport widget) : super(widget); - - void layout(BoxConstraints constraints) { - // We enter a build scope (meaning that markNeedsBuild() is forbidden) - // because we are in the middle of layout and if we allowed people to set - // state, they'd expect to have that state reflected immediately, which, if - // we were to try to honour it, would potentially result in assertions - // because you can't normally mutate the render object tree during layout. - // (If there were a way to limit these writes to descendants of this, it'd - // be ok because we are exempt from that assert since we are still actively - // doing our own layout.) - BuildableElement.lockState(() { - double mainAxisExtent = widget.direction == Axis.vertical ? constraints.maxHeight : constraints.maxWidth; - double offset; - if (widget.startOffset <= 0.0 && !widget.itemsWrap) { - _layoutFirstIndex = 0; - offset = -widget.startOffset; - } else { - _layoutFirstIndex = (widget.startOffset / widget.itemExtent).floor(); - offset = -(widget.startOffset % widget.itemExtent); - } - if (mainAxisExtent < double.INFINITY) { - _layoutItemCount = ((mainAxisExtent - offset) / widget.itemExtent).ceil(); - if (widget.itemCount != null && !widget.itemsWrap) - _layoutItemCount = math.min(_layoutItemCount, widget.itemCount - _layoutFirstIndex); - } else { - assert(() { - 'This HomogeneousViewport has no specified number of items (meaning it has infinite items), ' + - 'and has been placed in an unconstrained environment where all items can be rendered. ' + - 'It is most likely that you have placed your HomogeneousViewport (which is an internal ' + - 'component of several scrollable widgets) inside either another scrolling box, a flexible ' + - 'box (Row, Column), or a Stack, without giving it a specific size.'; - return widget.itemCount != null; - }); - _layoutItemCount = widget.itemCount - _layoutFirstIndex; - } - _layoutItemCount = math.max(0, _layoutItemCount); - _updateChildren(); - // Update the renderObject configuration - renderObject.direction = widget.direction; - renderObject.itemExtent = widget.itemExtent; - renderObject.minExtent = getTotalExtent(null); - renderObject.startOffset = offset; - renderObject.overlayPainter = widget.overlayPainter; - }, building: true); - } - - double getTotalExtent(BoxConstraints constraints) { - // constraints is null when called by layout() above - return widget.itemCount != null ? widget.itemCount * widget.itemExtent : double.INFINITY; - } -} diff --git a/packages/flutter/lib/src/widgets/pageable_list.dart b/packages/flutter/lib/src/widgets/pageable_list.dart index cc0b8e684d..af657f4b94 100644 --- a/packages/flutter/lib/src/widgets/pageable_list.dart +++ b/packages/flutter/lib/src/widgets/pageable_list.dart @@ -180,9 +180,8 @@ class PageableListState extends ScrollableState { } } -class PageViewport extends VirtualViewport { +class PageViewport extends VirtualViewport with VirtualViewportIterableMixin { PageViewport({ - Key key, this.startOffset: 0.0, this.scrollDirection: Axis.vertical, this.itemsWrap: false, diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index 52630baa80..08649c0b64 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -14,7 +14,6 @@ import 'package:flutter/rendering.dart'; import 'basic.dart'; import 'framework.dart'; import 'gesture_detector.dart'; -import 'homogeneous_viewport.dart'; import 'mixed_viewport.dart'; import 'notification_listener.dart'; import 'page_storage.dart'; @@ -523,178 +522,6 @@ abstract class ScrollableListPainter extends Painter { Future scrollEnded() => new Future.value(); } -/// An optimized scrollable widget for a large number of children that are all -/// the same size (extent) in the scrollDirection. For example for -/// ScrollDirection.vertical itemExtent is the height of each item. Use this -/// widget when you have a large number of children or when you are concerned -// about offscreen widgets consuming resources. -abstract class ScrollableWidgetList extends Scrollable { - ScrollableWidgetList({ - Key key, - double initialScrollOffset, - Axis scrollDirection: Axis.vertical, - ScrollListener onScroll, - SnapOffsetCallback snapOffsetCallback, - double snapAlignmentOffset: 0.0, - this.itemsWrap: false, - this.itemExtent, - this.padding, - this.scrollableListPainter - }) : super( - key: key, - initialScrollOffset: initialScrollOffset, - scrollDirection: scrollDirection, - onScroll: onScroll, - snapOffsetCallback: snapOffsetCallback, - snapAlignmentOffset: snapAlignmentOffset - ) { - assert(itemExtent != null); - } - - final bool itemsWrap; - final double itemExtent; - final EdgeDims padding; - final ScrollableListPainter scrollableListPainter; -} - -abstract class ScrollableWidgetListState extends ScrollableState { - /// Subclasses must implement `get itemCount` to tell ScrollableWidgetList - /// how many items there are in the list. - int get itemCount; - int _previousItemCount; - - Size _containerSize = Size.zero; - - void didUpdateConfig(T oldConfig) { - super.didUpdateConfig(oldConfig); - - bool scrollBehaviorUpdateNeeded = - config.padding != oldConfig.padding || - config.itemExtent != oldConfig.itemExtent || - config.scrollDirection != oldConfig.scrollDirection; - - if (config.itemsWrap != oldConfig.itemsWrap) { - _scrollBehavior = null; - scrollBehaviorUpdateNeeded = true; - } - - if (itemCount != _previousItemCount) { - _previousItemCount = itemCount; - scrollBehaviorUpdateNeeded = true; - } - - if (scrollBehaviorUpdateNeeded) - _updateScrollBehavior(); - } - - ScrollBehavior createScrollBehavior() => new OverscrollBehavior(); - ExtentScrollBehavior get scrollBehavior => super.scrollBehavior; - - double get _containerExtent { - return config.scrollDirection == Axis.vertical - ? _containerSize.height - : _containerSize.width; - } - - void _handleSizeChanged(Size newSize) { - setState(() { - _containerSize = newSize; - _updateScrollBehavior(); - }); - } - - double get _leadingPadding { - EdgeDims padding = config.padding; - if (config.scrollDirection == Axis.vertical) - return padding != null ? padding.top : 0.0; - return padding != null ? padding.left : -.0; - } - - double get _trailingPadding { - EdgeDims padding = config.padding; - if (config.scrollDirection == Axis.vertical) - return padding != null ? padding.bottom : 0.0; - return padding != null ? padding.right : 0.0; - } - - EdgeDims get _crossAxisPadding { - EdgeDims padding = config.padding; - if (padding == null) - return null; - if (config.scrollDirection == Axis.vertical) - return new EdgeDims.only(left: padding.left, right: padding.right); - return new EdgeDims.only(top: padding.top, bottom: padding.bottom); - } - - double get _contentExtent { - if (itemCount == null) - return null; - double contentExtent = config.itemExtent * itemCount; - if (config.padding != null) - contentExtent += _leadingPadding + _trailingPadding; - return contentExtent; - } - - void _updateScrollBehavior() { - // if you don't call this from build(), you must call it from setState(). - if (config.scrollableListPainter != null) - config.scrollableListPainter.contentExtent = _contentExtent; - scrollTo(scrollBehavior.updateExtents( - contentExtent: _contentExtent, - containerExtent: _containerExtent, - scrollOffset: scrollOffset - )); - } - - void dispatchOnScrollStart() { - super.dispatchOnScrollStart(); - config.scrollableListPainter?.scrollStarted(); - } - - void dispatchOnScroll() { - super.dispatchOnScroll(); - if (config.scrollableListPainter != null) - config.scrollableListPainter.scrollOffset = scrollOffset; - } - - void dispatchOnScrollEnd() { - super.dispatchOnScrollEnd(); - config.scrollableListPainter?.scrollEnded(); - } - - Widget buildContent(BuildContext context) { - if (itemCount != _previousItemCount) { - _previousItemCount = itemCount; - _updateScrollBehavior(); - } - - return new SizeObserver( - onSizeChanged: _handleSizeChanged, - child: new Container( - padding: _crossAxisPadding, - child: new HomogeneousViewport( - builder: _buildItems, - itemsWrap: config.itemsWrap, - itemExtent: config.itemExtent, - itemCount: itemCount, - direction: config.scrollDirection, - startOffset: scrollOffset - _leadingPadding, - overlayPainter: config.scrollableListPainter - ) - ) - ); - } - - List _buildItems(BuildContext context, int start, int count) { - List result = buildItems(context, start, count); - assert(result.every((Widget item) => item.key != null)); - return result; - } - - List buildItems(BuildContext context, int start, int count); - -} - /// A general scrollable list for a large number of children that might not all /// have the same height. Prefer [ScrollableWidgetList] when all the children /// have the same height because it can use that property to be more efficient. diff --git a/packages/flutter/lib/src/widgets/scrollable_grid.dart b/packages/flutter/lib/src/widgets/scrollable_grid.dart index 04f16502a3..efe8ab6116 100644 --- a/packages/flutter/lib/src/widgets/scrollable_grid.dart +++ b/packages/flutter/lib/src/widgets/scrollable_grid.dart @@ -65,9 +65,8 @@ class _ScrollableGridState extends ScrollableState { } } -class GridViewport extends VirtualViewport { +class GridViewport extends VirtualViewport with VirtualViewportIterableMixin { GridViewport({ - Key key, this.startOffset, this.delegate, this.onExtentsChanged, diff --git a/packages/flutter/lib/src/widgets/scrollable_list.dart b/packages/flutter/lib/src/widgets/scrollable_list.dart index 60fbddc3f6..05f5398b3c 100644 --- a/packages/flutter/lib/src/widgets/scrollable_list.dart +++ b/packages/flutter/lib/src/widgets/scrollable_list.dart @@ -88,18 +88,16 @@ class _ScrollableListState extends ScrollableState { } } -class ListViewport extends VirtualViewport { - ListViewport({ - Key key, +class _VirtualListViewport extends VirtualViewport { + _VirtualListViewport( this.onExtentsChanged, - this.startOffset: 0.0, - this.scrollDirection: Axis.vertical, + this.startOffset, + this.scrollDirection, this.itemExtent, - this.itemsWrap: false, + this.itemsWrap, this.padding, - this.overlayPainter, - this.children - }) { + this.overlayPainter + ) { assert(scrollDirection != null); assert(itemExtent != null); } @@ -111,15 +109,14 @@ class ListViewport extends VirtualViewport { final bool itemsWrap; final EdgeDims padding; final Painter overlayPainter; - final Iterable children; RenderList createRenderObject() => new RenderList(itemExtent: itemExtent); - _ListViewportElement createElement() => new _ListViewportElement(this); + _VirtualListViewportElement createElement() => new _VirtualListViewportElement(this); } -class _ListViewportElement extends VirtualViewportElement { - _ListViewportElement(ListViewport widget) : super(widget); +class _VirtualListViewportElement extends VirtualViewportElement<_VirtualListViewport> { + _VirtualListViewportElement(VirtualViewport widget) : super(widget); RenderList get renderObject => super.renderObject; @@ -135,11 +132,12 @@ class _ListViewportElement extends VirtualViewportElement { double get startOffsetLimit =>_startOffsetLimit; double _startOffsetLimit; - void updateRenderObject(ListViewport oldWidget) { - renderObject.scrollDirection = widget.scrollDirection; - renderObject.itemExtent = widget.itemExtent; - renderObject.padding = widget.padding; - renderObject.overlayPainter = widget.overlayPainter; + void updateRenderObject(_VirtualListViewport oldWidget) { + renderObject + ..scrollDirection = widget.scrollDirection + ..itemExtent = widget.itemExtent + ..padding = widget.padding + ..overlayPainter = widget.overlayPainter; super.updateRenderObject(oldWidget); } @@ -160,13 +158,13 @@ class _ListViewportElement extends VirtualViewportElement { final double itemExtent = widget.itemExtent; final EdgeDims padding = widget.padding ?? EdgeDims.zero; - double contentExtent = widget.itemExtent * length + padding.top + padding.bottom; + double contentExtent = length == null ? double.INFINITY : widget.itemExtent * length + padding.top + padding.bottom; double containerExtent = _getContainerExtentFromRenderObject(); _materializedChildBase = math.max(0, (widget.startOffset - padding.top) ~/ itemExtent); int materializedChildLimit = math.max(0, ((widget.startOffset + containerExtent) / itemExtent).ceil()); - if (!widget.itemsWrap) { + if (!widget.itemsWrap && length != null) { _materializedChildBase = math.min(length, _materializedChildBase); materializedChildLimit = math.min(length, materializedChildLimit); } else if (length == 0) { @@ -186,3 +184,133 @@ class _ListViewportElement extends VirtualViewportElement { } } } + +class ListViewport extends _VirtualListViewport with VirtualViewportIterableMixin { + ListViewport({ + ExtentsChangedCallback onExtentsChanged, + double startOffset: 0.0, + Axis scrollDirection: Axis.vertical, + double itemExtent, + bool itemsWrap: false, + EdgeDims padding, + Painter overlayPainter, + this.children + }) : super( + onExtentsChanged, + startOffset, + scrollDirection, + itemExtent, + itemsWrap, + padding, + overlayPainter + ); + + final Iterable children; +} + +/// An optimized scrollable widget for a large number of children that are all +/// the same size (extent) in the scrollDirection. For example for +/// ScrollDirection.vertical itemExtent is the height of each item. Use this +/// widget when you have a large number of children or when you are concerned +// about offscreen widgets consuming resources. +class ScrollableLazyList extends Scrollable { + ScrollableLazyList({ + Key key, + double initialScrollOffset, + Axis scrollDirection: Axis.vertical, + ScrollListener onScroll, + SnapOffsetCallback snapOffsetCallback, + double snapAlignmentOffset: 0.0, + this.itemExtent, + this.itemCount, + this.itemBuilder, + this.padding, + this.scrollableListPainter + }) : super( + key: key, + initialScrollOffset: initialScrollOffset, + scrollDirection: scrollDirection, + onScroll: onScroll, + snapOffsetCallback: snapOffsetCallback, + snapAlignmentOffset: snapAlignmentOffset + ) { + assert(itemExtent != null); + assert(itemBuilder != null); + } + + final double itemExtent; + final int itemCount; + final ItemListBuilder itemBuilder; + final EdgeDims padding; + final ScrollableListPainter scrollableListPainter; + + ScrollableState createState() => new _ScrollableLazyListState(); +} + +class _ScrollableLazyListState extends ScrollableState { + ScrollBehavior createScrollBehavior() => new OverscrollBehavior(); + ExtentScrollBehavior get scrollBehavior => super.scrollBehavior; + + void _handleExtentsChanged(double contentExtent, double containerExtent) { + config.scrollableListPainter?.contentExtent = contentExtent; + setState(() { + scrollTo(scrollBehavior.updateExtents( + contentExtent: 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 LazyListViewport( + onExtentsChanged: _handleExtentsChanged, + startOffset: scrollOffset, + scrollDirection: config.scrollDirection, + itemExtent: config.itemExtent, + itemCount: config.itemCount, + itemBuilder: config.itemBuilder, + padding: config.padding, + overlayPainter: config.scrollableListPainter + ); + } +} + +class LazyListViewport extends _VirtualListViewport with VirtualViewportLazyMixin { + LazyListViewport({ + ExtentsChangedCallback onExtentsChanged, + double startOffset: 0.0, + Axis scrollDirection: Axis.vertical, + double itemExtent, + EdgeDims padding, + Painter overlayPainter, + this.itemCount, + this.itemBuilder + }) : super( + onExtentsChanged, + startOffset, + scrollDirection, + itemExtent, + false, // Don't support wrapping yet. + padding, + overlayPainter + ); + + final int itemCount; + final ItemListBuilder itemBuilder; +} diff --git a/packages/flutter/lib/src/widgets/virtual_viewport.dart b/packages/flutter/lib/src/widgets/virtual_viewport.dart index c4cd9310ca..2cdd635e5d 100644 --- a/packages/flutter/lib/src/widgets/virtual_viewport.dart +++ b/packages/flutter/lib/src/widgets/virtual_viewport.dart @@ -14,7 +14,15 @@ typedef void ExtentsChangedCallback(double contentExtent, double containerExtent abstract class VirtualViewport extends RenderObjectWidget { double get startOffset; Axis get scrollDirection; - Iterable get children; + + _WidgetProvider _createWidgetProvider(); +} + +abstract class _WidgetProvider { + void didUpdateWidget(VirtualViewport oldWidget, VirtualViewport newWidget); + int get virtualChildCount; + void prepareChildren(VirtualViewportElement context, int base, int count); + Widget getChild(int i); } abstract class VirtualViewportElement extends RenderObjectElement { @@ -38,10 +46,12 @@ abstract class VirtualViewportElement extends RenderO visitor(child); } + _WidgetProvider _widgetProvider; + void mount(Element parent, dynamic newSlot) { + _widgetProvider = widget._createWidgetProvider(); + _widgetProvider.didUpdateWidget(null, widget); super.mount(parent, newSlot); - _iterator = null; - _widgets = []; renderObject.callback = layout; updateRenderObject(null); } @@ -52,11 +62,8 @@ abstract class VirtualViewportElement extends RenderO } void update(T newWidget) { - if (widget.children != newWidget.children) { - _iterator = null; - _widgets = []; - } T oldWidget = widget; + _widgetProvider.didUpdateWidget(oldWidget, newWidget); super.update(newWidget); updateRenderObject(oldWidget); if (!renderObject.needsLayout) @@ -75,7 +82,7 @@ abstract class VirtualViewportElement extends RenderO } void updateRenderObject(T oldWidget) { - renderObject.virtualChildCount = widget.children.length; + renderObject.virtualChildCount = _widgetProvider.virtualChildCount; if (startOffsetBase != null) { _updatePaintOffset(); @@ -111,37 +118,16 @@ abstract class VirtualViewportElement extends RenderO BuildableElement.lockState(_materializeChildren, building: true); } - 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 < 0 ? length : math.min(length, base + count)); + _widgetProvider.prepareChildren(this, base, count); List newWidgets = new List(count); for (int i = 0; i < count; ++i) { int childIndex = base + i; - Widget child = _widgets[(childIndex % length).abs()]; + Widget child = _widgetProvider.getChild(childIndex); Key key = new ValueKey(child.key ?? childIndex); newWidgets[i] = new RepaintBoundary(key: key, child: child); } @@ -162,3 +148,84 @@ abstract class VirtualViewportElement extends RenderO renderObject.remove(child); } } + +abstract class VirtualViewportIterableMixin extends VirtualViewport { + Iterable get children; + + _IterableWidgetProvider _createWidgetProvider() => new _IterableWidgetProvider(); +} + +class _IterableWidgetProvider extends _WidgetProvider { + int _length; + Iterator _iterator; + List _widgets; + + void didUpdateWidget(VirtualViewportIterableMixin oldWidget, VirtualViewportIterableMixin newWidget) { + if (oldWidget == null || newWidget.children != oldWidget.children) { + _iterator = null; + _widgets = []; + _length = newWidget.children.length; + } + } + + int get virtualChildCount => _length; + + void prepareChildren(VirtualViewportElement context, int base, int count) { + int limit = base < 0 ? _length : math.min(_length, base + count); + if (limit <= _widgets.length) + return; + VirtualViewportIterableMixin widget = context.widget; + 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); + } + } + + Widget getChild(int i) => _widgets[(i % _length).abs()]; +} + +typedef List ItemListBuilder(BuildContext context, int start, int count); + +abstract class VirtualViewportLazyMixin extends VirtualViewport { + int get itemCount; + ItemListBuilder get itemBuilder; + + _LazyWidgetProvider _createWidgetProvider() => new _LazyWidgetProvider(); +} + +class _LazyWidgetProvider extends _WidgetProvider { + int _length; + int _base; + List _widgets; + + void didUpdateWidget(VirtualViewportLazyMixin oldWidget, VirtualViewportLazyMixin newWidget) { + if (_length != newWidget.itemCount || oldWidget?.itemBuilder != newWidget.itemBuilder) { + _length = newWidget.itemCount; + _base = null; + _widgets = null; + } + } + + int get virtualChildCount => _length; + + void prepareChildren(VirtualViewportElement context, int base, int count) { + if (_widgets != null && _widgets.length == count && _base == base) + return; + VirtualViewportLazyMixin widget = context.widget; + _base = base; + _widgets = widget.itemBuilder(context, base, count); + } + + Widget getChild(int i) { + int n = _length ?? _widgets.length; + return _widgets[(i % n).abs()]; + } +} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index cb9d8e83f2..13415af98a 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -18,7 +18,6 @@ export 'src/widgets/framework.dart'; export 'src/widgets/gesture_detector.dart'; export 'src/widgets/gridpaper.dart'; export 'src/widgets/heroes.dart'; -export 'src/widgets/homogeneous_viewport.dart'; export 'src/widgets/implicit_animations.dart'; export 'src/widgets/locale_query.dart'; export 'src/widgets/media_query.dart'; diff --git a/packages/flutter/test/widget/remember_scroll_position_test.dart b/packages/flutter/test/widget/remember_scroll_position_test.dart index f37dbd42aa..720e8de92c 100644 --- a/packages/flutter/test/widget/remember_scroll_position_test.dart +++ b/packages/flutter/test/widget/remember_scroll_position_test.dart @@ -6,26 +6,20 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; import 'package:test/test.dart'; -class ThePositiveNumbers extends ScrollableWidgetList { - ThePositiveNumbers() : super(itemExtent: 100.0); - ThePositiveNumbersState createState() => new ThePositiveNumbersState(); -} - -class ThePositiveNumbersState extends ScrollableWidgetListState { - - ScrollBehavior createScrollBehavior() => new UnboundedBehavior(); - - int get itemCount => null; - - List buildItems(BuildContext context, int start, int count) { - List result = new List(); - for (int index = start; index < start + count; index += 1) - result.add(new Text('$index', key: new ValueKey(index))); - return result; +class ThePositiveNumbers extends StatelessComponent { + Widget build(BuildContext context) { + return new ScrollableLazyList( + itemExtent: 100.0, + itemBuilder: (BuildContext context, int start, int count) { + List result = new List(); + for (int index = start; index < start + count; index += 1) + result.add(new Text('$index', key: new ValueKey(index))); + return result; + } + ); } } - void main() { test('whether we remember our scroll position', () { testWidgets((WidgetTester tester) { @@ -53,8 +47,8 @@ void main() { expect(tester.findText('10'), isNull); expect(tester.findText('100'), isNull); - StatefulComponentElement target = - tester.findElement((Element element) => element.widget is ThePositiveNumbers); + StatefulComponentElement> target = + tester.findElement((Element element) => element.widget is ScrollableLazyList); target.state.scrollTo(1000.0); tester.pump(new Duration(seconds: 1)); diff --git a/packages/flutter/test/widget/homogeneous_viewport_test.dart b/packages/flutter/test/widget/scrollable_lazy_list_test.dart similarity index 71% rename from packages/flutter/test/widget/homogeneous_viewport_test.dart rename to packages/flutter/test/widget/scrollable_lazy_list_test.dart index 192ba0bb37..077b23a343 100644 --- a/packages/flutter/test/widget/homogeneous_viewport_test.dart +++ b/packages/flutter/test/widget/scrollable_lazy_list_test.dart @@ -18,8 +18,8 @@ void main() { Widget builder() { return new FlipComponent( - left: new HomogeneousViewport( - builder: (BuildContext context, int start, int count) { + left: new ScrollableLazyList( + itemBuilder: (BuildContext context, int start, int count) { List result = []; for (int index = start; index < start + count; index += 1) { callbackTracker.add(index); @@ -31,7 +31,6 @@ void main() { } return result; }, - startOffset: 0.0, itemExtent: 100.0 ), right: new Text('Not Today') @@ -67,9 +66,7 @@ void main() { // so if our widget is 200 pixels tall, it should fit exactly 3 times. // but if we are offset by 300 pixels, there will be 4, numbered 1-4. - double offset = 300.0; - - ListBuilder itemBuilder = (BuildContext context, int start, int count) { + ItemListBuilder itemBuilder = (BuildContext context, int start, int count) { List result = []; for (int index = start; index < start + count; index += 1) { callbackTracker.add(index); @@ -83,28 +80,27 @@ void main() { return result; }; - FlipComponent testComponent; - Widget builder() { - testComponent = new FlipComponent( - left: new HomogeneousViewport( - builder: itemBuilder, - startOffset: offset, - itemExtent: 200.0 - ), - right: new Text('Not Today') - ); - return testComponent; - } + GlobalKey> scrollableKey = new GlobalKey>(); + FlipComponent testComponent = new FlipComponent( + left: new ScrollableLazyList( + key: scrollableKey, + itemBuilder: itemBuilder, + itemExtent: 200.0, + initialScrollOffset: 300.0 + ), + right: new Text('Not Today') + ); - tester.pumpWidget(builder()); + tester.pumpWidget(testComponent); expect(callbackTracker, equals([1, 2, 3, 4])); callbackTracker.clear(); - offset = 400.0; // now only 3 should fit, numbered 2-4. + scrollableKey.currentState.scrollTo(400.0); + // now only 3 should fit, numbered 2-4. - tester.pumpWidget(builder()); + tester.pumpWidget(testComponent); expect(callbackTracker, equals([2, 3, 4])); @@ -120,9 +116,7 @@ void main() { // so if our widget is 200 pixels wide, it should fit exactly 4 times. // but if we are offset by 300 pixels, there will be 5, numbered 1-5. - double offset = 300.0; - - ListBuilder itemBuilder = (BuildContext context, int start, int count) { + ItemListBuilder itemBuilder = (BuildContext context, int start, int count) { List result = []; for (int index = start; index < start + count; index += 1) { callbackTracker.add(index); @@ -136,29 +130,28 @@ void main() { return result; }; - FlipComponent testComponent; - Widget builder() { - testComponent = new FlipComponent( - left: new HomogeneousViewport( - builder: itemBuilder, - startOffset: offset, - itemExtent: 200.0, - direction: Axis.horizontal - ), - right: new Text('Not Today') - ); - return testComponent; - } + GlobalKey> scrollableKey = new GlobalKey>(); + FlipComponent testComponent = new FlipComponent( + left: new ScrollableLazyList( + key: scrollableKey, + itemBuilder: itemBuilder, + itemExtent: 200.0, + initialScrollOffset: 300.0, + scrollDirection: Axis.horizontal + ), + right: new Text('Not Today') + ); - tester.pumpWidget(builder()); + tester.pumpWidget(testComponent); expect(callbackTracker, equals([1, 2, 3, 4, 5])); callbackTracker.clear(); - offset = 400.0; // now only 4 should fit, numbered 2-5. + scrollableKey.currentState.scrollTo(400.0); + // now only 4 should fit, numbered 2-5. - tester.pumpWidget(builder()); + tester.pumpWidget(testComponent); expect(callbackTracker, equals([2, 3, 4, 5]));