Reland "Prevent viewport.showOnScreen from scrolling the viewport if the specified Rect is already visible. (#56413)" reverted in #64091 (#64513)

diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart
index d4d9c3e..7122452 100644
--- a/packages/flutter/lib/src/material/app_bar.dart
+++ b/packages/flutter/lib/src/material/app_bar.dart
@@ -817,12 +817,18 @@
     @required this.topPadding,
     @required this.floating,
     @required this.pinned,
+    @required this.vsync,
     @required this.snapConfiguration,
     @required this.stretchConfiguration,
+    @required this.showOnScreenConfiguration,
     @required this.shape,
     @required this.toolbarHeight,
     @required this.leadingWidth,
   }) : assert(primary || topPadding == 0.0),
+       assert(
+         !floating || (snapConfiguration == null && showOnScreenConfiguration == null) || vsync != null,
+         'vsync cannot be null when snapConfiguration or showOnScreenConfiguration is not null, and floating is true',
+       ),
        _bottomHeight = bottom?.preferredSize?.height ?? 0.0;
 
   final Widget leading;
@@ -861,12 +867,18 @@
   double get maxExtent => math.max(topPadding + (expandedHeight ?? (toolbarHeight ?? kToolbarHeight) + _bottomHeight), minExtent);
 
   @override
+  final TickerProvider vsync;
+
+  @override
   final FloatingHeaderSnapConfiguration snapConfiguration;
 
   @override
   final OverScrollHeaderStretchConfiguration stretchConfiguration;
 
   @override
+  final PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration;
+
+  @override
   Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
     final double visibleMainHeight = maxExtent - shrinkOffset - topPadding;
     final double extraToolbarHeight = math.max(minExtent - _bottomHeight - topPadding - (toolbarHeight ?? kToolbarHeight), 0.0);
@@ -935,8 +947,10 @@
         || topPadding != oldDelegate.topPadding
         || pinned != oldDelegate.pinned
         || floating != oldDelegate.floating
+        || vsync != oldDelegate.vsync
         || snapConfiguration != oldDelegate.snapConfiguration
         || stretchConfiguration != oldDelegate.stretchConfiguration
+        || showOnScreenConfiguration != oldDelegate.showOnScreenConfiguration
         || forceElevated != oldDelegate.forceElevated
         || toolbarHeight != oldDelegate.toolbarHeight
         || leadingWidth != leadingWidth;
@@ -1325,9 +1339,14 @@
   /// into view.
   ///
   /// If [snap] is true then a scroll that exposes the floating app bar will
-  /// trigger an animation that slides the entire app bar into view. Similarly if
-  /// a scroll dismisses the app bar, the animation will slide the app bar
-  /// completely out of view.
+  /// trigger an animation that slides the entire app bar into view. Similarly
+  /// if a scroll dismisses the app bar, the animation will slide the app bar
+  /// completely out of view. Additionally, setting [snap] to true will fully
+  /// expand the floating app bar when the framework tries to reveal the
+  /// contents of the app bar by calling [RenderObject.showOnScreen]. For
+  /// example, when a [TextField] in the floating app bar gains focus, if [snap]
+  /// is true, the framework will always fully expand the floating app bar, in
+  /// order to reveal the focused [TextField].
   ///
   /// Snapping only applies when the app bar is floating, not when the app bar
   /// appears at the top of its scroll view.
@@ -1382,17 +1401,21 @@
 class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMixin {
   FloatingHeaderSnapConfiguration _snapConfiguration;
   OverScrollHeaderStretchConfiguration _stretchConfiguration;
+  PersistentHeaderShowOnScreenConfiguration _showOnScreenConfiguration;
 
   void _updateSnapConfiguration() {
     if (widget.snap && widget.floating) {
       _snapConfiguration = FloatingHeaderSnapConfiguration(
-        vsync: this,
         curve: Curves.easeOut,
         duration: const Duration(milliseconds: 200),
       );
     } else {
       _snapConfiguration = null;
     }
+
+    _showOnScreenConfiguration = widget.floating & widget.snap
+      ? const PersistentHeaderShowOnScreenConfiguration(minShowOnScreenExtent: double.infinity)
+      : null;
   }
 
   void _updateStretchConfiguration() {
@@ -1438,6 +1461,7 @@
         floating: widget.floating,
         pinned: widget.pinned,
         delegate: _SliverAppBarDelegate(
+          vsync: this,
           leading: widget.leading,
           automaticallyImplyLeading: widget.automaticallyImplyLeading,
           title: widget.title,
@@ -1464,6 +1488,7 @@
           shape: widget.shape,
           snapConfiguration: _snapConfiguration,
           stretchConfiguration: _stretchConfiguration,
+          showOnScreenConfiguration: _showOnScreenConfiguration,
           toolbarHeight: widget.toolbarHeight,
           leadingWidth: widget.leadingWidth,
         ),
diff --git a/packages/flutter/lib/src/rendering/sliver_persistent_header.dart b/packages/flutter/lib/src/rendering/sliver_persistent_header.dart
index 54d9348..4913b2a 100644
--- a/packages/flutter/lib/src/rendering/sliver_persistent_header.dart
+++ b/packages/flutter/lib/src/rendering/sliver_persistent_header.dart
@@ -17,6 +17,15 @@
 import 'viewport.dart';
 import 'viewport_offset.dart';
 
+// Trims the specified edges of the given `Rect` [original], so that they do not
+// exceed the given values.
+Rect? _trim(Rect? original, {
+  double top = -double.infinity,
+  double right = double.infinity,
+  double bottom = double.infinity,
+  double left = -double.infinity,
+}) => original?.intersect(Rect.fromLTRB(left, top, right, bottom));
+
 /// Specifies how a stretched header is to trigger an [AsyncCallback].
 ///
 /// See also:
@@ -39,6 +48,60 @@
   final AsyncCallback? onStretchTrigger;
 }
 
+/// {@template flutter.rendering.persistentHeader.showOnScreenConfiguration}
+/// Specifies how a pinned header or a floating header should react to
+/// [RenderObject.showOnScreen] calls.
+/// {@endtemplate}
+@immutable
+class PersistentHeaderShowOnScreenConfiguration {
+  /// Creates an object that specifies how a pinned or floating persistent header
+  /// should behave in response to [RenderObject.showOnScreen] calls.
+  const PersistentHeaderShowOnScreenConfiguration({
+    this.minShowOnScreenExtent = double.negativeInfinity,
+    this.maxShowOnScreenExtent = double.infinity,
+  }) : assert(minShowOnScreenExtent <= maxShowOnScreenExtent);
+
+  /// The smallest the floating header can expand to in the main axis direction,
+  /// in response to a [RenderObject.showOnScreen] call, in addition to its
+  /// [RenderSliverPersistentHeader.minExtent].
+  ///
+  /// When a floating persistent header is told to show a [Rect] on screen, it
+  /// may expand itself to accomodate the [Rect]. The minimum extent that is
+  /// allowed for such expansion is either
+  /// [RenderSliverPersistentHeader.minExtent] or [minShowOnScreenExtent],
+  /// whichever is larger. If the persistent header's current extent is already
+  /// larger than that maximum extent, it will remain unchanged.
+  ///
+  /// This parameter can be set to the persistent header's `maxExtent` (or
+  /// `double.infinity`) so the persistent header will always try to expand when
+  /// [RenderObject.showOnScreen] is called on it.
+  ///
+  /// Defaults to [double.negativeInfinity], must be less than or equal to
+  /// [maxShowOnScreenExtent]. Has no effect unless the persistent header is a
+  /// floating header.
+  final double minShowOnScreenExtent;
+
+  /// The biggest the floating header can expand to in the main axis direction,
+  /// in response to a [RenderObject.showOnScreen] call, in addition to its
+  /// [RenderSliverPersistentHeader.maxExtent].
+  ///
+  /// When a floating persistent header is told to show a [Rect] on screen, it
+  /// may expand itself to accomodate the [Rect]. The maximum extent that is
+  /// allowed for such expansion is either
+  /// [RenderSliverPersistentHeader.maxExtent] or [maxShowOnScreenExtent],
+  /// whichever is smaller. If the persistent header's current extent is already
+  /// larger than that maximum extent, it will remain unchanged.
+  ///
+  /// This parameter can be set to the persistent header's `minExtent` (or
+  /// `double.negativeInfinity`) so the persistent header will never try to
+  /// expand when [RenderObject.showOnScreen] is called on it.
+  ///
+  /// Defaults to [double.infinity], must be greater than or equal to
+  /// [minShowOnScreenExtent]. Has no effect unless the persistent header is a
+  /// floating header.
+  final double maxShowOnScreenExtent;
+}
+
 /// A base class for slivers that have a [RenderBox] child which scrolls
 /// normally, except that when it hits the leading edge (typically the top) of
 /// the viewport, it shrinks to a minimum size ([minExtent]).
@@ -347,11 +410,18 @@
   RenderSliverPinnedPersistentHeader({
     RenderBox? child,
     OverScrollHeaderStretchConfiguration? stretchConfiguration,
+    this.showOnScreenConfiguration = const PersistentHeaderShowOnScreenConfiguration(),
   }) : super(
     child: child,
     stretchConfiguration: stretchConfiguration,
   );
 
+  /// Specifies the persistent header's behavior when `showOnScreen` is called.
+  ///
+  /// If set to null, the persistent header will delegate the `showOnScreen` call
+  /// to it's parent [RenderObject].
+  PersistentHeaderShowOnScreenConfiguration? showOnScreenConfiguration;
+
   @override
   void performLayout() {
     final SliverConstraints constraints = this.constraints;
@@ -377,6 +447,41 @@
 
   @override
   double childMainAxisPosition(RenderBox child) => 0.0;
+
+  @override
+  void showOnScreen({
+    RenderObject? descendant,
+    Rect? rect,
+    Duration duration = Duration.zero,
+    Curve curve = Curves.ease,
+  }) {
+    final Rect? localBounds = descendant != null
+      ? MatrixUtils.transformRect(descendant.getTransformTo(this), rect ?? descendant.paintBounds)
+      : rect;
+
+    Rect? newRect;
+    switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
+      case AxisDirection.up:
+        newRect = _trim(localBounds, bottom: childExtent);
+        break;
+      case AxisDirection.right:
+        newRect = _trim(localBounds, left: 0);
+        break;
+      case AxisDirection.down:
+        newRect = _trim(localBounds, top: 0);
+        break;
+      case AxisDirection.left:
+        newRect = _trim(localBounds, right: childExtent);
+        break;
+    }
+
+    super.showOnScreen(
+      descendant: this,
+      rect: newRect,
+      duration: duration,
+      curve: curve,
+    );
+  }
 }
 
 /// Specifies how a floating header is to be "snapped" (animated) into or out
