Commit 4ba5e712 authored by Adam Barth's avatar Adam Barth

Teach PageableList about scroll anchors

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