From 4ba5e712bce08354fac24bfc24639ba9df9f73d4 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Wed, 10 Feb 2016 10:32:20 -0800 Subject: [PATCH] Teach PageableList about scroll anchors --- .../lib/src/widgets/pageable_list.dart | 98 +++++++++++----- .../test/widget/pageable_list_test.dart | 110 +++++++++++++++--- 2 files changed, 163 insertions(+), 45 deletions(-) diff --git a/packages/flutter/lib/src/widgets/pageable_list.dart b/packages/flutter/lib/src/widgets/pageable_list.dart index d1c5a351eb..e08c101185 100644 --- a/packages/flutter/lib/src/widgets/pageable_list.dart +++ b/packages/flutter/lib/src/widgets/pageable_list.dart @@ -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 onPageChanged; final ScrollableListPainter scrollableListPainter; final Duration duration; final Curve curve; @@ -61,7 +61,7 @@ class PageableList extends Scrollable { } class PageableListState extends ScrollableState { - 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 extends ScrollableState { 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 extends ScrollableState { 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 extends ScrollableState { } 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 extends ScrollableState { 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 extends ScrollableState { 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 children; @@ -224,11 +240,11 @@ class _PageViewportElement extends VirtualViewportElement { 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 { 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(); + final int length = renderObject.virtualChildCount; - _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; + switch (widget.scrollDirection) { + case Axis.vertical: + _containerExtent = renderObject.size.height; + break; + case Axis.horizontal: + _containerExtent = renderObject.size.width; + break; } - _materializedChildCount = materializedChildLimit - _materializedChildBase; + 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(); - _repaintOffsetBase = _materializedChildBase.toDouble(); - _repaintOffsetLimit = (materializedChildLimit - 1).toDouble(); + 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); } } diff --git a/packages/flutter/test/widget/pageable_list_test.dart b/packages/flutter/test/widget/pageable_list_test.dart index e6d14d42fb..01ab72cf00 100644 --- a/packages/flutter/test/widget/pageable_list_test.dart +++ b/packages/flutter/test/widget/pageable_list_test.dart @@ -11,7 +11,6 @@ Size pageSize = new Size(600.0, 300.0); const List defaultPages = const [0, 1, 2, 3, 4, 5]; final List 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 pages: defaultPages }) { - final list = new PageableList( +Widget buildFrame({ + bool itemsWrap: false, + ViewportAnchor scrollAnchor: ViewportAnchor.start, + List 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: [0, 1])); + tester.pumpWidget(buildFrame(itemsWrap: true, pages: [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: [0])); + tester.pumpWidget(buildFrame(itemsWrap: true, pages: [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: [])); + tester.pumpWidget(buildFrame(itemsWrap: true, pages: [])); 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);