Commit d46f0ceb authored by Adam Barth's avatar Adam Barth

Merge pull request #1098 from abarth/pageable_list2

Switch PageableList over to using RenderList
parents e1b16729 e64d93a5
......@@ -29,7 +29,7 @@ class _TabsDemoState extends State<TabsDemo> {
Widget build(_) {
return new TabBarView<String>(
items: _iconNames,
itemBuilder: (BuildContext context, String iconName, int index) {
itemBuilder: (String iconName) {
return new Container(
key: new ValueKey<String>(iconName),
padding: const EdgeDims.all(12.0),
......
......@@ -273,7 +273,7 @@ class StockHomeState extends State<StockHome> {
drawer: _buildDrawer(context),
body: new TabBarView<StockHomeTab>(
items: <StockHomeTab>[StockHomeTab.market, StockHomeTab.portfolio],
itemBuilder: (BuildContext context, StockHomeTab tab, _) {
itemBuilder: (StockHomeTab tab) {
switch (tab) {
case StockHomeTab.market:
return _buildStockTab(context, tab, config.symbols);
......
......@@ -41,7 +41,7 @@ class PageableListAppState extends State<PageableListApp> {
ScrollDirection scrollDirection = ScrollDirection.horizontal;
bool itemsWrap = false;
Widget buildCard(BuildContext context, CardModel cardModel, int index) {
Widget buildCard(CardModel cardModel) {
Widget card = new Card(
color: cardModel.color,
child: new Container(
......@@ -114,10 +114,9 @@ class PageableListAppState extends State<PageableListApp> {
}
Widget _buildBody(BuildContext context) {
return new PageableList<CardModel>(
items: cardModels,
return new PageableList(
children: cardModels.map(buildCard),
itemsWrap: itemsWrap,
itemBuilder: buildCard,
scrollDirection: scrollDirection
);
}
......
......@@ -772,26 +772,30 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect
}
}
class TabBarView<T> extends PageableList<T> {
typedef Widget TabItemBuilder<T>(T item);
class TabBarView<T> extends PageableList {
TabBarView({
Key key,
List<T> items,
ItemBuilder<T> itemBuilder
}) : super(
TabItemBuilder<T> itemBuilder
}) : items = items, itemBuilder = itemBuilder, super(
key: key,
scrollDirection: ScrollDirection.horizontal,
items: items,
itemBuilder: itemBuilder,
children: items.map((T item) => itemBuilder(item)),
itemsWrap: false
) {
assert(items != null);
assert(items.length > 1);
}
final List<T> items;
final TabItemBuilder<T> itemBuilder;
_TabBarViewState createState() => new _TabBarViewState<T>();
}
class _TabBarViewState<T> extends PageableListState<T, TabBarView<T>> implements TabBarSelectionPerformanceListener {
class _TabBarViewState<T> extends PageableListState<TabBarView<T>> implements TabBarSelectionPerformanceListener {
TabBarSelectionState _selection;
List<int> _itemIndices = [0, 1];
......@@ -916,14 +920,10 @@ class _TabBarViewState<T> extends PageableListState<T, TabBarView<T>> implements
return settleScrollOffset();
}
List<Widget> buildItems(BuildContext context, int start, int count) {
Widget buildContent(BuildContext context) {
TabBarSelectionState<T> newSelection = TabBarSelection.of(context);
if (_selection != newSelection)
_initSelection(newSelection);
return _itemIndices
.skip(start)
.take(count)
.map((int i) => config.itemBuilder(context, config.items[i], i))
.toList();
return super.buildContent(context);
}
}
......@@ -28,7 +28,6 @@ class RenderList extends RenderVirtualViewport<ListParentData> implements HasScr
paintOffset: paintOffset,
callback: callback
) {
assert(itemExtent != null);
addAll(children);
}
......@@ -75,6 +74,8 @@ class RenderList extends RenderVirtualViewport<ListParentData> implements HasScr
}
double get _preferredExtent {
if (itemExtent == null)
return double.INFINITY;
double extent = itemExtent * virtualChildCount;
if (padding != null)
extent += _scrollAxisPadding;
......@@ -118,8 +119,16 @@ class RenderList extends RenderVirtualViewport<ListParentData> implements HasScr
}
void performLayout() {
size = new Size(constraints.maxWidth,
constraints.constrainHeight(_preferredExtent));
switch (scrollDirection) {
case ScrollDirection.vertical:
size = new Size(constraints.maxWidth,
constraints.constrainHeight(_preferredExtent));
break;
case ScrollDirection.horizontal:
size = new Size(constraints.constrainWidth(_preferredExtent),
constraints.maxHeight);
break;
}
if (callback != null)
invokeLayoutCallback(callback);
......@@ -136,15 +145,15 @@ class RenderList extends RenderVirtualViewport<ListParentData> implements HasScr
switch (scrollDirection) {
case ScrollDirection.vertical:
itemWidth = math.max(0, size.width - (padding == null ? 0.0 : padding.horizontal));
itemHeight = itemExtent;
itemHeight = itemExtent ?? size.height;
y = padding != null ? padding.top : 0.0;
dy = itemExtent;
dy = itemHeight;
break;
case ScrollDirection.horizontal:
itemWidth = itemExtent;
itemWidth = itemExtent ?? size.width;
itemHeight = math.max(0, size.height - (padding == null ? 0.0 : padding.vertical));
x = padding != null ? padding.left : 0.0;
dx = itemExtent;
dx = itemWidth;
break;
}
......
......@@ -220,80 +220,3 @@ class _HomogeneousViewportElement extends _ViewportBaseElement<HomogeneousViewpo
return widget.itemCount != null ? widget.itemCount * widget.itemExtent : double.INFINITY;
}
}
class HomogeneousPageViewport extends _ViewportBase {
HomogeneousPageViewport({
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
ScrollDirection direction: ScrollDirection.vertical,
double startOffset: 0.0,
Painter overlayPainter
}) : super(
key: key,
builder: builder,
itemsWrap: itemsWrap,
itemCount: itemCount,
direction: direction,
startOffset: startOffset,
overlayPainter: overlayPainter
);
_HomogeneousPageViewportElement createElement() => new _HomogeneousPageViewportElement(this);
}
class _HomogeneousPageViewportElement extends _ViewportBaseElement<HomogeneousPageViewport> {
_HomogeneousPageViewportElement(HomogeneousPageViewport 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 itemExtent = widget.direction == ScrollDirection.vertical ? constraints.maxHeight : constraints.maxWidth;
double offset;
if (widget.startOffset <= 0.0 && !widget.itemsWrap) {
_layoutFirstIndex = 0;
offset = -widget.startOffset * itemExtent;
} else {
_layoutFirstIndex = widget.startOffset.floor();
offset = -((widget.startOffset * itemExtent) % itemExtent);
}
if (itemExtent < double.INFINITY && widget.itemCount != null) {
final double contentExtent = itemExtent * widget.itemCount;
_layoutItemCount = contentExtent == 0.0 ? 0 : ((contentExtent - offset) / contentExtent).ceil();
if (!widget.itemsWrap)
_layoutItemCount = math.min(_layoutItemCount, widget.itemCount - _layoutFirstIndex);
} else {
assert(() {
'This HomogeneousPageViewport 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 HomogeneousPageViewport (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 == ScrollDirection.vertical ? BlockDirection.vertical : BlockDirection.horizontal;
renderObject.itemExtent = itemExtent;
renderObject.minExtent = itemExtent;
renderObject.startOffset = offset;
renderObject.overlayPainter = widget.overlayPainter;
}, building: true);
}
double getTotalExtent(BoxConstraints constraints) {
double itemExtent = widget.direction == ScrollDirection.vertical ? constraints.maxHeight : constraints.maxWidth;
return widget.itemCount != null ? widget.itemCount * itemExtent : double.INFINITY;
}
}
......@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/gestures.dart';
......@@ -11,8 +10,8 @@ import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'framework.dart';
import 'homogeneous_viewport.dart';
import 'scrollable.dart';
import 'virtual_viewport.dart';
/// Controls what alignment items use when settling.
enum ItemsSnapAlignment {
......@@ -22,7 +21,7 @@ enum ItemsSnapAlignment {
typedef void PageChangedCallback(int newPage);
class PageableList<T> extends Scrollable {
class PageableList extends Scrollable {
PageableList({
Key key,
initialScrollOffset,
......@@ -32,14 +31,13 @@ class PageableList<T> extends Scrollable {
ScrollListener onScrollEnd,
SnapOffsetCallback snapOffsetCallback,
double snapAlignmentOffset: 0.0,
this.items,
this.itemBuilder,
this.itemsWrap: false,
this.itemsSnapAlignment: ItemsSnapAlignment.adjacentItem,
this.onPageChanged,
this.scrollableListPainter,
this.duration: const Duration(milliseconds: 200),
this.curve: Curves.ease
this.curve: Curves.ease,
this.children
}) : super(
key: key,
initialScrollOffset: initialScrollOffset,
......@@ -51,21 +49,19 @@ class PageableList<T> extends Scrollable {
snapAlignmentOffset: snapAlignmentOffset
);
final List<T> items;
final ItemBuilder<T> itemBuilder;
final ItemsSnapAlignment itemsSnapAlignment;
final bool itemsWrap;
final ItemsSnapAlignment itemsSnapAlignment;
final PageChangedCallback onPageChanged;
final ScrollableListPainter scrollableListPainter;
final Duration duration;
final Curve curve;
final Iterable<Widget> children;
PageableListState<T, PageableList<T>> createState() => new PageableListState<T, PageableList<T>>();
PageableListState createState() => new PageableListState();
}
class PageableListState<T, Config extends PageableList<T>> extends ScrollableState<Config> {
int get itemCount => config.items?.length ?? 0;
class PageableListState<T extends PageableList> extends ScrollableState<T> {
int get itemCount => config.children?.length ?? 0;
int _previousItemCount;
double pixelToScrollOffset(double value) {
......@@ -76,7 +72,12 @@ class PageableListState<T, Config extends PageableList<T>> extends ScrollableSta
return pixelScrollExtent == 0.0 ? 0.0 : value / pixelScrollExtent;
}
void didUpdateConfig(Config oldConfig) {
void initState() {
super.initState();
_updateScrollBehavior();
}
void didUpdateConfig(PageableList oldConfig) {
super.didUpdateConfig(oldConfig);
bool scrollBehaviorUpdateNeeded = config.scrollDirection != oldConfig.scrollDirection;
......@@ -94,9 +95,7 @@ class PageableListState<T, Config extends PageableList<T>> extends ScrollableSta
}
void _updateScrollBehavior() {
// if you don't call this from build(), you must call it from setState().
if (config.scrollableListPainter != null)
config.scrollableListPainter.contentExtent = itemCount.toDouble();
config.scrollableListPainter?.contentExtent = itemCount.toDouble();
scrollTo(scrollBehavior.updateExtents(
contentExtent: itemCount.toDouble(),
containerExtent: 1.0,
......@@ -111,8 +110,7 @@ class PageableListState<T, Config extends PageableList<T>> extends ScrollableSta
void dispatchOnScroll() {
super.dispatchOnScroll();
if (config.scrollableListPainter != null)
config.scrollableListPainter.scrollOffset = scrollOffset;
config.scrollableListPainter?.scrollOffset = scrollOffset;
}
void dispatchOnScrollEnd() {
......@@ -121,17 +119,12 @@ class PageableListState<T, Config extends PageableList<T>> extends ScrollableSta
}
Widget buildContent(BuildContext context) {
if (itemCount != _previousItemCount) {
_previousItemCount = itemCount;
_updateScrollBehavior();
}
return new HomogeneousPageViewport(
builder: buildItems,
return new PageViewport(
itemsWrap: config.itemsWrap,
itemCount: itemCount,
direction: config.scrollDirection,
scrollDirection: config.scrollDirection,
startOffset: scrollOffset,
overlayPainter: config.scrollableListPainter
overlayPainter: config.scrollableListPainter,
children: config.children
);
}
......@@ -180,18 +173,94 @@ class PageableListState<T, Config extends PageableList<T>> extends ScrollableSta
.then(_notifyPageChanged);
}
List<Widget> buildItems(BuildContext context, int start, int count) {
final List<Widget> result = new List<Widget>();
final int begin = config.itemsWrap ? start : math.max(0, start);
final int end = config.itemsWrap ? begin + count : math.min(begin + count, itemCount);
for (int i = begin; i < end; ++i)
result.add(config.itemBuilder(context, config.items[i % itemCount], i));
assert(result.every((Widget item) => item.key != null));
return result;
}
void _notifyPageChanged(_) {
if (config.onPageChanged != null)
config.onPageChanged(itemCount == 0 ? 0 : scrollOffset.floor() % itemCount);
}
}
class PageViewport extends VirtualViewport {
PageViewport({
Key key,
this.startOffset: 0.0,
this.scrollDirection: ScrollDirection.vertical,
this.itemsWrap: false,
this.overlayPainter,
this.children
}) {
assert(scrollDirection != null);
}
final double startOffset;
final ScrollDirection scrollDirection;
final bool itemsWrap;
final Painter overlayPainter;
final Iterable<Widget> children;
RenderList createRenderObject() => new RenderList();
_PageViewportElement createElement() => new _PageViewportElement(this);
}
class _PageViewportElement extends VirtualViewportElement<PageViewport> {
_PageViewportElement(PageViewport widget) : super(widget);
RenderList get renderObject => super.renderObject;
int get materializedChildBase => _materializedChildBase;
int _materializedChildBase;
int get materializedChildCount => _materializedChildCount;
int _materializedChildCount;
double get startOffsetBase => _repaintOffsetBase;
double _repaintOffsetBase;
double get startOffsetLimit =>_repaintOffsetLimit;
double _repaintOffsetLimit;
double get paintOffset {
if (_containerExtent == null)
return 0.0;
return -(widget.startOffset - startOffsetBase) * _containerExtent;
}
void updateRenderObject() {
renderObject.scrollDirection = widget.scrollDirection;
renderObject.overlayPainter = widget.overlayPainter;
super.updateRenderObject();
}
double _containerExtent;
double _getContainerExtentFromRenderObject() {
switch (widget.scrollDirection) {
case ScrollDirection.vertical:
return renderObject.size.height;
case ScrollDirection.horizontal:
return renderObject.size.width;
}
}
void layout(BoxConstraints constraints) {
int length = renderObject.virtualChildCount;
_containerExtent = _getContainerExtentFromRenderObject();
_materializedChildBase = widget.startOffset.floor();
int materializedChildLimit = (widget.startOffset + 1.0).ceil();
if (!widget.itemsWrap) {
_materializedChildBase = _materializedChildBase.clamp(0, length);
materializedChildLimit = materializedChildLimit.clamp(0, length);
} else if (length == 0) {
materializedChildLimit = _materializedChildBase;
}
_materializedChildCount = materializedChildLimit - _materializedChildBase;
_repaintOffsetBase = _materializedChildBase.toDouble();
_repaintOffsetLimit = (materializedChildLimit - 1).toDouble();
super.layout(constraints);
}
}
......@@ -664,8 +664,6 @@ abstract class ScrollableWidgetListState<T extends ScrollableWidgetList> extends
}
typedef Widget ItemBuilder<T>(BuildContext context, T item, int index);
/// 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.
......
......@@ -116,11 +116,11 @@ class _GridViewportElement extends VirtualViewportElement<GridViewport> {
int get materializedChildCount => _materializedChildCount;
int _materializedChildCount;
double get repaintOffsetBase => _repaintOffsetBase;
double _repaintOffsetBase;
double get startOffsetBase => _startOffsetBase;
double _startOffsetBase;
double get repaintOffsetLimit =>_repaintOffsetLimit;
double _repaintOffsetLimit;
double get startOffsetLimit =>_startOffsetLimit;
double _startOffsetLimit;
void updateRenderObject() {
renderObject.delegate = widget.delegate;
......@@ -141,8 +141,8 @@ class _GridViewportElement extends VirtualViewportElement<GridViewport> {
_materializedChildBase = (materializedRowBase * _specification.columnCount).clamp(0, renderObject.virtualChildCount);
_materializedChildCount = (materializedRowLimit * _specification.columnCount).clamp(0, renderObject.virtualChildCount) - _materializedChildBase;
_repaintOffsetBase = _specification.rowOffsets[materializedRowBase];
_repaintOffsetLimit = _specification.rowOffsets[materializedRowLimit] - containerExtent;
_startOffsetBase = _specification.rowOffsets[materializedRowBase];
_startOffsetLimit = _specification.rowOffsets[materializedRowLimit] - containerExtent;
super.layout(constraints);
......
......@@ -129,11 +129,11 @@ class _ListViewportElement extends VirtualViewportElement<ListViewport> {
int get materializedChildCount => _materializedChildCount;
int _materializedChildCount;
double get repaintOffsetBase => _repaintOffsetBase;
double _repaintOffsetBase;
double get startOffsetBase => _startOffsetBase;
double _startOffsetBase;
double get repaintOffsetLimit =>_repaintOffsetLimit;
double _repaintOffsetLimit;
double get startOffsetLimit =>_startOffsetLimit;
double _startOffsetLimit;
void updateRenderObject() {
renderObject.scrollDirection = widget.scrollDirection;
......@@ -156,21 +156,25 @@ class _ListViewportElement extends VirtualViewportElement<ListViewport> {
}
void layout(BoxConstraints constraints) {
int length = renderObject.virtualChildCount;
final int length = renderObject.virtualChildCount;
final double itemExtent = widget.itemExtent;
double contentExtent = widget.itemExtent * length;
double containerExtent = _getContainerExtentFromRenderObject();
_materializedChildBase = math.max(0, widget.startOffset ~/ widget.itemExtent);
int materializedChildLimit = math.max(0, ((widget.startOffset + containerExtent) / widget.itemExtent).ceil());
_materializedChildBase = math.max(0, widget.startOffset ~/ itemExtent);
int materializedChildLimit = math.max(0, ((widget.startOffset + containerExtent) / itemExtent).ceil());
if (!widget.itemsWrap) {
_materializedChildBase = math.min(length, _materializedChildBase);
materializedChildLimit = math.min(length, materializedChildLimit);
} else if (length == 0) {
materializedChildLimit = _materializedChildBase;
}
_materializedChildCount = materializedChildLimit - _materializedChildBase;
_repaintOffsetBase = _materializedChildBase * widget.itemExtent;
_repaintOffsetLimit = materializedChildLimit * widget.itemExtent - containerExtent;
_startOffsetBase = _materializedChildBase * itemExtent;
_startOffsetLimit = materializedChildLimit * itemExtent - containerExtent;
super.layout(constraints);
......
......@@ -2,6 +2,8 @@
// 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 'basic.dart';
import 'framework.dart';
......@@ -20,8 +22,10 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
int get materializedChildBase;
int get materializedChildCount;
double get repaintOffsetBase;
double get repaintOffsetLimit;
double get startOffsetBase;
double get startOffsetLimit;
double get paintOffset => -(widget.startOffset - startOffsetBase);
List<Element> _materializedChildren = const <Element>[];
......@@ -61,10 +65,10 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
void _updatePaintOffset() {
switch (widget.scrollDirection) {
case ScrollDirection.vertical:
renderObject.paintOffset = new Offset(0.0, -(widget.startOffset - repaintOffsetBase));
renderObject.paintOffset = new Offset(0.0, paintOffset);
break;
case ScrollDirection.horizontal:
renderObject.paintOffset = new Offset(-(widget.startOffset - repaintOffsetBase), 0.0);
renderObject.paintOffset = new Offset(paintOffset, 0.0);
break;
}
}
......@@ -72,24 +76,25 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
void updateRenderObject() {
renderObject.virtualChildCount = widget.children.length;
if (repaintOffsetBase != null) {
if (startOffsetBase != null) {
_updatePaintOffset();
// If we don't already need layout, we need to request a layout if the
// viewport has shifted to expose new children.
if (!renderObject.needsLayout) {
if (repaintOffsetBase != null && widget.startOffset < repaintOffsetBase)
if (startOffsetBase != null && widget.startOffset < startOffsetBase)
renderObject.markNeedsLayout();
else if (repaintOffsetLimit != null && widget.startOffset > repaintOffsetLimit)
else if (startOffsetLimit != null && widget.startOffset > startOffsetLimit)
renderObject.markNeedsLayout();
}
}
}
void layout(BoxConstraints constraints) {
assert(repaintOffsetBase != null);
assert(repaintOffsetLimit != null);
assert(startOffsetBase != null);
assert(startOffsetLimit != null);
_updatePaintOffset();
// TODO(abarth): Set building: true here.
BuildableElement.lockState(_materializeChildren);
}
......@@ -119,11 +124,11 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
int length = renderObject.virtualChildCount;
assert(base != null);
assert(count != null);
_populateWidgets(base + count);
_populateWidgets(base < 0 ? length : math.min(length, 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];
Widget child = _widgets[(childIndex % length).abs()];
Key key = new ValueKey(child.key ?? childIndex);
newWidgets[i] = new RepaintBoundary(key: key, child: child);
}
......
......@@ -13,7 +13,7 @@ final List<GlobalKey> globalKeys = defaultPages.map((_) => new GlobalKey()).toLi
int currentPage = null;
bool itemsWrap = false;
Widget buildPage(BuildContext context, int page, int index) {
Widget buildPage(int page) {
return new Container(
key: globalKeys[page],
width: pageSize.width,
......@@ -23,9 +23,8 @@ Widget buildPage(BuildContext context, int page, int index) {
}
Widget buildFrame({ List<int> pages: defaultPages }) {
final list = new PageableList<int>(
items: pages,
itemBuilder: buildPage,
final list = new PageableList(
children: pages.map(buildPage),
itemsWrap: itemsWrap,
scrollDirection: ScrollDirection.horizontal,
onPageChanged: (int page) { currentPage = page; }
......@@ -136,7 +135,7 @@ void main() {
testWidgets((WidgetTester tester) {
currentPage = null;
itemsWrap = true;
tester.pumpWidget(buildFrame(pages: null));
tester.pumpWidget(buildFrame(pages: <int>[]));
expect(currentPage, isNull);
});
});
......
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