@@ -393,16 +498,23 @@
   /// Creates an object that specifies how a floating header is to be "snapped"
   /// (animated) into or out of view.
   FloatingHeaderSnapConfiguration({
-    required this.vsync,
+    @Deprecated(
+      'Specify SliverPersistentHeaderDelegate.vsync instead. '
+      'This feature was deprecated after v1.19.0.'
+    )
+    this.vsync,
     this.curve = Curves.ease,
     this.duration = const Duration(milliseconds: 300),
-  }) : assert(vsync != null),
-       assert(curve != null),
+  }) : assert(curve != null),
        assert(duration != null);
 
-  /// The [TickerProvider] for the [AnimationController] that causes a
-  /// floating header to snap in or out of view.
-  final TickerProvider vsync;
+  /// The [TickerProvider] for the [AnimationController] that causes a floating
+  /// header to snap in or out of view.
+  @Deprecated(
+    'Specify SliverPersistentHeaderDelegate.vsync instead. '
+    'This feature was deprecated after v1.19.0.'
+  )
+  final TickerProvider? vsync;
 
   /// The snap animation curve.
   final Curve curve;
@@ -425,13 +537,15 @@
   /// direction.
   RenderSliverFloatingPersistentHeader({
     RenderBox? child,
-    FloatingHeaderSnapConfiguration? snapConfiguration,
+    TickerProvider? vsync,
+    this.snapConfiguration,
     OverScrollHeaderStretchConfiguration? stretchConfiguration,
-  }) : _snapConfiguration = snapConfiguration,
+    required this.showOnScreenConfiguration,
+  }) : _vsync = vsync,
        super(
-      child: child,
-      stretchConfiguration: stretchConfiguration,
-    );
+    child: child,
+    stretchConfiguration: stretchConfiguration,
+  );
 
   AnimationController? _controller;
   late Animation<double> _animation;
@@ -449,6 +563,22 @@
     super.detach();
   }
 
+
+  /// A [TickerProvider] to use when animating the scroll position.
+  TickerProvider? get vsync => _vsync;
+  TickerProvider? _vsync;
+  set vsync(TickerProvider? value) {
+    if (value == _vsync)
+      return;
+    _vsync = value;
+    if (value == null) {
+      _controller?.dispose();
+      _controller = null;
+    } else {
+      _controller?.resync(value);
+    }
+  }
+
   /// Defines the parameters used to snap (animate) the floating header in and
   /// out of view.
   ///
@@ -461,20 +591,13 @@
   ///    start or stop the floating header's animation.
   ///  * [SliverAppBar], which creates a header that can be pinned, floating,
   ///    and snapped into view via the corresponding parameters.
-  FloatingHeaderSnapConfiguration? get snapConfiguration => _snapConfiguration;
-  FloatingHeaderSnapConfiguration? _snapConfiguration;
-  set snapConfiguration(FloatingHeaderSnapConfiguration? value) {
-    if (value == _snapConfiguration)
-      return;
-    if (value == null) {
-      _controller?.dispose();
-      _controller = null;
-    } else {
-      if (_snapConfiguration != null && value.vsync != _snapConfiguration!.vsync)
-        _controller?.resync(value.vsync);
-    }
-    _snapConfiguration = value;
-  }
+  FloatingHeaderSnapConfiguration? snapConfiguration;
+
+  /// {@macro flutter.rendering.persistentHeader.showOnScreenConfiguration}
+  ///
+  /// If set to null, the persistent header will delegate the `showOnScreen` call
+  /// to it's parent [RenderObject].
+  PersistentHeaderShowOnScreenConfiguration? showOnScreenConfiguration;
 
   /// Updates [geometry], and returns the new value for [childMainAxisPosition].
   ///
