v1.12.13+hotfix.1 cherry-picks (#45962)

flutter/engine@c1e322b Dart cherry-picks for 1.12.13
37f9c54 Use RenderSliverPadding to inset SliverFillViewport
diff --git a/bin/internal/engine.version b/bin/internal/engine.version
index 7ddc943..7ad3d1a 100644
--- a/bin/internal/engine.version
+++ b/bin/internal/engine.version
@@ -1 +1 @@
-b6b54fd60631a3828c2e2c9b079b5d1d2d8c8c37
+c1e322b685a81c11c16bddd22282925b7d0272e8
diff --git a/packages/flutter/lib/src/rendering/sliver_fill.dart b/packages/flutter/lib/src/rendering/sliver_fill.dart
index fefafe3..5d99b35 100644
--- a/packages/flutter/lib/src/rendering/sliver_fill.dart
+++ b/packages/flutter/lib/src/rendering/sliver_fill.dart
@@ -57,41 +57,6 @@
     _viewportFraction = value;
     markNeedsLayout();
   }
-
-  double get _padding => (1.0 - viewportFraction) * constraints.viewportMainAxisExtent * 0.5;
-
-  @override
-  double indexToLayoutOffset(double itemExtent, int index) {
-    return _padding + super.indexToLayoutOffset(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 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;
-  }
 }
 
 /// A sliver that contains a single box child that fills the remaining space in
