mirror of
https://github.com/flutter/flutter
synced 2024-10-13 19:52:53 +00:00
Prevent viewport.showOnScreen from scrolling the viewport if the specified Rect is already visible. (#56413)
This commit is contained in:
parent
6f26e806ab
commit
64d76f2fb7
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -19,6 +19,25 @@ 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,
|
||||
}) {
|
||||
if (original == null)
|
||||
return null;
|
||||
|
||||
return Rect.fromLTRB(
|
||||
math.max(original.left, left),
|
||||
math.max(original.top, top),
|
||||
math.min(original.right, right),
|
||||
math.min(original.bottom, bottom),
|
||||
);
|
||||
}
|
||||
|
||||
/// Specifies how a stretched header is to trigger an [AsyncCallback].
|
||||
///
|
||||
/// See also:
|
||||
|
@ -41,6 +60,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,
|
||||
this.maxShowOnScreenExtent,
|
||||
}) : assert(minShowOnScreenExtent == null || maxShowOnScreenExtent == null || 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 null, in which case no additional constraints are applied.
|
||||
/// Must be less than or equal to [maxShowOnScreenExtent] if it is also not
|
||||
/// null. 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 null, in which case no additional constraints are applied.
|
||||
/// Must be greater than or equal to [minShowOnScreenExtent] if it is also not
|
||||
/// null. 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]).
|
||||
|
@ -348,11 +421,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;
|
||||
|
@ -378,6 +458,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
|
||||
|
@ -394,15 +509,22 @@ 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.
|
||||
/// 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.
|
||||
|
@ -426,13 +548,15 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
|
|||
/// direction.
|
||||
RenderSliverFloatingPersistentHeader({
|
||||
RenderBox child,
|
||||
FloatingHeaderSnapConfiguration snapConfiguration,
|
||||
@required TickerProvider vsync,
|
||||
this.snapConfiguration,
|
||||
OverScrollHeaderStretchConfiguration stretchConfiguration,
|
||||
}) : _snapConfiguration = snapConfiguration,
|
||||
@required this.showOnScreenConfiguration,
|
||||
}) : _vsync = vsync,
|
||||
super(
|
||||
child: child,
|
||||
stretchConfiguration: stretchConfiguration,
|
||||
);
|
||||
child: child,
|
||||
stretchConfiguration: stretchConfiguration,
|
||||
);
|
||||
|
||||
AnimationController _controller;
|
||||
Animation<double> _animation;
|
||||
|
@ -450,6 +574,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(_vsync);
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the parameters used to snap (animate) the floating header in and
|
||||
/// out of view.
|
||||
///
|
||||
|
@ -462,20 +602,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].
|
||||
///
|
||||
|
@ -500,6 +633,31 @@ 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);
|
||||
if (_controller == null) {
|
||||
assert(
|
||||
vsync != null,
|
||||
'vsync must not be null if the floating header changes size animatedly.',
|
||||
);
|
||||
_controller = AnimationController(vsync: vsync, duration: duration)
|
||||
..addListener(() {
|
||||
if (_effectiveScrollOffset == _animation.value)
|
||||
return;
|
||||
_effectiveScrollOffset = _animation.value;
|
||||
markNeedsLayout();
|
||||
});
|
||||
}
|
||||
_animation = _controller.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)
|
||||
|
@ -509,29 +667,16 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
|
|||
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(
|
||||
snapConfiguration.duration,
|
||||
direction == ScrollDirection.forward ? 0.0 : maxExtent,
|
||||
snapConfiguration.curve,
|
||||
);
|
||||
|
||||
_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 +713,78 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
|
|||
_lastActualScrollOffset = constraints.scrollOffset;
|
||||
}
|
||||
|
||||
@override
|
||||
void showOnScreen({
|
||||
RenderObject descendant,
|
||||
Rect rect,
|
||||
Duration duration = Duration.zero,
|
||||
Curve curve = Curves.ease,
|
||||
}) {
|
||||
if (showOnScreenConfiguration == 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(
|
||||
showOnScreenConfiguration.minShowOnScreenExtent ?? double.negativeInfinity,
|
||||
showOnScreenConfiguration.maxShowOnScreenExtent ?? double.infinity,
|
||||
)
|
||||
// Clamp the value back to the valid range after applying additional
|
||||
// constriants. Contracting is not allowed.
|
||||
.clamp(childExtent, effectiveMaxExtent) as double;
|
||||
|
||||
// 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);
|
||||
|
@ -595,12 +812,16 @@ abstract class RenderSliverFloatingPinnedPersistentHeader extends RenderSliverFl
|
|||
/// scroll direction.
|
||||
RenderSliverFloatingPinnedPersistentHeader({
|
||||
RenderBox child,
|
||||
@required TickerProvider vsync,
|
||||
FloatingHeaderSnapConfiguration snapConfiguration,
|
||||
OverScrollHeaderStretchConfiguration stretchConfiguration,
|
||||
@required PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration,
|
||||
}) : super(
|
||||
child: child,
|
||||
vsync: vsync,
|
||||
snapConfiguration: snapConfiguration,
|
||||
stretchConfiguration: stretchConfiguration,
|
||||
showOnScreenConfiguration: showOnScreenConfiguration,
|
||||
);
|
||||
|
||||
@override
|
||||
|
|
|
@ -727,58 +727,26 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
|
|||
final Matrix4 transform = target.getTransformTo(pivot);
|
||||
final Rect bounds = MatrixUtils.transformRect(transform, rect);
|
||||
|
||||
// Convert `rect`'s leading edge from `pivot`'s RenderBox coordinate
|
||||
// system to the scrollOffset within `pivot.parent`. For `up` and `left`
|
||||
// AxisDirections here, the leading edge of the render box is the
|
||||
// bottom/right edge.
|
||||
final GrowthDirection growthDirection = pivotParent.constraints.growthDirection;
|
||||
switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
|
||||
case AxisDirection.up:
|
||||
double offset;
|
||||
switch (growthDirection) {
|
||||
case GrowthDirection.forward:
|
||||
offset = bounds.bottom;
|
||||
break;
|
||||
case GrowthDirection.reverse:
|
||||
offset = bounds.top;
|
||||
break;
|
||||
}
|
||||
leadingScrollOffset += pivot.size.height - offset;
|
||||
leadingScrollOffset += pivot.size.height - bounds.bottom;
|
||||
targetMainAxisExtent = bounds.height;
|
||||
break;
|
||||
case AxisDirection.right:
|
||||
double offset;
|
||||
switch (growthDirection) {
|
||||
case GrowthDirection.forward:
|
||||
offset = bounds.left;
|
||||
break;
|
||||
case GrowthDirection.reverse:
|
||||
offset = bounds.right;
|
||||
break;
|
||||
}
|
||||
leadingScrollOffset += offset;
|
||||
leadingScrollOffset += bounds.left;
|
||||
targetMainAxisExtent = bounds.width;
|
||||
break;
|
||||
case AxisDirection.down:
|
||||
double offset;
|
||||
switch (growthDirection) {
|
||||
case GrowthDirection.forward:
|
||||
offset = bounds.top;
|
||||
break;
|
||||
case GrowthDirection.reverse:
|
||||
offset = bounds.bottom;
|
||||
break;
|
||||
}
|
||||
leadingScrollOffset += offset;
|
||||
leadingScrollOffset += bounds.top;
|
||||
targetMainAxisExtent = bounds.height;
|
||||
break;
|
||||
case AxisDirection.left:
|
||||
double offset;
|
||||
switch (growthDirection) {
|
||||
case GrowthDirection.forward:
|
||||
offset = bounds.right;
|
||||
break;
|
||||
case GrowthDirection.reverse:
|
||||
offset = bounds.left;
|
||||
break;
|
||||
}
|
||||
leadingScrollOffset += pivot.size.width - offset;
|
||||
leadingScrollOffset += pivot.size.width - bounds.right;
|
||||
targetMainAxisExtent = bounds.width;
|
||||
break;
|
||||
}
|
||||
|
@ -792,14 +760,44 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
|
|||
assert(child.parent == this);
|
||||
assert(child is RenderSliver);
|
||||
final RenderSliver sliver = child as RenderSliver;
|
||||
|
||||
// This step assumes the viewport's layout is up-to-date, i.e., if
|
||||
// offset.pixels is changed after the last performLayout, the new scroll
|
||||
// position will not be accounted for.
|
||||
final Matrix4 transform = target.getTransformTo(this);
|
||||
Rect targetRect = MatrixUtils.transformRect(transform, rect);
|
||||
|
||||
// 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;
|
||||
|
||||
final double extentOfPinnedSlivers = maxScrollObstructionExtentBefore(sliver);
|
||||
// The additional scroll offset needed to move the leading edge of the
|
||||
// `target` to align with the leading edge of the viewport.
|
||||
leadingScrollOffset = scrollOffsetOf(sliver, leadingScrollOffset);
|
||||
|
||||
switch (sliver.constraints.growthDirection) {
|
||||
case GrowthDirection.forward:
|
||||
if (isPinned && alignment <= 0)
|
||||
return RevealedOffset(offset: double.infinity, rect: targetRect);
|
||||
leadingScrollOffset -= extentOfPinnedSlivers;
|
||||
break;
|
||||
case GrowthDirection.reverse:
|
||||
// Nothing to do.
|
||||
if (isPinned && alignment >= 1)
|
||||
return RevealedOffset(offset: double.negativeInfinity, rect: targetRect);
|
||||
switch (axis) {
|
||||
case Axis.vertical:
|
||||
leadingScrollOffset -= targetRect.height;
|
||||
break;
|
||||
case Axis.horizontal:
|
||||
leadingScrollOffset -= targetRect.width;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -816,9 +814,6 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
|
|||
final double targetOffset = leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
|
||||
final double offsetDifference = offset.pixels - targetOffset;
|
||||
|
||||
final Matrix4 transform = target.getTransformTo(this);
|
||||
Rect targetRect = MatrixUtils.transformRect(transform, rect);
|
||||
|
||||
switch (axisDirection) {
|
||||
case AxisDirection.down:
|
||||
targetRect = targetRect.translate(0.0, offsetDifference);
|
||||
|
|
|
@ -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;
|
||||
|
@ -220,7 +252,7 @@ void main() {
|
|||
);
|
||||
children.add(sliver);
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.all(22.0),
|
||||
padding: const EdgeInsets.only(top: 22.0, bottom: 23.0),
|
||||
sliver: sliver,
|
||||
);
|
||||
}),
|
||||
|
@ -234,10 +266,10 @@ void main() {
|
|||
|
||||
final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false));
|
||||
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
|
||||
expect(revealed.offset, 5 * (100 + 22 + 22) + 22);
|
||||
expect(revealed.offset, 5 * (100 + 22 + 23) + 22);
|
||||
|
||||
revealed = viewport.getOffsetToReveal(target, 1.0);
|
||||
expect(revealed.offset, 5 * (100 + 22 + 22) + 22 - 100);
|
||||
expect(revealed.offset, 5 * (100 + 22 + 23) + 22 - 100);
|
||||
});
|
||||
|
||||
testWidgets('Viewport getOffsetToReveal Sliver - right', (WidgetTester tester) async {
|
||||
|
@ -261,7 +293,7 @@ void main() {
|
|||
);
|
||||
children.add(sliver);
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.all(22.0),
|
||||
padding: const EdgeInsets.only(left: 22.0, right: 23.0),
|
||||
sliver: sliver,
|
||||
);
|
||||
}),
|
||||
|
@ -275,10 +307,10 @@ void main() {
|
|||
|
||||
final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false));
|
||||
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
|
||||
expect(revealed.offset, 5 * (100 + 22 + 22) + 22);
|
||||
expect(revealed.offset, 5 * (100 + 22 + 23) + 22);
|
||||
|
||||
revealed = viewport.getOffsetToReveal(target, 1.0);
|
||||
expect(revealed.offset, 5 * (100 + 22 + 22) + 22 - 100);
|
||||
expect(revealed.offset, 5 * (100 + 22 + 23) + 22 - 100);
|
||||
});
|
||||
|
||||
testWidgets('Viewport getOffsetToReveal Sliver - up', (WidgetTester tester) async {
|
||||
|
@ -302,7 +334,7 @@ void main() {
|
|||
);
|
||||
children.add(sliver);
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.all(22.0),
|
||||
padding: const EdgeInsets.only(top: 22.0, bottom: 23.0),
|
||||
sliver: sliver,
|
||||
);
|
||||
}),
|
||||
|
@ -316,17 +348,19 @@ void main() {
|
|||
|
||||
final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false));
|
||||
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
|
||||
expect(revealed.offset, 5 * (100 + 22 + 22) + 22);
|
||||
// Does not include the bottom padding of children[5] thus + 23 instead of + 22.
|
||||
expect(revealed.offset, 5 * (100 + 22 + 23) + 23);
|
||||
|
||||
revealed = viewport.getOffsetToReveal(target, 1.0);
|
||||
expect(revealed.offset, 5 * (100 + 22 + 22) + 22 - 100);
|
||||
expect(revealed.offset, 5 * (100 + 22 + 23) + 23 - 100);
|
||||
});
|
||||
|
||||
testWidgets('Viewport getOffsetToReveal Sliver - up - reverse growth', (WidgetTester tester) async {
|
||||
const Key centerKey = ValueKey<String>('center');
|
||||
const EdgeInsets padding = EdgeInsets.only(top: 22.0, bottom: 23.0);
|
||||
final Widget centerSliver = SliverPadding(
|
||||
key: centerKey,
|
||||
padding: const EdgeInsets.all(22.0),
|
||||
padding: padding,
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Container(
|
||||
height: 100.0,
|
||||
|
@ -339,7 +373,7 @@ void main() {
|
|||
child: const Text('Tile lower'),
|
||||
);
|
||||
final Widget lowerSliver = SliverPadding(
|
||||
padding: const EdgeInsets.all(22.0),
|
||||
padding: padding,
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: lowerItem,
|
||||
),
|
||||
|
@ -374,9 +408,10 @@ void main() {
|
|||
|
||||
testWidgets('Viewport getOffsetToReveal Sliver - left - reverse growth', (WidgetTester tester) async {
|
||||
const Key centerKey = ValueKey<String>('center');
|
||||
const EdgeInsets padding = EdgeInsets.only(left: 22.0, right: 23.0);
|
||||
final Widget centerSliver = SliverPadding(
|
||||
key: centerKey,
|
||||
padding: const EdgeInsets.all(22.0),
|
||||
padding: padding,
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Container(
|
||||
width: 100.0,
|
||||
|
@ -389,7 +424,7 @@ void main() {
|
|||
child: const Text('Tile lower'),
|
||||
);
|
||||
final Widget lowerSliver = SliverPadding(
|
||||
padding: const EdgeInsets.all(22.0),
|
||||
padding: padding,
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: lowerItem,
|
||||
),
|
||||
|
@ -445,7 +480,7 @@ void main() {
|
|||
);
|
||||
children.add(sliver);
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.all(22.0),
|
||||
padding: const EdgeInsets.only(left: 22.0, right: 23.0),
|
||||
sliver: sliver,
|
||||
);
|
||||
}),
|
||||
|
@ -459,10 +494,10 @@ void main() {
|
|||
|
||||
final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false));
|
||||
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
|
||||
expect(revealed.offset, 5 * (100 + 22 + 22) + 22);
|
||||
expect(revealed.offset, 5 * (100 + 22 + 23) + 23);
|
||||
|
||||
revealed = viewport.getOffsetToReveal(target, 1.0);
|
||||
expect(revealed.offset, 5 * (100 + 22 + 22) + 22 - 100);
|
||||
expect(revealed.offset, 5 * (100 + 22 + 23) + 23 - 100);
|
||||
});
|
||||
|
||||
testWidgets('Nested Viewports showOnScreen', (WidgetTester tester) async {
|
||||
|
@ -988,6 +1023,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 {
|
||||
|
|
Loading…
Reference in a new issue