Unverified Commit fe55dc2b authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Reland "Prevent viewport.showOnScreen from scrolling the viewport if the...

Reland "Prevent viewport.showOnScreen from scrolling the viewport if the specified Rect is already visible. (#56413)" reverted in #64091 (#64513)
parent 8ce4f41e
......@@ -817,12 +817,18 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@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;
......@@ -860,12 +866,18 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@override
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;
......@@ -935,8 +947,10 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|| 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 @@ class SliverAppBar extends StatefulWidget {
/// 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 SliverAppBar extends StatefulWidget {
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 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
floating: widget.floating,
pinned: widget.pinned,
delegate: _SliverAppBarDelegate(
vsync: this,
leading: widget.leading,
automaticallyImplyLeading: widget.automaticallyImplyLeading,
title: widget.title,
......@@ -1464,6 +1488,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
shape: widget.shape,
snapConfiguration: _snapConfiguration,
stretchConfiguration: _stretchConfiguration,
showOnScreenConfiguration: _showOnScreenConfiguration,
toolbarHeight: widget.toolbarHeight,
leadingWidth: widget.leadingWidth,
),
......
......@@ -17,6 +17,15 @@ import 'sliver.dart';
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 @@ class OverScrollHeaderStretchConfiguration {
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 @@ abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistent
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 @@ abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistent
@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 @@ class FloatingHeaderSnapConfiguration {
/// 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 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
/// 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 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
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 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
/// 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 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
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();
}
......@@ -567,6 +704,79 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
_lastActualScrollOffset = constraints.scrollOffset;
}
@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);
......@@ -594,12 +804,16 @@ abstract class RenderSliverFloatingPinnedPersistentHeader extends RenderSliverFl
/// 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
......
......@@ -816,7 +816,7 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
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 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
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 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
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.
......
......@@ -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 @@ abstract class SliverPersistentHeaderDelegate {
/// 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 @@ abstract class SliverPersistentHeaderDelegate {
/// 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 @@ class _SliverPinnedPersistentHeader extends _SliverPersistentHeaderRenderObjectW
@override
_RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
return _RenderSliverPinnedPersistentHeaderForWidgets(
stretchConfiguration: delegate.stretchConfiguration
stretchConfiguration: delegate.stretchConfiguration,
showOnScreenConfiguration: delegate.showOnScreenConfiguration,
);
}
}
......@@ -356,9 +370,11 @@ class _RenderSliverPinnedPersistentHeaderForWidgets extends RenderSliverPinnedPe
_RenderSliverPinnedPersistentHeaderForWidgets({
RenderBox child,
OverScrollHeaderStretchConfiguration stretchConfiguration,
PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration,
}) : super(
child: child,
stretchConfiguration: stretchConfiguration,
showOnScreenConfiguration: showOnScreenConfiguration,
);
}
......@@ -374,15 +390,19 @@ class _SliverFloatingPersistentHeader extends _SliverPersistentHeaderRenderObjec
@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 @@ class _RenderSliverFloatingPinnedPersistentHeaderForWidgets extends RenderSliver
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 @@ class _SliverFloatingPinnedPersistentHeader extends _SliverPersistentHeaderRende
@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 @@ class _RenderSliverFloatingPersistentHeaderForWidgets extends RenderSliverFloati
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,
);
}
......@@ -1975,6 +1975,41 @@ void main() {
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(
......
......@@ -49,7 +49,7 @@ void main() {
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 TestRenderSliverFloatingPersistentHeader extends RenderSliverFloatingPersi
class TestRenderSliverFloatingPinnedPersistentHeader extends RenderSliverFloatingPinnedPersistentHeader {
TestRenderSliverFloatingPinnedPersistentHeader({
RenderBox child,
}) : super(child: child);
}) : super(child: child, vsync: null, showOnScreenConfiguration: null);
@override
double get maxExtent => 200;
......
......@@ -16,6 +16,38 @@ import 'package:flutter/foundation.dart';
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 @@ void main() {
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();
......
......@@ -10,6 +10,35 @@ import 'package:flutter/material.dart';
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 @@ void main() {
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 {
......
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