Merge pull request #1758 from abarth/page_anchor
Teach PageableList about scroll anchors
This commit is contained in:
commit
2c993549b5
@ -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 {
|
|||||||
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 {
|
|||||||
|
|
||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
|||||||
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> {
|
|||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
|
|
||||||
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> {
|
|||||||
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> {
|
|||||||
|
|
||||||
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();
|
switch (widget.scrollDirection) {
|
||||||
int materializedChildLimit = (widget.startOffset + 1.0).ceil();
|
case Axis.vertical:
|
||||||
|
_containerExtent = renderObject.size.height;
|
||||||
if (!widget.itemsWrap) {
|
break;
|
||||||
_materializedChildBase = _materializedChildBase.clamp(0, length);
|
case Axis.horizontal:
|
||||||
materializedChildLimit = materializedChildLimit.clamp(0, length);
|
_containerExtent = renderObject.size.width;
|
||||||
} else if (length == 0) {
|
break;
|
||||||
materializedChildLimit = _materializedChildBase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_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();
|
if (!widget.itemsWrap) {
|
||||||
_repaintOffsetLimit = (materializedChildLimit - 1).toDouble();
|
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);
|
|||||||
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) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
|||||||
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() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user