@@ -499,38 +622,52 @@
     return stretchOffset > 0 ? 0.0 : math.min(0.0, paintExtent - childExtent);
   }
 
+  void _updateAnimation(Duration duration, double endValue, Curve curve) {
+    assert(duration != null);
+    assert(endValue != null);
+    assert(curve != null);
+    assert(
+      vsync != null,
+      'vsync must not be null if the floating header changes size animatedly.',
+    );
+
+    final AnimationController effectiveController =
+      _controller ??= AnimationController(vsync: vsync!, duration: duration)
+                        ..addListener(() {
+      if (_effectiveScrollOffset == _animation.value)
+        return;
+      _effectiveScrollOffset = _animation.value;
+      markNeedsLayout();
+    });
+
+    _animation = effectiveController.drive(
+      Tween<double>(
+        begin: _effectiveScrollOffset,
+        end: endValue,
+      ).chain(CurveTween(curve: curve)),
+    );
+  }
+
   /// If the header isn't already fully exposed, then scroll it into view.
   void maybeStartSnapAnimation(ScrollDirection direction) {
-    if (snapConfiguration == null)
+    final FloatingHeaderSnapConfiguration? snap = snapConfiguration;
+    if (snap == null)
       return;
     if (direction == ScrollDirection.forward && _effectiveScrollOffset! <= 0.0)
       return;
     if (direction == ScrollDirection.reverse && _effectiveScrollOffset! >= maxExtent)
       return;
 
-    final TickerProvider vsync = snapConfiguration!.vsync;
-    final Duration duration = snapConfiguration!.duration;
-    _controller ??= AnimationController(vsync: vsync, duration: duration)
-      ..addListener(() {
-        if (_effectiveScrollOffset == _animation.value)
-          return;
-        _effectiveScrollOffset = _animation.value;
-        markNeedsLayout();
-      });
-
-    _animation = _controller!.drive(
-      Tween<double>(
-        begin: _effectiveScrollOffset,
-        end: direction == ScrollDirection.forward ? 0.0 : maxExtent,
-      ).chain(CurveTween(
-        curve: snapConfiguration!.curve,
-      )),
+    _updateAnimation(
+      snap.duration,
+      direction == ScrollDirection.forward ? 0.0 : maxExtent,
+      snap.curve,
     );
-
-    _controller!.forward(from: 0.0);
+    _controller?.forward(from: 0.0);
   }
 
-  /// If a header snap animation is underway then stop it.
+  /// If a header snap animation or a [showOnScreen] expand animation is underway
+  /// then stop it.
   void maybeStopSnapAnimation(ScrollDirection direction) {
     _controller?.stop();
   }
@@ -568,6 +705,79 @@
   }
 
   @override
+  void showOnScreen({
+    RenderObject? descendant,
+    Rect? rect,
+    Duration duration = Duration.zero,
+    Curve curve = Curves.ease,
+  }) {
+    final PersistentHeaderShowOnScreenConfiguration? showOnScreen = showOnScreenConfiguration;
+    if (showOnScreen == null)
+      return super.showOnScreen(descendant: descendant, rect: rect, duration: duration, curve: curve);
+
+    assert(child != null || descendant == null);
+    // We prefer the child's coordinate space (instead of the sliver's) because
+    // it's easier for us to convert the target rect into target extents: when
+    // the sliver is sitting above the leading edge (not possible with pinned
+    // headers), the leading edge of the sliver and the leading edge of the child
+    // will not be aligned. The only exception is when child is null (and thus
+    // descendant == null).
+    final Rect? childBounds = descendant != null
+      ? MatrixUtils.transformRect(descendant.getTransformTo(child), rect ?? descendant.paintBounds)
+      : rect;
+
+    double targetExtent;
+    Rect? targetRect;
+    switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
+      case AxisDirection.up:
+        targetExtent = childExtent - (childBounds?.top ?? 0);
+        targetRect = _trim(childBounds, bottom: childExtent);
+        break;
+      case AxisDirection.right:
+        targetExtent = childBounds?.right ?? childExtent;
+        targetRect = _trim(childBounds, left: 0);
+        break;
+      case AxisDirection.down:
+        targetExtent = childBounds?.bottom ?? childExtent;
+        targetRect = _trim(childBounds, top: 0);
+        break;
+      case AxisDirection.left:
+        targetExtent = childExtent - (childBounds?.left ?? 0);
+        targetRect = _trim(childBounds, right: childExtent);
+        break;
+    }
+
+    // A stretch header can have a bigger childExtent than maxExtent.
+    final double effectiveMaxExtent = math.max(childExtent, maxExtent);
+
+    targetExtent = targetExtent.clamp(
+        showOnScreen.minShowOnScreenExtent,
+        showOnScreen.maxShowOnScreenExtent,
+      )
+      // Clamp the value back to the valid range after applying additional
+      // constriants. Contracting is not allowed.
+      .clamp(childExtent, effectiveMaxExtent);
+
+    // Expands the header if needed, with animation.
+    if (targetExtent > childExtent) {
+      final double targetScrollOffset = maxExtent - targetExtent;
+      assert(
+        vsync != null,
+        'vsync must not be null if the floating header changes size animatedly.',
+      );
+      _updateAnimation(duration, targetScrollOffset, curve);
+      _controller?.forward(from: 0.0);
+    }
+
+    super.showOnScreen(
+      descendant: descendant == null ? this : child,
+      rect: targetRect,
+      duration: duration,
+      curve: curve,
+    );
+  }
+
+  @override
   double childMainAxisPosition(RenderBox child) {
     assert(child == this.child);
     return _childPosition ?? 0.0;
@@ -594,12 +804,16 @@
   /// scroll direction.
   RenderSliverFloatingPinnedPersistentHeader({
     RenderBox? child,
+    TickerProvider? vsync,
     FloatingHeaderSnapConfiguration? snapConfiguration,
     OverScrollHeaderStretchConfiguration? stretchConfiguration,
+    PersistentHeaderShowOnScreenConfiguration? showOnScreenConfiguration,
   }) : super(
     child: child,
+    vsync: vsync,
     snapConfiguration: snapConfiguration,
     stretchConfiguration: stretchConfiguration,
+    showOnScreenConfiguration: showOnScreenConfiguration,
   );
 
   @override
diff --git a/packages/flutter/lib/src/rendering/viewport.dart b/packages/flutter/lib/src/rendering/viewport.dart
index 4497ddc..f47d40b 100644
--- a/packages/flutter/lib/src/rendering/viewport.dart
+++ b/packages/flutter/lib/src/rendering/viewport.dart
@@ -816,7 +816,7 @@
     final RenderSliver sliver = child as RenderSliver;
 
     double targetMainAxisExtent;
-    // The scroll offset within `child` to `rect`.
+    // The scroll offset of `rect` within `child`.
     switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
       case AxisDirection.up:
         leadingScrollOffset += pivotExtent - rectLocal.bottom;
@@ -836,6 +836,14 @@
         break;
     }
 
