Add bottom to CupertinoSliverNavigationBar (#155841)

Fixes [Support search box inside large title nav bar](https://github.com/flutter/flutter/issues/18103)
Part of [Support segmented controls in nav bars and double row nav bars](https://github.com/flutter/flutter/issues/10469)

## None mode (Current default)

https://github.com/user-attachments/assets/d798314e-940f-4311-9a9a-fe999c65f280

## Always mode

https://github.com/user-attachments/assets/950a85aa-8ca2-42ea-bf8b-3cb8f95e616e

## Automatic mode

https://github.com/user-attachments/assets/c7c7240b-d493-4036-a987-30f61d02bac3

## With CupertinoSlidingSegmentedControl

https://github.com/user-attachments/assets/59f4aec4-8d9c-4223-915b-97b73cb25dc8

<details>
<summary>Sample Code</summary>

```dart
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/cupertino.dart';

/// Flutter code sample for [CupertinoSliverNavigationBar].

void main() => runApp(const SliverNavBarApp());

class SliverNavBarApp extends StatelessWidget {
  const SliverNavBarApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const CupertinoApp(
      theme: CupertinoThemeData(brightness: Brightness.light),
      home: SliverNavBarExample(),
    );
  }
}

class SliverNavBarExample extends StatelessWidget {
  const SliverNavBarExample({super.key});

  @override
  Widget build(BuildContext context) {
    return const CupertinoPageScaffold(
      // A ScrollView that creates custom scroll effects using slivers.
      child: SafeArea(
        child: CustomScrollView(
          // A list of sliver widgets.
          slivers: <Widget>[
            CupertinoSliverNavigationBar(
              leading: SizedBox(
                width: 100,
                child: Row(
                  children: [
                    Icon(CupertinoIcons.back),
                    Text(
                      'Lists',
                      style: TextStyle(color: CupertinoColors.activeBlue),
                    ),
                  ],
                ),
              ),
              trailing: Icon(CupertinoIcons.plus),
              largeTitle: Text('iPhone'),
              // Change to desired mode.
              drawerMode: NavigationDrawerMode.none,
              drawer: Padding(
                padding: EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 16.0),
                child: CupertinoSearchTextField(),
              ),
            ),
            SliverFillRemaining(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  Text('Drag me up', textAlign: TextAlign.center),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```

</details>
This commit is contained in:
Victor Sanni 2024-10-16 11:25:06 -07:00 committed by GitHub
parent e08ad36dd9
commit 293ae2e5ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 612 additions and 39 deletions

View File

@ -0,0 +1,119 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
/// Flutter code sample for [CupertinoSliverNavigationBar].
void main() => runApp(const SliverNavBarApp());
class SliverNavBarApp extends StatelessWidget {
const SliverNavBarApp({super.key});
@override
Widget build(BuildContext context) {
return const CupertinoApp(
theme: CupertinoThemeData(brightness: Brightness.light),
home: SliverNavBarExample(),
);
}
}
class SliverNavBarExample extends StatelessWidget {
const SliverNavBarExample({super.key});
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(
leading: Icon(CupertinoIcons.person_2),
largeTitle: Text('Contacts'),
trailing: Icon(CupertinoIcons.add_circled),
),
SliverFillRemaining(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
const Text('Drag me up', textAlign: TextAlign.center),
CupertinoButton.filled(
onPressed: () {
Navigator.push(
context,
CupertinoPageRoute<Widget>(
builder: (BuildContext context) {
return const NextPage();
},
),
);
},
child: const Text('Bottom Automatic mode'),
),
CupertinoButton.filled(
onPressed: () {
Navigator.push(
context,
CupertinoPageRoute<Widget>(
builder: (BuildContext context) {
return const NextPage(
bottomMode: NavigationBarBottomMode.always,
);
},
),
);
},
child: const Text('Bottom Always mode'),
),
],
),
),
),
],
),
);
}
}
class NextPage extends StatelessWidget {
const NextPage({super.key, this.bottomMode = NavigationBarBottomMode.automatic});
final NavigationBarBottomMode bottomMode;
@override
Widget build(BuildContext context) {
final Brightness brightness = CupertinoTheme.brightnessOf(context);
return CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar.search(
backgroundColor: CupertinoColors.systemYellow,
border: Border(
bottom: BorderSide(
color: brightness == Brightness.light
? CupertinoColors.black
: CupertinoColors.white,
),
),
middle: const Text('Contacts Group'),
largeTitle: const Text('Family'),
bottomMode: bottomMode,
),
const SliverFillRemaining(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text('Drag me up', textAlign: TextAlign.center),
Text('Tap on the leading button to navigate back',
textAlign: TextAlign.center),
],
),
),
],
),
);
}
}