diff --git a/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart b/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart
index 5a607fb..7e6b2d1 100644
--- a/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart
+++ b/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart
@@ -195,7 +195,7 @@
     if (firstChild == null) {
       if (!addInitialChild(index: firstIndex, layoutOffset: indexToLayoutOffset(itemExtent, firstIndex))) {
         // There are either no children, or we are past the end of all our children.
-        // If it is the later, we will need to find the first available child.
+        // If it is the latter, we will need to find the first available child.
         double max;
         if (childManager.childCount != null) {
           max = computeMaxScrollOffset(constraints, itemExtent);
diff --git a/packages/flutter/lib/src/rendering/sliver_padding.dart b/packages/flutter/lib/src/rendering/sliver_padding.dart
index 01cf8a2..56b53cb 100644
--- a/packages/flutter/lib/src/rendering/sliver_padding.dart
+++ b/packages/flutter/lib/src/rendering/sliver_padding.dart
@@ -12,73 +12,27 @@
 import 'object.dart';
 import 'sliver.dart';
 
-/// Inset a [RenderSliver], applying padding on each side.
+/// Insets a [RenderSliver] by applying [resolvedPadding] on each side.
 ///
-/// A [RenderSliverPadding] object wraps the [SliverGeometry.layoutExtent] of
-/// its child. Any incoming [SliverConstraints.overlap] is ignored and not
+/// A [RenderSliverEdgeInsetsPadding] subclass wraps the [SliverGeometry.layoutExtent]
+/// of its child. Any incoming [SliverConstraints.overlap] is ignored and not
 /// passed on to the child.
 ///
+/// {@template flutter.rendering.sliverPadding.limitation}
 /// Applying padding to anything but the most mundane sliver is likely to have
-/// undesired effects. For example, wrapping a
-/// [RenderSliverPinnedPersistentHeader] will cause the app bar to overlap
-/// earlier slivers (contrary to the normal behavior of pinned app bars), and
-/// while the app bar is pinned, the padding will scroll away.
-class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<RenderSliver> {
-  /// Creates a render object that insets its child in a viewport.
-  ///
-  /// The [padding] argument must not be null and must have non-negative insets.
-  RenderSliverPadding({
-    @required EdgeInsetsGeometry padding,
-    TextDirection textDirection,
-    RenderSliver child,
-  }) : assert(padding != null),
-       assert(padding.isNonNegative),
-       _padding = padding,
-       _textDirection = textDirection {
-    this.child = child;
-  }
-
-  EdgeInsets _resolvedPadding;
-
-  void _resolve() {
-    if (_resolvedPadding != null)
-      return;
-    _resolvedPadding = padding.resolve(textDirection);
-    assert(_resolvedPadding.isNonNegative);
-  }
-
-  void _markNeedResolution() {
-    _resolvedPadding = null;
-    markNeedsLayout();
-  }
-
+/// undesired effects. For example, wrapping a [RenderSliverPinnedPersistentHeader]
+/// will cause the app bar to overlap earlier slivers (contrary to the normal
+/// behavior of pinned app bars), and while the app bar is pinned, the padding
+/// will scroll away.
+/// {@endtemplate}
+abstract class RenderSliverEdgeInsetsPadding extends RenderSliver with RenderObjectWithChildMixin<RenderSliver> {
   /// The amount to pad the child in each dimension.
   ///
-  /// If this is set to an [EdgeInsetsDirectional] object, then [textDirection]
-  /// must not be null.
-  EdgeInsetsGeometry get padding => _padding;
-  EdgeInsetsGeometry _padding;
-  set padding(EdgeInsetsGeometry value) {
-    assert(value != null);
-    assert(padding.isNonNegative);
-    if (_padding == value)
-      return;
-    _padding = value;
-    _markNeedResolution();
-  }
-
-  /// The text direction with which to resolve [padding].
+  /// The offsets are specified in terms of visual edges, left, top, right, and
+  /// bottom. These values are not affected by the [TextDirection].
   ///
-  /// This may be changed to null, but only after the [padding] has been changed
-  /// to a value that does not depend on the direction.
-  TextDirection get textDirection => _textDirection;
-  TextDirection _textDirection;
-  set textDirection(TextDirection value) {
-    if (_textDirection == value)
-      return;
-    _textDirection = value;
-    _markNeedResolution();
-  }
+  /// Must not be null or contain negative values when [performLayout] is called.
+  EdgeInsets get resolvedPadding;
 
   /// The padding in the scroll direction on the side nearest the 0.0 scroll direction.
   ///
@@ -88,16 +42,16 @@
     assert(constraints != null);
     assert(constraints.axisDirection != null);
     assert(constraints.growthDirection != null);
-    assert(_resolvedPadding != null);
+    assert(resolvedPadding != null);
     switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
       case AxisDirection.up:
-        return _resolvedPadding.bottom;
+        return resolvedPadding.bottom;
       case AxisDirection.right:
-        return _resolvedPadding.left;
+        return resolvedPadding.left;
       case AxisDirection.down:
-        return _resolvedPadding.top;
+        return resolvedPadding.top;
       case AxisDirection.left:
-        return _resolvedPadding.right;
+        return resolvedPadding.right;
     }
     return null;
   }
@@ -110,16 +64,16 @@
     assert(constraints != null);
     assert(constraints.axisDirection != null);
     assert(constraints.growthDirection != null);
-    assert(_resolvedPadding != null);
+    assert(resolvedPadding != null);
     switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
       case AxisDirection.up:
-        return _resolvedPadding.top;
+        return resolvedPadding.top;
       case AxisDirection.right:
-        return _resolvedPadding.right;
+        return resolvedPadding.right;
       case AxisDirection.down:
-        return _resolvedPadding.bottom;
+        return resolvedPadding.bottom;
       case AxisDirection.left:
-        return _resolvedPadding.left;
+        return resolvedPadding.left;
     }
     return null;
   }
@@ -133,8 +87,8 @@
   double get mainAxisPadding {
     assert(constraints != null);
     assert(constraints.axis != null);
-    assert(_resolvedPadding != null);
-    return _resolvedPadding.along(constraints.axis);
+    assert(resolvedPadding != null);
+    return resolvedPadding.along(constraints.axis);
   }
 
   /// The total padding in the cross-axis direction. (In other words, for a
@@ -146,12 +100,12 @@
   double get crossAxisPadding {
     assert(constraints != null);
     assert(constraints.axis != null);
-    assert(_resolvedPadding != null);
+    assert(resolvedPadding != null);
     switch (constraints.axis) {
       case Axis.horizontal:
-        return _resolvedPadding.vertical;
+        return resolvedPadding.vertical;
       case Axis.vertical:
-        return _resolvedPadding.horizontal;
+        return resolvedPadding.horizontal;
     }
     return null;
   }
@@ -164,8 +118,7 @@
 
   @override
   void performLayout() {
-    _resolve();
-    assert(_resolvedPadding != null);
+    assert(resolvedPadding != null);
     final double beforePadding = this.beforePadding;
     final double afterPadding = this.afterPadding;
     final double mainAxisPadding = this.mainAxisPadding;
@@ -240,16 +193,16 @@
     assert(constraints.growthDirection != null);
     switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
       case AxisDirection.up:
-        childParentData.paintOffset = Offset(_resolvedPadding.left, calculatePaintOffset(constraints, from: _resolvedPadding.bottom + childLayoutGeometry.scrollExtent, to: _resolvedPadding.bottom + childLayoutGeometry.scrollExtent + _resolvedPadding.top));
+        childParentData.paintOffset = Offset(resolvedPadding.left, calculatePaintOffset(constraints, from: resolvedPadding.bottom + childLayoutGeometry.scrollExtent, to: resolvedPadding.bottom + childLayoutGeometry.scrollExtent + resolvedPadding.top));
         break;
       case AxisDirection.right:
-        childParentData.paintOffset = Offset(calculatePaintOffset(constraints, from: 0.0, to: _resolvedPadding.left), _resolvedPadding.top);
+        childParentData.paintOffset = Offset(calculatePaintOffset(constraints, from: 0.0, to: resolvedPadding.left), resolvedPadding.top);
         break;
       case AxisDirection.down:
-        childParentData.paintOffset = Offset(_resolvedPadding.left, calculatePaintOffset(constraints, from: 0.0, to: _resolvedPadding.top));
+        childParentData.paintOffset = Offset(resolvedPadding.left, calculatePaintOffset(constraints, from: 0.0, to: resolvedPadding.top));
         break;
       case AxisDirection.left:
-        childParentData.paintOffset = Offset(calculatePaintOffset(constraints, from: _resolvedPadding.right + childLayoutGeometry.scrollExtent, to: _resolvedPadding.right + childLayoutGeometry.scrollExtent + _resolvedPadding.left), _resolvedPadding.top);
+        childParentData.paintOffset = Offset(calculatePaintOffset(constraints, from: resolvedPadding.right + childLayoutGeometry.scrollExtent, to: resolvedPadding.right + childLayoutGeometry.scrollExtent + resolvedPadding.left), resolvedPadding.top);
         break;
     }
     assert(childParentData.paintOffset != null);
@@ -289,14 +242,14 @@
     assert(constraints != null);
     assert(constraints.axisDirection != null);
     assert(constraints.growthDirection != null);
-    assert(_resolvedPadding != null);
+    assert(resolvedPadding != null);
     switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
       case AxisDirection.up:
       case AxisDirection.down:
-        return _resolvedPadding.left;
+        return resolvedPadding.left;
       case AxisDirection.left:
       case AxisDirection.right:
-        return _resolvedPadding.top;
+        return resolvedPadding.top;
     }
     return null;
   }