+    // So far leadingScrollOffset is the scroll offset of `rect` in the `child`
+    // sliver's sliver coordinate system. The sign of this value indicates
+    // whether the `rect` protrudes the leading edge of the `child` sliver. When
+    // this value is non-negative and `child`'s `maxScrollObstructionExtent` is
+    // greater than 0, we assume `rect` can't be obstructed by the leading edge
+    // of the viewport (i.e. its pinned to the leading edge).
+    final bool isPinned = sliver.geometry!.maxScrollObstructionExtent > 0 && leadingScrollOffset >= 0;
+
     // The scroll offset in the viewport to `rect`.
     leadingScrollOffset = scrollOffsetOf(sliver, leadingScrollOffset);
 
@@ -845,11 +853,16 @@
     final Matrix4 transform = target.getTransformTo(this);
     Rect targetRect = MatrixUtils.transformRect(transform, rect);
     final double extentOfPinnedSlivers = maxScrollObstructionExtentBefore(sliver);
+
     switch (sliver.constraints.growthDirection) {
       case GrowthDirection.forward:
+        if (isPinned && alignment <= 0)
+          return RevealedOffset(offset: double.infinity, rect: targetRect);
         leadingScrollOffset -= extentOfPinnedSlivers;
         break;
       case GrowthDirection.reverse:
+        if (isPinned && alignment >= 1)
+          return RevealedOffset(offset: double.negativeInfinity, rect: targetRect);
         // If child's growth direction is reverse, when viewport.offset is
         // `leadingScrollOffset`, it is positioned just outside of the leading
         // edge of the viewport.
diff --git a/packages/flutter/lib/src/widgets/sliver_persistent_header.dart b/packages/flutter/lib/src/widgets/sliver_persistent_header.dart
index 673e1cb..4f70ba5 100644
--- a/packages/flutter/lib/src/widgets/sliver_persistent_header.dart
+++ b/packages/flutter/lib/src/widgets/sliver_persistent_header.dart
@@ -6,6 +6,7 @@
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter/rendering.dart';
+import 'package:flutter/scheduler.dart' show TickerProvider;
 
 import 'framework.dart';
 
@@ -59,6 +60,12 @@
   /// different value.
   double get maxExtent;
 
+  /// A [TickerProvider] to use when animating the header's size changes.
+  ///
+  /// Must not be null if the persistent header is a floating header, and
+  /// [snapConfiguration] or [showOnScreenConfiguration] is not null.
+  TickerProvider get vsync => null;
+
   /// Specifies how floating headers should animate in and out of view.
   ///
   /// If the value of this property is null, then floating headers will
@@ -81,6 +88,12 @@
   /// Defaults to null.
   OverScrollHeaderStretchConfiguration get stretchConfiguration => null;
 
+  /// Specifies how floating headers and pinned pinned headers should behave in
+  /// response to [RenderObject.showOnScreen] calls.
+  ///
+  /// Defaults to null.
+  PersistentHeaderShowOnScreenConfiguration get showOnScreenConfiguration => null;
+
   /// Whether this delegate is meaningfully different from the old delegate.
   ///
   /// If this returns false, then the header might not be rebuilt, even though
@@ -346,7 +359,8 @@
   @override
   _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
     return _RenderSliverPinnedPersistentHeaderForWidgets(
-      stretchConfiguration: delegate.stretchConfiguration
+      stretchConfiguration: delegate.stretchConfiguration,
+      showOnScreenConfiguration: delegate.showOnScreenConfiguration,
     );
   }
 }
@@ -356,9 +370,11 @@
   _RenderSliverPinnedPersistentHeaderForWidgets({
     RenderBox child,
     OverScrollHeaderStretchConfiguration stretchConfiguration,
+    PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration,
   }) : super(
     child: child,
     stretchConfiguration: stretchConfiguration,
+    showOnScreenConfiguration: showOnScreenConfiguration,
   );
 }
 
@@ -374,15 +390,19 @@
   @override
   _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
     return _RenderSliverFloatingPersistentHeaderForWidgets(
+      vsync: delegate.vsync,
       snapConfiguration: delegate.snapConfiguration,
       stretchConfiguration: delegate.stretchConfiguration,
+      showOnScreenConfiguration: delegate.showOnScreenConfiguration,
     );
   }
 
   @override
   void updateRenderObject(BuildContext context, _RenderSliverFloatingPersistentHeaderForWidgets renderObject) {
+    renderObject.vsync = delegate.vsync;
     renderObject.snapConfiguration = delegate.snapConfiguration;
     renderObject.stretchConfiguration = delegate.stretchConfiguration;
+    renderObject.showOnScreenConfiguration = delegate.showOnScreenConfiguration;
   }
 }
 
@@ -390,12 +410,16 @@
   with _RenderSliverPersistentHeaderForWidgetsMixin {
   _RenderSliverFloatingPinnedPersistentHeaderForWidgets({
     RenderBox child,
+    @required TickerProvider vsync,
     FloatingHeaderSnapConfiguration snapConfiguration,
     OverScrollHeaderStretchConfiguration stretchConfiguration,
+    PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration,
   }) : super(
     child: child,
+    vsync: vsync,
     snapConfiguration: snapConfiguration,
     stretchConfiguration: stretchConfiguration,
+    showOnScreenConfiguration: showOnScreenConfiguration,
   );
 }
 
@@ -411,15 +435,19 @@
   @override
   _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
     return _RenderSliverFloatingPinnedPersistentHeaderForWidgets(
+      vsync: delegate.vsync,
       snapConfiguration: delegate.snapConfiguration,
       stretchConfiguration: delegate.stretchConfiguration,
+      showOnScreenConfiguration: delegate.showOnScreenConfiguration,
     );
   }
 
   @override
   void updateRenderObject(BuildContext context, _RenderSliverFloatingPinnedPersistentHeaderForWidgets renderObject) {
+    renderObject.vsync = delegate.vsync;
     renderObject.snapConfiguration = delegate.snapConfiguration;
     renderObject.stretchConfiguration = delegate.stretchConfiguration;
+    renderObject.showOnScreenConfiguration = delegate.showOnScreenConfiguration;
   }
 }
 
@@ -427,11 +455,15 @@
   with _RenderSliverPersistentHeaderForWidgetsMixin {
   _RenderSliverFloatingPersistentHeaderForWidgets({
     RenderBox child,
+    @required TickerProvider vsync,
     FloatingHeaderSnapConfiguration snapConfiguration,
     OverScrollHeaderStretchConfiguration stretchConfiguration,
+    PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration,
   }) : super(
     child: child,
+    vsync: vsync,
     snapConfiguration: snapConfiguration,
     stretchConfiguration: stretchConfiguration,
+    showOnScreenConfiguration: showOnScreenConfiguration,
   );
 }
diff --git a/packages/flutter/test/material/app_bar_test.dart b/packages/flutter/test/material/app_bar_test.dart
index 93938a4..1f8e4cc 100644
--- a/packages/flutter/test/material/app_bar_test.dart
+++ b/packages/flutter/test/material/app_bar_test.dart
@@ -1975,6 +1975,41 @@
     expect(tester.getCenter(appBarTitle).dy, tester.getCenter(toolbar).dy);
   });
 
