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
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);
});
}