Commit 0403ed46 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Add viewportFraction feature to PageView (#8539)

This feature lets you see a portion of the next and previous page in a
PageView.

Fixes #8408
parent bb1dea74
......@@ -18,13 +18,41 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
/// The main-axis extent of each item.
double get itemExtent;
@protected
double indexToScrollOffset(double itemExtent, int index) => itemExtent * index;
@protected
int getMinChildIndexForScrollOffset(double scrollOffset, double itemExtent) {
return itemExtent > 0.0 ? math.max(0, scrollOffset ~/ itemExtent) : 0;
}
@protected
int getMaxChildIndexForScrollOffset(double scrollOffset, double itemExtent) {
return itemExtent > 0.0 ? math.max(0, (scrollOffset / itemExtent).ceil() - 1) : 0;
}
@protected
double estimateMaxScrollOffset(SliverConstraints constraints, {
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
}) {
return childManager.estimateMaxScrollOffset(
constraints,
firstIndex: firstIndex,
lastIndex: lastIndex,
leadingScrollOffset: leadingScrollOffset,
trailingScrollOffset: trailingScrollOffset,
);
}
@override
void performLayout() {
assert(childManager.debugAssertChildListLocked());
childManager.setDidUnderflow(false);
final double itemExtent = this.itemExtent;
double indexToScrollOffset(int index) => itemExtent * index;
final double scrollOffset = constraints.scrollOffset;
assert(scrollOffset >= 0.0);
......@@ -37,8 +65,8 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
maxExtent: itemExtent,
);
final int firstIndex = itemExtent > 0.0 ? math.max(0, scrollOffset ~/ itemExtent) : 0;
final int targetLastIndex = itemExtent > 0.0 ? math.max(0, (targetEndScrollOffset / itemExtent).ceil() - 1) : 0;
final int firstIndex = getMinChildIndexForScrollOffset(scrollOffset, itemExtent);
final int targetLastIndex = getMaxChildIndexForScrollOffset(targetEndScrollOffset, itemExtent);
if (firstChild != null) {
final int oldFirstIndex = indexOf(firstChild);
......@@ -50,7 +78,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
}
if (firstChild == null) {
if (!addInitialChild(index: firstIndex, scrollOffset: indexToScrollOffset(firstIndex))) {
if (!addInitialChild(index: firstIndex, scrollOffset: indexToScrollOffset(itemExtent, firstIndex))) {
// There are no children.
geometry = SliverGeometry.zero;
return;
......@@ -62,7 +90,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
for (int index = indexOf(firstChild) - 1; index >= firstIndex; --index) {
final RenderBox child = insertAndLayoutLeadingChild(childConstraints);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
childParentData.layoutOffset = indexToScrollOffset(index);
childParentData.layoutOffset = indexToScrollOffset(itemExtent, index);
assert(childParentData.index == index);
trailingChildWithLayout ??= child;
}
......@@ -70,7 +98,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
if (trailingChildWithLayout == null) {
firstChild.layout(childConstraints);
final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData;
childParentData.layoutOffset = indexToScrollOffset(firstIndex);
childParentData.layoutOffset = indexToScrollOffset(itemExtent, firstIndex);
trailingChildWithLayout = firstChild;
}
......@@ -88,19 +116,19 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
trailingChildWithLayout = child;
assert(child != null);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
childParentData.layoutOffset = indexToScrollOffset(childParentData.index);
childParentData.layoutOffset = indexToScrollOffset(itemExtent, childParentData.index);
}
final int lastIndex = indexOf(lastChild);
final double leadingScrollOffset = indexToScrollOffset(firstIndex);
final double trailingScrollOffset = indexToScrollOffset(lastIndex + 1);
final double leadingScrollOffset = indexToScrollOffset(itemExtent, firstIndex);
final double trailingScrollOffset = indexToScrollOffset(itemExtent, lastIndex + 1);
assert(childScrollOffset(firstChild) <= scrollOffset);
assert(firstIndex == 0 || childScrollOffset(firstChild) <= scrollOffset);
assert(debugAssertChildListIsNonEmptyAndContiguous());
assert(indexOf(firstChild) == firstIndex);
assert(lastIndex <= targetLastIndex);
final double estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
final double estimatedMaxScrollOffset = estimateMaxScrollOffset(
constraints,
firstIndex: firstIndex,
lastIndex: lastIndex,
......@@ -147,8 +175,56 @@ class RenderSliverFixedExtentList extends RenderSliverFixedExtentBoxAdaptor {
class RenderSliverFill extends RenderSliverFixedExtentBoxAdaptor {
RenderSliverFill({
@required RenderSliverBoxChildManager childManager,
}) : super(childManager: childManager);
double viewportFraction: 1.0,
}) : _viewportFraction = viewportFraction, super(childManager: childManager) {
assert(viewportFraction != null);
assert(viewportFraction > 0.0);
}
@override
double get itemExtent => constraints.viewportMainAxisExtent * viewportFraction;
double get viewportFraction => _viewportFraction;
double _viewportFraction;
set viewportFraction (double newValue) {
assert(newValue != null);
if (_viewportFraction == newValue)
return;
_viewportFraction = newValue;
markNeedsLayout();
}
double get _padding => (1.0 - viewportFraction) * constraints.viewportMainAxisExtent * 0.5;
@override
double indexToScrollOffset(double itemExtent, int index) {
return _padding + super.indexToScrollOffset(itemExtent, index);
}
@override
int getMinChildIndexForScrollOffset(double scrollOffset, double itemExtent) {
return super.getMinChildIndexForScrollOffset(math.max(scrollOffset - _padding, 0.0), itemExtent);
}
@override
int getMaxChildIndexForScrollOffset(double scrollOffset, double itemExtent) {
return super.getMaxChildIndexForScrollOffset(math.max(scrollOffset - _padding, 0.0), itemExtent);
}
@override
double get itemExtent => constraints.viewportMainAxisExtent;
double estimateMaxScrollOffset(SliverConstraints constraints, {
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
}) {
final double padding = _padding;
return childManager.estimateMaxScrollOffset(
constraints,
firstIndex: firstIndex,
lastIndex: lastIndex,
leadingScrollOffset: leadingScrollOffset - padding,
trailingScrollOffset: trailingScrollOffset - padding,
) + padding + padding;
}
}
......@@ -29,6 +29,14 @@ class ScrollMetrics {
@required this.viewportDimension,
});
/// Creates a [ScrollMetrics] that has the same properties as the given
/// [ScrollMetrics].
ScrollMetrics.clone(ScrollMetrics other)
: extentBefore = other.extentBefore,
extentInside = other.extentInside,
extentAfter = other.extentAfter,
viewportDimension = other.viewportDimension;
/// The quantity of content conceptually "above" the currently visible content
/// of the viewport in the scrollable. This is the content above the content
/// described by [extentInside].
......
......@@ -166,36 +166,3 @@ class AlwaysScrollableScrollPhysics extends ScrollPhysics {
@override
bool shouldAcceptUserOffset(ScrollPosition position) => true;
}
class PageScrollPhysics extends ScrollPhysics {
const PageScrollPhysics({ ScrollPhysics parent }) : super(parent);
@override
PageScrollPhysics applyTo(ScrollPhysics parent) => new PageScrollPhysics(parent: parent);
double _roundToPage(ScrollPosition position, double pixels, double pageSize) {
final int index = (pixels + pageSize / 2.0) ~/ pageSize;
return (pageSize * index).clamp(position.minScrollExtent, position.maxScrollExtent);
}
double _getTargetPixels(ScrollPosition position, Tolerance tolerance, double velocity) {
final double pageSize = position.viewportDimension;
if (velocity < -tolerance.velocity)
return _roundToPage(position, position.pixels - pageSize / 2.0, pageSize);
if (velocity > tolerance.velocity)
return _roundToPage(position, position.pixels + pageSize / 2.0, pageSize);
return _roundToPage(position, position.pixels, pageSize);
}
@override
Simulation createBallisticSimulation(ScrollPosition position, double velocity) {
// If we're out of range and not headed back in range, defer to the parent
// ballistics, which should put us back in range at a page boundary.
if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
(velocity >= 0.0 && position.pixels >= position.maxScrollExtent))
return super.createBallisticSimulation(position, velocity);
final Tolerance tolerance = this.tolerance;
final double target = _getTargetPixels(position, tolerance, velocity);
return new ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance);
}
}
......@@ -247,12 +247,23 @@ class SliverFill extends SliverMultiBoxAdaptorWidget {
SliverFill({
Key key,
@required SliverChildDelegate delegate,
}) : super(key: key, delegate: delegate);
this.viewportFraction: 1.0,
}) : super(key: key, delegate: delegate) {
assert(viewportFraction != null);
assert(viewportFraction > 0.0);
}
final double viewportFraction;
@override
RenderSliverFill createRenderObject(BuildContext context) {
final SliverMultiBoxAdaptorElement element = context;
return new RenderSliverFill(childManager: element);
return new RenderSliverFill(childManager: element, viewportFraction: viewportFraction);
}
@override
void updateRenderObject(BuildContext context, RenderSliverFill renderObject) {
renderObject.viewportFraction = viewportFraction;
}
}
......
......@@ -264,4 +264,108 @@ void main() {
expect(find.text('Alaska'), findsOneWidget);
});
testWidgets('PageView viewportFraction', (WidgetTester tester) async {
PageController controller = new PageController(viewportFraction: 7/8);
Widget build(PageController controller) {
return new PageView.builder(
controller: controller,
itemCount: kStates.length,
itemBuilder: (BuildContext context, int index) {
return new Container(
height: 200.0,
color: index % 2 == 0 ? const Color(0xFF0000FF) : const Color(0xFF00FF00),
child: new Text(kStates[index]),
);
}
);
}
await tester.pumpWidget(build(controller));
expect(tester.getTopLeft(find.text('Alabama')), const Point(50.0, 0.0));
expect(tester.getTopLeft(find.text('Alaska')), const Point(750.0, 0.0));
controller.jumpToPage(10);
await tester.pump();
expect(tester.getTopLeft(find.text('Georgia')), const Point(-650.0, 0.0));
expect(tester.getTopLeft(find.text('Hawaii')), const Point(50.0, 0.0));
expect(tester.getTopLeft(find.text('Idaho')), const Point(750.0, 0.0));
controller = new PageController(viewportFraction: 39/40);
await tester.pumpWidget(build(controller));
expect(tester.getTopLeft(find.text('Georgia')), const Point(-770.0, 0.0));
expect(tester.getTopLeft(find.text('Hawaii')), const Point(10.0, 0.0));
expect(tester.getTopLeft(find.text('Idaho')), const Point(790.0, 0.0));
});
testWidgets('PageView small viewportFraction', (WidgetTester tester) async {
PageController controller = new PageController(viewportFraction: 1/8);
Widget build(PageController controller) {
return new PageView.builder(
controller: controller,
itemCount: kStates.length,
itemBuilder: (BuildContext context, int index) {
return new Container(
height: 200.0,
color: index % 2 == 0 ? const Color(0xFF0000FF) : const Color(0xFF00FF00),
child: new Text(kStates[index]),
);
}
);
}
await tester.pumpWidget(build(controller));
expect(tester.getTopLeft(find.text('Alabama')), const Point(350.0, 0.0));
expect(tester.getTopLeft(find.text('Alaska')), const Point(450.0, 0.0));
expect(tester.getTopLeft(find.text('Arizona')), const Point(550.0, 0.0));
expect(tester.getTopLeft(find.text('Arkansas')), const Point(650.0, 0.0));
expect(tester.getTopLeft(find.text('California')), const Point(750.0, 0.0));
controller.jumpToPage(10);
await tester.pump();
expect(tester.getTopLeft(find.text('Connecticut')), const Point(-50.0, 0.0));
expect(tester.getTopLeft(find.text('Delaware')), const Point(50.0, 0.0));
expect(tester.getTopLeft(find.text('Florida')), const Point(150.0, 0.0));
expect(tester.getTopLeft(find.text('Georgia')), const Point(250.0, 0.0));
expect(tester.getTopLeft(find.text('Hawaii')), const Point(350.0, 0.0));
expect(tester.getTopLeft(find.text('Idaho')), const Point(450.0, 0.0));
expect(tester.getTopLeft(find.text('Illinois')), const Point(550.0, 0.0));
expect(tester.getTopLeft(find.text('Indiana')), const Point(650.0, 0.0));
expect(tester.getTopLeft(find.text('Iowa')), const Point(750.0, 0.0));
});
testWidgets('PageView large viewportFraction', (WidgetTester tester) async {
PageController controller = new PageController(viewportFraction: 5/4);
Widget build(PageController controller) {
return new PageView.builder(
controller: controller,
itemCount: kStates.length,
itemBuilder: (BuildContext context, int index) {
return new Container(
height: 200.0,
color: index % 2 == 0 ? const Color(0xFF0000FF) : const Color(0xFF00FF00),
child: new Text(kStates[index]),
);
}
);
}
await tester.pumpWidget(build(controller));
expect(tester.getTopLeft(find.text('Alabama')), const Point(-100.0, 0.0));
expect(tester.getBottomRight(find.text('Alabama')), const Point(900.0, 600.0));
controller.jumpToPage(10);
await tester.pump();
expect(tester.getTopLeft(find.text('Hawaii')), const Point(-100.0, 0.0));
});
}
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