Commit 68f33d30 authored by Adam Barth's avatar Adam Barth

Merge pull request #1642 from abarth/rm_viewport

Remove HomogeneousViewport
parents bc3202ff 5b896694
......@@ -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';
......@@ -597,178 +596,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