@@ -346,6 +299,79 @@
       return true;
     }());
   }
+}
+
+/// Insets a [RenderSliver], applying padding on each side.
+///
+/// A [RenderSliverPadding] object wraps the [SliverGeometry.layoutExtent] of
+/// its child. Any incoming [SliverConstraints.overlap] is ignored and not
+/// passed on to the child.
+///
+/// {@macro flutter.rendering.sliverPadding.limitation}
+class RenderSliverPadding extends RenderSliverEdgeInsetsPadding {
+  /// Creates a render object that insets its child in a viewport.
+  ///
+  /// The [padding] argument must not be null and must have non-negative insets.
+  RenderSliverPadding({
+    @required EdgeInsetsGeometry padding,
+    TextDirection textDirection,
+    RenderSliver child,
+  }) : assert(padding != null),
+       assert(padding.isNonNegative),
+       _padding = padding,
+       _textDirection = textDirection {
+    this.child = child;
+  }
+
+  @override
+  EdgeInsets get resolvedPadding => _resolvedPadding;
+  EdgeInsets _resolvedPadding;
+
+  void _resolve() {
+    if (resolvedPadding != null)
+      return;
+    _resolvedPadding = padding.resolve(textDirection);
+    assert(resolvedPadding.isNonNegative);
+  }
+
+  void _markNeedsResolution() {
+    _resolvedPadding = null;
+    markNeedsLayout();
+  }
+
+  /// The amount to pad the child in each dimension.
+  ///
+  /// If this is set to an [EdgeInsetsDirectional] object, then [textDirection]
+  /// must not be null.
+  EdgeInsetsGeometry get padding => _padding;
+  EdgeInsetsGeometry _padding;
+  set padding(EdgeInsetsGeometry value) {
+    assert(value != null);
+    assert(padding.isNonNegative);
+    if (_padding == value)
+      return;
+    _padding = value;
+    _markNeedsResolution();
+  }
+
+  /// The text direction with which to resolve [padding].
+  ///
+  /// This may be changed to null, but only after the [padding] has been changed
+  /// to a value that does not depend on the direction.
+  TextDirection get textDirection => _textDirection;
+  TextDirection _textDirection;
+  set textDirection(TextDirection value) {
+    if (_textDirection == value)
+      return;
+    _textDirection = value;
+    _markNeedsResolution();
+  }
+
+  @override
+  void performLayout() {
+    _resolve();
+    super.performLayout();
+  }
 
   @override
   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
diff --git a/packages/flutter/lib/src/widgets/page_view.dart b/packages/flutter/lib/src/widgets/page_view.dart
index b08d0ec..fd6c078 100644
--- a/packages/flutter/lib/src/widgets/page_view.dart
+++ b/packages/flutter/lib/src/widgets/page_view.dart
@@ -345,8 +345,16 @@
       forcePixels(getPixelsFromPage(oldPage));
   }
 
