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 { ...@@ -268,15 +268,15 @@ class DayPicker extends StatelessComponent {
} }
} }
// Scrollable list of DayPickers to allow choosing a month class MonthPicker extends StatefulComponent {
class MonthPicker extends ScrollableWidgetList {
MonthPicker({ MonthPicker({
Key key,
this.selectedDate, this.selectedDate,
this.onChanged, this.onChanged,
this.firstDate, this.firstDate,
this.lastDate, this.lastDate,
double itemExtent this.itemExtent
}) : super(itemExtent: itemExtent) { }) : super(key: key) {
assert(selectedDate != null); assert(selectedDate != null);
assert(onChanged != null); assert(onChanged != null);
assert(lastDate.isAfter(firstDate)); assert(lastDate.isAfter(firstDate));
...@@ -286,11 +286,12 @@ class MonthPicker extends ScrollableWidgetList { ...@@ -286,11 +286,12 @@ class MonthPicker extends ScrollableWidgetList {
final ValueChanged<DateTime> onChanged; final ValueChanged<DateTime> onChanged;
final DateTime firstDate; final DateTime firstDate;
final DateTime lastDate; final DateTime lastDate;
final double itemExtent;
_MonthPickerState createState() => new _MonthPickerState(); _MonthPickerState createState() => new _MonthPickerState();
} }
class _MonthPickerState extends ScrollableWidgetListState<MonthPicker> { class _MonthPickerState extends State<MonthPicker> {
void initState() { void initState() {
super.initState(); super.initState();
_updateCurrentDate(); _updateCurrentDate();
...@@ -313,8 +314,6 @@ class _MonthPickerState extends ScrollableWidgetListState<MonthPicker> { ...@@ -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> buildItems(BuildContext context, int start, int count) {
List<Widget> result = new List<Widget>(); List<Widget> result = new List<Widget>();
DateTime startDate = new DateTime(config.firstDate.year + start ~/ 12, config.firstDate.month + start % 12); DateTime startDate = new DateTime(config.firstDate.year + start ~/ 12, config.firstDate.month + start % 12);
...@@ -335,6 +334,14 @@ class _MonthPickerState extends ScrollableWidgetListState<MonthPicker> { ...@@ -335,6 +334,14 @@ class _MonthPickerState extends ScrollableWidgetListState<MonthPicker> {
return result; 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() { void dispose() {
if (_timer != null) if (_timer != null)
_timer.cancel(); _timer.cancel();
...@@ -343,13 +350,14 @@ class _MonthPickerState extends ScrollableWidgetListState<MonthPicker> { ...@@ -343,13 +350,14 @@ class _MonthPickerState extends ScrollableWidgetListState<MonthPicker> {
} }
// Scrollable list of years to allow picking a year // Scrollable list of years to allow picking a year
class YearPicker extends ScrollableWidgetList { class YearPicker extends StatefulComponent {
YearPicker({ YearPicker({
Key key,
this.selectedDate, this.selectedDate,
this.onChanged, this.onChanged,
this.firstDate, this.firstDate,
this.lastDate this.lastDate
}) : super(itemExtent: 50.0) { }) : super(key: key) {
assert(selectedDate != null); assert(selectedDate != null);
assert(onChanged != null); assert(onChanged != null);
assert(lastDate.isAfter(firstDate)); assert(lastDate.isAfter(firstDate));
...@@ -363,8 +371,8 @@ class YearPicker extends ScrollableWidgetList { ...@@ -363,8 +371,8 @@ class YearPicker extends ScrollableWidgetList {
_YearPickerState createState() => new _YearPickerState(); _YearPickerState createState() => new _YearPickerState();
} }
class _YearPickerState extends ScrollableWidgetListState<YearPicker> { class _YearPickerState extends State<YearPicker> {
int get itemCount => config.lastDate.year - config.firstDate.year + 1; static const double _itemExtent = 50.0;
List<Widget> buildItems(BuildContext context, int start, int count) { List<Widget> buildItems(BuildContext context, int start, int count) {
TextStyle style = Theme.of(context).text.body1.copyWith(color: Colors.black54); TextStyle style = Theme.of(context).text.body1.copyWith(color: Colors.black54);
...@@ -379,7 +387,7 @@ class _YearPickerState extends ScrollableWidgetListState<YearPicker> { ...@@ -379,7 +387,7 @@ class _YearPickerState extends ScrollableWidgetListState<YearPicker> {
config.onChanged(result); config.onChanged(result);
}, },
child: new Container( child: new Container(
height: config.itemExtent, height: _itemExtent,
decoration: year == config.selectedDate.year ? new BoxDecoration( decoration: year == config.selectedDate.year ? new BoxDecoration(
backgroundColor: Theme.of(context).primarySwatch[100], backgroundColor: Theme.of(context).primarySwatch[100],
shape: BoxShape.circle shape: BoxShape.circle
...@@ -393,4 +401,12 @@ class _YearPickerState extends ScrollableWidgetListState<YearPicker> { ...@@ -393,4 +401,12 @@ class _YearPickerState extends ScrollableWidgetListState<YearPicker> {
} }
return items; 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> { ...@@ -344,6 +344,8 @@ class RenderGrid extends RenderVirtualViewport<GridParentData> {
_delegate = newDelegate; _delegate = newDelegate;
} }
int get virtualChildCount => super.virtualChildCount ?? childCount;
/// The virtual index of the first child. /// The virtual index of the first child.
/// ///
/// When asking the delegate for the position of each child, the grid will add /// 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 ...@@ -76,7 +76,10 @@ class RenderList extends RenderVirtualViewport<ListParentData> implements HasScr
double get _preferredExtent { double get _preferredExtent {
if (itemExtent == null) if (itemExtent == null)
return double.INFINITY; return double.INFINITY;
double extent = itemExtent * virtualChildCount; int count = virtualChildCount;
if (count == null)
return double.INFINITY;
double extent = itemExtent * count;
if (padding != null) if (padding != null)
extent += _scrollAxisPadding; extent += _scrollAxisPadding;
return extent; return extent;
......
...@@ -192,7 +192,7 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende ...@@ -192,7 +192,7 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
_callback = callback, _callback = callback,
_overlayPainter = overlayPainter; _overlayPainter = overlayPainter;
int get virtualChildCount => _virtualChildCount ?? childCount; int get virtualChildCount => _virtualChildCount;
int _virtualChildCount; int _virtualChildCount;
void set virtualChildCount(int value) { void set virtualChildCount(int value) {
if (_virtualChildCount == 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> { ...@@ -180,9 +180,8 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> {
} }
} }
class PageViewport extends VirtualViewport { class PageViewport extends VirtualViewport with VirtualViewportIterableMixin {
PageViewport({ PageViewport({
Key key,
this.startOffset: 0.0, this.startOffset: 0.0,
this.scrollDirection: Axis.vertical, this.scrollDirection: Axis.vertical,
this.itemsWrap: false, this.itemsWrap: false,
......
...@@ -14,7 +14,6 @@ import 'package:flutter/rendering.dart'; ...@@ -14,7 +14,6 @@ import 'package:flutter/rendering.dart';
import 'basic.dart'; import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'gesture_detector.dart'; import 'gesture_detector.dart';
import 'homogeneous_viewport.dart';
import 'mixed_viewport.dart'; import 'mixed_viewport.dart';
import 'notification_listener.dart'; import 'notification_listener.dart';
import 'page_storage.dart'; import 'page_storage.dart';
...@@ -523,178 +522,6 @@ abstract class ScrollableListPainter extends Painter { ...@@ -523,178 +522,6 @@ abstract class ScrollableListPainter extends Painter {
Future scrollEnded() => new Future.value(); 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 /// 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. Prefer [ScrollableWidgetList] when all the children
/// have the same height because it can use that property to be more efficient. /// have the same height because it can use that property to be more efficient.
......
...@@ -65,9 +65,8 @@ class _ScrollableGridState extends ScrollableState<ScrollableGrid> { ...@@ -65,9 +65,8 @@ class _ScrollableGridState extends ScrollableState<ScrollableGrid> {
} }
} }
class GridViewport extends VirtualViewport { class GridViewport extends VirtualViewport with VirtualViewportIterableMixin {
GridViewport({ GridViewport({
Key key,
this.startOffset, this.startOffset,
this.delegate, this.delegate,
this.onExtentsChanged, this.onExtentsChanged,
......
...@@ -88,18 +88,16 @@ class _ScrollableListState extends ScrollableState<ScrollableList> { ...@@ -88,18 +88,16 @@ class _ScrollableListState extends ScrollableState<ScrollableList> {
} }
} }
class ListViewport extends VirtualViewport { class _VirtualListViewport extends VirtualViewport {
ListViewport({ _VirtualListViewport(
Key key,
this.onExtentsChanged, this.onExtentsChanged,
this.startOffset: 0.0, this.startOffset,
this.scrollDirection: Axis.vertical, this.scrollDirection,
this.itemExtent, this.itemExtent,
this.itemsWrap: false, this.itemsWrap,
this.padding, this.padding,
this.overlayPainter, this.overlayPainter
this.children ) {
}) {
assert(scrollDirection != null); assert(scrollDirection != null);
assert(itemExtent != null); assert(itemExtent != null);
} }
...@@ -111,15 +109,14 @@ class ListViewport extends VirtualViewport { ...@@ -111,15 +109,14 @@ class ListViewport extends VirtualViewport {
final bool itemsWrap; final bool itemsWrap;
final EdgeDims padding; final EdgeDims padding;
final Painter overlayPainter; final Painter overlayPainter;
final Iterable<Widget> children;
RenderList createRenderObject() => new RenderList(itemExtent: itemExtent); RenderList createRenderObject() => new RenderList(itemExtent: itemExtent);
_ListViewportElement createElement() => new _ListViewportElement(this); _VirtualListViewportElement createElement() => new _VirtualListViewportElement(this);
} }
class _ListViewportElement extends VirtualViewportElement<ListViewport> { class _VirtualListViewportElement extends VirtualViewportElement<_VirtualListViewport> {
_ListViewportElement(ListViewport widget) : super(widget); _VirtualListViewportElement(VirtualViewport widget) : super(widget);
RenderList get renderObject => super.renderObject; RenderList get renderObject => super.renderObject;
...@@ -135,11 +132,12 @@ class _ListViewportElement extends VirtualViewportElement<ListViewport> { ...@@ -135,11 +132,12 @@ class _ListViewportElement extends VirtualViewportElement<ListViewport> {
double get startOffsetLimit =>_startOffsetLimit; double get startOffsetLimit =>_startOffsetLimit;
double _startOffsetLimit; double _startOffsetLimit;
void updateRenderObject(ListViewport oldWidget) { void updateRenderObject(_VirtualListViewport oldWidget) {
renderObject.scrollDirection = widget.scrollDirection; renderObject
renderObject.itemExtent = widget.itemExtent; ..scrollDirection = widget.scrollDirection
renderObject.padding = widget.padding; ..itemExtent = widget.itemExtent
renderObject.overlayPainter = widget.overlayPainter; ..padding = widget.padding
..overlayPainter = widget.overlayPainter;
super.updateRenderObject(oldWidget); super.updateRenderObject(oldWidget);
} }
...@@ -160,13 +158,13 @@ class _ListViewportElement extends VirtualViewportElement<ListViewport> { ...@@ -160,13 +158,13 @@ class _ListViewportElement extends VirtualViewportElement<ListViewport> {
final double itemExtent = widget.itemExtent; final double itemExtent = widget.itemExtent;
final EdgeDims padding = widget.padding ?? EdgeDims.zero; 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(); double containerExtent = _getContainerExtentFromRenderObject();
_materializedChildBase = math.max(0, (widget.startOffset - padding.top) ~/ itemExtent); _materializedChildBase = math.max(0, (widget.startOffset - padding.top) ~/ itemExtent);
int materializedChildLimit = math.max(0, ((widget.startOffset + containerExtent) / itemExtent).ceil()); int materializedChildLimit = math.max(0, ((widget.startOffset + containerExtent) / itemExtent).ceil());
if (!widget.itemsWrap) { if (!widget.itemsWrap && length != null) {
_materializedChildBase = math.min(length, _materializedChildBase); _materializedChildBase = math.min(length, _materializedChildBase);
materializedChildLimit = math.min(length, materializedChildLimit); materializedChildLimit = math.min(length, materializedChildLimit);
} else if (length == 0) { } else if (length == 0) {
...@@ -186,3 +184,133 @@ class _ListViewportElement extends VirtualViewportElement<ListViewport> { ...@@ -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 ...@@ -14,7 +14,15 @@ typedef void ExtentsChangedCallback(double contentExtent, double containerExtent
abstract class VirtualViewport extends RenderObjectWidget { abstract class VirtualViewport extends RenderObjectWidget {
double get startOffset; double get startOffset;
Axis get scrollDirection; 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> { abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderObjectElement<T> {
...@@ -38,10 +46,12 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO ...@@ -38,10 +46,12 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
visitor(child); visitor(child);
} }
_WidgetProvider _widgetProvider;
void mount(Element parent, dynamic newSlot) { void mount(Element parent, dynamic newSlot) {
_widgetProvider = widget._createWidgetProvider();
_widgetProvider.didUpdateWidget(null, widget);
super.mount(parent, newSlot); super.mount(parent, newSlot);
_iterator = null;
_widgets = <Widget>[];
renderObject.callback = layout; renderObject.callback = layout;
updateRenderObject(null); updateRenderObject(null);
} }
...@@ -52,11 +62,8 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO ...@@ -52,11 +62,8 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
} }
void update(T newWidget) { void update(T newWidget) {
if (widget.children != newWidget.children) {
_iterator = null;
_widgets = <Widget>[];
}
T oldWidget = widget; T oldWidget = widget;
_widgetProvider.didUpdateWidget(oldWidget, newWidget);
super.update(newWidget); super.update(newWidget);
updateRenderObject(oldWidget); updateRenderObject(oldWidget);
if (!renderObject.needsLayout) if (!renderObject.needsLayout)
...@@ -75,7 +82,7 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO ...@@ -75,7 +82,7 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
} }
void updateRenderObject(T oldWidget) { void updateRenderObject(T oldWidget) {
renderObject.virtualChildCount = widget.children.length; renderObject.virtualChildCount = _widgetProvider.virtualChildCount;
if (startOffsetBase != null) { if (startOffsetBase != null) {
_updatePaintOffset(); _updatePaintOffset();
...@@ -111,37 +118,16 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO ...@@ -111,37 +118,16 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
BuildableElement.lockState(_materializeChildren, building: true); 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() { void _materializeChildren() {
int base = materializedChildBase; int base = materializedChildBase;
int count = materializedChildCount; int count = materializedChildCount;
int length = renderObject.virtualChildCount;
assert(base != null); assert(base != null);
assert(count != 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); List<Widget> newWidgets = new List<Widget>(count);
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
int childIndex = base + i; int childIndex = base + i;
Widget child = _widgets[(childIndex % length).abs()]; Widget child = _widgetProvider.getChild(childIndex);
Key key = new ValueKey(child.key ?? childIndex); Key key = new ValueKey(child.key ?? childIndex);
newWidgets[i] = new RepaintBoundary(key: key, child: child); newWidgets[i] = new RepaintBoundary(key: key, child: child);
} }
...@@ -162,3 +148,84 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO ...@@ -162,3 +148,84 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
renderObject.remove(child); 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'; ...@@ -18,7 +18,6 @@ export 'src/widgets/framework.dart';
export 'src/widgets/gesture_detector.dart'; export 'src/widgets/gesture_detector.dart';
export 'src/widgets/gridpaper.dart'; export 'src/widgets/gridpaper.dart';
export 'src/widgets/heroes.dart'; export 'src/widgets/heroes.dart';
export 'src/widgets/homogeneous_viewport.dart';
export 'src/widgets/implicit_animations.dart'; export 'src/widgets/implicit_animations.dart';
export 'src/widgets/locale_query.dart'; export 'src/widgets/locale_query.dart';
export 'src/widgets/media_query.dart'; export 'src/widgets/media_query.dart';
......
...@@ -6,26 +6,20 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -6,26 +6,20 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
class ThePositiveNumbers extends ScrollableWidgetList { class ThePositiveNumbers extends StatelessComponent {
ThePositiveNumbers() : super(itemExtent: 100.0); Widget build(BuildContext context) {
ThePositiveNumbersState createState() => new ThePositiveNumbersState(); return new ScrollableLazyList(
} itemExtent: 100.0,
itemBuilder: (BuildContext context, int start, int count) {
class ThePositiveNumbersState extends ScrollableWidgetListState<ThePositiveNumbers> { List<Widget> result = new List<Widget>();
for (int index = start; index < start + count; index += 1)
ScrollBehavior createScrollBehavior() => new UnboundedBehavior(); result.add(new Text('$index', key: new ValueKey<int>(index)));
return result;
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;
} }
} }
void main() { void main() {
test('whether we remember our scroll position', () { test('whether we remember our scroll position', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
...@@ -53,8 +47,8 @@ void main() { ...@@ -53,8 +47,8 @@ void main() {
expect(tester.findText('10'), isNull); expect(tester.findText('10'), isNull);
expect(tester.findText('100'), isNull); expect(tester.findText('100'), isNull);
StatefulComponentElement<ThePositiveNumbers, ThePositiveNumbersState> target = StatefulComponentElement<ScrollableLazyList, ScrollableState<ScrollableLazyList>> target =
tester.findElement((Element element) => element.widget is ThePositiveNumbers); tester.findElement((Element element) => element.widget is ScrollableLazyList);
target.state.scrollTo(1000.0); target.state.scrollTo(1000.0);
tester.pump(new Duration(seconds: 1)); tester.pump(new Duration(seconds: 1));
......
...@@ -18,8 +18,8 @@ void main() { ...@@ -18,8 +18,8 @@ void main() {
Widget builder() { Widget builder() {
return new FlipComponent( return new FlipComponent(
left: new HomogeneousViewport( left: new ScrollableLazyList(
builder: (BuildContext context, int start, int count) { itemBuilder: (BuildContext context, int start, int count) {
List<Widget> result = <Widget>[]; List<Widget> result = <Widget>[];
for (int index = start; index < start + count; index += 1) { for (int index = start; index < start + count; index += 1) {
callbackTracker.add(index); callbackTracker.add(index);
...@@ -31,7 +31,6 @@ void main() { ...@@ -31,7 +31,6 @@ void main() {
} }
return result; return result;
}, },
startOffset: 0.0,
itemExtent: 100.0 itemExtent: 100.0
), ),
right: new Text('Not Today') right: new Text('Not Today')
...@@ -67,9 +66,7 @@ void main() { ...@@ -67,9 +66,7 @@ void main() {
// so if our widget is 200 pixels tall, it should fit exactly 3 times. // 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. // but if we are offset by 300 pixels, there will be 4, numbered 1-4.
double offset = 300.0; ItemListBuilder itemBuilder = (BuildContext context, int start, int count) {
ListBuilder itemBuilder = (BuildContext context, int start, int count) {
List<Widget> result = <Widget>[]; List<Widget> result = <Widget>[];
for (int index = start; index < start + count; index += 1) { for (int index = start; index < start + count; index += 1) {
callbackTracker.add(index); callbackTracker.add(index);
...@@ -83,28 +80,27 @@ void main() { ...@@ -83,28 +80,27 @@ void main() {
return result; return result;
}; };
FlipComponent testComponent; GlobalKey<ScrollableState<ScrollableLazyList>> scrollableKey = new GlobalKey<ScrollableState<ScrollableLazyList>>();
Widget builder() { FlipComponent testComponent = new FlipComponent(
testComponent = new FlipComponent( left: new ScrollableLazyList(
left: new HomogeneousViewport( key: scrollableKey,
builder: itemBuilder, itemBuilder: itemBuilder,
startOffset: offset, itemExtent: 200.0,
itemExtent: 200.0 initialScrollOffset: 300.0
), ),
right: new Text('Not Today') right: new Text('Not Today')
); );
return testComponent;
}
tester.pumpWidget(builder()); tester.pumpWidget(testComponent);
expect(callbackTracker, equals([1, 2, 3, 4])); expect(callbackTracker, equals([1, 2, 3, 4]));
callbackTracker.clear(); 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])); expect(callbackTracker, equals([2, 3, 4]));
...@@ -120,9 +116,7 @@ void main() { ...@@ -120,9 +116,7 @@ void main() {
// so if our widget is 200 pixels wide, it should fit exactly 4 times. // 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. // but if we are offset by 300 pixels, there will be 5, numbered 1-5.
double offset = 300.0; ItemListBuilder itemBuilder = (BuildContext context, int start, int count) {
ListBuilder itemBuilder = (BuildContext context, int start, int count) {
List<Widget> result = <Widget>[]; List<Widget> result = <Widget>[];
for (int index = start; index < start + count; index += 1) { for (int index = start; index < start + count; index += 1) {
callbackTracker.add(index); callbackTracker.add(index);
...@@ -136,29 +130,28 @@ void main() { ...@@ -136,29 +130,28 @@ void main() {
return result; return result;
}; };
FlipComponent testComponent; GlobalKey<ScrollableState<ScrollableLazyList>> scrollableKey = new GlobalKey<ScrollableState<ScrollableLazyList>>();
Widget builder() { FlipComponent testComponent = new FlipComponent(
testComponent = new FlipComponent( left: new ScrollableLazyList(
left: new HomogeneousViewport( key: scrollableKey,
builder: itemBuilder, itemBuilder: itemBuilder,
startOffset: offset, itemExtent: 200.0,
itemExtent: 200.0, initialScrollOffset: 300.0,
direction: Axis.horizontal scrollDirection: Axis.horizontal
), ),
right: new Text('Not Today') right: new Text('Not Today')
); );
return testComponent;
}
tester.pumpWidget(builder()); tester.pumpWidget(testComponent);
expect(callbackTracker, equals([1, 2, 3, 4, 5])); expect(callbackTracker, equals([1, 2, 3, 4, 5]));
callbackTracker.clear(); 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])); 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