Commit b07203a8 authored by Adam Barth's avatar Adam Barth

Merge pull request #1749 from abarth/viewport_scroll_anchor

Teach ScrollableList about scroll anchors
parents 244239d9 d6ae53fe
...@@ -140,7 +140,7 @@ class RenderList extends RenderVirtualViewport<ListParentData> { ...@@ -140,7 +140,7 @@ class RenderList extends RenderVirtualViewport<ListParentData> {
switch (scrollDirection) { switch (scrollDirection) {
case Axis.vertical: case Axis.vertical:
itemWidth = math.max(0, size.width - (padding == null ? 0.0 : padding.horizontal)); itemWidth = math.max(0.0, size.width - (padding == null ? 0.0 : padding.horizontal));
itemHeight = itemExtent ?? size.height; itemHeight = itemExtent ?? size.height;
y = padding != null ? padding.top : 0.0; y = padding != null ? padding.top : 0.0;
dy = itemHeight; dy = itemHeight;
......
...@@ -230,15 +230,16 @@ class _PageViewportElement extends VirtualViewportElement<PageViewport> { ...@@ -230,15 +230,16 @@ class _PageViewportElement extends VirtualViewportElement<PageViewport> {
double get startOffsetLimit =>_repaintOffsetLimit; double get startOffsetLimit =>_repaintOffsetLimit;
double _repaintOffsetLimit; double _repaintOffsetLimit;
double get paintOffset { double scrollOffsetToPixelOffset(double scrollOffset) {
if (_containerExtent == null) if (_containerExtent == null)
return 0.0; return 0.0;
return -(widget.startOffset - startOffsetBase) * _containerExtent; return super.scrollOffsetToPixelOffset(scrollOffset) * _containerExtent;
} }
void updateRenderObject(PageViewport oldWidget) { void updateRenderObject(PageViewport oldWidget) {
renderObject.scrollDirection = widget.scrollDirection; renderObject
renderObject.overlayPainter = widget.overlayPainter; ..scrollDirection = widget.scrollDirection
..overlayPainter = widget.overlayPainter;
super.updateRenderObject(oldWidget); super.updateRenderObject(oldWidget);
} }
......
...@@ -16,6 +16,7 @@ class ScrollableList extends Scrollable { ...@@ -16,6 +16,7 @@ class ScrollableList extends Scrollable {
Key key, Key key,
double initialScrollOffset, double initialScrollOffset,
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
ScrollListener onScroll, ScrollListener onScroll,
SnapOffsetCallback snapOffsetCallback, SnapOffsetCallback snapOffsetCallback,
double snapAlignmentOffset: 0.0, double snapAlignmentOffset: 0.0,
...@@ -28,6 +29,7 @@ class ScrollableList extends Scrollable { ...@@ -28,6 +29,7 @@ class ScrollableList extends Scrollable {
key: key, key: key,
initialScrollOffset: initialScrollOffset, initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
scrollAnchor: scrollAnchor,
onScroll: onScroll, onScroll: onScroll,
snapOffsetCallback: snapOffsetCallback, snapOffsetCallback: snapOffsetCallback,
snapAlignmentOffset: snapAlignmentOffset snapAlignmentOffset: snapAlignmentOffset
...@@ -79,6 +81,7 @@ class _ScrollableListState extends ScrollableState<ScrollableList> { ...@@ -79,6 +81,7 @@ class _ScrollableListState extends ScrollableState<ScrollableList> {
onExtentsChanged: _handleExtentsChanged, onExtentsChanged: _handleExtentsChanged,
startOffset: scrollOffset, startOffset: scrollOffset,
scrollDirection: config.scrollDirection, scrollDirection: config.scrollDirection,
scrollAnchor: config.scrollAnchor,
itemExtent: config.itemExtent, itemExtent: config.itemExtent,
itemsWrap: config.itemsWrap, itemsWrap: config.itemsWrap,
padding: config.padding, padding: config.padding,
...@@ -93,6 +96,7 @@ class _VirtualListViewport extends VirtualViewport { ...@@ -93,6 +96,7 @@ class _VirtualListViewport extends VirtualViewport {
this.onExtentsChanged, this.onExtentsChanged,
this.startOffset, this.startOffset,
this.scrollDirection, this.scrollDirection,
this.scrollAnchor,
this.itemExtent, this.itemExtent,
this.itemsWrap, this.itemsWrap,
this.padding, this.padding,
...@@ -105,6 +109,7 @@ class _VirtualListViewport extends VirtualViewport { ...@@ -105,6 +109,7 @@ class _VirtualListViewport extends VirtualViewport {
final ExtentsChangedCallback onExtentsChanged; final ExtentsChangedCallback onExtentsChanged;
final double startOffset; final double startOffset;
final Axis scrollDirection; final Axis scrollDirection;
final ViewportAnchor scrollAnchor;
final double itemExtent; final double itemExtent;
final bool itemsWrap; final bool itemsWrap;
final EdgeDims padding; final EdgeDims padding;
...@@ -135,6 +140,7 @@ class _VirtualListViewportElement extends VirtualViewportElement<_VirtualListVie ...@@ -135,6 +140,7 @@ class _VirtualListViewportElement extends VirtualViewportElement<_VirtualListVie
void updateRenderObject(_VirtualListViewport oldWidget) { void updateRenderObject(_VirtualListViewport oldWidget) {
renderObject renderObject
..scrollDirection = widget.scrollDirection ..scrollDirection = widget.scrollDirection
..scrollAnchor = widget.scrollAnchor
..itemExtent = widget.itemExtent ..itemExtent = widget.itemExtent
..padding = widget.padding ..padding = widget.padding
..overlayPainter = widget.overlayPainter; ..overlayPainter = widget.overlayPainter;
...@@ -144,36 +150,76 @@ class _VirtualListViewportElement extends VirtualViewportElement<_VirtualListVie ...@@ -144,36 +150,76 @@ class _VirtualListViewportElement extends VirtualViewportElement<_VirtualListVie
double _contentExtent; double _contentExtent;
double _containerExtent; double _containerExtent;
double _getContainerExtentFromRenderObject() {
switch (widget.scrollDirection) {
case Axis.vertical:
return renderObject.size.height;
case Axis.horizontal:
return renderObject.size.width;
}
}
void layout(BoxConstraints constraints) { void layout(BoxConstraints constraints) {
final int length = renderObject.virtualChildCount; final int length = renderObject.virtualChildCount;
final double itemExtent = widget.itemExtent; final double itemExtent = widget.itemExtent;
final EdgeDims padding = widget.padding ?? EdgeDims.zero; final EdgeDims padding = widget.padding ?? EdgeDims.zero;
final Size containerSize = renderObject.size;
double contentExtent = length == null ? double.INFINITY : widget.itemExtent * length + padding.top + padding.bottom; double containerExtent;
double containerExtent = _getContainerExtentFromRenderObject(); double contentExtent;
double leadingPadding;
_materializedChildBase = math.max(0, (widget.startOffset - padding.top) ~/ itemExtent); switch (widget.scrollDirection) {
int materializedChildLimit = math.max(0, ((widget.startOffset + containerExtent) / itemExtent).ceil()); case Axis.vertical:
containerExtent = containerSize.height;
contentExtent = length == null ? double.INFINITY : widget.itemExtent * length + padding.vertical;
switch (widget.scrollAnchor) {
case ViewportAnchor.start:
leadingPadding = padding.top;
break;
case ViewportAnchor.end:
leadingPadding = padding.bottom;
break;
}
break;
case Axis.horizontal:
containerExtent = renderObject.size.width;
contentExtent = length == null ? double.INFINITY : widget.itemExtent * length + padding.horizontal;
switch (widget.scrollAnchor) {
case ViewportAnchor.start:
leadingPadding = padding.left;
break;
case ViewportAnchor.end:
leadingPadding = padding.right;
break;
}
break;
}
if (!widget.itemsWrap && length != null) { if (length == 0) {
_materializedChildBase = math.min(length, _materializedChildBase); _materializedChildBase = 0;
materializedChildLimit = math.min(length, materializedChildLimit); _materializedChildCount = 0;
} else if (length == 0) { _startOffsetBase = 0.0;
materializedChildLimit = _materializedChildBase; _startOffsetLimit = double.INFINITY;
} else {
int startItem = math.max(0, (widget.startOffset + leadingPadding) ~/ itemExtent);
int limitItem = math.max(0, ((widget.startOffset + leadingPadding + containerExtent) / itemExtent).ceil());
if (!widget.itemsWrap && length != null) {
startItem = math.min(length, startItem);
limitItem = math.min(length, limitItem);
}
_materializedChildBase = startItem;
_materializedChildCount = limitItem - startItem;
_startOffsetBase = startItem * itemExtent;
_startOffsetLimit = limitItem * itemExtent - containerExtent;
if (widget.scrollAnchor == ViewportAnchor.end)
_materializedChildBase = (length - _materializedChildBase - _materializedChildCount) % length;
} }
_materializedChildCount = materializedChildLimit - _materializedChildBase; Size materializedContentSize;
_startOffsetBase = _materializedChildBase * itemExtent; switch (widget.scrollDirection) {
_startOffsetLimit = materializedChildLimit * itemExtent - containerExtent; case Axis.vertical:
materializedContentSize = new Size(containerSize.width, _materializedChildCount * itemExtent);
break;
case Axis.horizontal:
materializedContentSize = new Size(_materializedChildCount * itemExtent, containerSize.height);
break;
}
renderObject.dimensions = new ViewportDimensions(containerSize: containerSize, contentSize: materializedContentSize);
super.layout(constraints); super.layout(constraints);
...@@ -190,6 +236,7 @@ class ListViewport extends _VirtualListViewport with VirtualViewportIterableMixi ...@@ -190,6 +236,7 @@ class ListViewport extends _VirtualListViewport with VirtualViewportIterableMixi
ExtentsChangedCallback onExtentsChanged, ExtentsChangedCallback onExtentsChanged,
double startOffset: 0.0, double startOffset: 0.0,
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
double itemExtent, double itemExtent,
bool itemsWrap: false, bool itemsWrap: false,
EdgeDims padding, EdgeDims padding,
...@@ -199,6 +246,7 @@ class ListViewport extends _VirtualListViewport with VirtualViewportIterableMixi ...@@ -199,6 +246,7 @@ class ListViewport extends _VirtualListViewport with VirtualViewportIterableMixi
onExtentsChanged, onExtentsChanged,
startOffset, startOffset,
scrollDirection, scrollDirection,
scrollAnchor,
itemExtent, itemExtent,
itemsWrap, itemsWrap,
padding, padding,
...@@ -218,6 +266,7 @@ class ScrollableLazyList extends Scrollable { ...@@ -218,6 +266,7 @@ class ScrollableLazyList extends Scrollable {
Key key, Key key,
double initialScrollOffset, double initialScrollOffset,
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
ScrollListener onScroll, ScrollListener onScroll,
SnapOffsetCallback snapOffsetCallback, SnapOffsetCallback snapOffsetCallback,
double snapAlignmentOffset: 0.0, double snapAlignmentOffset: 0.0,
...@@ -230,12 +279,14 @@ class ScrollableLazyList extends Scrollable { ...@@ -230,12 +279,14 @@ class ScrollableLazyList extends Scrollable {
key: key, key: key,
initialScrollOffset: initialScrollOffset, initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
scrollAnchor: scrollAnchor,
onScroll: onScroll, onScroll: onScroll,
snapOffsetCallback: snapOffsetCallback, snapOffsetCallback: snapOffsetCallback,
snapAlignmentOffset: snapAlignmentOffset snapAlignmentOffset: snapAlignmentOffset
) { ) {
assert(itemExtent != null); assert(itemExtent != null);
assert(itemBuilder != null); assert(itemBuilder != null);
assert(itemCount != null || scrollAnchor == ViewportAnchor.start);
} }
final double itemExtent; final double itemExtent;
...@@ -282,6 +333,7 @@ class _ScrollableLazyListState extends ScrollableState<ScrollableLazyList> { ...@@ -282,6 +333,7 @@ class _ScrollableLazyListState extends ScrollableState<ScrollableLazyList> {
onExtentsChanged: _handleExtentsChanged, onExtentsChanged: _handleExtentsChanged,
startOffset: scrollOffset, startOffset: scrollOffset,
scrollDirection: config.scrollDirection, scrollDirection: config.scrollDirection,
scrollAnchor: config.scrollAnchor,
itemExtent: config.itemExtent, itemExtent: config.itemExtent,
itemCount: config.itemCount, itemCount: config.itemCount,
itemBuilder: config.itemBuilder, itemBuilder: config.itemBuilder,
...@@ -296,6 +348,7 @@ class LazyListViewport extends _VirtualListViewport with VirtualViewportLazyMixi ...@@ -296,6 +348,7 @@ class LazyListViewport extends _VirtualListViewport with VirtualViewportLazyMixi
ExtentsChangedCallback onExtentsChanged, ExtentsChangedCallback onExtentsChanged,
double startOffset: 0.0, double startOffset: 0.0,
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
double itemExtent, double itemExtent,
EdgeDims padding, EdgeDims padding,
Painter overlayPainter, Painter overlayPainter,
...@@ -305,6 +358,7 @@ class LazyListViewport extends _VirtualListViewport with VirtualViewportLazyMixi ...@@ -305,6 +358,7 @@ class LazyListViewport extends _VirtualListViewport with VirtualViewportLazyMixi
onExtentsChanged, onExtentsChanged,
startOffset, startOffset,
scrollDirection, scrollDirection,
scrollAnchor,
itemExtent, itemExtent,
false, // Don't support wrapping yet. false, // Don't support wrapping yet.
padding, padding,
......
...@@ -13,7 +13,6 @@ typedef void ExtentsChangedCallback(double contentExtent, double containerExtent ...@@ -13,7 +13,6 @@ typedef void ExtentsChangedCallback(double contentExtent, double containerExtent
abstract class VirtualViewport extends RenderObjectWidget { abstract class VirtualViewport extends RenderObjectWidget {
double get startOffset; double get startOffset;
Axis get scrollDirection;
_WidgetProvider _createWidgetProvider(); _WidgetProvider _createWidgetProvider();
} }
...@@ -33,7 +32,27 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO ...@@ -33,7 +32,27 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
double get startOffsetBase; double get startOffsetBase;
double get startOffsetLimit; double get startOffsetLimit;
double get paintOffset => -(widget.startOffset - startOffsetBase); /// Returns the pixel offset for a scroll offset, accounting for the scroll
/// anchor.
double scrollOffsetToPixelOffset(double scrollOffset) {
switch (renderObject.scrollAnchor) {
case ViewportAnchor.start:
return -scrollOffset;
case ViewportAnchor.end:
return scrollOffset;
}
}
/// Returns a two-dimensional representation of the scroll offset, accounting
/// for the scroll direction and scroll anchor.
Offset scrollOffsetToPixelDelta(double scrollOffset) {
switch (renderObject.scrollDirection) {
case Axis.horizontal:
return new Offset(scrollOffsetToPixelOffset(scrollOffset), 0.0);
case Axis.vertical:
return new Offset(0.0, scrollOffsetToPixelOffset(scrollOffset));
}
}
List<Element> _materializedChildren = const <Element>[]; List<Element> _materializedChildren = const <Element>[];
...@@ -71,14 +90,7 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO ...@@ -71,14 +90,7 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
} }
void _updatePaintOffset() { void _updatePaintOffset() {
switch (widget.scrollDirection) { renderObject.paintOffset = scrollOffsetToPixelDelta(widget.startOffset - startOffsetBase);
case Axis.vertical:
renderObject.paintOffset = new Offset(0.0, paintOffset);
break;
case Axis.horizontal:
renderObject.paintOffset = new Offset(paintOffset, 0.0);
break;
}
} }
void updateRenderObject(T oldWidget) { void updateRenderObject(T oldWidget) {
......
...@@ -9,13 +9,14 @@ import 'package:test/test.dart'; ...@@ -9,13 +9,14 @@ import 'package:test/test.dart';
const List<int> items = const <int>[0, 1, 2, 3, 4, 5]; const List<int> items = const <int>[0, 1, 2, 3, 4, 5];
Widget buildFrame() { Widget buildFrame(ViewportAnchor scrollAnchor) {
return new Center( return new Center(
child: new Container( child: new Container(
height: 50.0, height: 50.0,
child: new ScrollableList( child: new ScrollableList(
itemExtent: 290.0, itemExtent: 290.0,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
scrollAnchor: scrollAnchor,
children: items.map((int item) { children: items.map((int item) {
return new Container( return new Container(
child: new Text('$item') child: new Text('$item')
...@@ -27,9 +28,9 @@ Widget buildFrame() { ...@@ -27,9 +28,9 @@ Widget buildFrame() {
} }
void main() { void main() {
test('Drag horizontally', () { test('Drag horizontally with scroll anchor at top', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
tester.pumpWidget(buildFrame()); tester.pumpWidget(buildFrame(ViewportAnchor.start));
tester.pump(const Duration(seconds: 1)); tester.pump(const Duration(seconds: 1));
tester.scroll(tester.findText('1'), const Offset(-300.0, 0.0)); tester.scroll(tester.findText('1'), const Offset(-300.0, 0.0));
...@@ -46,7 +47,7 @@ void main() { ...@@ -46,7 +47,7 @@ void main() {
expect(tester.findText('5'), isNull); expect(tester.findText('5'), isNull);
// the center of item 3 is visible, so this works; // the center of item 3 is visible, so this works;
// if item 3 was a bit wider, such that it's center was past the 800px mark, this would fail, // if item 3 was a bit wider, such that its center was past the 800px mark, this would fail,
// because it wouldn't be hit tested when scrolling from its center, as scroll() does. // because it wouldn't be hit tested when scrolling from its center, as scroll() does.
tester.pump(const Duration(seconds: 1)); tester.pump(const Duration(seconds: 1));
tester.scroll(tester.findText('3'), const Offset(-290.0, 0.0)); tester.scroll(tester.findText('3'), const Offset(-290.0, 0.0));
...@@ -117,7 +118,7 @@ void main() { ...@@ -117,7 +118,7 @@ void main() {
expect(tester.findText('5'), isNotNull); expect(tester.findText('5'), isNotNull);
tester.pumpWidget(new Container()); tester.pumpWidget(new Container());
tester.pumpWidget(buildFrame(), const Duration(seconds: 1)); tester.pumpWidget(buildFrame(ViewportAnchor.start), const Duration(seconds: 1));
tester.scroll(tester.findText('2'), const Offset(-280.0, 0.0)); tester.scroll(tester.findText('2'), const Offset(-280.0, 0.0));
tester.pump(const Duration(seconds: 1)); tester.pump(const Duration(seconds: 1));
// screen is 800px wide, and has the following items: // screen is 800px wide, and has the following items:
...@@ -147,4 +148,106 @@ void main() { ...@@ -147,4 +148,106 @@ void main() {
expect(tester.findText('5'), isNull); expect(tester.findText('5'), isNull);
}); });
}); });
test('Drag horizontally with scroll anchor at end', () {
testWidgets((WidgetTester tester) {
tester.pumpWidget(buildFrame(ViewportAnchor.end));
tester.pump(const Duration(seconds: 1));
// screen is 800px wide, and has the following items:
// -70..220 = 3
// 220..510 = 4
// 510..800 = 5
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNull);
expect(tester.findText('2'), isNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNotNull);
expect(tester.findText('5'), isNotNull);
tester.scroll(tester.findText('5'), const Offset(300.0, 0.0));
tester.pump(const Duration(seconds: 1));
// screen is 800px wide, and has the following items:
// -80..210 = 2
// 230..520 = 3
// 520..810 = 4
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNull);
expect(tester.findText('2'), isNotNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNotNull);
expect(tester.findText('5'), isNull);
// the center of item 3 is visible, so this works;
// if item 3 was a bit wider, such that its center was past the 800px mark, this would fail,
// because it wouldn't be hit tested when scrolling from its center, as scroll() does.
tester.pump(const Duration(seconds: 1));
tester.scroll(tester.findText('3'), const Offset(290.0, 0.0));
tester.pump(const Duration(seconds: 1));
// screen is 800px wide, and has the following items:
// -10..280 = 1
// 280..570 = 2
// 570..860 = 3
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNotNull);
expect(tester.findText('2'), isNotNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNull);
expect(tester.findText('5'), isNull);
tester.pump(const Duration(seconds: 1));
tester.scroll(tester.findText('3'), const Offset(0.0, 290.0));
tester.pump(const Duration(seconds: 1));
// unchanged
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNotNull);
expect(tester.findText('2'), isNotNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNull);
expect(tester.findText('5'), isNull);
tester.pump(const Duration(seconds: 1));
tester.scroll(tester.findText('2'), const Offset(290.0, 0.0));
tester.pump(const Duration(seconds: 1));
// screen is 800px wide, and has the following items:
// -10..280 = 0
// 280..570 = 1
// 570..860 = 2
expect(tester.findText('0'), isNotNull);
expect(tester.findText('1'), isNotNull);
expect(tester.findText('2'), isNotNull);
expect(tester.findText('3'), isNull);
expect(tester.findText('4'), isNull);
expect(tester.findText('5'), isNull);
tester.pump(const Duration(seconds: 1));
// at this point we can drag 60 pixels further before we hit the friction zone
// then, every pixel we drag is equivalent to half a pixel of movement
// to move item 3 entirely off screen therefore takes:
// 60 + (290-60)*2 = 520 pixels
// plus a couple more to be sure
tester.scroll(tester.findText('1'), const Offset(522.0, 0.0));
tester.pump(); // just after release
// screen is 800px wide, and has the following items:
// 280..570 = 0
// 570..860 = 1
expect(tester.findText('0'), isNotNull);
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);
tester.pump(const Duration(seconds: 1)); // a second after release
// screen is 800px wide, and has the following items:
// 0..290 = 0
// 290..580 = 1
// 580..870 = 2
expect(tester.findText('0'), isNotNull);
expect(tester.findText('1'), isNotNull);
expect(tester.findText('2'), isNotNull);
expect(tester.findText('3'), isNull);
expect(tester.findText('4'), isNull);
expect(tester.findText('5'), 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