+  // The amount of offset that will be added to [minScrollExtent] and subtracted
+  // from [maxScrollExtent], such that every page will properly snap to the center
+  // of the viewport when viewportFraction is greater than 1.
+  //
+  // The value is 0 if viewportFraction is less than or equal to 1, larger than 0
+  // otherwise.
+  double get _initialPageOffset => math.max(0, viewportDimension * (viewportFraction - 1) / 2);
+
   double getPageFromPixels(double pixels, double viewportDimension) {
-    final double actual = math.max(0.0, pixels) / math.max(1.0, viewportDimension * viewportFraction);
+    final double actual = math.max(0.0, pixels - _initialPageOffset) / math.max(1.0, viewportDimension * viewportFraction);
     final double round = actual.roundToDouble();
     if ((actual - round).abs() < precisionErrorTolerance) {
       return round;
@@ -355,7 +363,7 @@
   }
 
   double getPixelsFromPage(double page) {
-    return page * viewportDimension * viewportFraction;
+    return page * viewportDimension * viewportFraction + _initialPageOffset;
   }
 
   @override
@@ -397,6 +405,15 @@
   }
 
   @override
+  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
+    final double newMinScrollExtent = minScrollExtent + _initialPageOffset;
+    return super.applyContentDimensions(
+      newMinScrollExtent,
+      math.max(newMinScrollExtent, maxScrollExtent - _initialPageOffset),
+    );
+  }
+
+  @override
   PageMetrics copyWith({
     double minScrollExtent,
     double maxScrollExtent,
diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart
index 0cab703..d9f380e 100644
--- a/packages/flutter/lib/src/widgets/sliver.dart
+++ b/packages/flutter/lib/src/widgets/sliver.dart
@@ -725,6 +725,7 @@
   }) : assert(delegate != null),
        super(key: key);
 
+  /// {@template flutter.widgets.sliverMultiBoxAdaptor.delegate}
   /// The delegate that provides the children for this widget.
   ///
   /// The children are constructed lazily using this delegate to avoid creating
