Unverified Commit 64d76f2f authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Prevent viewport.showOnScreen from scrolling the viewport if the specified...

Prevent viewport.showOnScreen from scrolling the viewport if the specified Rect is already visible.  (#56413)
parent 6f26e806
...@@ -817,12 +817,18 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -817,12 +817,18 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@required this.topPadding, @required this.topPadding,
@required this.floating, @required this.floating,
@required this.pinned, @required this.pinned,
@required this.vsync,
@required this.snapConfiguration, @required this.snapConfiguration,
@required this.stretchConfiguration, @required this.stretchConfiguration,
@required this.showOnScreenConfiguration,
@required this.shape, @required this.shape,
@required this.toolbarHeight, @required this.toolbarHeight,
@required this.leadingWidth, @required this.leadingWidth,
}) : assert(primary || topPadding == 0.0), }) : 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; _bottomHeight = bottom?.preferredSize?.height ?? 0.0;
final Widget leading; final Widget leading;
...@@ -860,12 +866,18 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -860,12 +866,18 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@override @override
double get maxExtent => math.max(topPadding + (expandedHeight ?? (toolbarHeight ?? kToolbarHeight) + _bottomHeight), minExtent); double get maxExtent => math.max(topPadding + (expandedHeight ?? (toolbarHeight ?? kToolbarHeight) + _bottomHeight), minExtent);
@override
final TickerProvider vsync;
@override @override
final FloatingHeaderSnapConfiguration snapConfiguration; final FloatingHeaderSnapConfiguration snapConfiguration;
@override @override
final OverScrollHeaderStretchConfiguration stretchConfiguration; final OverScrollHeaderStretchConfiguration stretchConfiguration;
@override
final PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration;
@override @override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final double visibleMainHeight = maxExtent - shrinkOffset - topPadding; final double visibleMainHeight = maxExtent - shrinkOffset - topPadding;
...@@ -935,8 +947,10 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -935,8 +947,10 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|| topPadding != oldDelegate.topPadding || topPadding != oldDelegate.topPadding
|| pinned != oldDelegate.pinned || pinned != oldDelegate.pinned
|| floating != oldDelegate.floating || floating != oldDelegate.floating
|| vsync != oldDelegate.vsync
|| snapConfiguration != oldDelegate.snapConfiguration || snapConfiguration != oldDelegate.snapConfiguration
|| stretchConfiguration != oldDelegate.stretchConfiguration || stretchConfiguration != oldDelegate.stretchConfiguration
|| showOnScreenConfiguration != oldDelegate.showOnScreenConfiguration
|| forceElevated != oldDelegate.forceElevated || forceElevated != oldDelegate.forceElevated
|| toolbarHeight != oldDelegate.toolbarHeight || toolbarHeight != oldDelegate.toolbarHeight
|| leadingWidth != leadingWidth; || leadingWidth != leadingWidth;
...@@ -1325,9 +1339,14 @@ class SliverAppBar extends StatefulWidget { ...@@ -1325,9 +1339,14 @@ class SliverAppBar extends StatefulWidget {
/// into view. /// into view.
/// ///
/// If [snap] is true then a scroll that exposes the floating app bar will /// 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 /// trigger an animation that slides the entire app bar into view. Similarly
/// a scroll dismisses the app bar, the animation will slide the app bar /// if a scroll dismisses the app bar, the animation will slide the app bar
/// completely out of view. /// 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 /// Snapping only applies when the app bar is floating, not when the app bar
/// appears at the top of its scroll view. /// appears at the top of its scroll view.
...@@ -1382,17 +1401,21 @@ class SliverAppBar extends StatefulWidget { ...@@ -1382,17 +1401,21 @@ class SliverAppBar extends StatefulWidget {
class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMixin { class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMixin {
FloatingHeaderSnapConfiguration _snapConfiguration; FloatingHeaderSnapConfiguration _snapConfiguration;
OverScrollHeaderStretchConfiguration _stretchConfiguration; OverScrollHeaderStretchConfiguration _stretchConfiguration;
PersistentHeaderShowOnScreenConfiguration _showOnScreenConfiguration;
void _updateSnapConfiguration() { void _updateSnapConfiguration() {
if (widget.snap && widget.floating) { if (widget.snap && widget.floating) {
_snapConfiguration = FloatingHeaderSnapConfiguration( _snapConfiguration = FloatingHeaderSnapConfiguration(
vsync: this,
curve: Curves.easeOut, curve: Curves.easeOut,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
); );
} else { } else {
_snapConfiguration = null; _snapConfiguration = null;
} }
_showOnScreenConfiguration = widget.floating & widget.snap
? const PersistentHeaderShowOnScreenConfiguration(minShowOnScreenExtent: double.infinity)
: null;
} }
void _updateStretchConfiguration() { void _updateStretchConfiguration() {
...@@ -1438,6 +1461,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix ...@@ -1438,6 +1461,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
floating: widget.floating, floating: widget.floating,
pinned: widget.pinned, pinned: widget.pinned,
delegate: _SliverAppBarDelegate( delegate: _SliverAppBarDelegate(
vsync: this,
leading: widget.leading, leading: widget.leading,
automaticallyImplyLeading: widget.automaticallyImplyLeading, automaticallyImplyLeading: widget.automaticallyImplyLeading,
title: widget.title, title: widget.title,
...@@ -1464,6 +1488,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix ...@@ -1464,6 +1488,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
shape: widget.shape, shape: widget.shape,
snapConfiguration: _snapConfiguration, snapConfiguration: _snapConfiguration,
stretchConfiguration: _stretchConfiguration, stretchConfiguration: _stretchConfiguration,
showOnScreenConfiguration: _showOnScreenConfiguration,
toolbarHeight: widget.toolbarHeight, toolbarHeight: widget.toolbarHeight,
leadingWidth: widget.leadingWidth, leadingWidth: widget.leadingWidth,
), ),
......
...@@ -727,58 +727,26 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -727,58 +727,26 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
final Matrix4 transform = target.getTransformTo(pivot); final Matrix4 transform = target.getTransformTo(pivot);
final Rect bounds = MatrixUtils.transformRect(transform, rect); 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; final GrowthDirection growthDirection = pivotParent.constraints.growthDirection;
switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) { switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
case AxisDirection.up: case AxisDirection.up:
double offset; leadingScrollOffset += pivot.size.height - bounds.bottom;
switch (growthDirection) {
case GrowthDirection.forward:
offset = bounds.bottom;
break;
case GrowthDirection.reverse:
offset = bounds.top;
break;
}
leadingScrollOffset += pivot.size.height - offset;
targetMainAxisExtent = bounds.height; targetMainAxisExtent = bounds.height;
break; break;
case AxisDirection.right: case AxisDirection.right:
double offset; leadingScrollOffset += bounds.left;
switch (growthDirection) {
case GrowthDirection.forward:
offset = bounds.left;
break;
case GrowthDirection.reverse:
offset = bounds.right;
break;
}
leadingScrollOffset += offset;
targetMainAxisExtent = bounds.width; targetMainAxisExtent = bounds.width;
break; break;
case AxisDirection.down: case AxisDirection.down:
double offset; leadingScrollOffset += bounds.top;
switch (growthDirection) {
case GrowthDirection.forward:
offset = bounds.top;
break;
case GrowthDirection.reverse:
offset = bounds.bottom;
break;
}
leadingScrollOffset += offset;
targetMainAxisExtent = bounds.height; targetMainAxisExtent = bounds.height;
break; break;
case AxisDirection.left: case AxisDirection.left:
double offset; leadingScrollOffset += pivot.size.width - bounds.right;
switch (growthDirection) {
case GrowthDirection.forward:
offset = bounds.right;
break;
case GrowthDirection.reverse:
offset = bounds.left;
break;
}
leadingScrollOffset += pivot.size.width - offset;
targetMainAxisExtent = bounds.width; targetMainAxisExtent = bounds.width;
break; break;
} }
...@@ -792,14 +760,44 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -792,14 +760,44 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
assert(child.parent == this); assert(child.parent == this);
assert(child is RenderSliver); assert(child is RenderSliver);
final RenderSliver sliver = child as 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); 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); leadingScrollOffset = scrollOffsetOf(sliver, leadingScrollOffset);
switch (sliver.constraints.growthDirection) { switch (sliver.constraints.growthDirection) {
case GrowthDirection.forward: case GrowthDirection.forward:
if (isPinned && alignment <= 0)
return RevealedOffset(offset: double.infinity, rect: targetRect);
leadingScrollOffset -= extentOfPinnedSlivers; leadingScrollOffset -= extentOfPinnedSlivers;
break; break;
case GrowthDirection.reverse: 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; break;
} }
...@@ -816,9 +814,6 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -816,9 +814,6 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
final double targetOffset = leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment; final double targetOffset = leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
final double offsetDifference = offset.pixels - targetOffset; final double offsetDifference = offset.pixels - targetOffset;
final Matrix4 transform = target.getTransformTo(this);
Rect targetRect = MatrixUtils.transformRect(transform, rect);
switch (axisDirection) { switch (axisDirection) {
case AxisDirection.down: case AxisDirection.down:
targetRect = targetRect.translate(0.0, offsetDifference); targetRect = targetRect.translate(0.0, offsetDifference);
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart' show TickerProvider;
import 'framework.dart'; import 'framework.dart';
...@@ -59,6 +60,12 @@ abstract class SliverPersistentHeaderDelegate { ...@@ -59,6 +60,12 @@ abstract class SliverPersistentHeaderDelegate {
/// different value. /// different value.
double get maxExtent; 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. /// Specifies how floating headers should animate in and out of view.
/// ///
/// If the value of this property is null, then floating headers will /// If the value of this property is null, then floating headers will
...@@ -81,6 +88,12 @@ abstract class SliverPersistentHeaderDelegate { ...@@ -81,6 +88,12 @@ abstract class SliverPersistentHeaderDelegate {
/// Defaults to null. /// Defaults to null.
OverScrollHeaderStretchConfiguration get stretchConfiguration => 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. /// Whether this delegate is meaningfully different from the old delegate.
/// ///
/// If this returns false, then the header might not be rebuilt, even though /// If this returns false, then the header might not be rebuilt, even though
...@@ -346,7 +359,8 @@ class _SliverPinnedPersistentHeader extends _SliverPersistentHeaderRenderObjectW ...@@ -346,7 +359,8 @@ class _SliverPinnedPersistentHeader extends _SliverPersistentHeaderRenderObjectW
@override @override
_RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) { _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
return _RenderSliverPinnedPersistentHeaderForWidgets( return _RenderSliverPinnedPersistentHeaderForWidgets(
stretchConfiguration: delegate.stretchConfiguration stretchConfiguration: delegate.stretchConfiguration,
showOnScreenConfiguration: delegate.showOnScreenConfiguration,
); );
} }
} }
...@@ -356,9 +370,11 @@ class _RenderSliverPinnedPersistentHeaderForWidgets extends RenderSliverPinnedPe ...@@ -356,9 +370,11 @@ class _RenderSliverPinnedPersistentHeaderForWidgets extends RenderSliverPinnedPe
_RenderSliverPinnedPersistentHeaderForWidgets({ _RenderSliverPinnedPersistentHeaderForWidgets({
RenderBox child, RenderBox child,
OverScrollHeaderStretchConfiguration stretchConfiguration, OverScrollHeaderStretchConfiguration stretchConfiguration,
PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration,
}) : super( }) : super(
child: child, child: child,
stretchConfiguration: stretchConfiguration, stretchConfiguration: stretchConfiguration,
showOnScreenConfiguration: showOnScreenConfiguration,
); );
} }
...@@ -374,15 +390,19 @@ class _SliverFloatingPersistentHeader extends _SliverPersistentHeaderRenderObjec ...@@ -374,15 +390,19 @@ class _SliverFloatingPersistentHeader extends _SliverPersistentHeaderRenderObjec
@override @override
_RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) { _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
return _RenderSliverFloatingPersistentHeaderForWidgets( return _RenderSliverFloatingPersistentHeaderForWidgets(
vsync: delegate.vsync,
snapConfiguration: delegate.snapConfiguration, snapConfiguration: delegate.snapConfiguration,
stretchConfiguration: delegate.stretchConfiguration, stretchConfiguration: delegate.stretchConfiguration,
showOnScreenConfiguration: delegate.showOnScreenConfiguration,
); );
} }
@override @override
void updateRenderObject(BuildContext context, _RenderSliverFloatingPersistentHeaderForWidgets renderObject) { void updateRenderObject(BuildContext context, _RenderSliverFloatingPersistentHeaderForWidgets renderObject) {
renderObject.vsync = delegate.vsync;
renderObject.snapConfiguration = delegate.snapConfiguration; renderObject.snapConfiguration = delegate.snapConfiguration;
renderObject.stretchConfiguration = delegate.stretchConfiguration; renderObject.stretchConfiguration = delegate.stretchConfiguration;
renderObject.showOnScreenConfiguration = delegate.showOnScreenConfiguration;
} }
} }
...@@ -390,12 +410,16 @@ class _RenderSliverFloatingPinnedPersistentHeaderForWidgets extends RenderSliver ...@@ -390,12 +410,16 @@ class _RenderSliverFloatingPinnedPersistentHeaderForWidgets extends RenderSliver
with _RenderSliverPersistentHeaderForWidgetsMixin { with _RenderSliverPersistentHeaderForWidgetsMixin {
_RenderSliverFloatingPinnedPersistentHeaderForWidgets({ _RenderSliverFloatingPinnedPersistentHeaderForWidgets({
RenderBox child, RenderBox child,
@required TickerProvider vsync,
FloatingHeaderSnapConfiguration snapConfiguration, FloatingHeaderSnapConfiguration snapConfiguration,
OverScrollHeaderStretchConfiguration stretchConfiguration, OverScrollHeaderStretchConfiguration stretchConfiguration,
PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration,
}) : super( }) : super(
child: child, child: child,
vsync: vsync,
snapConfiguration: snapConfiguration, snapConfiguration: snapConfiguration,
stretchConfiguration: stretchConfiguration, stretchConfiguration: stretchConfiguration,
showOnScreenConfiguration: showOnScreenConfiguration,
); );
} }
...@@ -411,15 +435,19 @@ class _SliverFloatingPinnedPersistentHeader extends _SliverPersistentHeaderRende ...@@ -411,15 +435,19 @@ class _SliverFloatingPinnedPersistentHeader extends _SliverPersistentHeaderRende
@override @override
_RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) { _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
return _RenderSliverFloatingPinnedPersistentHeaderForWidgets( return _RenderSliverFloatingPinnedPersistentHeaderForWidgets(
vsync: delegate.vsync,
snapConfiguration: delegate.snapConfiguration, snapConfiguration: delegate.snapConfiguration,
stretchConfiguration: delegate.stretchConfiguration, stretchConfiguration: delegate.stretchConfiguration,
showOnScreenConfiguration: delegate.showOnScreenConfiguration,
); );
} }
@override @override
void updateRenderObject(BuildContext context, _RenderSliverFloatingPinnedPersistentHeaderForWidgets renderObject) { void updateRenderObject(BuildContext context, _RenderSliverFloatingPinnedPersistentHeaderForWidgets renderObject) {
renderObject.vsync = delegate.vsync;
renderObject.snapConfiguration = delegate.snapConfiguration; renderObject.snapConfiguration = delegate.snapConfiguration;
renderObject.stretchConfiguration = delegate.stretchConfiguration; renderObject.stretchConfiguration = delegate.stretchConfiguration;
renderObject.showOnScreenConfiguration = delegate.showOnScreenConfiguration;
} }
} }
...@@ -427,11 +455,15 @@ class _RenderSliverFloatingPersistentHeaderForWidgets extends RenderSliverFloati ...@@ -427,11 +455,15 @@ class _RenderSliverFloatingPersistentHeaderForWidgets extends RenderSliverFloati
with _RenderSliverPersistentHeaderForWidgetsMixin { with _RenderSliverPersistentHeaderForWidgetsMixin {
_RenderSliverFloatingPersistentHeaderForWidgets({ _RenderSliverFloatingPersistentHeaderForWidgets({
RenderBox child, RenderBox child,
@required TickerProvider vsync,
FloatingHeaderSnapConfiguration snapConfiguration, FloatingHeaderSnapConfiguration snapConfiguration,
OverScrollHeaderStretchConfiguration stretchConfiguration, OverScrollHeaderStretchConfiguration stretchConfiguration,
PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration,
}) : super( }) : super(
child: child, child: child,
vsync: vsync,
snapConfiguration: snapConfiguration, snapConfiguration: snapConfiguration,
stretchConfiguration: stretchConfiguration, stretchConfiguration: stretchConfiguration,
showOnScreenConfiguration: showOnScreenConfiguration,
); );
} }
...@@ -1975,6 +1975,41 @@ void main() { ...@@ -1975,6 +1975,41 @@ void main() {
expect(tester.getCenter(appBarTitle).dy, tester.getCenter(toolbar).dy); 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 { testWidgets('AppBar respects toolbarHeight', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
......
...@@ -49,7 +49,7 @@ void main() { ...@@ -49,7 +49,7 @@ void main() {
class TestRenderSliverFloatingPersistentHeader extends RenderSliverFloatingPersistentHeader { class TestRenderSliverFloatingPersistentHeader extends RenderSliverFloatingPersistentHeader {
TestRenderSliverFloatingPersistentHeader({ TestRenderSliverFloatingPersistentHeader({
RenderBox child, RenderBox child,
}) : super(child: child); }) : super(child: child, vsync: null, showOnScreenConfiguration: null);
@override @override
double get maxExtent => 200; double get maxExtent => 200;
...@@ -61,7 +61,7 @@ class TestRenderSliverFloatingPersistentHeader extends RenderSliverFloatingPersi ...@@ -61,7 +61,7 @@ class TestRenderSliverFloatingPersistentHeader extends RenderSliverFloatingPersi
class TestRenderSliverFloatingPinnedPersistentHeader extends RenderSliverFloatingPinnedPersistentHeader { class TestRenderSliverFloatingPinnedPersistentHeader extends RenderSliverFloatingPinnedPersistentHeader {
TestRenderSliverFloatingPinnedPersistentHeader({ TestRenderSliverFloatingPinnedPersistentHeader({
RenderBox child, RenderBox child,
}) : super(child: child); }) : super(child: child, vsync: null, showOnScreenConfiguration: null);
@override @override
double get maxExtent => 200; double get maxExtent => 200;
......
...@@ -10,6 +10,35 @@ import 'package:flutter/material.dart'; ...@@ -10,6 +10,35 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/services.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() { void main() {
const TextStyle textStyle = TextStyle(); const TextStyle textStyle = TextStyle();
...@@ -339,6 +368,131 @@ void main() { ...@@ -339,6 +368,131 @@ void main() {
expect(scrollController.offset, greaterThan(0.0)); expect(scrollController.offset, greaterThan(0.0));
expect(find.byKey(container), findsNothing); 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 { 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