View File

@ -0,0 +1,122 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter_api_samples/cupertino/nav_bar/cupertino_sliver_nav_bar.1.dart' as example;
import 'package:flutter_test/flutter_test.dart';
const Offset titleDragUp = Offset(0.0, -100.0);
const Offset bottomDragUp = Offset(0.0, -50.0);
void main() {
testWidgets('Collapse and expand CupertinoSliverNavigationBar changes title position', (WidgetTester tester) async {
await tester.pumpWidget(
const example.SliverNavBarApp(),
);
// Large title is visible and at lower position.
expect(tester.getBottomLeft(find.text('Contacts').first).dy, 88.0);
await tester.fling(find.text('Drag me up'), titleDragUp, 500.0);
await tester.pumpAndSettle();
// Large title is hidden and at higher position.
expect(tester.getBottomLeft(find.text('Contacts').first).dy, 36.0 + 8.0); // Static part + _kNavBarBottomPadding.
});
testWidgets('Search field is hidden in bottom automatic mode', (WidgetTester tester) async {
await tester.pumpWidget(
const example.SliverNavBarApp(),
);
// Navigate to a page with bottom automatic mode.
final Finder nextButton = find.text('Bottom Automatic mode');
expect(nextButton, findsOneWidget);
await tester.tap(nextButton);
await tester.pumpAndSettle();
// Middle, large title, and search field are visible.
expect(tester.getBottomLeft(find.text('Contacts Group').first).dy, 30.5);
expect(tester.getBottomLeft(find.text('Family').first).dy, 88.0);
expect(tester.getTopLeft(find.byType(CupertinoSearchTextField)).dy, 104.0);
expect(tester.getBottomLeft(find.byType(CupertinoSearchTextField)).dy, 139.0);
await tester.fling(find.text('Drag me up'), bottomDragUp, 50.0);
await tester.pumpAndSettle();
// Search field is hidden, but large title and middle title are visible.
expect(tester.getBottomLeft(find.text('Contacts Group').first).dy, 30.5);
expect(tester.getBottomLeft(find.text('Family').first).dy, 88.0);
expect(tester.getTopLeft(find.byType(CupertinoSearchTextField)).dy, 104.0);
expect(tester.getBottomLeft(find.byType(CupertinoSearchTextField)).dy, 104.0);
await tester.fling(find.text('Drag me up'), titleDragUp, 50.0);
await tester.pumpAndSettle();
// Large title and search field are hidden and middle title is visible.
expect(tester.getBottomLeft(find.text('Contacts Group').first).dy, 30.5);
expect(tester.getBottomLeft(find.text('Family').first).dy, 36.0 + 8.0); // Static part + _kNavBarBottomPadding.
expect(tester.getBottomLeft(find.byType(CupertinoSearchTextField)).dy, 52.0);
});
testWidgets('Search field is always shown in bottom always mode', (WidgetTester tester) async {
await tester.pumpWidget(
const example.SliverNavBarApp(),
);
// Navigate to a page with bottom always mode.
final Finder nextButton = find.text('Bottom Always mode');
expect(nextButton, findsOneWidget);
await tester.tap(nextButton);
await tester.pumpAndSettle();
// Middle, large title, and search field are visible.
expect(tester.getBottomLeft(find.text('Contacts Group').first).dy, 30.5);
expect(tester.getBottomLeft(find.text('Family').first).dy, 88.0);
expect(tester.getTopLeft(find.byType(CupertinoSearchTextField)).dy, 104.0);
expect(tester.getBottomLeft(find.byType(CupertinoSearchTextField)).dy, 139.0);
await tester.fling(find.text('Drag me up'), titleDragUp, 50.0);
await tester.pumpAndSettle();
// Large title is hidden, but search field and middle title are visible.
expect(tester.getBottomLeft(find.text('Contacts Group').first).dy, 30.5);
expect(tester.getBottomLeft(find.text('Family').first).dy, 36.0 + 8.0); // Static part + _kNavBarBottomPadding.
expect(tester.getTopLeft(find.byType(CupertinoSearchTextField)).dy, 52.0);
expect(tester.getBottomLeft(find.byType(CupertinoSearchTextField)).dy, 87.0);
});
testWidgets('CupertinoSliverNavigationBar with previous route has back button', (WidgetTester tester) async {
await tester.pumpWidget(
const example.SliverNavBarApp(),
);
// Navigate to the first page.
final Finder nextButton1 = find.text('Bottom Automatic mode');
expect(nextButton1, findsOneWidget);
await tester.tap(nextButton1);
await tester.pumpAndSettle();
expect(nextButton1, findsNothing);
// Go back to the previous page.
final Finder backButton1 = find.byType(CupertinoButton);
expect(backButton1, findsOneWidget);
await tester.tap(backButton1);
await tester.pumpAndSettle();
expect(nextButton1, findsOneWidget);
// Navigate to the second page.
final Finder nextButton2 = find.text('Bottom Always mode');
expect(nextButton2, findsOneWidget);
await tester.tap(nextButton2);
await tester.pumpAndSettle();
expect(nextButton2, findsNothing);
// Go back to the previous page.
final Finder backButton2 = find.byType(CupertinoButton);
expect(backButton2, findsOneWidget);
await tester.tap(backButton2);
await tester.pumpAndSettle();
expect(nextButton2, findsOneWidget);
});
}