+  testWidgets('SliverAppBar configures the delegate properly', (WidgetTester tester) async {
+    Future<void> buildAndVerifyDelegate({ bool pinned, bool floating, bool snap }) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: CustomScrollView(
+            slivers: <Widget>[
+              SliverAppBar(
+                title: const Text('Jumbo'),
+                pinned: pinned,
+                floating: floating,
+                snap: snap,
+              ),
+            ],
+          ),
+        ),
+      );
+
+      final SliverPersistentHeaderDelegate delegate = tester
+        .widget<SliverPersistentHeader>(find.byType(SliverPersistentHeader))
+        .delegate;
+
+      // Ensure we have a non-null vsync when it's needed.
+      if (!floating || (delegate.snapConfiguration == null && delegate.showOnScreenConfiguration == null))
+        expect(delegate.vsync, isNotNull);
+
+      expect(delegate.showOnScreenConfiguration != null, snap && floating);
+    }
+
+    await buildAndVerifyDelegate(pinned: false, floating: true, snap: false);
+    await buildAndVerifyDelegate(pinned: false, floating: true, snap: true);
+
+    await buildAndVerifyDelegate(pinned: true, floating: true, snap: false);
+    await buildAndVerifyDelegate(pinned: true, floating: true, snap: true);
+  });
+
   testWidgets('AppBar respects toolbarHeight', (WidgetTester tester) async {
     await tester.pumpWidget(
       MaterialApp(
diff --git a/packages/flutter/test/rendering/sliver_persistent_header_test.dart b/packages/flutter/test/rendering/sliver_persistent_header_test.dart
index ab18603..bafe3b0 100644
--- a/packages/flutter/test/rendering/sliver_persistent_header_test.dart
+++ b/packages/flutter/test/rendering/sliver_persistent_header_test.dart
@@ -49,7 +49,7 @@
 class TestRenderSliverFloatingPersistentHeader extends RenderSliverFloatingPersistentHeader {
   TestRenderSliverFloatingPersistentHeader({
     RenderBox child,
-  }) : super(child: child);
+  }) : super(child: child, vsync: null, showOnScreenConfiguration: null);
 
   @override
   double get maxExtent => 200;
@@ -61,7 +61,7 @@
 class TestRenderSliverFloatingPinnedPersistentHeader extends RenderSliverFloatingPinnedPersistentHeader {
   TestRenderSliverFloatingPinnedPersistentHeader({
     RenderBox child,
-  }) : super(child: child);
+  }) : super(child: child, vsync: null, showOnScreenConfiguration: null);
 
   @override
   double get maxExtent => 200;
diff --git a/packages/flutter/test/rendering/viewport_test.dart b/packages/flutter/test/rendering/viewport_test.dart
index f4c65a6..eed1589 100644
--- a/packages/flutter/test/rendering/viewport_test.dart
+++ b/packages/flutter/test/rendering/viewport_test.dart
@@ -16,6 +16,38 @@
 import 'package:flutter/rendering.dart';
 import 'package:flutter/widgets.dart';
 
+class _TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
+  _TestSliverPersistentHeaderDelegate({
+    this.key,
+    this.minExtent,
+    this.maxExtent,
+    this.child,
+    this.vsync = const TestVSync(),
+    this.showOnScreenConfiguration = const PersistentHeaderShowOnScreenConfiguration(),
+  });
+
+  final Key key;
+  final Widget child;
+
+  @override
+  final double maxExtent;
+
+  @override
+  final double minExtent;
+
+  @override
+  final TickerProvider vsync;
+
+  @override
+  final PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration;
+
+  @override
+  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => child ?? SizedBox.expand(key: key);
+
+  @override
+  bool shouldRebuild(_TestSliverPersistentHeaderDelegate oldDelegate) => true;
+}
+
 void main() {
   testWidgets('Viewport getOffsetToReveal - down', (WidgetTester tester) async {
     List<Widget> children;
@@ -1027,6 +1059,488 @@
     expect(controller.offset, 300.0);
   });
 
+  testWidgets(
+    'Viewport showOnScreen should not scroll if the rect is already visible, even if it does not scroll linearly',
+    (WidgetTester tester) async {
+      List<Widget> children;
+      ScrollController controller;
+
+      const Key headerKey = Key('header');
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: Center(
+            child: Container(
+              height: 600.0,
+              child: CustomScrollView(
+                controller: controller = ScrollController(initialScrollOffset: 300.0),
+                slivers: children = List<Widget>.generate(20, (int i) {
+                  return i == 10
+                  ? SliverPersistentHeader(
+                    pinned: true,
+                    floating: false,
+                    delegate: _TestSliverPersistentHeaderDelegate(
+                      minExtent: 100,
+                      maxExtent: 300,
+                      key: headerKey,
+                    ),
+                  )
+                  : SliverToBoxAdapter(
+                    child: Container(
+                      height: 300.0,
+                      child: Text('Tile $i'),
+                    ),
+                  );
+                }),
+              ),
+            ),
+          ),
+        ),
+      );
+
+      controller.jumpTo(300.0 * 15);
+      await tester.pumpAndSettle();
+
+      final Finder pinnedHeaderContent = find.descendant(
+        of: find.byWidget(children[10]),
+        matching: find.byKey(headerKey),
+      );
+
+      // The persistent header is pinned to the leading edge thus still visible,
+      // the viewport should not scroll.
+      tester.renderObject(pinnedHeaderContent).showOnScreen();
+      await tester.pumpAndSettle();
+      expect(controller.offset, 300.0 * 15);
+
+      // The 11th child will be partially obstructed by the persistent header,
+      // the viewport should scroll to reveal it.
+      controller.jumpTo(
+        11 * 300.0  // Preceding headers
+        + 200.0     // Shrinks the pinned header to minExtent
+        + 100.0     // Obstructs the leading 100 pixels of the 11th header
+      );
+      await tester.pumpAndSettle();
+
+      tester.renderObject(find.byWidget(children[11], skipOffstage: false)).showOnScreen();
+      await tester.pumpAndSettle();
+      expect(controller.offset, lessThan(11 * 300.0 + 200.0 + 100.0));
+  });
+
+  void testFloatingHeaderShowOnScreen({ bool animated = true, Axis axis = Axis.vertical }) {
+    final TickerProvider vsync = animated ? const TestVSync() : null;
+    const Key headerKey = Key('header');
+    List<Widget> children;
+    ScrollController controller;
+
+    Widget buildList({ SliverPersistentHeader floatingHeader, bool reversed = false }) {
+      return Directionality(
+        textDirection: TextDirection.ltr,
+        child: Center(
+          child: Container(
+            height: 400.0,
+            width: 400.0,
+            child: CustomScrollView(
+              scrollDirection: axis,
+              center: reversed ? const Key('19') : null,
+              controller: controller = ScrollController(initialScrollOffset: 300.0),
+              slivers: children = List<Widget>.generate(20, (int i) {
+                  return i == 10
+                  ? floatingHeader
+                  : SliverToBoxAdapter(
+                    key: (i == 19) ? const Key('19') : null,
+                    child: Container(
+                      height: 300.0,
+                      width: 300,
+                      child: Text('Tile $i'),
+                    ),
+                  );
+              }),
+            ),
+          ),
+        ),
+      );
+    }
+
+    double mainAxisExtent(WidgetTester tester, Finder finder) {
+      final RenderObject renderObject = tester.renderObject(finder);
+      if (renderObject is RenderSliver) {
+        return renderObject.geometry.paintExtent;
+      }
+
+      final RenderBox renderBox = renderObject as RenderBox;
+      switch (axis) {
+        case Axis.horizontal:
+          return renderBox.size.width;
+        case Axis.vertical:
+          return renderBox.size.height;
+      }
+      assert(false);
+      return null;
+    }
+
+    group('animated: $animated, scrollDirection: $axis', () {
+      testWidgets(
+        'RenderViewportBase.showOnScreen',
+        (WidgetTester tester) async {
+          await tester.pumpWidget(
+            buildList(
+              floatingHeader: SliverPersistentHeader(
+                pinned: true,
+                floating: true,
+                delegate: _TestSliverPersistentHeaderDelegate(minExtent: 100, maxExtent: 300, key: headerKey, vsync: vsync),
+              ),
+            )
+          );
+
+          final Finder pinnedHeaderContent = find.byKey(headerKey, skipOffstage: false);
+
+          controller.jumpTo(300.0 * 15);
+          await tester.pumpAndSettle();
+          expect(mainAxisExtent(tester, pinnedHeaderContent), lessThan(300));
+
+          // The persistent header is pinned to the leading edge thus still visible,
+          // the viewport should not scroll.
+          tester.renderObject(pinnedHeaderContent).showOnScreen(
+            descendant: tester.renderObject(pinnedHeaderContent),
+            rect: Offset.zero & const Size(300, 300),
+          );
+          await tester.pumpAndSettle();
+          // The header expands but doesn't move.
+          expect(controller.offset, 300.0 * 15);
+          expect(mainAxisExtent(tester, pinnedHeaderContent), 300);
+
+          // The rect specifies that the persistent header needs to be 1 pixel away
+          // from the leading edge of the viewport. Ignore the 1 pixel, the viewport
+          // should not scroll.
+          //
+          // See: https://github.com/flutter/flutter/issues/25507.
+          tester.renderObject(pinnedHeaderContent).showOnScreen(
+            descendant: tester.renderObject(pinnedHeaderContent),
+            rect: const Offset(-1, -1) & const Size(300, 300),
+          );
+          await tester.pumpAndSettle();
+          expect(controller.offset, 300.0 * 15);
+          expect(mainAxisExtent(tester, pinnedHeaderContent), 300);
+      });
+
+      testWidgets(
+        'RenderViewportBase.showOnScreen but no child',
+        (WidgetTester tester) async {
+          await tester.pumpWidget(
+            buildList(
+              floatingHeader: SliverPersistentHeader(
+                key: headerKey,
+                pinned: true,
+                floating: true,
+                delegate: _TestSliverPersistentHeaderDelegate(minExtent: 100, maxExtent: 300, child: null, vsync: vsync),
+              ),
+            )
+          );
+
+          final Finder pinnedHeaderContent = find.byKey(headerKey, skipOffstage: false);
+
+          controller.jumpTo(300.0 * 15);
+          await tester.pumpAndSettle();
+          expect(mainAxisExtent(tester, pinnedHeaderContent), lessThan(300));
+
+          // The persistent header is pinned to the leading edge thus still visible,
+          // the viewport should not scroll.
+          tester.renderObject(pinnedHeaderContent).showOnScreen(
+            rect: Offset.zero & const Size(300, 300),
+          );
+          await tester.pumpAndSettle();
+          // The header expands but doesn't move.
+          expect(controller.offset, 300.0 * 15);
+          expect(mainAxisExtent(tester, pinnedHeaderContent), 300);
+
+          // The rect specifies that the persistent header needs to be 1 pixel away
+          // from the leading edge of the viewport. Ignore the 1 pixel, the viewport
+          // should not scroll.
+          //
+          // See: https://github.com/flutter/flutter/issues/25507.
+          tester.renderObject(pinnedHeaderContent).showOnScreen(
+            rect: const Offset(-1, -1) & const Size(300, 300),
+          );
+          await tester.pumpAndSettle();
+          expect(controller.offset, 300.0 * 15);
+          expect(mainAxisExtent(tester, pinnedHeaderContent), 300);
+      });
+
+      testWidgets(
+        'RenderViewportBase.showOnScreen with maxShowOnScreenExtent ',
+        (WidgetTester tester) async {
+          await tester.pumpWidget(
+            buildList(
+              floatingHeader: SliverPersistentHeader(
+                pinned: true,
+                floating: true,
+                delegate: _TestSliverPersistentHeaderDelegate(
+                  minExtent: 100,
+                  maxExtent: 300,
+                  key: headerKey,
+                  vsync: vsync,
+                  showOnScreenConfiguration: const PersistentHeaderShowOnScreenConfiguration(maxShowOnScreenExtent: 200),
+                ),
+              ),
+            )
+          );
+
+          final Finder pinnedHeaderContent = find.byKey(headerKey, skipOffstage: false);
+
+          controller.jumpTo(300.0 * 15);
+          await tester.pumpAndSettle();
+          // childExtent was initially 100.
+          expect(mainAxisExtent(tester, pinnedHeaderContent), 100);
+
+          tester.renderObject(pinnedHeaderContent).showOnScreen(
+            descendant: tester.renderObject(pinnedHeaderContent),
+            rect: Offset.zero & const Size(300, 300),
+          );
+          await tester.pumpAndSettle();
+          // The header doesn't move. It would have expanded to 300 but
+          // maxShowOnScreenExtent is 200, preventing it from doing so.
+          expect(controller.offset, 300.0 * 15);
+          expect(mainAxisExtent(tester, pinnedHeaderContent), 200);
+
+          // ignoreLeading still works.
+          tester.renderObject(pinnedHeaderContent).showOnScreen(
+            descendant: tester.renderObject(pinnedHeaderContent),
+            rect: const Offset(-1, -1) & const Size(300, 300),
+          );
+          await tester.pumpAndSettle();
+          expect(controller.offset, 300.0 * 15);
+          expect(mainAxisExtent(tester, pinnedHeaderContent), 200);
+
+          // Move the viewport so that its childExtent reaches 250.
+          controller.jumpTo(300.0 * 10 + 50.0);
+          await tester.pumpAndSettle();
+          expect(mainAxisExtent(tester, pinnedHeaderContent), 250);
+
+          // Doesn't move, doesn't expand or shrink, leading still ignored.
+          tester.renderObject(pinnedHeaderContent).showOnScreen(
+            descendant: tester.renderObject(pinnedHeaderContent),
+            rect: const Offset(-1, -1) & const Size(300, 300),
+          );
+          await tester.pumpAndSettle();
+          expect(controller.offset, 300.0 * 10 + 50.0);
+          expect(mainAxisExtent(tester, pinnedHeaderContent), 250);
+      });
+
+      testWidgets(
+        'RenderViewportBase.showOnScreen with minShowOnScreenExtent ',
+        (WidgetTester tester) async {
+          await tester.pumpWidget(
+            buildList(
+              floatingHeader: SliverPersistentHeader(
+                pinned: true,
+                floating: true,
+                delegate: _TestSliverPersistentHeaderDelegate(
+                  minExtent: 100,
+                  maxExtent: 300,
+                  key: headerKey,
+                  vsync: vsync,
+                  showOnScreenConfiguration: const PersistentHeaderShowOnScreenConfiguration(minShowOnScreenExtent: 200),
+                ),
+              ),
+            )
+          );
+
+          final Finder pinnedHeaderContent = find.byKey(headerKey, skipOffstage: false);
+
+          controller.jumpTo(300.0 * 15);
+          await tester.pumpAndSettle();
+          // childExtent was initially 100.
+          expect(mainAxisExtent(tester, pinnedHeaderContent), 100);
+
+          tester.renderObject(pinnedHeaderContent).showOnScreen(
+            descendant: tester.renderObject(pinnedHeaderContent),
+            rect: Offset.zero & const Size(110, 110),
+          );
+          await tester.pumpAndSettle();
+          // The header doesn't move. It would have expanded to 110 but
+          // minShowOnScreenExtent is 200, preventing it from doing so.
+          expect(controller.offset, 300.0 * 15);
+          expect(mainAxisExtent(tester, pinnedHeaderContent), 200);
+
+          // ignoreLeading still works.
+          tester.renderObject(pinnedHeaderContent).showOnScreen(
+            descendant: tester.renderObject(pinnedHeaderContent),
+            rect: const Offset(-1, -1) & const Size(110, 110),
+          );
+          await tester.pumpAndSettle();
+          expect(controller.offset, 300.0 * 15);
+          expect(mainAxisExtent(tester, pinnedHeaderContent), 200);
+
+          // Move the viewport so that its childExtent reaches 250.
+          controller.jumpTo(300.0 * 10 + 50.0);
+          await tester.pumpAndSettle();
+          expect(mainAxisExtent(tester, pinnedHeaderContent), 250);
+
+          // Doesn't move, doesn't expand or shrink, leading still ignored.
+          tester.renderObject(pinnedHeaderContent).showOnScreen(
+            descendant: tester.renderObject(pinnedHeaderContent),
+            rect: const Offset(-1, -1) & const Size(110, 110),
+          );
+          await tester.pumpAndSettle();
+          expect(controller.offset, 300.0 * 10 + 50.0);
+          expect(mainAxisExtent(tester, pinnedHeaderContent), 250);
+      });
+
+      testWidgets(
+        'RenderViewportBase.showOnScreen should not scroll if the rect is already visible, '
+        'even if it does not scroll linearly (reversed order version)',
+        (WidgetTester tester) async {
+          await tester.pumpWidget(
+            buildList(
+              floatingHeader: SliverPersistentHeader(
+                pinned: true,
+                floating: true,
+                delegate: _TestSliverPersistentHeaderDelegate(minExtent: 100, maxExtent: 300, key: headerKey, vsync: vsync),
+              ),
+              reversed: true,
+            )
+          );
+
+          controller.jumpTo(-300.0 * 15);
+          await tester.pumpAndSettle();
+
+          final Finder pinnedHeaderContent = find.byKey(headerKey, skipOffstage: false);
+
+          // The persistent header is pinned to the leading edge thus still visible,
+          // the viewport should not scroll.
+          tester.renderObject(pinnedHeaderContent).showOnScreen();
+          await tester.pumpAndSettle();
+          expect(controller.offset, -300.0 * 15);
+
+          // children[9] will be partially obstructed by the persistent header,
+          // the viewport should scroll to reveal it.
+          controller.jumpTo(
+            - 8 * 300.0 // Preceding headers 11 - 18, children[11]'s top edge is aligned to the leading edge.
+            - 400.0     // Viewport height. children[10] (the pinned header) becomes pinned at the bottom of the screen.
+            - 200.0     // Shrinks the pinned header to minExtent (100).
+            - 100.0     // Obstructs the leading 100 pixels of the 11th header
+          );
+          await tester.pumpAndSettle();
+
+          tester.renderObject(find.byWidget(children[9], skipOffstage: false)).showOnScreen();
+          await tester.pumpAndSettle();
+          expect(controller.offset, -8 * 300.0 - 400.0 - 200.0);
+      });
+    });
+  }
+
+  group('Floating header showOnScreen', () {
+    testFloatingHeaderShowOnScreen(animated: true, axis: Axis.vertical);
+    testFloatingHeaderShowOnScreen(animated: true, axis: Axis.horizontal);
+  });
+
+  group('RenderViewport getOffsetToReveal renderBox to sliver coordinates conversion', () {
+    const EdgeInsets padding = EdgeInsets.fromLTRB(22, 22, 34, 34);
+    const Key centerKey = Key('5');
+    Widget buildList({ Axis axis, bool reverse = false, bool reverseGrowth = false }) {
+      return Directionality(
+        textDirection: TextDirection.ltr,
+        child: Center(
+          child: Container(
+            height: 400.0,
+            width: 400.0,
+            child: CustomScrollView(
+              scrollDirection: axis,
+              reverse: reverse,
+              center: reverseGrowth ? centerKey : null,
+              slivers: List<Widget>.generate(6, (int i) {
+                return SliverPadding(
+                  key: i == 5 ? centerKey : null,
+                  padding: padding,
+                  sliver: SliverToBoxAdapter(
+                    child: Container(
+                      padding: padding,
+                      height: 300.0,
+                      width: 300.0,
+                      child: Text('Tile $i'),
+                    ),
+                  ),
+                );
+              }),
+            ),
+          ),
+        ),
+      );
+    }
+
+    testWidgets('up, forward growth', (WidgetTester tester) async {
+      await tester.pumpWidget(buildList(axis: Axis.vertical, reverse: true, reverseGrowth: false));
+      final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
+
+      final RenderObject target = tester.renderObject(find.text('Tile 5', skipOffstage: false));
+      final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
+      expect(revealOffset, (300.0 + padding.horizontal)  * 5 + 34.0 * 2);
+    });
+
+    testWidgets('up, reverse growth', (WidgetTester tester) async {
+      await tester.pumpWidget(buildList(axis: Axis.vertical, reverse: true, reverseGrowth: true));
+      final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
+
+      final RenderObject target = tester.renderObject(find.text('Tile 0', skipOffstage: false));
+      final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
+      expect(revealOffset, -(300.0 + padding.horizontal)  * 5 + 34.0 * 2);
+    });
+
+    testWidgets('right, forward growth', (WidgetTester tester) async {
+      await tester.pumpWidget(buildList(axis: Axis.horizontal, reverse: false, reverseGrowth: false));
+      final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
+
+      final RenderObject target = tester.renderObject(find.text('Tile 5', skipOffstage: false));
+      final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
+      expect(revealOffset, (300.0 + padding.horizontal)  * 5 + 22.0 * 2);
+    });
+
+    testWidgets('right, reverse growth', (WidgetTester tester) async {
+      await tester.pumpWidget(buildList(axis: Axis.horizontal, reverse: false, reverseGrowth: true));
+      final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
+
+      final RenderObject target = tester.renderObject(find.text('Tile 0', skipOffstage: false));
+      final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
+      expect(revealOffset, -(300.0 + padding.horizontal)  * 5 + 22.0 * 2);
+    });
+
+    testWidgets('down, forward growth', (WidgetTester tester) async {
+      await tester.pumpWidget(buildList(axis: Axis.vertical, reverse: false, reverseGrowth: false));
+      final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
+
+      final RenderObject target = tester.renderObject(find.text('Tile 5', skipOffstage: false));
+      final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
+      expect(revealOffset, (300.0 + padding.horizontal)  * 5 + 22.0 * 2);
+    });
+
+    testWidgets('down, reverse growth', (WidgetTester tester) async {
+      await tester.pumpWidget(buildList(axis: Axis.vertical, reverse: false, reverseGrowth: true));
+      final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
+
+      final RenderObject target = tester.renderObject(find.text('Tile 0', skipOffstage: false));
+      final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
+      expect(revealOffset, -(300.0 + padding.horizontal)  * 5 + 22.0 * 2);
+    });
+
+    testWidgets('left, forward growth', (WidgetTester tester) async {
+      await tester.pumpWidget(buildList(axis: Axis.horizontal, reverse: true, reverseGrowth: false));
+      final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
+
+      final RenderObject target = tester.renderObject(find.text('Tile 5', skipOffstage: false));
+      final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
+      expect(revealOffset, (300.0 + padding.horizontal)  * 5 + 34.0 * 2);
+    });
+
+    testWidgets('left, reverse growth', (WidgetTester tester) async {
+      await tester.pumpWidget(buildList(axis: Axis.horizontal, reverse: true, reverseGrowth: true));
+      final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
+
+      final RenderObject target = tester.renderObject(find.text('Tile 0', skipOffstage: false));
+      final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
+      expect(revealOffset, -(300.0 + padding.horizontal)  * 5 + 34.0 * 2);
+    });
+  });
+
   testWidgets('RenderViewportBase.showOnScreen reports the correct targetRect', (WidgetTester tester) async {
     final ScrollController innerController = ScrollController();
     final ScrollController outerController = ScrollController();
diff --git a/packages/flutter/test/widgets/editable_text_show_on_screen_test.dart b/packages/flutter/test/widgets/editable_text_show_on_screen_test.dart
index 7ef32db..0530c55 100644
--- a/packages/flutter/test/widgets/editable_text_show_on_screen_test.dart
+++ b/packages/flutter/test/widgets/editable_text_show_on_screen_test.dart
@@ -10,6 +10,35 @@
 import 'package:flutter/widgets.dart';
 import 'package:flutter/services.dart';
 
+class _TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
+  _TestSliverPersistentHeaderDelegate({
+    this.minExtent,
+    this.maxExtent,
+    this.child,
+    this.vsync = const TestVSync(),
+    this.showOnScreenConfiguration = const PersistentHeaderShowOnScreenConfiguration(),
+  });
+
+  final Widget child;
+
+  @override
+  final double maxExtent;
+
+  @override
+  final double minExtent;
+
+  @override
+  final TickerProvider vsync;
+
+  @override
+  final PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration;
+
+  @override
+  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => child;
+
+  @override
+  bool shouldRebuild(_TestSliverPersistentHeaderDelegate oldDelegate) => true;
+}
 
 void main() {
   const TextStyle textStyle = TextStyle();
@@ -339,6 +368,131 @@
     expect(scrollController.offset, greaterThan(0.0));
     expect(find.byKey(container), findsNothing);
   });
