Add bottom to CupertinoNavigationBar to allow for a double-row nav bar (#155959)

Fixes [Support segmented controls in nav bars and double row nav bars](https://github.com/flutter/flutter/issues/10469)

<img width="270" height="600"  alt="Screenshot 2024-09-30 at 3 09 04 PM" src="https://github.com/user-attachments/assets/4003116f-69dd-4f8f-a185-6ca151b74d2d">
This commit is contained in:
Victor Sanni
2024-10-16 12:36:55 -07:00
committed by GitHub
parent fbbbb7d452
commit f9eebe0319
4 changed files with 175 additions and 4 deletions

View File

@@ -0,0 +1,72 @@
// 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 [CupertinoNavigationBar] showing a
/// [CupertinoSearchTextField] with padding at the bottom of the navigation bar.
void main() => runApp(const NavBarApp());
class NavBarApp extends StatelessWidget {
const NavBarApp({super.key});
@override
Widget build(BuildContext context) {
return const CupertinoApp(
theme: CupertinoThemeData(brightness: Brightness.light),
home: NavBarExample(),
);
}
}
class NavBarExample extends StatefulWidget {
const NavBarExample({super.key});
@override
State<NavBarExample> createState() => _NavBarExampleState();
}
class _NavBarExampleState extends State<NavBarExample> {
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('CupertinoNavigationBar Sample'),
bottom: _NavigationBarSearchField(),
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),
],
),
);
}
}
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

@@ -0,0 +1,27 @@
// 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_navigation_bar.1.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('CupertinoNavigationBar with bottom widget', (WidgetTester tester) async {
await tester.pumpWidget(
const example.NavBarApp(),
);
final Finder navBarFinder = find.byType(CupertinoNavigationBar);
final Finder searchFieldFinder = find.byType(CupertinoSearchTextField);
expect(navBarFinder, findsOneWidget);
expect(searchFieldFinder, findsOneWidget);
// The bottom widget is bounded by the navigation bar.
expect(
tester.getBottomLeft(searchFieldFinder).dy,
lessThan(tester.getBottomLeft(navBarFinder).dy),
);
});
}

View File

@@ -289,6 +289,7 @@ class CupertinoNavigationBar extends StatefulWidget implements ObstructingPrefer
this.padding,
this.transitionBetweenRoutes = true,
this.heroTag = _defaultHeroTag,
this.bottom,
}) : assert(
!transitionBetweenRoutes || identical(heroTag, _defaultHeroTag),
'Cannot specify a heroTag override if this navigation bar does not '
@@ -465,6 +466,23 @@ class CupertinoNavigationBar extends StatefulWidget implements ObstructingPrefer
/// {@endtemplate}
final Object heroTag;
/// A widget to place at the bottom of the navigation bar.
///
/// Only widgets that implement [PreferredSizeWidget] can be used at the
/// bottom of a navigation bar.
///
/// {@tool dartpad}
/// This example shows a [CupertinoSearchTextField] at the bottom of a
/// [CupertinoNavigationBar].
///
/// ** See code in examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.1.dart **
/// {@end-tool}
///
/// See also:
///
/// * [PreferredSize], which can be used to give an arbitrary widget a preferred size.
final PreferredSizeWidget? bottom;
/// True if the navigation bar's background color has no transparency.
@override
bool shouldFullyObstruct(BuildContext context) {
@@ -475,7 +493,8 @@ class CupertinoNavigationBar extends StatefulWidget implements ObstructingPrefer
@override
Size get preferredSize {
return const Size.fromHeight(_kNavBarPersistentHeight);
final double heightForDrawer = bottom?.preferredSize.height ?? 0.0;
return Size.fromHeight(_kNavBarPersistentHeight + heightForDrawer);
}
@override
@@ -586,9 +605,14 @@ class _CupertinoNavigationBarState extends State<CupertinoNavigationBar> {
enableBackgroundFilterBlur: widget.enableBackgroundFilterBlur,
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: _PersistentNavigationBar(
components: components,
padding: widget.padding,
child: Column(
children: <Widget>[
_PersistentNavigationBar(
components: components,
padding: widget.padding,
),
if (widget.bottom != null) widget.bottom!,
],
),
),
);

View File

@@ -2086,6 +2086,54 @@ void main() {
bottomHeight,
);
});
testWidgets('CupertinoNavigationBar with bottom widget', (WidgetTester tester) async {
const double persistentHeight = 44.0;
const double bottomHeight = 10.0;
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Middle'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(bottomHeight),
child: Placeholder(),
)
),
child: Container(),
),
),
);
final Finder navBarFinder = find.byType(CupertinoNavigationBar);
expect(navBarFinder, findsOneWidget);
final CupertinoNavigationBar navBar = tester.widget<CupertinoNavigationBar>(navBarFinder);
final Finder columnFinder = find.descendant(
of: navBarFinder,
matching: find.byType(Column),
);
expect(columnFinder, findsOneWidget);
final Column column = tester.widget<Column>(columnFinder);
expect(column.children.length, 2);
expect(
find.descendant(
of: find.byWidget(column.children.first),
matching: find.text('Middle'),
),
findsOneWidget,
);
expect(
find.descendant(
of: find.byWidget(column.children.last),
matching: find.byType(Placeholder),
),
findsOneWidget,
);
expect(navBar.preferredSize.height, persistentHeight + bottomHeight);
});
}
class _ExpectStyles extends StatelessWidget {