Commit 2c993549 authored by Adam Barth's avatar Adam Barth

Merge pull request #1758 from abarth/page_anchor

Teach PageableList about scroll anchors
parents b07203a8 4ba5e712
......@@ -19,13 +19,12 @@ enum ItemsSnapAlignment {
adjacentItem
}
typedef void PageChangedCallback(int newPage);
class PageableList extends Scrollable {
PageableList({
Key key,
initialScrollOffset,
double initialScrollOffset,
Axis scrollDirection: Axis.vertical,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
ScrollListener onScrollStart,
ScrollListener onScroll,
ScrollListener onScrollEnd,
......@@ -42,6 +41,7 @@ class PageableList extends Scrollable {
key: key,
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
scrollAnchor: scrollAnchor,
onScrollStart: onScrollStart,
onScroll: onScroll,
onScrollEnd: onScrollEnd,
......@@ -51,7 +51,7 @@ class PageableList extends Scrollable {
final bool itemsWrap;
final ItemsSnapAlignment itemsSnapAlignment;
final PageChangedCallback onPageChanged;
final ValueChanged<int> onPageChanged;
final ScrollableListPainter scrollableListPainter;
final Duration duration;
final Curve curve;
......@@ -61,7 +61,7 @@ class PageableList extends Scrollable {
}
class PageableListState<T extends PageableList> extends ScrollableState<T> {
int get itemCount => config.children?.length ?? 0;
int get _itemCount => config.children?.length ?? 0;
int _previousItemCount;
double get _pixelsPerScrollUnit {
......@@ -85,6 +85,19 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> {
return super.scrollOffsetToPixelOffset(scrollOffset * _pixelsPerScrollUnit);
}
int _scrollOffsetToPageIndex(double scrollOffset) {
int itemCount = _itemCount;
if (itemCount == 0)
return 0;
int scrollIndex = scrollOffset.floor();
switch (config.scrollAnchor) {
case ViewportAnchor.start:
return scrollIndex % itemCount;
case ViewportAnchor.end:
return (_itemCount - scrollIndex - 1) % itemCount;
}
}
void initState() {
super.initState();
_updateScrollBehavior();
......@@ -98,8 +111,8 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> {
if (config.itemsWrap != oldConfig.itemsWrap)
scrollBehaviorUpdateNeeded = true;
if (itemCount != _previousItemCount) {
_previousItemCount = itemCount;
if (_itemCount != _previousItemCount) {
_previousItemCount = _itemCount;
scrollBehaviorUpdateNeeded = true;
}
......@@ -108,9 +121,9 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> {
}
void _updateScrollBehavior() {
config.scrollableListPainter?.contentExtent = itemCount.toDouble();
config.scrollableListPainter?.contentExtent = _itemCount.toDouble();
scrollTo(scrollBehavior.updateExtents(
contentExtent: itemCount.toDouble(),
contentExtent: _itemCount.toDouble(),
containerExtent: 1.0,
scrollOffset: scrollOffset
));
......@@ -135,6 +148,7 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> {
return new PageViewport(
itemsWrap: config.itemsWrap,
scrollDirection: config.scrollDirection,
scrollAnchor: config.scrollAnchor,
startOffset: scrollOffset,
overlayPainter: config.scrollableListPainter,
children: config.children
......@@ -187,7 +201,7 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> {
void _notifyPageChanged(_) {
if (config.onPageChanged != null)
config.onPageChanged(itemCount == 0 ? 0 : scrollOffset.floor() % itemCount);
config.onPageChanged(_scrollOffsetToPageIndex(scrollOffset));
}
}
......@@ -195,6 +209,7 @@ class PageViewport extends VirtualViewport with VirtualViewportIterableMixin {
PageViewport({
this.startOffset: 0.0,
this.scrollDirection: Axis.vertical,
this.scrollAnchor: ViewportAnchor.start,
this.itemsWrap: false,
this.overlayPainter,
this.children
......@@ -204,6 +219,7 @@ class PageViewport extends VirtualViewport with VirtualViewportIterableMixin {
final double startOffset;
final Axis scrollDirection;
final ViewportAnchor scrollAnchor;
final bool itemsWrap;
final Painter overlayPainter;
final Iterable<Widget> children;
......@@ -224,11 +240,11 @@ class _PageViewportElement extends VirtualViewportElement<PageViewport> {
int get materializedChildCount => _materializedChildCount;
int _materializedChildCount;
double get startOffsetBase => _repaintOffsetBase;
double _repaintOffsetBase;
double get startOffsetBase => _startOffsetBase;
double _startOffsetBase;
double get startOffsetLimit =>_repaintOffsetLimit;
double _repaintOffsetLimit;
double get startOffsetLimit =>_startOffsetLimit;
double _startOffsetLimit;
double scrollOffsetToPixelOffset(double scrollOffset) {
if (_containerExtent == null)
......@@ -245,34 +261,56 @@ class _PageViewportElement extends VirtualViewportElement<PageViewport> {
double _containerExtent;
double _getContainerExtentFromRenderObject() {
void _updateViewportDimensions() {
final Size containerSize = renderObject.size;
Size materializedContentSize;
switch (widget.scrollDirection) {
case Axis.vertical:
return renderObject.size.height;
materializedContentSize = new Size(containerSize.width, _materializedChildCount * containerSize.height);
break;
case Axis.horizontal:
return renderObject.size.width;
materializedContentSize = new Size(_materializedChildCount * containerSize.width, containerSize.height);
break;
}
renderObject.dimensions = new ViewportDimensions(containerSize: containerSize, contentSize: materializedContentSize);
}
void layout(BoxConstraints constraints) {
int length = renderObject.virtualChildCount;
_containerExtent = _getContainerExtentFromRenderObject();
_materializedChildBase = widget.startOffset.floor();
int materializedChildLimit = (widget.startOffset + 1.0).ceil();
final int length = renderObject.virtualChildCount;
if (!widget.itemsWrap) {
_materializedChildBase = _materializedChildBase.clamp(0, length);
materializedChildLimit = materializedChildLimit.clamp(0, length);
} else if (length == 0) {
materializedChildLimit = _materializedChildBase;
switch (widget.scrollDirection) {
case Axis.vertical:
_containerExtent = renderObject.size.height;
break;
case Axis.horizontal:
_containerExtent = renderObject.size.width;
break;
}
_materializedChildCount = materializedChildLimit - _materializedChildBase;
_repaintOffsetBase = _materializedChildBase.toDouble();
_repaintOffsetLimit = (materializedChildLimit - 1).toDouble();
if (length == 0) {
_materializedChildBase = 0;
_materializedChildCount = 0;
_startOffsetBase = 0.0;
_startOffsetLimit = double.INFINITY;
} else {
int startItem = widget.startOffset.floor();
int limitItem = (widget.startOffset + 1.0).ceil();
if (!widget.itemsWrap) {
startItem = startItem.clamp(0, length);
limitItem = limitItem.clamp(0, length);
}
_materializedChildBase = startItem;
_materializedChildCount = limitItem - startItem;
_startOffsetBase = startItem.toDouble();
_startOffsetLimit = (limitItem - 1).toDouble();
if (widget.scrollAnchor == ViewportAnchor.end)
_materializedChildBase = (length - _materializedChildBase - _materializedChildCount) % length;
}
_updateViewportDimensions();
super.layout(constraints);
}
}
......@@ -11,7 +11,6 @@ Size pageSize = new Size(600.0, 300.0);
const List<int> defaultPages = const <int>[0, 1, 2, 3, 4, 5];
final List<GlobalKey> globalKeys = defaultPages.map((_) => new GlobalKey()).toList();
int currentPage = null;
bool itemsWrap = false;
Widget buildPage(int page) {
return new Container(
......@@ -22,11 +21,16 @@ Widget buildPage(int page) {
);
}
Widget buildFrame({ List<int> pages: defaultPages }) {
final list = new PageableList(
Widget buildFrame({
bool itemsWrap: false,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
List<int> pages: defaultPages
}) {
final PageableList list = new PageableList(
children: pages.map(buildPage),
itemsWrap: itemsWrap,
scrollDirection: Axis.horizontal,
scrollAnchor: scrollAnchor,
onPageChanged: (int page) { currentPage = page; }
);
......@@ -58,7 +62,6 @@ void main() {
test('PageableList with itemsWrap: false', () {
testWidgets((WidgetTester tester) {
currentPage = null;
itemsWrap = false;
tester.pumpWidget(buildFrame());
expect(currentPage, isNull);
pageLeft(tester);
......@@ -86,11 +89,46 @@ void main() {
});
});
test('PageableList with end scroll anchor', () {
testWidgets((WidgetTester tester) {
currentPage = 5;
tester.pumpWidget(buildFrame(scrollAnchor: ViewportAnchor.end));
pageRight(tester);
expect(currentPage, equals(4));
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNull);
expect(tester.findText('2'), isNull);
expect(tester.findText('3'), isNull);
expect(tester.findText('4'), isNotNull);
expect(tester.findText('5'), isNull);
pageLeft(tester);
expect(currentPage, equals(5));
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNull);
expect(tester.findText('2'), isNull);
expect(tester.findText('3'), isNull);
expect(tester.findText('4'), isNull);
expect(tester.findText('5'), isNotNull);
pageLeft(tester);
expect(currentPage, equals(5));
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNull);
expect(tester.findText('2'), isNull);
expect(tester.findText('3'), isNull);
expect(tester.findText('4'), isNull);
expect(tester.findText('5'), isNotNull);
});
});
test('PageableList with itemsWrap: true', () {
testWidgets((WidgetTester tester) {
currentPage = null;
itemsWrap = true;
tester.pumpWidget(buildFrame());
tester.pumpWidget(buildFrame(itemsWrap: true));
expect(currentPage, isNull);
pageLeft(tester);
expect(currentPage, equals(1));
......@@ -101,11 +139,56 @@ void main() {
});
});
test('PageableList with end and itemsWrap: true', () {
testWidgets((WidgetTester tester) {
currentPage = 5;
tester.pumpWidget(buildFrame(itemsWrap: true, scrollAnchor: ViewportAnchor.end));
pageRight(tester);
expect(currentPage, equals(4));
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNull);
expect(tester.findText('2'), isNull);
expect(tester.findText('3'), isNull);
expect(tester.findText('4'), isNotNull);
expect(tester.findText('5'), isNull);
pageLeft(tester);
expect(currentPage, equals(5));
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNull);
expect(tester.findText('2'), isNull);
expect(tester.findText('3'), isNull);
expect(tester.findText('4'), isNull);
expect(tester.findText('5'), isNotNull);
pageLeft(tester);
expect(currentPage, equals(0));
expect(tester.findText('0'), isNotNull);
expect(tester.findText('1'), isNull);
expect(tester.findText('2'), isNull);
expect(tester.findText('3'), isNull);
expect(tester.findText('4'), isNull);
expect(tester.findText('5'), isNull);
pageLeft(tester);
expect(currentPage, equals(1));
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNotNull);
expect(tester.findText('2'), isNull);
expect(tester.findText('3'), isNull);
expect(tester.findText('4'), isNull);
expect(tester.findText('5'), isNull);
});
});
test('PageableList with two items', () {
testWidgets((WidgetTester tester) {
currentPage = null;
itemsWrap = true;
tester.pumpWidget(buildFrame(pages: <int>[0, 1]));
tester.pumpWidget(buildFrame(itemsWrap: true, pages: <int>[0, 1]));
expect(currentPage, isNull);
pageLeft(tester);
expect(currentPage, equals(1));
......@@ -119,8 +202,7 @@ void main() {
test('PageableList with one item', () {
testWidgets((WidgetTester tester) {
currentPage = null;
itemsWrap = true;
tester.pumpWidget(buildFrame(pages: <int>[0]));
tester.pumpWidget(buildFrame(itemsWrap: true, pages: <int>[0]));
expect(currentPage, isNull);
pageLeft(tester);
expect(currentPage, equals(0));
......@@ -134,8 +216,7 @@ void main() {
test('PageableList with no items', () {
testWidgets((WidgetTester tester) {
currentPage = null;
itemsWrap = true;
tester.pumpWidget(buildFrame(pages: <int>[]));
tester.pumpWidget(buildFrame(itemsWrap: true, pages: <int>[]));
expect(currentPage, isNull);
});
});
......@@ -144,9 +225,8 @@ void main() {
testWidgets((WidgetTester tester) {
tester.pumpWidget(new Container());
currentPage = null;
itemsWrap = true;
tester.pumpWidget(buildFrame());
tester.pumpWidget(buildFrame(itemsWrap: true));
expect(currentPage, isNull);
pageRight(tester);
expect(currentPage, equals(5));
......@@ -156,7 +236,7 @@ void main() {
expect(box.size.height, equals(pageSize.height));
pageSize = new Size(pageSize.height, pageSize.width);
tester.pumpWidget(buildFrame());
tester.pumpWidget(buildFrame(itemsWrap: true));
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), 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