Make CupertinoSheetRoute usable with Cupertino(Sliver)NavigationBar (#162181)

Working on the cupertino nav bars recently gave me some context on those
widgets, so when I saw this
[comment](https://github.com/flutter/flutter/pull/157568#issuecomment-2590955571)
I was inspired to add a fix :)

Thanks @MaherSafadii for [starting the
exploration](https://github.com/flutter/flutter/pull/157568#issuecomment-2592535332)
and also for the very helpful
[screenshots](https://github.com/flutter/flutter/issues/162021#issuecomment-2608430023).

Removes the following when
CupertinoNavigationBar/CupertinoSliverNavigationBar is used in a
CupertinoSheetRoute:
- Unneeded back button
- Superfluous top padding in CupertinoNavigationBar
- Full page route transitions

## Before:


https://github.com/user-attachments/assets/a6da3957-0cff-4491-9380-bbc676ac799d


## After:


https://github.com/user-attachments/assets/37cd1628-a47e-44aa-85c7-abceda6e7944

<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 [CupertinoSheetRoute].

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

  @override
  Widget build(BuildContext context) {
    return const CupertinoApp(title: 'Cupertino Sheet', home: HomePage());
  }
}

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

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Sheet Example'),
        automaticBackgroundVisibility: false,
      ),
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            CupertinoButton.filled(
              onPressed: () {
                Navigator.of(context).push(
                  CupertinoSheetRoute<void>(
                    builder: (BuildContext context) => const _SheetScaffold(),
                  ),
                );
              },
              child: const Text('Open Bottom Sheet'),
            ),
          ],
        ),
      ),
    );
  }
}