+
+  testWidgets(
+    'A pinned persistent header should not scroll when its descendant EditableText gains focus',
+    (WidgetTester tester) async {
+      // Regression test for https://github.com/flutter/flutter/issues/25507.
+      ScrollController controller;
+      final TextEditingController textEditingController = TextEditingController();
+      final FocusNode focusNode = FocusNode();
+
+      const Key headerKey = Key('header');
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Center(
+            child: SizedBox(
+              height: 600.0,
+              width: 600.0,
+              child: CustomScrollView(
+                controller: controller = ScrollController(initialScrollOffset: 0),
+                slivers: List<Widget>.generate(50, (int i) {
+                  return i == 10
+                  ? SliverPersistentHeader(
+                    pinned: true,
+                    floating: false,
+                    delegate: _TestSliverPersistentHeaderDelegate(
+                      minExtent: 50,
+                      maxExtent: 50,
+                      child: Container(
+                        alignment: Alignment.topCenter,
+                        child: EditableText(
+                          key: headerKey,
+                          backgroundCursorColor: Colors.grey,
+                          controller: textEditingController,
+                          focusNode: focusNode,
+                          style: textStyle,
+                          cursorColor: cursorColor,
+                        ),
+                      ),
+                    ),
+                  )
+                  : SliverToBoxAdapter(
+                    child: Container(
+                      height: 100.0,
+                      child: Text('Tile $i'),
+                    ),
+                  );
+                }),
+              ),
+            ),
+          ),
+        ),
+      );
+
+      // The persistent header should now be pinned at the top.
+      controller.jumpTo(100.0 * 15);
+      await tester.pumpAndSettle();
+      expect(controller.offset, 100.0 * 15);
+
+      focusNode.requestFocus();
+      await tester.pumpAndSettle();
+      // The scroll offset should remain the same.
+      expect(controller.offset, 100.0 * 15);
+  });
+
+  testWidgets(
+    'A pinned persistent header should not scroll when its descendant EditableText gains focus (no animation)',
+    (WidgetTester tester) async {
+      // Regression test for https://github.com/flutter/flutter/issues/25507.
+      ScrollController controller;
+      final TextEditingController textEditingController = TextEditingController();
+      final FocusNode focusNode = FocusNode();
+
+      const Key headerKey = Key('header');
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Center(
+            child: SizedBox(
+              height: 600.0,
+              width: 600.0,
+              child: CustomScrollView(
+                controller: controller = ScrollController(initialScrollOffset: 0),
+                slivers: List<Widget>.generate(50, (int i) {
+                  return i == 10
+                    ? SliverPersistentHeader(
+                      pinned: true,
+                      floating: false,
+                      delegate: _TestSliverPersistentHeaderDelegate(
+                        minExtent: 50,
+                        maxExtent: 50,
+                        vsync: null,
+                        child: Container(
+                          alignment: Alignment.topCenter,
+                          child: EditableText(
+                            key: headerKey,
+                            backgroundCursorColor: Colors.grey,
+                            controller: textEditingController,
+                            focusNode: focusNode,
+                            style: textStyle,
+                            cursorColor: cursorColor,
+                          ),
+                        ),
+                      ),
+                    )
+                    : SliverToBoxAdapter(
+                      child: Container(
+                        height: 100.0,
+                        child: Text('Tile $i'),
+                      ),
+                    );
+                }),
+              ),
+            ),
+          ),
+        ),
+      );
+
+      // The persistent header should now be pinned at the top.
+      controller.jumpTo(100.0 * 15);
+      await tester.pumpAndSettle();
+      expect(controller.offset, 100.0 * 15);
+
+      focusNode.requestFocus();
+      await tester.pumpAndSettle();
+      // The scroll offset should remain the same.
+      expect(controller.offset, 100.0 * 15);
+  });
 }
 
 class NoImplicitScrollPhysics extends AlwaysScrollableScrollPhysics {