diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart index 056b99124a..7be0158ed6 100644 --- a/packages/flutter/lib/src/cupertino/nav_bar.dart +++ b/packages/flutter/lib/src/cupertino/nav_bar.dart @@ -551,6 +551,11 @@ class _CupertinoNavigationBarState extends State { /// from the operating system can be retrieved in many ways, such as querying /// [MediaQuery.textScaleFactorOf] against [CupertinoApp]'s [BuildContext]. /// +/// The [stretch] parameter determines whether the nav bar should stretch to +/// fill the over-scroll area. The nav bar can still expand and contract as the +/// user scrolls, but it will also stretch when the user over-scrolls if the +/// [stretch] value is `true`. Defaults to `true`. +/// /// See also: /// /// * [CupertinoNavigationBar], an iOS navigation bar for use on non-scrolling @@ -575,6 +580,7 @@ class CupertinoSliverNavigationBar extends StatefulWidget { this.actionsForegroundColor, this.transitionBetweenRoutes = true, this.heroTag = _defaultHeroTag, + this.stretch = true, }) : assert(automaticallyImplyLeading != null), assert(automaticallyImplyTitle != null), assert( @@ -672,6 +678,13 @@ class CupertinoSliverNavigationBar extends StatefulWidget { /// True if the navigation bar's background color has no transparency. bool get opaque => backgroundColor?.alpha == 0xFF; + /// Whether the nav bar should stretch to fill the over-scroll area. + /// + /// The nav bar can still expand and contract as the user scrolls, but it will + /// also stretch when the user over-scrolls if the [stretch] value is `true`. + /// Defaults to `true`. + final bool stretch; + @override _CupertinoSliverNavigationBarState createState() => _CupertinoSliverNavigationBarState(); } @@ -729,6 +742,7 @@ class _CupertinoSliverNavigationBarState extends State persistentHeight + _kNavBarLargeTitleHeightExtension; + @override + OverScrollHeaderStretchConfiguration? stretchConfiguration; + @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { final bool showLargeTitle = shrinkOffset < maxExtent - minExtent - _kNavBarShowLargeTitleThreshold; diff --git a/packages/flutter/test/cupertino/nav_bar_test.dart b/packages/flutter/test/cupertino/nav_bar_test.dart index e18effd2bc..c24bbc044c 100644 --- a/packages/flutter/test/cupertino/nav_bar_test.dart +++ b/packages/flutter/test/cupertino/nav_bar_test.dart @@ -1185,6 +1185,113 @@ void main() { expect(barItems2.length, greaterThan(0)); expect(barItems2.any((RichText t) => t.textScaleFactor != 1), isFalse); }); + + testWidgets( + 'CupertinoSliverNavigationBar stretches upon over-scroll and bounces back once over-scroll ends', + (WidgetTester tester) async { + const Text trailingText = Text('Bar Button'); + const Text titleText = Text('Large Title'); + + await tester.pumpWidget( + CupertinoApp( + home: CupertinoPageScaffold( + child: CustomScrollView( + slivers: [ + const CupertinoSliverNavigationBar( + trailing: trailingText, + largeTitle: titleText, + ), + SliverToBoxAdapter( + child: Container( + height: 1200.0, + ), + ), + ], + ), + ), + ), + ); + + final Finder trailingTextFinder = find.byWidget(trailingText).first; + final Finder titleTextFinder = find.byWidget(titleText).first; + + final Offset initialTrailingTextToLargeTitleOffset = tester.getTopLeft(trailingTextFinder) - tester.getTopLeft(titleTextFinder); + + // Drag for overscroll + await tester.drag(find.byType(Scrollable), const Offset(0.0, 150.0)); + await tester.pump(); + + final Offset stretchedTrailingTextToLargeTitleOffset = tester.getTopLeft(trailingTextFinder) - tester.getTopLeft(titleTextFinder); + + expect( + stretchedTrailingTextToLargeTitleOffset.dy.abs(), + greaterThan(initialTrailingTextToLargeTitleOffset.dy.abs()) + ); + + // Ensure overscroll retracts to original size after releasing gesture + await tester.pumpAndSettle(); + + final Offset finalTrailingTextToLargeTitleOffset = tester.getTopLeft(trailingTextFinder) - tester.getTopLeft(titleTextFinder); + + expect( + finalTrailingTextToLargeTitleOffset.dy.abs(), + initialTrailingTextToLargeTitleOffset.dy.abs(), + ); + }); + + testWidgets( + 'CupertinoSliverNavigationBar does not stretch upon over-scroll if stretch parameter is false', + (WidgetTester tester) async { + const Text trailingText = Text('Bar Button'); + const Text titleText = Text('Large Title'); + + await tester.pumpWidget( + CupertinoApp( + home: CupertinoPageScaffold( + child: CustomScrollView( + slivers: [ + const CupertinoSliverNavigationBar( + trailing: trailingText, + largeTitle: titleText, + stretch: false, + ), + SliverToBoxAdapter( + child: Container( + height: 1200.0, + ), + ), + ], + ), + ), + ), + ); + + final Finder trailingTextFinder = find.byWidget(trailingText).first; + final Finder titleTextFinder = find.byWidget(titleText).first; + + final Offset initialTrailingTextToLargeTitleOffset = tester.getTopLeft(trailingTextFinder) - tester.getTopLeft(titleTextFinder); + + // Drag for overscroll + await tester.drag(find.byType(Scrollable), const Offset(0.0, 150.0)); + await tester.pump(); + + final Offset stretchedTrailingTextToLargeTitleOffset = tester.getTopLeft(trailingTextFinder) - tester.getTopLeft(titleTextFinder); + + expect( + stretchedTrailingTextToLargeTitleOffset.dy.abs(), + initialTrailingTextToLargeTitleOffset.dy.abs(), + ); + + // Ensure overscroll is zero after releasing gesture + await tester.pumpAndSettle(); + + final Offset finalTrailingTextToLargeTitleOffset = tester.getTopLeft(trailingTextFinder) - tester.getTopLeft(titleTextFinder); + + expect( + finalTrailingTextToLargeTitleOffset.dy.abs(), + initialTrailingTextToLargeTitleOffset.dy.abs(), + ); + }); } class _ExpectStyles extends StatelessWidget {