@@ -735,6 +736,7 @@
   ///  * [SliverChildBuilderDelegate] and [SliverChildListDelegate], which are
   ///    commonly used subclasses of [SliverChildDelegate] that use a builder
   ///    callback and an explicit child list, respectively.
+  /// {@endtemplate}
   final SliverChildDelegate delegate;
 
   @override
@@ -1023,7 +1025,7 @@
   }
 }
 
-/// A sliver that contains a multiple box children that each fill the viewport.
+/// A sliver that contains multiple box children that each fills the viewport.
 ///
 /// [SliverFillViewport] places its children in a linear array along the main
 /// axis. Each child is sized to fill the viewport, both in the main and cross
@@ -1038,21 +1040,47 @@
 ///    the main axis extent of each item.
 ///  * [SliverList], which does not require its children to have the same
 ///    extent in the main axis.
-class SliverFillViewport extends SliverMultiBoxAdaptorWidget {
+class SliverFillViewport extends StatelessWidget {
   /// Creates a sliver whose box children that each fill the viewport.
   const SliverFillViewport({
     Key key,
+    @required this.delegate,
+    this.viewportFraction = 1.0,
+  }) : assert(viewportFraction != null),
+       assert(viewportFraction > 0.0),
+       super(key: key);
+
+  /// The fraction of the viewport that each child should fill in the main axis.
+  ///
+  /// If this fraction is less than 1.0, more than one child will be visible at
+  /// once. If this fraction is greater than 1.0, each child will be larger than
+  /// the viewport in the main axis.
+  final double viewportFraction;
+
+  /// {@macro flutter.widgets.sliverMultiBoxAdaptor.delegate}
+  final SliverChildDelegate delegate;
+
+  @override
+  Widget build(BuildContext context) {
+    return _SliverFractionalPadding(
+      viewportFraction: (1 - viewportFraction).clamp(0, 1) / 2,
+      sliver: _SliverFillViewportRenderObjectWidget(
+        viewportFraction: viewportFraction,
+        delegate: delegate,
+      ),
+    );
+  }
+}
+
+class _SliverFillViewportRenderObjectWidget extends SliverMultiBoxAdaptorWidget {
+  const _SliverFillViewportRenderObjectWidget({
+    Key key,
     @required SliverChildDelegate delegate,
     this.viewportFraction = 1.0,
   }) : assert(viewportFraction != null),
        assert(viewportFraction > 0.0),
        super(key: key, delegate: delegate);
 
-  /// The fraction of the viewport that each child should fill in the main axis.
-  ///
-  /// If this fraction is less than 1.0, more than one child will be visible at
-  /// once. If this fraction is greater than 1.0, each child will be larger than
-  /// the viewport in the main axis.
   final double viewportFraction;
 
   @override
@@ -1067,6 +1095,77 @@
   }
 }
 
+class _SliverFractionalPadding extends SingleChildRenderObjectWidget {
+  const _SliverFractionalPadding({
+    this.viewportFraction = 0,
+    Widget sliver,
+  }) : assert(viewportFraction != null),
+       assert(viewportFraction >= 0),
+       assert(viewportFraction <= 0.5),
+       super(child: sliver);
+
+  final double viewportFraction;
+
+  @override
+  RenderObject createRenderObject(BuildContext context) => _RenderSliverFractionalPadding(viewportFraction: viewportFraction);
+
+  @override
+  void updateRenderObject(BuildContext context, _RenderSliverFractionalPadding renderObject) {
+    renderObject.viewportFraction = viewportFraction;
+  }
+}
+
+class _RenderSliverFractionalPadding extends RenderSliverEdgeInsetsPadding {
+  _RenderSliverFractionalPadding({
+    double viewportFraction = 0,
+  }) : assert(viewportFraction != null),
+       assert(viewportFraction <= 0.5),
+       assert(viewportFraction >= 0),
+       _viewportFraction = viewportFraction;
+
+  double get viewportFraction => _viewportFraction;
+  double _viewportFraction;
+  set viewportFraction(double newValue) {
+    assert(newValue != null);
+    if (_viewportFraction == newValue)
+      return;
+    _viewportFraction = newValue;
+    _markNeedsResolution();
+  }
+
+  @override
+  EdgeInsets get resolvedPadding => _resolvedPadding;
+  EdgeInsets _resolvedPadding;
+
+  void _markNeedsResolution() {
+    _resolvedPadding = null;
+    markNeedsLayout();
+  }
+
+  void _resolve() {
+    if (_resolvedPadding != null)
+      return;
+    assert(constraints.axis != null);
+    final double paddingValue = constraints.viewportMainAxisExtent * viewportFraction;
+    switch (constraints.axis) {
+      case Axis.horizontal:
+        _resolvedPadding = EdgeInsets.symmetric(horizontal: paddingValue);
+        break;
+      case Axis.vertical:
+        _resolvedPadding = EdgeInsets.symmetric(vertical: paddingValue);
+        break;
+    }
+
+    return;
+  }
+
+  @override
+  void performLayout() {
+    _resolve();
+    super.performLayout();
+  }
+}
+
 /// An element that lazily builds children for a [SliverMultiBoxAdaptorWidget].
 ///
 /// Implements [RenderSliverBoxChildManager], which lets this element manage
