Commit 5b896694 authored by Adam Barth's avatar Adam Barth

Remove HomogeneousViewport

The virtual viewport machinery now handles all of these use cases.
Previous clients of ScrollableWidgetList can use ScrollableLazyList
instead.
parent 1d3ce8e8
......@@ -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<DateTime> onChanged;
final DateTime firstDate;
final DateTime lastDate;
final double itemExtent;
_MonthPickerState createState() => new _MonthPickerState();
}
class _MonthPickerState extends ScrollableWidgetListState<MonthPicker> {
class _MonthPickerState extends State<MonthPicker> {
void initState() {
super.initState();
_updateCurrentDate();
......@@ -313,8 +314,6 @@ class _MonthPickerState extends ScrollableWidgetListState<MonthPicker> {
});
}
int get itemCount => (config.lastDate.year - config.firstDate.year) * 12 + config.lastDate.month - config.firstDate.month + 1;
List<Widget> buildItems(BuildContext context, int start, int count) {
List<Widget> result = new List<Widget>();
DateTime startDate = new DateTime(config.firstDate.year + start ~/ 12, config.firstDate.month + start % 12);
......@@ -335,6 +334,14 @@ class _MonthPickerState extends ScrollableWidgetListState<MonthPicker> {
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<MonthPicker> {
}
// 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<YearPicker> {
int get itemCount => config.lastDate.year - config.firstDate.year + 1;
class _YearPickerState extends State<YearPicker> {
static const double _itemExtent = 50.0;
List<Widget> 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<YearPicker> {
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<YearPicker> {
}
return items;
}
Widget build(BuildContext context) {
return new ScrollableLazyList(
itemExtent: _itemExtent,
itemCount: config.lastDate.year - config.firstDate.year + 1,
itemBuilder: buildItems
);
}
}
......@@ -344,6 +344,8 @@ class RenderGrid extends RenderVirtualViewport<GridParentData> {
_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
......
......@@ -76,7 +76,10 @@ class RenderList extends RenderVirtualViewport<ListParentData> 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;
......
......@@ -192,7 +192,7 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
_callback = callback,
_overlayPainter = overlayPainter;
int get virtualChildCount => _virtualChildCount ?? childCount;
int get virtualChildCount => _virtualChildCount;
int _virtualChildCount;
void set virtualChildCount(int value) {
if (_virtualChildCount == value)
......
// 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<Widget> 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<T extends _ViewportBase> extends RenderObjectElement<T> {
_ViewportBaseElement(T widget) : super(widget);
List<Element> _children = const <Element>[];
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<Widget> newWidgets;
if (_layoutItemCount > 0)
newWidgets = widget.builder(this, _layoutFirstIndex, _layoutItemCount).map((Widget widget) {
return new RepaintBoundary(key: new ValueKey<Key>(widget.key), child: widget);
}).toList();
else
newWidgets = <Widget>[];
_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<HomogeneousViewport> {
_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;
}
}
......@@ -180,9 +180,8 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> {
}
}
class PageViewport extends VirtualViewport {
class PageViewport extends VirtualViewport with VirtualViewportIterableMixin {
PageViewport({
Key key,
this.startOffset: 0.0,
this.scrollDirection: Axis.vertical,
this.itemsWrap: false,
......
......@@ -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<T extends ScrollableWidgetList> extends ScrollableState<T> {
/// 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<Widget> _buildItems(BuildContext context, int start, int count) {
List<Widget> result = buildItems(context, start, count);
assert(result.every((Widget item) => item.key != null));
return result;
}
List<Widget> 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.
......
......@@ -65,9 +65,8 @@ class _ScrollableGridState extends ScrollableState<ScrollableGrid> {
}
}
class GridViewport extends VirtualViewport {
class GridViewport extends VirtualViewport with VirtualViewportIterableMixin {
GridViewport({
Key key,
this.startOffset,
this.delegate,
this.onExtentsChanged,
......
......@@ -88,18 +88,16 @@ class _ScrollableListState extends ScrollableState<ScrollableList> {
}
}
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<Widget> children;
RenderList createRenderObject() => new RenderList(itemExtent: itemExtent);
_ListViewportElement createElement() => new _ListViewportElement(this);
_VirtualListViewportElement createElement() => new _VirtualListViewportElement(this);
}
class _ListViewportElement extends VirtualViewportElement<ListViewport> {
_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<ListViewport> {
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<ListViewport> {
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<ListViewport> {
}
}
}
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<Widget> 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<ScrollableLazyList> {
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;
}
......@@ -14,7 +14,15 @@ typedef void ExtentsChangedCallback(double contentExtent, double containerExtent
abstract class VirtualViewport extends RenderObjectWidget {
double get startOffset;
Axis get scrollDirection;
Iterable<Widget> 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<T extends VirtualViewport> extends RenderObjectElement<T> {
......@@ -38,10 +46,12 @@ abstract class VirtualViewportElement<T extends VirtualViewport> 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 = <Widget>[];
renderObject.callback = layout;
updateRenderObject(null);
}
......@@ -52,11 +62,8 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
}
void update(T newWidget) {
if (widget.children != newWidget.children) {
_iterator = null;
_widgets = <Widget>[];
}
T oldWidget = widget;
_widgetProvider.didUpdateWidget(oldWidget, newWidget);
super.update(newWidget);
updateRenderObject(oldWidget);
if (!renderObject.needsLayout)
......@@ -75,7 +82,7 @@ abstract class VirtualViewportElement<T extends VirtualViewport> 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<T extends VirtualViewport> extends RenderO
BuildableElement.lockState(_materializeChildren, building: true);
}
Iterator<Widget> _iterator;
List<Widget> _widgets;
void _populateWidgets(int limit) {
if (limit <= _widgets.length)
return;
if (widget.children is List<Widget>) {
_widgets = widget.children;
return;
}
_iterator ??= widget.children.iterator;
while (_widgets.length < limit) {
bool moved = _iterator.moveNext();
assert(moved);
Widget current = _iterator.current;
assert(current != null);
_widgets.add(current);
}
}
void _materializeChildren() {
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<Widget> newWidgets = new List<Widget>(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<T extends VirtualViewport> extends RenderO
renderObject.remove(child);
}
}
abstract class VirtualViewportIterableMixin extends VirtualViewport {
Iterable<Widget> get children;
_IterableWidgetProvider _createWidgetProvider() => new _IterableWidgetProvider();
}
class _IterableWidgetProvider extends _WidgetProvider {
int _length;
Iterator<Widget> _iterator;
List<Widget> _widgets;
void didUpdateWidget(VirtualViewportIterableMixin oldWidget, VirtualViewportIterableMixin newWidget) {
if (oldWidget == null || newWidget.children != oldWidget.children) {
_iterator = null;
_widgets = <Widget>[];
_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<Widget>) {
_widgets = widget.children;
return;
}
_iterator ??= widget.children.iterator;
while (_widgets.length < limit) {
bool moved = _iterator.moveNext();
assert(moved);
Widget current = _iterator.current;
assert(current != null);
_widgets.add(current);
}
}
Widget getChild(int i) => _widgets[(i % _length).abs()];
}
typedef List<Widget> 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<Widget> _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()];
}
}
......@@ -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';
......
......@@ -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<ThePositiveNumbers> {
ScrollBehavior createScrollBehavior() => new UnboundedBehavior();
int get itemCount => null;
List<Widget> buildItems(BuildContext context, int start, int count) {
List<Widget> result = new List<Widget>();
for (int index = start; index < start + count; index += 1)
result.add(new Text('$index', key: new ValueKey<int>(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<Widget> result = new List<Widget>();
for (int index = start; index < start + count; index += 1)
result.add(new Text('$index', key: new ValueKey<int>(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<ThePositiveNumbers, ThePositiveNumbersState> target =
tester.findElement((Element element) => element.widget is ThePositiveNumbers);
StatefulComponentElement<ScrollableLazyList, ScrollableState<ScrollableLazyList>> target =
tester.findElement((Element element) => element.widget is ScrollableLazyList);
target.state.scrollTo(1000.0);
tester.pump(new Duration(seconds: 1));
......
......@@ -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<Widget> result = <Widget>[];
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<Widget> result = <Widget>[];
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<ScrollableState<ScrollableLazyList>> scrollableKey = new GlobalKey<ScrollableState<ScrollableLazyList>>();
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<Widget> result = <Widget>[];
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<ScrollableState<ScrollableLazyList>> scrollableKey = new GlobalKey<ScrollableState<ScrollableLazyList>>();
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]));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment