Commit e64d93a5 authored by Adam Barth's avatar Adam Barth

Switch PageableList over to using RenderList

This patch moves PageableList off HomogeneousViewport and onto RenderList and
friends, making it match the new ScrollableList.
parent a271eb56
......@@ -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