class _SheetScaffold extends StatelessWidget {
  const _SheetScaffold();

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        backgroundColor: CupertinoColors.systemGrey.withOpacity(0.5),
        middle: const Text('CupertinoNavigationBar Sample'),
        automaticBackgroundVisibility: false,
      ),
      child: Column(
        children: <Widget>[
          Container(height: 50, color: CupertinoColors.systemRed),
          Container(height: 50, color: CupertinoColors.systemGreen),
          Container(height: 50, color: CupertinoColors.systemBlue),
          Container(height: 50, color: CupertinoColors.systemYellow),
          Center(
            child: CupertinoButton.filled(
              onPressed: () {
                Navigator.of(context).push(
                  CupertinoSheetRoute<void>(
                    builder: (BuildContext context) =>
                        const SliverNavBarExample(),
                  ),
                );
              },
              child: const Text('Open Bottom Sheet'),
            ),
          )
        ],
      ),
    );
  }
}

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: () {},
                    child: const Text('Bottom Automatic mode'),
                  ),
                  CupertinoButton.filled(
                    onPressed: () {},
                    child: const Text('Bottom Always mode'),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

```
</details>


Fixes [Cupertino navbars apply too much top padding within a
sheet](https://github.com/flutter/flutter/issues/162021).

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
This commit is contained in:
Victor Sanni 2025-02-04 17:49:25 -08:00 committed by GitHub
parent d3c96c65e5
commit dfb36f032e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 111 additions and 6 deletions

View File

@ -20,6 +20,7 @@ import 'icons.dart';
import 'page_scaffold.dart';
import 'route.dart';
import 'search_field.dart';
import 'sheet.dart';
import 'theme.dart';
/// Modes that determine how to display the navigation bar's bottom in relation to scroll events.
@ -212,7 +213,9 @@ bool _isTransitionable(BuildContext context) {
// Fullscreen dialogs never transitions their nav bar with other push-style
// pages' nav bars or with other fullscreen dialog pages on the way in or on
// the way out.
return route is PageRoute && !route.fullscreenDialog;
return route is PageRoute &&
!route.fullscreenDialog &&
!CupertinoSheetRoute.hasParentSheet(context);
}
/// An iOS-styled navigation bar.
@ -1571,7 +1574,10 @@ class _PersistentNavigationBar extends StatelessWidget {
final Widget? backChevron = components.backChevron;
final Widget? backLabel = components.backLabel;
if (leading == null && backChevron != null && backLabel != null) {
if (leading == null &&
backChevron != null &&
backLabel != null &&
!CupertinoSheetRoute.hasParentSheet(context)) {
leading = CupertinoNavigationBarBackButton._assemble(backChevron, backLabel);
}
@ -1591,7 +1597,11 @@ class _PersistentNavigationBar extends StatelessWidget {
return SizedBox(
height: _kNavBarPersistentHeight + MediaQuery.paddingOf(context).top,
child: SafeArea(bottom: false, child: paddedToolbar),
child: SafeArea(
top: !CupertinoSheetRoute.hasParentSheet(context),
bottom: false,
child: paddedToolbar,
),
);
}
}

View File

@ -461,8 +461,6 @@ void main() {
testWidgets('Media padding is applied to CupertinoSliverNavigationBar', (
WidgetTester tester,
) async {
final ScrollController scrollController = ScrollController();
addTearDown(scrollController.dispose);
final Key leadingKey = GlobalKey();
final Key middleKey = GlobalKey();
final Key trailingKey = GlobalKey();
@ -475,7 +473,6 @@ void main() {
),
child: CupertinoPageScaffold(
child: CustomScrollView(
controller: scrollController,
slivers: <Widget>[
CupertinoSliverNavigationBar(
leading: Placeholder(key: leadingKey),
@ -797,6 +794,57 @@ void main() {
expect(find.text('Home page'), findsOneWidget);
});
testWidgets('Navigation bars in a CupertinoSheetRoute have no back button', (
WidgetTester tester,
) async {
await tester.pumpWidget(
const CupertinoApp(home: CupertinoNavigationBar(middle: Text('Home page'))),
);
expect(find.byType(CupertinoButton), findsNothing);
tester
.state<NavigatorState>(find.byType(Navigator))
.push(
CupertinoSheetRoute<void>(
builder: (BuildContext context) {
return const CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(middle: Text('Page 2')),
child: Placeholder(),
);
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
// No back button is found.
expect(find.byType(CupertinoButton), findsNothing);
expect(find.text(String.fromCharCode(CupertinoIcons.back.codePoint)), findsNothing);
tester
.state<NavigatorState>(find.byType(Navigator))
.push(
CupertinoSheetRoute<void>(
builder: (BuildContext context) {
return const CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[CupertinoSliverNavigationBar(largeTitle: Text('Page 3'))],
),
);
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
// No back button is found.
expect(find.byType(CupertinoButton), findsNothing);
expect(find.text(String.fromCharCode(CupertinoIcons.back.codePoint)), findsNothing);
});
testWidgets('Long back label turns into "back"', (WidgetTester tester) async {
await tester.pumpWidget(const CupertinoApp(home: Placeholder()));

View File

@ -343,6 +343,53 @@ void main() {
expect(() => flying(tester, find.text('Page 2')), throwsAssertionError);
});
testWidgets('Navigation bars in a CupertinoSheetRoute have no hero transitions', (
WidgetTester tester,
) async {
await tester.pumpWidget(
CupertinoApp(
builder: (BuildContext context, Widget? navigator) {
return navigator!;
},
home: const Placeholder(),
),
);
tester
.state<NavigatorState>(find.byType(Navigator))
.push(
CupertinoSheetRoute<void>(
builder:
(BuildContext context) =>
scaffoldForNavBar(const CupertinoNavigationBar(middle: Text('Page 1')))!,
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
tester
.state<NavigatorState>(find.byType(Navigator))
.push(
CupertinoSheetRoute<void>(
builder:
(BuildContext context) =>
scaffoldForNavBar(
const CupertinoSliverNavigationBar(largeTitle: Text('Page 2')),
)!,
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
expect(find.byType(Hero), findsNothing);
// No Hero transition happened.
expect(() => flying(tester, find.text('Page 1')), throwsAssertionError);
expect(() => flying(tester, find.text('Page 2')), throwsAssertionError);
});
testWidgets('Popping mid-transition is symmetrical', (WidgetTester tester) async {
await startTransitionBetween(tester, fromTitle: 'Page 1');