Commit d6ae53fe authored by Adam Barth's avatar Adam Barth

Teach ScrollableList about scroll anchors

parent efd5aea0
......@@ -140,7 +140,7 @@ class RenderList extends RenderVirtualViewport<ListParentData> {
switch (scrollDirection) {
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;
y = padding != null ? padding.top : 0.0;
dy = itemHeight;
......
......@@ -230,15 +230,16 @@ class _PageViewportElement extends VirtualViewportElement<PageViewport> {
double get startOffsetLimit =>_repaintOffsetLimit;
double _repaintOffsetLimit;
double get paintOffset {
double scrollOffsetToPixelOffset(double scrollOffset) {
if (_containerExtent == null)
return 0.0;
return -(widget.startOffset - startOffsetBase) * _containerExtent;
return super.scrollOffsetToPixelOffset(scrollOffset) * _containerExtent;
}
void updateRenderObject(PageViewport oldWidget) {
renderObject.scrollDirection = widget.scrollDirection;
renderObject.overlayPainter = widget.overlayPainter;
renderObject
..scrollDirection = widget.scrollDirection
..overlayPainter = widget.overlayPainter;
super.updateRenderObject(oldWidget);
}
......
......@@ -16,6 +16,7 @@ class ScrollableList extends Scrollable {
Key key,
double initialScrollOffset,
Axis scrollDirection: Axis.vertical,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
ScrollListener onScroll,
SnapOffsetCallback snapOffsetCallback,
double snapAlignmentOffset: 0.0,
......@@ -28,6 +29,7 @@ class ScrollableList extends Scrollable {
key: key,
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
scrollAnchor: scrollAnchor,
onScroll: onScroll,
snapOffsetCallback: snapOffsetCallback,
snapAlignmentOffset: snapAlignmentOffset
......@@ -79,6 +81,7 @@ class _ScrollableListState extends ScrollableState<ScrollableList> {
onExtentsChanged: _handleExtentsChanged,
startOffset: scrollOffset,
scrollDirection: config.scrollDirection,
scrollAnchor: config.scrollAnchor,
itemExtent: config.itemExtent,
itemsWrap: config.itemsWrap,
padding: config.padding,
......@@ -93,6 +96,7 @@ class _VirtualListViewport extends VirtualViewport {
this.onExtentsChanged,
this.startOffset,
this.scrollDirection,
this.scrollAnchor,
this.itemExtent,
this.itemsWrap,
this.padding,
......@@ -105,6 +109,7 @@ class _VirtualListViewport extends VirtualViewport {
final ExtentsChangedCallback onExtentsChanged;
final double startOffset;
final Axis scrollDirection;
final ViewportAnchor scrollAnchor;
final double itemExtent;
final bool itemsWrap;
final EdgeDims padding;
......@@ -135,6 +140,7 @@ class _VirtualListViewportElement extends VirtualViewportElement<_VirtualListVie
void updateRenderObject(_VirtualListViewport oldWidget) {
renderObject
..scrollDirection = widget.scrollDirection
..scrollAnchor = widget.scrollAnchor
..itemExtent = widget.itemExtent
..padding = widget.padding
..overlayPainter = widget.overlayPainter;
......@@ -144,36 +150,76 @@ class _VirtualListViewportElement extends VirtualViewportElement<_VirtualListVie
double _contentExtent;
double _containerExtent;
double _getContainerExtentFromRenderObject() {
void layout(BoxConstraints constraints) {
final int length = renderObject.virtualChildCount;
final double itemExtent = widget.itemExtent;
final EdgeDims padding = widget.padding ?? EdgeDims.zero;
final Size containerSize = renderObject.size;
double containerExtent;
double contentExtent;
double leadingPadding;
switch (widget.scrollDirection) {
case Axis.vertical:
return renderObject.size.height;
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:
return renderObject.size.width;
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;
}
void layout(BoxConstraints constraints) {
final int length = renderObject.virtualChildCount;
final double itemExtent = widget.itemExtent;
final EdgeDims padding = widget.padding ?? EdgeDims.zero;
if (length == 0) {
_materializedChildBase = 0;
_materializedChildCount = 0;
_startOffsetBase = 0.0;
_startOffsetLimit = double.INFINITY;
} else {
int startItem = math.max(0, (widget.startOffset + leadingPadding) ~/ itemExtent);
int limitItem = math.max(0, ((widget.startOffset + leadingPadding + containerExtent) / itemExtent).ceil());
double contentExtent = length == null ? double.INFINITY : widget.itemExtent * length + padding.top + padding.bottom;
double containerExtent = _getContainerExtentFromRenderObject();
if (!widget.itemsWrap && length != null) {
startItem = math.min(length, startItem);
limitItem = math.min(length, limitItem);
}
_materializedChildBase = math.max(0, (widget.startOffset - padding.top) ~/ itemExtent);
int materializedChildLimit = math.max(0, ((widget.startOffset + containerExtent) / itemExtent).ceil());
_materializedChildBase = startItem;
_materializedChildCount = limitItem - startItem;
_startOffsetBase = startItem * itemExtent;
_startOffsetLimit = limitItem * itemExtent - containerExtent;
if (!widget.itemsWrap && length != null) {
_materializedChildBase = math.min(length, _materializedChildBase);
materializedChildLimit = math.min(length, materializedChildLimit);
} else if (length == 0) {
materializedChildLimit = _materializedChildBase;
if (widget.scrollAnchor == ViewportAnchor.end)
_materializedChildBase = (length - _materializedChildBase - _materializedChildCount) % length;
}
_materializedChildCount = materializedChildLimit - _materializedChildBase;
_startOffsetBase = _materializedChildBase * itemExtent;
_startOffsetLimit = materializedChildLimit * itemExtent - containerExtent;
Size materializedContentSize;
switch (widget.scrollDirection) {
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);
......@@ -190,6 +236,7 @@ class ListViewport extends _VirtualListViewport with VirtualViewportIterableMixi
ExtentsChangedCallback onExtentsChanged,
double startOffset: 0.0,
Axis scrollDirection: Axis.vertical,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
double itemExtent,
bool itemsWrap: false,
EdgeDims padding,
......@@ -199,6 +246,7 @@ class ListViewport extends _VirtualListViewport with VirtualViewportIterableMixi
onExtentsChanged,
startOffset,
scrollDirection,
scrollAnchor,
itemExtent,
itemsWrap,
padding,
......@@ -218,6 +266,7 @@ class ScrollableLazyList extends Scrollable {
Key key,
double initialScrollOffset,
Axis scrollDirection: Axis.vertical,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
ScrollListener onScroll,
SnapOffsetCallback snapOffsetCallback,
double snapAlignmentOffset: 0.0,
......@@ -230,12 +279,14 @@ class ScrollableLazyList extends Scrollable {
key: key,
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
scrollAnchor: scrollAnchor,
onScroll: onScroll,
snapOffsetCallback: snapOffsetCallback,
snapAlignmentOffset: snapAlignmentOffset
) {
assert(itemExtent != null);
assert(itemBuilder != null);
assert(itemCount != null || scrollAnchor == ViewportAnchor.start);
}
final double itemExtent;
......@@ -282,6 +333,7 @@ class _ScrollableLazyListState extends ScrollableState<ScrollableLazyList> {
onExtentsChanged: _handleExtentsChanged,
startOffset: scrollOffset,
scrollDirection: config.scrollDirection,
scrollAnchor: config.scrollAnchor,
itemExtent: config.itemExtent,
itemCount: config.itemCount,
itemBuilder: config.itemBuilder,
......@@ -296,6 +348,7 @@ class LazyListViewport extends _VirtualListViewport with VirtualViewportLazyMixi
ExtentsChangedCallback onExtentsChanged,
double startOffset: 0.0,
Axis scrollDirection: Axis.vertical,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
double itemExtent,
EdgeDims padding,
Painter overlayPainter,
......@@ -305,6 +358,7 @@ class LazyListViewport extends _VirtualListViewport with VirtualViewportLazyMixi
onExtentsChanged,
startOffset,
scrollDirection,
scrollAnchor,
itemExtent,
false, // Don't support wrapping yet.
padding,
......
......@@ -13,7 +13,6 @@ typedef void ExtentsChangedCallback(double contentExtent, double containerExtent
abstract class VirtualViewport extends RenderObjectWidget {
double get startOffset;
Axis get scrollDirection;
_WidgetProvider _createWidgetProvider();
}
......@@ -33,7 +32,27 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
double get startOffsetBase;
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>[];
......@@ -71,14 +90,7 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
}
void _updatePaintOffset() {
switch (widget.scrollDirection) {
case Axis.vertical:
renderObject.paintOffset = new Offset(0.0, paintOffset);
break;
case Axis.horizontal:
renderObject.paintOffset = new Offset(paintOffset, 0.0);
break;
}
renderObject.paintOffset = scrollOffsetToPixelDelta(widget.startOffset - startOffsetBase);
}
void updateRenderObject(T oldWidget) {
......
......@@ -9,13 +9,14 @@ import 'package:test/test.dart';
const List<int> items = const <int>[0, 1, 2, 3, 4, 5];
Widget buildFrame() {
Widget buildFrame(ViewportAnchor scrollAnchor) {
return new Center(
child: new Container(
height: 50.0,
child: new ScrollableList(
itemExtent: 290.0,
scrollDirection: Axis.horizontal,
scrollAnchor: scrollAnchor,
children: items.map((int item) {
return new Container(
child: new Text('$item')
......@@ -27,9 +28,9 @@ Widget buildFrame() {
}
void main() {
test('Drag horizontally', () {
test('Drag horizontally with scroll anchor at top', () {
testWidgets((WidgetTester tester) {
tester.pumpWidget(buildFrame());
tester.pumpWidget(buildFrame(ViewportAnchor.start));
tester.pump(const Duration(seconds: 1));
tester.scroll(tester.findText('1'), const Offset(-300.0, 0.0));
......@@ -46,7 +47,7 @@ void main() {
expect(tester.findText('5'), isNull);
// 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.
tester.pump(const Duration(seconds: 1));
tester.scroll(tester.findText('3'), const Offset(-290.0, 0.0));
......@@ -117,7 +118,7 @@ void main() {
expect(tester.findText('5'), isNotNull);
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.pump(const Duration(seconds: 1));
// screen is 800px wide, and has the following items:
......@@ -147,4 +148,106 @@ void main() {
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