Let CupertinoNavBar automatically have a back or close button (#12575) * create auto back button behaviour * cosmetic fidelity * tests * review * document new icon
diff --git a/packages/flutter/lib/src/cupertino/button.dart b/packages/flutter/lib/src/cupertino/button.dart index 172ef2f..2fa9e83 100644 --- a/packages/flutter/lib/src/cupertino/button.dart +++ b/packages/flutter/lib/src/cupertino/button.dart
@@ -190,6 +190,7 @@ final Color backgroundColor = widget.color; return new GestureDetector( + behavior: HitTestBehavior.opaque, onTapDown: enabled ? _handleTapDown : null, onTapUp: enabled ? _handleTapUp : null, onTapCancel: enabled ? _handleTapCancel : null,
diff --git a/packages/flutter/lib/src/cupertino/icons.dart b/packages/flutter/lib/src/cupertino/icons.dart index cb65f09..b7048b6 100644 --- a/packages/flutter/lib/src/cupertino/icons.dart +++ b/packages/flutter/lib/src/cupertino/icons.dart
@@ -77,4 +77,7 @@ /// A checkmark in a circle. static const IconData check_mark_circled = const IconData(0xf41f, fontFamily: iconFont, fontPackage: iconFontPackage); -} \ No newline at end of file + + /// A thicker left chevron used in iOS for the nav bar back button. + static const IconData back = const IconData(0xf3f0, fontFamily: iconFont, fontPackage: iconFontPackage); +}
diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart index ed4a604..50935fe 100644 --- a/packages/flutter/lib/src/cupertino/nav_bar.dart +++ b/packages/flutter/lib/src/cupertino/nav_bar.dart
@@ -7,7 +7,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'button.dart'; import 'colors.dart'; +import 'icons.dart'; /// Standard iOS navigation bar height without the status bar. const double _kNavBarPersistentHeight = 44.0; @@ -22,6 +24,11 @@ const double _kNavBarEdgePadding = 16.0; +// The back chevron has a special padding in iOS. +const double _kNavBarBackButtonPadding = 0.0; + +const double _kNavBarBackButtonTapWidth = 50.0; + /// Title text transfer fade. const Duration _kNavBarTitleFadeDuration = const Duration(milliseconds: 150); @@ -44,6 +51,10 @@ /// It also supports a [leading] and [trailing] widget before and after the /// [middle] widget while keeping the [middle] widget centered. /// +/// The [leading] widget will automatically be a back chevron icon button (or a +/// close button in case of a fullscreen dialog) to pop the current route if none +/// is provided and [automaticallyImplyLeading] is true (true by default). +/// /// It should be placed at top of the screen and automatically accounts for /// the OS's status bar. /// @@ -56,25 +67,31 @@ /// [CupertinoNavigationBar]. /// * [CupertinoSliverNavigationBar] for a navigation bar to be placed in a /// scrolling list and that supports iOS-11-style large titles. -// -// TODO(xster): document automatic addition of a CupertinoBackButton. -// TODO(xster): add sample code using icons. class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWidget { /// Creates a navigation bar in the iOS style. const CupertinoNavigationBar({ Key key, this.leading, - @required this.middle, + this.automaticallyImplyLeading: true, + this.middle, this.trailing, this.backgroundColor: _kDefaultNavBarBackgroundColor, this.actionsForegroundColor: CupertinoColors.activeBlue, - }) : assert(middle != null, 'There must be a middle widget, usually a title.'), + }) : assert(automaticallyImplyLeading != null), super(key: key); /// Widget to place at the start of the navigation bar. Normally a back button /// for a normal page or a cancel button for full page dialogs. final Widget leading; + /// Controls whether we should try to imply the leading widget if null. + /// + /// If true and [leading] is null, automatically try to deduce what the [leading] + /// widget should be. If [leading] widget is not null, this parameter has no effect. + /// + /// This value cannot be null. + final bool automaticallyImplyLeading; + /// Widget to place in the middle of the navigation bar. Normally a title or /// a segmented control. final Widget middle; @@ -111,6 +128,7 @@ backgroundColor: backgroundColor, child: new _CupertinoPersistentNavigationBar( leading: leading, + automaticallyImplyLeading: automaticallyImplyLeading, middle: middle, trailing: trailing, actionsForegroundColor: actionsForegroundColor, @@ -137,6 +155,13 @@ /// For advanced uses, an optional [middle] widget can be supplied to show a /// different widget in the middle of the navigation bar when the sliver is collapsed. /// +/// Like [CupertinoNavigationBar], it also supports a [leading] and [trailing] +/// widget on the static section on top that remains while scrolling. +/// +/// The [leading] widget will automatically be a back chevron icon button (or a +/// close button in case of a fullscreen dialog) to pop the current route if none +/// is provided and [automaticallyImplyLeading] is true (true by default). +/// /// See also: /// /// * [CupertinoNavigationBar], an iOS navigation bar for use on non-scrolling @@ -149,11 +174,13 @@ Key key, @required this.largeTitle, this.leading, + this.automaticallyImplyLeading: true, this.middle, this.trailing, this.backgroundColor: _kDefaultNavBarBackgroundColor, this.actionsForegroundColor: CupertinoColors.activeBlue, }) : assert(largeTitle != null), + assert(automaticallyImplyLeading != null), super(key: key); /// The navigation bar's title. @@ -178,6 +205,14 @@ /// This widget is visible in both collapsed and expanded states. final Widget leading; + /// Controls whether we should try to imply the leading widget if null. + /// + /// If true and [leading] is null, automatically try to deduce what the [leading] + /// widget should be. If [leading] widget is not null, this parameter has no effect. + /// + /// This value cannot be null. + final bool automaticallyImplyLeading; + /// A widget to place in the middle of the static navigation bar instead of /// the [largeTitle]. /// @@ -215,6 +250,7 @@ persistentHeight: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top, title: largeTitle, leading: leading, + automaticallyImplyLeading: automaticallyImplyLeading, middle: middle, trailing: trailing, backgroundColor: backgroundColor, @@ -261,7 +297,8 @@ const _CupertinoPersistentNavigationBar({ Key key, this.leading, - @required this.middle, + this.automaticallyImplyLeading, + this.middle, this.trailing, this.actionsForegroundColor, this.middleVisible, @@ -269,6 +306,8 @@ final Widget leading; + final bool automaticallyImplyLeading; + final Widget middle; final Widget trailing; @@ -320,7 +359,27 @@ child: styledMiddle, ); - // TODO(xster): automatically build a CupertinoBackButton. + // Auto add back button if leading not provided. + Widget backOrCloseButton; + bool useBackButton = false; + if (styledLeading == null && automaticallyImplyLeading) { + final ModalRoute<dynamic> currentRoute = ModalRoute.of(context); + if (currentRoute?.canPop == true) { + useBackButton = !(currentRoute is PageRoute && currentRoute?.fullscreenDialog == true); + backOrCloseButton = new CupertinoButton( + child: useBackButton + ? new Container( + height: _kNavBarPersistentHeight, + width: _kNavBarBackButtonTapWidth, + alignment: Alignment.centerLeft, + child: const Icon(CupertinoIcons.back, size: 34.0,) + ) + : const Text('Close'), + padding: EdgeInsets.zero, + onPressed: () { Navigator.of(context).maybePop(); }, + ); + } + } return new SizedBox( height: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top, @@ -332,16 +391,14 @@ child: new Padding( padding: new EdgeInsets.only( top: MediaQuery.of(context).padding.top, - // TODO(xster): dynamically reduce padding when an automatic - // CupertinoBackButton is present. - left: _kNavBarEdgePadding, + left: useBackButton ? _kNavBarBackButtonPadding : _kNavBarEdgePadding, right: _kNavBarEdgePadding, ), child: new MediaQuery.removePadding( context: context, removeTop: true, child: new NavigationToolbar( - leading: styledLeading, + leading: styledLeading ?? backOrCloseButton, middle: animatedStyledMiddle, trailing: styledTrailing, centerMiddle: true, @@ -358,6 +415,7 @@ @required this.persistentHeight, @required this.title, this.leading, + this.automaticallyImplyLeading, this.middle, this.trailing, this.backgroundColor, @@ -370,6 +428,8 @@ final Widget leading; + final bool automaticallyImplyLeading; + final Widget middle; final Widget trailing; @@ -391,6 +451,7 @@ final _CupertinoPersistentNavigationBar persistentNavigationBar = new _CupertinoPersistentNavigationBar( leading: leading, + automaticallyImplyLeading: automaticallyImplyLeading, middle: middle ?? title, trailing: trailing, // If middle widget exists, always show it. Otherwise, show title
diff --git a/packages/flutter/test/cupertino/nav_bar_test.dart b/packages/flutter/test/cupertino/nav_bar_test.dart index f3ebcd4..74c8e9a 100644 --- a/packages/flutter/test/cupertino/nav_bar_test.dart +++ b/packages/flutter/test/cupertino/nav_bar_test.dart
@@ -275,6 +275,70 @@ expect(tester.getBottomLeft(find.text('Title')).dy, 44.0 - 8.0); // Extension gone, (static part - padding) left. }); + + testWidgets('Auto back/close button', (WidgetTester tester) async { + await tester.pumpWidget( + new WidgetsApp( + color: const Color(0xFFFFFFFF), + onGenerateRoute: (RouteSettings settings) { + return new CupertinoPageRoute<Null>( + settings: settings, + builder: (BuildContext context) { + return const CupertinoNavigationBar( + middle: const Text('Home page'), + ); + }, + ); + }, + ), + ); + + expect(find.byType(CupertinoButton), findsNothing); + + tester.state<NavigatorState>(find.byType(Navigator)).push(new CupertinoPageRoute<Null>( + builder: (BuildContext context) { + return const CupertinoNavigationBar( + middle: const Text('Page 2'), + ); + }, + )); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + + expect(find.byType(CupertinoButton), findsOneWidget); + expect(find.byType(Icon), findsOneWidget); + + tester.state<NavigatorState>(find.byType(Navigator)).push(new CupertinoPageRoute<Null>( + fullscreenDialog: true, + builder: (BuildContext context) { + return const CupertinoNavigationBar( + middle: const Text('Dialog page'), + ); + }, + )); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + + expect(find.byType(CupertinoButton), findsNWidgets(2)); + expect(find.text('Close'), findsOneWidget); + + // Test popping goes back correctly. + await tester.tap(find.text('Close')); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + + expect(find.text('Page 2'), findsOneWidget); + + await tester.tap(find.byType(Icon)); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + + expect(find.text('Home page'), findsOneWidget); + }); } class _ExpectStyles extends StatelessWidget {