View File

@ -19,8 +19,25 @@ import 'constants.dart';
import 'icons.dart';
import 'page_scaffold.dart';
import 'route.dart';
import 'search_field.dart';
import 'theme.dart';
/// Modes that determine how to display the navigation bar's bottom in relation to scroll events.
enum NavigationBarBottomMode {
/// Enable hiding the bottom in response to scrolling.
///
/// As scrolling starts, the large title stays pinned while the bottom resizes
/// until it is completely consumed. Then, the large title scrolls under the
/// persistent navigation bar.
automatic,
/// Always display the bottom regardless of the scroll activity.
///
/// When scrolled, the bottom stays pinned while the large title scrolls under
/// the persistent navigation bar.
always,
}
/// Standard iOS navigation bar height without the status bar.
///
/// This height is constant and independent of accessibility as it is in iOS.
@ -667,6 +684,13 @@ class _CupertinoNavigationBarState extends State<CupertinoNavigationBar> {
/// ** See code in examples/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example shows how to add a bottom (typically a
/// [CupertinoSearchTextField]) to a [CupertinoSliverNavigationBar].
///
/// ** See code in examples/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.1.dart **
/// {@end-tool}
///
/// See also:
///
/// * [CupertinoNavigationBar], an iOS navigation bar for use on non-scrolling
@ -697,13 +721,52 @@ class CupertinoSliverNavigationBar extends StatefulWidget {
this.transitionBetweenRoutes = true,
this.heroTag = _defaultHeroTag,
this.stretch = false,
this.bottom,
this.bottomMode,
}) : assert(
automaticallyImplyTitle || largeTitle != null,
'No largeTitle has been provided but automaticallyImplyTitle is also '
'false. Either provide a largeTitle or set automaticallyImplyTitle to '
'true.',
),
assert(
bottomMode == null || bottom != null,
'A bottomMode was provided without a corresponding bottom.',
);
/// Create a navigation bar for scrolling lists with [bottom] set to a
/// [CupertinoSearchTextField] with padding.
///
/// If [automaticallyImplyTitle] is false, then the [largeTitle] argument is
/// required.
const CupertinoSliverNavigationBar.search({
super.key,
this.largeTitle,
this.leading,
this.automaticallyImplyLeading = true,
this.automaticallyImplyTitle = true,
this.alwaysShowMiddle = true,
this.previousPageTitle,
this.middle,
this.trailing,
this.border = _kDefaultNavBarBorder,
this.backgroundColor,
this.automaticBackgroundVisibility = true,
this.enableBackgroundFilterBlur = true,
this.brightness,
this.padding,
this.transitionBetweenRoutes = true,
this.heroTag = _defaultHeroTag,
this.stretch = false,
this.bottomMode = NavigationBarBottomMode.automatic,
}) : assert(
automaticallyImplyTitle || largeTitle != null,
'No largeTitle has been provided but automaticallyImplyTitle is also '
'false. Either provide a largeTitle or set automaticallyImplyTitle to '
'true.',
),
bottom = const _NavigationBarSearchField();
/// The navigation bar's title.
///
/// This text will appear in the top static navigation bar when collapsed and
@ -794,6 +857,22 @@ class CupertinoSliverNavigationBar extends StatefulWidget {
/// {@macro flutter.cupertino.CupertinoNavigationBar.heroTag}
final Object heroTag;
/// A widget to place at the bottom of the large title or static navigation
/// bar if there is no large title.
///
/// Only widgets that implement [PreferredSizeWidget] can be used at the
/// bottom of a navigation bar.
///
/// See also:
///
/// * [PreferredSize], which can be used to give an arbitrary widget a preferred size.
final PreferredSizeWidget? bottom;
/// Modes that determine how to display the navigation bar's [bottom] and scrolling behavior.
///
/// Defaults to [NavigationBarBottomMode.automatic] if this is null and a [bottom] is provided.
final NavigationBarBottomMode? bottomMode;
/// True if the navigation bar's background color has no transparency.
bool get opaque => backgroundColor?.alpha == 0xFF;
@ -860,6 +939,9 @@ class _CupertinoSliverNavigationBarState extends State<CupertinoSliverNavigation
alwaysShowMiddle: widget.alwaysShowMiddle && widget.middle != null,
stretchConfiguration: widget.stretch ? OverScrollHeaderStretchConfiguration() : null,
enableBackgroundFilterBlur: widget.enableBackgroundFilterBlur,
bottom: widget.bottom ?? const SizedBox.shrink(),
bottomMode: widget.bottomMode ?? NavigationBarBottomMode.automatic,
bottomHeight: widget.bottom != null ? widget.bottom!.preferredSize.height : 0.0,
),
),
);
@ -884,6 +966,9 @@ class _LargeTitleNavigationBarSliverDelegate
required this.alwaysShowMiddle,
required this.stretchConfiguration,
required this.enableBackgroundFilterBlur,
required this.bottom,
required this.bottomMode,
required this.bottomHeight,
});
final _NavigationBarStaticComponentsKeys keys;
@ -900,12 +985,15 @@ class _LargeTitleNavigationBarSliverDelegate
final double persistentHeight;
final bool alwaysShowMiddle;
final bool enableBackgroundFilterBlur;
final Widget bottom;
final NavigationBarBottomMode bottomMode;
final double bottomHeight;
@override
double get minExtent => persistentHeight;
double get minExtent => persistentHeight + (bottomMode == NavigationBarBottomMode.always ? bottomHeight : 0.0);
@override
double get maxExtent => persistentHeight + _kNavBarLargeTitleHeightExtension;
double get maxExtent => persistentHeight + _kNavBarLargeTitleHeightExtension + bottomHeight;
@override
OverScrollHeaderStretchConfiguration? stretchConfiguration;
@ -914,6 +1002,10 @@ class _LargeTitleNavigationBarSliverDelegate
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final double largeTitleThreshold = maxExtent - minExtent - _kNavBarShowLargeTitleThreshold;
final bool showLargeTitle = shrinkOffset < largeTitleThreshold;
// Calculate how much the bottom should shrink.
final double bottomShrinkFactor = clampDouble(shrinkOffset / bottomHeight, 0, 1);
final double shrinkAnimationValue = clampDouble(
(shrinkOffset - largeTitleThreshold - _kNavBarScrollUnderAnimationExtent) / _kNavBarScrollUnderAnimationExtent,
0,
@ -947,50 +1039,68 @@ class _LargeTitleNavigationBarSliverDelegate
enableBackgroundFilterBlur: enableBackgroundFilterBlur,
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: Stack(
fit: StackFit.expand,
child: Column(
children: <Widget>[
Positioned(
top: persistentHeight,
left: 0.0,
right: 0.0,
bottom: 0.0,
child: ClipRect(
child: Padding(
padding: const EdgeInsetsDirectional.only(
start: _kNavBarEdgePadding,
bottom: _kNavBarBottomPadding
),
child: SafeArea(
top: false,
bottom: false,
child: AnimatedOpacity(
opacity: showLargeTitle ? 1.0 : 0.0,
duration: _kNavBarTitleFadeDuration,
child: Semantics(
header: true,
child: DefaultTextStyle(
style: CupertinoTheme.of(context)
.textTheme
.navLargeTitleTextStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: _LargeTitle(
child: components.largeTitle,
Expanded(
child: Stack(
children: <Widget>[
Positioned(
top: persistentHeight,
left: 0.0,
right: 0.0,
bottom: bottomMode == NavigationBarBottomMode.automatic
? bottomHeight * (1.0 - bottomShrinkFactor)
: 0.0,
child: ClipRect(
child: Padding(
padding: const EdgeInsetsDirectional.only(
start: _kNavBarEdgePadding,
bottom: _kNavBarBottomPadding
),
child: SafeArea(
top: false,
bottom: false,
child: AnimatedOpacity(
opacity: showLargeTitle ? 1.0 : 0.0,
duration: _kNavBarTitleFadeDuration,
child: Semantics(
header: true,
child: DefaultTextStyle(
style: CupertinoTheme.of(context)
.textTheme
.navLargeTitleTextStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: _LargeTitle(
child: components.largeTitle,
),
),
),
),
),
),
),
),
),
Positioned(
left: 0.0,
right: 0.0,
top: 0.0,
child: persistentNavigationBar,
),
if (bottomMode == NavigationBarBottomMode.automatic)
Positioned(
left: 0.0,
right: 0.0,
bottom: 0.0,
child: SizedBox(
height: bottomHeight * (1.0 - bottomShrinkFactor),
child: bottom,
),
),
],
),
),
Positioned(
left: 0.0,
right: 0.0,
top: 0.0,
child: persistentNavigationBar,
),
if (bottomMode == NavigationBarBottomMode.always) SizedBox(height: bottomHeight, child: bottom),
],
),
),
@ -1038,7 +1148,10 @@ class _LargeTitleNavigationBarSliverDelegate
|| persistentHeight != oldDelegate.persistentHeight
|| alwaysShowMiddle != oldDelegate.alwaysShowMiddle
|| heroTag != oldDelegate.heroTag
|| enableBackgroundFilterBlur != oldDelegate.enableBackgroundFilterBlur;
|| enableBackgroundFilterBlur != oldDelegate.enableBackgroundFilterBlur
|| bottom != oldDelegate.bottom
|| bottomMode != oldDelegate.bottomMode
|| bottomHeight != oldDelegate.bottomHeight;
}
}
@ -2633,3 +2746,24 @@ Widget _navBarHeroFlightShuttleBuilder(
);
}
}
class _NavigationBarSearchField extends StatelessWidget implements PreferredSizeWidget {
const _NavigationBarSearchField();
static const double padding = 8.0;
static const double searchFieldHeight = 35.0;
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.symmetric(horizontal: padding, vertical: padding),
child: SizedBox(
height: searchFieldHeight,
child: CupertinoSearchTextField()
),
);
}
@override
Size get preferredSize => const Size.fromHeight(searchFieldHeight + padding * 2);
}

View File

@ -1888,6 +1888,204 @@ void main() {
expect(largeTitleFinder.hitTestable(), findsOneWidget);
},
);
testWidgets('NavigationBarBottomMode.automatic mode for bottom', (WidgetTester tester) async {
const double persistentHeight = 44.0;
const double largeTitleHeight = 44.0;
const double bottomHeight = 10.0;
final ScrollController controller = ScrollController();
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
controller: controller,
slivers: <Widget>[
const CupertinoSliverNavigationBar(
largeTitle: Text('Large title'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(bottomHeight),
child: Placeholder(),
),
),
SliverToBoxAdapter(
child: Container(
height: 1200.0,
),
),
],
),
),
),
);
expect(controller.offset, 0.0);
final Finder largeTitleFinder = find.ancestor(
of: find.text('Large title').first,
matching: find.byType(Padding),
).first;
final Finder bottomFinder = find.byType(Placeholder);
// The persistent navigation bar, large title, and search field are all
// visible.
expect(tester.getTopLeft(largeTitleFinder).dy, persistentHeight);
expect(tester.getBottomLeft(largeTitleFinder).dy, persistentHeight + largeTitleHeight);
expect(tester.getTopLeft(bottomFinder).dy, 96.0);
expect(tester.getBottomLeft(bottomFinder).dy, 96.0 + bottomHeight);
// Scroll the length of the navigation bar search text field.
controller.jumpTo(bottomHeight);
await tester.pump();
// The search field is hidden, but the large title remains visible.
expect(tester.getBottomLeft(largeTitleFinder).dy, persistentHeight + largeTitleHeight);
expect(tester.getBottomLeft(bottomFinder).dy - tester.getTopLeft(bottomFinder).dy, 0.0);
// Scroll until the large title scrolls under the persistent navigation bar.
await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -400.0), 10.0);
await tester.pump();
// The large title and search field are both hidden.
expect(tester.getBottomLeft(largeTitleFinder).dy - tester.getTopLeft(bottomFinder).dy, 0.0);
expect(tester.getBottomLeft(bottomFinder).dy - tester.getTopLeft(bottomFinder).dy, 0.0);
controller.dispose();
});
testWidgets('NavigationBarBottomMode.always mode for bottom', (WidgetTester tester) async {
const double persistentHeight = 44.0;
const double largeTitleHeight = 44.0;
const double bottomHeight = 10.0;
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(
largeTitle: Text('Large title'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(bottomHeight),
child: Placeholder(),
),
bottomMode: NavigationBarBottomMode.always,
),
SliverToBoxAdapter(
child: Container(
height: 1200.0,
),
),
],
),
),
),
);
final Finder largeTitleFinder = find.ancestor(
of: find.text('Large title').first,
matching: find.byType(Padding),
).first;
final Finder bottomFinder = find.byType(Placeholder);
// The persistent navigation bar, large title, and search field are all
// visible.
expect(tester.getTopLeft(largeTitleFinder).dy, persistentHeight);
expect(tester.getBottomLeft(largeTitleFinder).dy, persistentHeight + largeTitleHeight);
expect(tester.getTopLeft(bottomFinder).dy, 96.0);
expect(tester.getBottomLeft(bottomFinder).dy, 96.0 + bottomHeight);
// Scroll until the large title scrolls under the persistent navigation bar.
await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -400.0), 10.0);
await tester.pump();
// Only the large title is hidden.
expect(tester.getBottomLeft(largeTitleFinder).dy - tester.getTopLeft(bottomFinder).dy, 0.0);
expect(tester.getTopLeft(bottomFinder).dy, persistentHeight);
expect(tester.getBottomLeft(bottomFinder).dy, persistentHeight + bottomHeight);
});
testWidgets('Disallow providing a bottomMode without a corresponding bottom', (WidgetTester tester) async {
expect(
() => const CupertinoSliverNavigationBar(
bottom: PreferredSize(
preferredSize: Size.fromHeight(10.0),
child: Placeholder(),
),
bottomMode: NavigationBarBottomMode.automatic,
),
returnsNormally,
);
expect(
() => const CupertinoSliverNavigationBar(
bottom: PreferredSize(
preferredSize: Size.fromHeight(10.0),
child: Placeholder(),
),
),
returnsNormally,
);
expect(
() => CupertinoSliverNavigationBar(
bottomMode: NavigationBarBottomMode.automatic,
),
throwsA(isA<AssertionError>().having(
(AssertionError e) => e.message,
'message',
contains('A bottomMode was provided without a corresponding bottom.'),
)),
);
});
testWidgets('Overscroll when stretched does not resize bottom in automatic mode', (WidgetTester tester) async {
const double bottomHeight = 10.0;
const double bottomDisplacement = 96.0;
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(
stretch: true,
largeTitle: Text('Large title'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(bottomHeight),
child: Placeholder(),
),
bottomMode: NavigationBarBottomMode.automatic,
),
SliverToBoxAdapter(
child: Container(
height: 1200.0,
),
),
],
),
),
),
);
final Finder bottomFinder = find.byType(Placeholder);
expect(tester.getTopLeft(bottomFinder).dy, bottomDisplacement);
expect(
tester.getBottomLeft(bottomFinder).dy - tester.getTopLeft(bottomFinder).dy,
bottomHeight,
);
// Overscroll to stretch the navigation bar.
await tester.fling(find.byType(CustomScrollView), const Offset(0.0, 50.0), 10.0);
await tester.pump();
// The bottom stretches without resizing.
expect(tester.getTopLeft(bottomFinder).dy, greaterThan(bottomDisplacement));
expect(
tester.getBottomLeft(bottomFinder).dy - tester.getTopLeft(bottomFinder).dy,
bottomHeight,
);
});
}
class _ExpectStyles extends StatelessWidget {