Commit b96fa4de authored by xster's avatar xster Committed by GitHub

Let CupertinoNavBar automatically have a back or close button (#12575)

* create auto back button behaviour

* cosmetic fidelity

* tests

* review

* document new icon
parent 4c1150dd
...@@ -190,6 +190,7 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv ...@@ -190,6 +190,7 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
final Color backgroundColor = widget.color; final Color backgroundColor = widget.color;
return new GestureDetector( return new GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: enabled ? _handleTapDown : null, onTapDown: enabled ? _handleTapDown : null,
onTapUp: enabled ? _handleTapUp : null, onTapUp: enabled ? _handleTapUp : null,
onTapCancel: enabled ? _handleTapCancel : null, onTapCancel: enabled ? _handleTapCancel : null,
......
...@@ -77,4 +77,7 @@ class CupertinoIcons { ...@@ -77,4 +77,7 @@ class CupertinoIcons {
/// A checkmark in a circle. /// A checkmark in a circle.
static const IconData check_mark_circled = const IconData(0xf41f, fontFamily: iconFont, fontPackage: iconFontPackage); static const IconData check_mark_circled = const IconData(0xf41f, fontFamily: iconFont, fontPackage: iconFontPackage);
/// A thicker left chevron used in iOS for the nav bar back button.
static const IconData back = const IconData(0xf3f0, fontFamily: iconFont, fontPackage: iconFontPackage);
} }
...@@ -7,7 +7,9 @@ import 'dart:ui' show ImageFilter; ...@@ -7,7 +7,9 @@ import 'dart:ui' show ImageFilter;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'button.dart';
import 'colors.dart'; import 'colors.dart';
import 'icons.dart';
/// Standard iOS navigation bar height without the status bar. /// Standard iOS navigation bar height without the status bar.
const double _kNavBarPersistentHeight = 44.0; const double _kNavBarPersistentHeight = 44.0;
...@@ -22,6 +24,11 @@ const double _kNavBarShowLargeTitleThreshold = 10.0; ...@@ -22,6 +24,11 @@ const double _kNavBarShowLargeTitleThreshold = 10.0;
const double _kNavBarEdgePadding = 16.0; 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. /// Title text transfer fade.
const Duration _kNavBarTitleFadeDuration = const Duration(milliseconds: 150); const Duration _kNavBarTitleFadeDuration = const Duration(milliseconds: 150);
...@@ -44,6 +51,10 @@ const TextStyle _kLargeTitleTextStyle = const TextStyle( ...@@ -44,6 +51,10 @@ const TextStyle _kLargeTitleTextStyle = const TextStyle(
/// It also supports a [leading] and [trailing] widget before and after the /// It also supports a [leading] and [trailing] widget before and after the
/// [middle] widget while keeping the [middle] widget centered. /// [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 /// It should be placed at top of the screen and automatically accounts for
/// the OS's status bar. /// the OS's status bar.
/// ///
...@@ -56,25 +67,31 @@ const TextStyle _kLargeTitleTextStyle = const TextStyle( ...@@ -56,25 +67,31 @@ const TextStyle _kLargeTitleTextStyle = const TextStyle(
/// [CupertinoNavigationBar]. /// [CupertinoNavigationBar].
/// * [CupertinoSliverNavigationBar] for a navigation bar to be placed in a /// * [CupertinoSliverNavigationBar] for a navigation bar to be placed in a
/// scrolling list and that supports iOS-11-style large titles. /// 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 { class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWidget {
/// Creates a navigation bar in the iOS style. /// Creates a navigation bar in the iOS style.
const CupertinoNavigationBar({ const CupertinoNavigationBar({
Key key, Key key,
this.leading, this.leading,
@required this.middle, this.automaticallyImplyLeading: true,
this.middle,
this.trailing, this.trailing,
this.backgroundColor: _kDefaultNavBarBackgroundColor, this.backgroundColor: _kDefaultNavBarBackgroundColor,
this.actionsForegroundColor: CupertinoColors.activeBlue, this.actionsForegroundColor: CupertinoColors.activeBlue,
}) : assert(middle != null, 'There must be a middle widget, usually a title.'), }) : assert(automaticallyImplyLeading != null),
super(key: key); super(key: key);
/// Widget to place at the start of the navigation bar. Normally a back button /// 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. /// for a normal page or a cancel button for full page dialogs.
final Widget leading; 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 /// Widget to place in the middle of the navigation bar. Normally a title or
/// a segmented control. /// a segmented control.
final Widget middle; final Widget middle;
...@@ -111,6 +128,7 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid ...@@ -111,6 +128,7 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
child: new _CupertinoPersistentNavigationBar( child: new _CupertinoPersistentNavigationBar(
leading: leading, leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
middle: middle, middle: middle,
trailing: trailing, trailing: trailing,
actionsForegroundColor: actionsForegroundColor, actionsForegroundColor: actionsForegroundColor,
...@@ -137,6 +155,13 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid ...@@ -137,6 +155,13 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid
/// For advanced uses, an optional [middle] widget can be supplied to show a /// 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. /// 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: /// See also:
/// ///
/// * [CupertinoNavigationBar], an iOS navigation bar for use on non-scrolling /// * [CupertinoNavigationBar], an iOS navigation bar for use on non-scrolling
...@@ -149,11 +174,13 @@ class CupertinoSliverNavigationBar extends StatelessWidget { ...@@ -149,11 +174,13 @@ class CupertinoSliverNavigationBar extends StatelessWidget {
Key key, Key key,
@required this.largeTitle, @required this.largeTitle,
this.leading, this.leading,
this.automaticallyImplyLeading: true,
this.middle, this.middle,
this.trailing, this.trailing,
this.backgroundColor: _kDefaultNavBarBackgroundColor, this.backgroundColor: _kDefaultNavBarBackgroundColor,
this.actionsForegroundColor: CupertinoColors.activeBlue, this.actionsForegroundColor: CupertinoColors.activeBlue,
}) : assert(largeTitle != null), }) : assert(largeTitle != null),
assert(automaticallyImplyLeading != null),
super(key: key); super(key: key);
/// The navigation bar's title. /// The navigation bar's title.
...@@ -178,6 +205,14 @@ class CupertinoSliverNavigationBar extends StatelessWidget { ...@@ -178,6 +205,14 @@ class CupertinoSliverNavigationBar extends StatelessWidget {
/// This widget is visible in both collapsed and expanded states. /// This widget is visible in both collapsed and expanded states.
final Widget leading; 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 /// A widget to place in the middle of the static navigation bar instead of
/// the [largeTitle]. /// the [largeTitle].
/// ///
...@@ -215,6 +250,7 @@ class CupertinoSliverNavigationBar extends StatelessWidget { ...@@ -215,6 +250,7 @@ class CupertinoSliverNavigationBar extends StatelessWidget {
persistentHeight: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top, persistentHeight: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
title: largeTitle, title: largeTitle,
leading: leading, leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
middle: middle, middle: middle,
trailing: trailing, trailing: trailing,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
...@@ -261,7 +297,8 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe ...@@ -261,7 +297,8 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
const _CupertinoPersistentNavigationBar({ const _CupertinoPersistentNavigationBar({
Key key, Key key,
this.leading, this.leading,
@required this.middle, this.automaticallyImplyLeading,
this.middle,
this.trailing, this.trailing,
this.actionsForegroundColor, this.actionsForegroundColor,
this.middleVisible, this.middleVisible,
...@@ -269,6 +306,8 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe ...@@ -269,6 +306,8 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
final Widget leading; final Widget leading;
final bool automaticallyImplyLeading;
final Widget middle; final Widget middle;
final Widget trailing; final Widget trailing;
...@@ -320,7 +359,27 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe ...@@ -320,7 +359,27 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
child: styledMiddle, 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( return new SizedBox(
height: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top, height: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
...@@ -332,16 +391,14 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe ...@@ -332,16 +391,14 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
child: new Padding( child: new Padding(
padding: new EdgeInsets.only( padding: new EdgeInsets.only(
top: MediaQuery.of(context).padding.top, top: MediaQuery.of(context).padding.top,
// TODO(xster): dynamically reduce padding when an automatic left: useBackButton ? _kNavBarBackButtonPadding : _kNavBarEdgePadding,
// CupertinoBackButton is present.
left: _kNavBarEdgePadding,
right: _kNavBarEdgePadding, right: _kNavBarEdgePadding,
), ),
child: new MediaQuery.removePadding( child: new MediaQuery.removePadding(
context: context, context: context,
removeTop: true, removeTop: true,
child: new NavigationToolbar( child: new NavigationToolbar(
leading: styledLeading, leading: styledLeading ?? backOrCloseButton,
middle: animatedStyledMiddle, middle: animatedStyledMiddle,
trailing: styledTrailing, trailing: styledTrailing,
centerMiddle: true, centerMiddle: true,
...@@ -358,6 +415,7 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe ...@@ -358,6 +415,7 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe
@required this.persistentHeight, @required this.persistentHeight,
@required this.title, @required this.title,
this.leading, this.leading,
this.automaticallyImplyLeading,
this.middle, this.middle,
this.trailing, this.trailing,
this.backgroundColor, this.backgroundColor,
...@@ -370,6 +428,8 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe ...@@ -370,6 +428,8 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe
final Widget leading; final Widget leading;
final bool automaticallyImplyLeading;
final Widget middle; final Widget middle;
final Widget trailing; final Widget trailing;
...@@ -391,6 +451,7 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe ...@@ -391,6 +451,7 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe
final _CupertinoPersistentNavigationBar persistentNavigationBar = final _CupertinoPersistentNavigationBar persistentNavigationBar =
new _CupertinoPersistentNavigationBar( new _CupertinoPersistentNavigationBar(
leading: leading, leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
middle: middle ?? title, middle: middle ?? title,
trailing: trailing, trailing: trailing,
// If middle widget exists, always show it. Otherwise, show title // If middle widget exists, always show it. Otherwise, show title
......
...@@ -275,6 +275,70 @@ void main() { ...@@ -275,6 +275,70 @@ void main() {
expect(tester.getBottomLeft(find.text('Title')).dy, 44.0 - 8.0); // Extension gone, (static part - padding) left. 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 { class _ExpectStyles extends StatelessWidget {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment