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 { ...@@ -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';
...@@ -597,178 +596,6 @@ abstract class ScrollableListPainter extends Painter { ...@@ -597,178 +596,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