From f9eebe03197e7289e0c043fa834860223d59c04f Mon Sep 17 00:00:00 2001 From: Victor Sanni Date: Wed, 16 Oct 2024 12:36:55 -0700 Subject: [PATCH] Add bottom to CupertinoNavigationBar to allow for a double-row nav bar (#155959) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes [Support segmented controls in nav bars and double row nav bars](https://github.com/flutter/flutter/issues/10469) Screenshot 2024-09-30 at 3 09 04 PM --- .../nav_bar/cupertino_navigation_bar.1.dart | 72 +++++++++++++++++++ .../cupertino_navigation_bar.1_test.dart | 27 +++++++ .../flutter/lib/src/cupertino/nav_bar.dart | 32 +++++++-- .../flutter/test/cupertino/nav_bar_test.dart | 48 +++++++++++++ 4 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.1.dart create mode 100644 examples/api/test/cupertino/nav_bar/cupertino_navigation_bar.1_test.dart diff --git a/examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.1.dart b/examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.1.dart new file mode 100644 index 0000000000..c07c1ec08f --- /dev/null +++ b/examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.1.dart @@ -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 createState() => _NavBarExampleState(); +} + +class _NavBarExampleState extends State { + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar( + middle: Text('CupertinoNavigationBar Sample'), + bottom: _NavigationBarSearchField(), + automaticBackgroundVisibility: false, + ), + child: Column( + children: [ + 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); +} diff --git a/examples/api/test/cupertino/nav_bar/cupertino_navigation_bar.1_test.dart b/examples/api/test/cupertino/nav_bar/cupertino_navigation_bar.1_test.dart new file mode 100644 index 0000000000..86ff7196b1 --- /dev/null +++ b/examples/api/test/cupertino/nav_bar/cupertino_navigation_bar.1_test.dart @@ -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), + ); + }); +} diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart index b6e4296d39..e59d91139f 100644 --- a/packages/flutter/lib/src/cupertino/nav_bar.dart +++ b/packages/flutter/lib/src/cupertino/nav_bar.dart @@ -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 { enableBackgroundFilterBlur: widget.enableBackgroundFilterBlur, child: DefaultTextStyle( style: CupertinoTheme.of(context).textTheme.textStyle, - child: _PersistentNavigationBar( - components: components, - padding: widget.padding, + child: Column( + children: [ + _PersistentNavigationBar( + components: components, + padding: widget.padding, + ), + if (widget.bottom != null) widget.bottom!, + ], ), ), ); diff --git a/packages/flutter/test/cupertino/nav_bar_test.dart b/packages/flutter/test/cupertino/nav_bar_test.dart index 6e10f78552..089567f8ad 100644 --- a/packages/flutter/test/cupertino/nav_bar_test.dart +++ b/packages/flutter/test/cupertino/nav_bar_test.dart @@ -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(navBarFinder); + + final Finder columnFinder = find.descendant( + of: navBarFinder, + matching: find.byType(Column), + ); + expect(columnFinder, findsOneWidget); + final Column column = tester.widget(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 {