diff --git a/packages/flutter/test/widgets/page_view_test.dart b/packages/flutter/test/widgets/page_view_test.dart
index 94823e5..02b8d4e 100644
--- a/packages/flutter/test/widgets/page_view_test.dart
+++ b/packages/flutter/test/widgets/page_view_test.dart
@@ -532,8 +532,7 @@
   });
 
   testWidgets('PageView large viewportFraction', (WidgetTester tester) async {
-    final PageController controller =
-        PageController(viewportFraction: 5/4);
+    final PageController controller = PageController(viewportFraction: 5/4);
 
     Widget build(PageController controller) {
       return Directionality(
@@ -601,6 +600,89 @@
       expect(tester.getTopLeft(find.text('Hawaii')), const Offset(-(4 - 1) * 800 / 2, 0));
   });
 
+  testWidgets(
+    'PageView large viewportFraction can scroll to the last page and snap',
+    (WidgetTester tester) async {
+      // Regression test for https://github.com/flutter/flutter/issues/45096.
+      final PageController controller = PageController(viewportFraction: 5/4);
+
+      Widget build(PageController controller) {
+        return Directionality(
+          textDirection: TextDirection.ltr,
+          child: PageView.builder(
+            controller: controller,
+            itemCount: 3,
+            itemBuilder: (BuildContext context, int index) {
+              return Container(
+                height: 200.0,
+                color: index % 2 == 0
+                  ? const Color(0xFF0000FF)
+                  : const Color(0xFF00FF00),
+                  child: Text(index.toString()),
+              );
+            },
+          ),
+        );
+      }
+
+      await tester.pumpWidget(build(controller));
+
+      expect(tester.getCenter(find.text('0')), const Offset(400, 300));
+
+      controller.jumpToPage(2);
+      await tester.pump();
+      await tester.pumpAndSettle();
+
+      expect(tester.getCenter(find.text('2')), const Offset(400, 300));
+  });
+
+  testWidgets(
+    'All visible pages are able to receive touch events',
+    (WidgetTester tester) async {
+      // Regression test for https://github.com/flutter/flutter/issues/23873.
+      final PageController controller = PageController(viewportFraction: 1/4, initialPage: 0);
+      int tappedIndex;
+
+      Widget build() {
+        return Directionality(
+          textDirection: TextDirection.ltr,
+          child: PageView.builder(
+            controller: controller,
+            itemCount: 20,
+            itemBuilder: (BuildContext context, int index) {
+              return GestureDetector(
+                onTap: () => tappedIndex = index,
+                child: SizedBox.expand(child: Text('$index')),
+              );
+            },
+          ),
+        );
+      }
+
+      Iterable<int> visiblePages = const <int> [0, 1, 2];
+      await tester.pumpWidget(build());
+
+      // The first 3 items should be visible and tappable.
+      for (int index in visiblePages) {
+        expect(find.text(index.toString()), findsOneWidget);
+        // The center of page 2's x-coordinate is 800, so we have to manually
+        // offset it a bit to make sure the tap lands within the screen.
+        final Offset center = tester.getCenter(find.text('$index')) - const Offset(3, 0);
+        await tester.tapAt(center);
+        expect(tappedIndex, index);
+      }
+
+      controller.jumpToPage(19);
+      await tester.pump();
+      // The last 3 items should be visible and tappable.
+      visiblePages = const <int> [17, 18, 19];
+      for (int index in visiblePages) {
+        expect(find.text('$index'), findsOneWidget);
+        await tester.tap(find.text('$index'));
+        expect(tappedIndex, index);
+      }
+  });
+
   testWidgets('PageView does not report page changed on overscroll', (WidgetTester tester) async {
     final PageController controller = PageController(
       initialPage: kStates.length - 1,
diff --git a/packages/flutter/test/widgets/sliver_fill_viewport_test.dart b/packages/flutter/test/widgets/sliver_fill_viewport_test.dart
index 0a6683b..12778be 100644
--- a/packages/flutter/test/widgets/sliver_fill_viewport_test.dart
+++ b/packages/flutter/test/widgets/sliver_fill_viewport_test.dart
@@ -64,7 +64,7 @@
     expect(
       viewport.toStringDeep(minLevel: DiagnosticLevel.info),
       equalsIgnoringHashCodes(
-        'RenderSliverFillViewport#00000 relayoutBoundary=up1\n'
+        '_RenderSliverFractionalPadding#00000 relayoutBoundary=up1\n'
         ' │ needs compositing\n'
         ' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n'
         ' │ constraints: SliverConstraints(AxisDirection.down,\n'
@@ -76,58 +76,71 @@
         ' │ geometry: SliverGeometry(scrollExtent: 12000.0, paintExtent:\n'
         ' │   600.0, maxPaintExtent: 12000.0, hasVisualOverflow: true,\n'
         ' │   cacheExtent: 850.0)\n'
-        ' │ currently live children: 0 to 1\n'
         ' │\n'
-        ' ├─child with index 0: RenderRepaintBoundary#00000\n'
-        ' │ │ needs compositing\n'
-        ' │ │ parentData: index=0; layoutOffset=0.0\n'
-        ' │ │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
-        ' │ │ layer: OffsetLayer#00000\n'
-        ' │ │ size: Size(800.0, 600.0)\n'
-        ' │ │ metrics: 66.7% useful (1 bad vs 2 good)\n'
-        ' │ │ diagnosis: insufficient data to draw conclusion (less than five\n'
-        ' │ │   repaints)\n'
-        ' │ │\n'
-        ' │ └─child: RenderParagraph#00000\n'
-        ' │   │ parentData: <none> (can use size)\n'
-        ' │   │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
-        ' │   │ semantics node: SemanticsNode#2\n'
-        ' │   │ size: Size(800.0, 600.0)\n'
-        ' │   │ textAlign: start\n'
-        ' │   │ textDirection: ltr\n'
-        ' │   │ softWrap: wrapping at box width\n'
-        ' │   │ overflow: clip\n'
-        ' │   │ maxLines: unlimited\n'
-        ' │   ╘═╦══ text ═══\n'
-        ' │     ║ TextSpan:\n'
-        ' │     ║   <all styles inherited>\n'
-        ' │     ║   "0"\n'
-        ' │     ╚═══════════\n'
-        ' └─child with index 1: RenderRepaintBoundary#00000\n'
+        ' └─child: RenderSliverFillViewport#00000 relayoutBoundary=up2\n'
         '   │ needs compositing\n'
-        '   │ parentData: index=1; layoutOffset=600.0\n'
-        '   │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
-        '   │ layer: OffsetLayer#00000 DETACHED\n'
-        '   │ size: Size(800.0, 600.0)\n'
-        '   │ metrics: 50.0% useful (1 bad vs 1 good)\n'
-        '   │ diagnosis: insufficient data to draw conclusion (less than five\n'
-        '   │   repaints)\n'
+        '   │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n'
+        '   │ constraints: SliverConstraints(AxisDirection.down,\n'
+        '   │   GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n'
+        '   │   0.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0,\n'
+        '   │   crossAxisDirection: AxisDirection.right,\n'
+        '   │   viewportMainAxisExtent: 600.0, remainingCacheExtent: 850.0\n'
+        '   │   cacheOrigin: 0.0 )\n'
+        '   │ geometry: SliverGeometry(scrollExtent: 12000.0, paintExtent:\n'
+        '   │   600.0, maxPaintExtent: 12000.0, hasVisualOverflow: true,\n'
+        '   │   cacheExtent: 850.0)\n'
+        '   │ currently live children: 0 to 1\n'
         '   │\n'
-        '   └─child: RenderParagraph#00000\n'
-        '     │ parentData: <none> (can use size)\n'
+        '   ├─child with index 0: RenderRepaintBoundary#00000\n'
+        '   │ │ needs compositing\n'
+        '   │ │ parentData: index=0; layoutOffset=0.0\n'
+        '   │ │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
+        '   │ │ layer: OffsetLayer#00000\n'
+        '   │ │ size: Size(800.0, 600.0)\n'
+        '   │ │ metrics: 66.7% useful (1 bad vs 2 good)\n'
+        '   │ │ diagnosis: insufficient data to draw conclusion (less than five\n'
+        '   │ │   repaints)\n'
+        '   │ │\n'
+        '   │ └─child: RenderParagraph#00000\n'
+        '   │   │ parentData: <none> (can use size)\n'
+        '   │   │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
+        '   │   │ semantics node: SemanticsNode#2\n'
+        '   │   │ size: Size(800.0, 600.0)\n'
+        '   │   │ textAlign: start\n'
+        '   │   │ textDirection: ltr\n'
+        '   │   │ softWrap: wrapping at box width\n'
+        '   │   │ overflow: clip\n'
+        '   │   │ maxLines: unlimited\n'
+        '   │   ╘═╦══ text ═══\n'
+        '   │     ║ TextSpan:\n'
+        '   │     ║   <all styles inherited>\n'
+        '   │     ║   "0"\n'
+        '   │     ╚═══════════\n'
+        '   └─child with index 1: RenderRepaintBoundary#00000\n'
+        '     │ needs compositing\n'
+        '     │ parentData: index=1; layoutOffset=600.0\n'
         '     │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
-        '     │ semantics node: SemanticsNode#3\n'
+        '     │ layer: OffsetLayer#00000 DETACHED\n'
         '     │ size: Size(800.0, 600.0)\n'
-        '     │ textAlign: start\n'
-        '     │ textDirection: ltr\n'
-        '     │ softWrap: wrapping at box width\n'
-        '     │ overflow: clip\n'
-        '     │ maxLines: unlimited\n'
-        '     ╘═╦══ text ═══\n'
-        '       ║ TextSpan:\n'
-        '       ║   <all styles inherited>\n'
-        '       ║   "1"\n'
-        '       ╚═══════════\n'
+        '     │ metrics: 50.0% useful (1 bad vs 1 good)\n'
+        '     │ diagnosis: insufficient data to draw conclusion (less than five\n'
+        '     │   repaints)\n'
+        '     │\n'
+        '     └─child: RenderParagraph#00000\n'
+        '       │ parentData: <none> (can use size)\n'
+        '       │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
+        '       │ semantics node: SemanticsNode#3\n'
+        '       │ size: Size(800.0, 600.0)\n'
+        '       │ textAlign: start\n'
+        '       │ textDirection: ltr\n'
+        '       │ softWrap: wrapping at box width\n'
+        '       │ overflow: clip\n'
+        '       │ maxLines: unlimited\n'
+        '       ╘═╦══ text ═══\n'
+        '         ║ TextSpan:\n'
+        '         ║   <all styles inherited>\n'
+        '         ║   "1"\n'
+        '         ╚═══════════\n'
         ''
       ),
     );