Auto populate nav bar title and previous from page route (#19637)
diff --git a/examples/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart b/examples/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart
index e9bb0ff..86b6588 100644
--- a/examples/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart
+++ b/examples/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart
@@ -47,51 +47,58 @@
return new WillPopScope(
// Prevent swipe popping of this page. Use explicit exit buttons only.
onWillPop: () => new Future<bool>.value(true),
- child: new CupertinoTabScaffold(
- tabBar: new CupertinoTabBar(
- items: const <BottomNavigationBarItem>[
- BottomNavigationBarItem(
- icon: Icon(CupertinoIcons.home),
- title: Text('Home'),
- ),
- BottomNavigationBarItem(
- icon: Icon(CupertinoIcons.conversation_bubble),
- title: Text('Support'),
- ),
- BottomNavigationBarItem(
- icon: Icon(CupertinoIcons.profile_circled),
- title: Text('Profile'),
- ),
- ],
+ child: new DefaultTextStyle(
+ style: const TextStyle(
+ fontFamily: '.SF UI Text',
+ fontSize: 17.0,
+ color: CupertinoColors.black,
),
- tabBuilder: (BuildContext context, int index) {
- return new DefaultTextStyle(
- style: const TextStyle(
- fontFamily: '.SF UI Text',
- fontSize: 17.0,
- color: CupertinoColors.black,
- ),
- child: new CupertinoTabView(
- builder: (BuildContext context) {
- switch (index) {
- case 0:
+ child: new CupertinoTabScaffold(
+ tabBar: new CupertinoTabBar(
+ items: const <BottomNavigationBarItem>[
+ BottomNavigationBarItem(
+ icon: Icon(CupertinoIcons.home),
+ title: Text('Home'),
+ ),
+ BottomNavigationBarItem(
+ icon: Icon(CupertinoIcons.conversation_bubble),
+ title: Text('Support'),
+ ),
+ BottomNavigationBarItem(
+ icon: Icon(CupertinoIcons.profile_circled),
+ title: Text('Profile'),
+ ),
+ ],
+ ),
+ tabBuilder: (BuildContext context, int index) {
+ switch (index) {
+ case 0:
+ return new CupertinoTabView(
+ builder: (BuildContext context) {
return new CupertinoDemoTab1(
colorItems: colorItems,
colorNameItems: colorNameItems
);
- break;
- case 1:
- return new CupertinoDemoTab2();
- break;
- case 2:
- return new CupertinoDemoTab3();
- break;
- default:
- }
- },
- ),
- );
- },
+ },
+ defaultTitle: 'Colors',
+ );
+ break;
+ case 1:
+ return new CupertinoTabView(
+ builder: (BuildContext context) => CupertinoDemoTab2(),
+ defaultTitle: 'Support Chat',
+ );
+ break;
+ case 2:
+ return new CupertinoTabView(
+ builder: (BuildContext context) => CupertinoDemoTab3(),
+ defaultTitle: 'Account',
+ );
+ break;
+ default:
+ }
+ },
+ ),
),
);
}
@@ -129,7 +136,6 @@
child: new CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(
- largeTitle: Text('Colors'),
trailing: ExitButton(),
),
new SliverPadding(
@@ -174,6 +180,7 @@
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).push(new CupertinoPageRoute<void>(
+ title: colorName,
builder: (BuildContext context) => new Tab1ItemPage(
color: color,
colorName: colorName,
@@ -285,9 +292,8 @@
@override
Widget build(BuildContext context) {
return new CupertinoPageScaffold(
- navigationBar: new CupertinoNavigationBar(
- middle: new Text(widget.colorName),
- trailing: const ExitButton(),
+ navigationBar: const CupertinoNavigationBar(
+ trailing: ExitButton(),
),
child: new SafeArea(
top: false,
@@ -415,7 +421,6 @@
Widget build(BuildContext context) {
return new CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
- middle: Text('Support Chat'),
trailing: ExitButton(),
),
child: new ListView(
@@ -699,7 +704,6 @@
Widget build(BuildContext context) {
return new CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
- middle: Text('Account'),
trailing: ExitButton(),
),
child: new DecoratedBox(
diff --git a/examples/flutter_gallery/lib/demo/cupertino/cupertino_refresh_demo.dart b/examples/flutter_gallery/lib/demo/cupertino/cupertino_refresh_demo.dart
index 4d8084e..f249dd4 100644
--- a/examples/flutter_gallery/lib/demo/cupertino/cupertino_refresh_demo.dart
+++ b/examples/flutter_gallery/lib/demo/cupertino/cupertino_refresh_demo.dart
@@ -50,6 +50,7 @@
slivers: <Widget>[
const CupertinoSliverNavigationBar(
largeTitle: Text('Cupertino Refresh'),
+ previousPageTitle: 'Cupertino',
),
new CupertinoSliverRefreshControl(
onRefresh: () {
diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart
index f99fb76..7c18074 100644
--- a/packages/flutter/lib/src/cupertino/nav_bar.dart
+++ b/packages/flutter/lib/src/cupertino/nav_bar.dart
@@ -12,6 +12,7 @@
import 'colors.dart';
import 'icons.dart';
import 'page_scaffold.dart';
+import 'route.dart';
/// Standard iOS navigation bar height without the status bar.
const double _kNavBarPersistentHeight = 44.0;
@@ -62,6 +63,10 @@
/// close button in case of a fullscreen dialog) to pop the current route if none
/// is provided and [automaticallyImplyLeading] is true (true by default).
///
+/// The [middle] widget will automatically be a title text from the current
+/// route if none is provided and [automaticallyImplyMiddle] is true (true by
+/// default).
+///
/// It should be placed at top of the screen and automatically accounts for
/// the OS's status bar.
///
@@ -80,6 +85,8 @@
Key key,
this.leading,
this.automaticallyImplyLeading = true,
+ this.automaticallyImplyMiddle = true,
+ this.previousPageTitle,
this.middle,
this.trailing,
this.border = _kDefaultNavBarBorder,
@@ -87,35 +94,83 @@
this.padding,
this.actionsForegroundColor = CupertinoColors.activeBlue,
}) : assert(automaticallyImplyLeading != null),
+ assert(automaticallyImplyMiddle != null),
super(key: key);
+ /// {@template flutter.cupertino.navBar.leading}
/// 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.
+ ///
+ /// If null and [automaticallyImplyLeading] is true, an appropriate button
+ /// will be automatically created.
+ /// {@endtemplate}
final Widget leading;
+ /// {@template flutter.cupertino.navBar.automaticallyImplyLeading}
/// 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.
///
+ /// Specifically this navigation bar will:
+ ///
+ /// 1. Show a 'Close' button if the current route is a `fullscreenDialog`.
+ /// 2. Show a back chevron with [previousPageTitle] if [previousPageTitle] is
+ /// not null.
+ /// 3. Show a back chevron with the previous route's `title` if the current
+ /// route is a [CupertinoPageRoute] and the previous route is also a
+ /// [CupertinoPageRoute].
+ ///
/// This value cannot be null.
+ /// {@endtemplate}
final bool automaticallyImplyLeading;
+ /// Controls whether we should try to imply the middle widget if null.
+ ///
+ /// If true and [middle] is null, automatically fill in a [Text] widget with
+ /// the current route's `title` if the route is a [CupertinoPageRoute].
+ /// If [middle] widget is not null, this parameter has no effect.
+ ///
+ /// This value cannot be null.
+ final bool automaticallyImplyMiddle;
+
+ /// {@template flutter.cupertino.navBar.previousPageTitle}
+ /// Manually specify the previous route's title when automatically implying
+ /// the leading back button.
+ ///
+ /// Overrides the text shown with the back chevron instead of automatically
+ /// showing the previous [CupertinoPageRoute]'s `title` when
+ /// [automaticallyImplyLeading] is true.
+ ///
+ /// Has no effect when [leading] is not null or if [automaticallyImplyLeading]
+ /// is false.
+ /// {@endtemplate}
+ final String previousPageTitle;
+
/// Widget to place in the middle of the navigation bar. Normally a title or
/// a segmented control.
+ ///
+ /// If null and [automaticallyImplyMiddle] is true, an appropriate [Text]
+ /// title will be created if the current route is a [CupertinoPageRoute] and
+ /// has a `title`.
final Widget middle;
+ /// {@template flutter.cupertino.navBar.trailing}
/// Widget to place at the end of the navigation bar. Normally additional actions
/// taken on the page such as a search or edit function.
+ /// {@endtemplate}
final Widget trailing;
// TODO(xster): implement support for double row navigation bars.
+ /// {@template flutter.cupertino.navBar.backgroundColor}
/// The background color of the navigation bar. If it contains transparency, the
/// tab bar will automatically produce a blurring effect to the content
/// behind it.
+ /// {@endtemplate}
final Color backgroundColor;
+ /// {@template flutter.cupertino.navBar.padding}
/// Padding for the contents of the navigation bar.
///
/// If null, the navigation bar will adopt the following defaults:
@@ -127,11 +182,14 @@
/// which case the padding will be 0.
///
/// Vertical padding won't change the height of the nav bar.
+ /// {@endtemplate}
final EdgeInsetsDirectional padding;
+ /// {@template flutter.cupertino.navBar.border}
/// The border of the navigation bar. By default renders a single pixel bottom border side.
///
/// If a border is null, the navigation bar will not display a border.
+ /// {@endtemplate}
final Border border;
/// Default color used for text and icons of the [leading] and [trailing]
@@ -152,13 +210,20 @@
@override
Widget build(BuildContext context) {
+ final Widget effectiveMiddle = _effectiveTitle(
+ title: middle,
+ automaticallyImplyTitle: automaticallyImplyMiddle,
+ currentRoute: ModalRoute.of(context),
+ );
+
return _wrapWithBackground(
border: border,
backgroundColor: backgroundColor,
child: new _CupertinoPersistentNavigationBar(
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
- middle: new Semantics(child: middle, header: true),
+ previousPageTitle: previousPageTitle,
+ middle: effectiveMiddle,
trailing: trailing,
padding: padding,
actionsForegroundColor: actionsForegroundColor,
@@ -192,6 +257,10 @@
/// close button in case of a fullscreen dialog) to pop the current route if none
/// is provided and [automaticallyImplyLeading] is true (true by default).
///
+/// The [largeTitle] widget will automatically be a title text from the current
+/// route if none is provided and [automaticallyImplyTitle] is true (true by
+/// default).
+///
/// See also:
///
/// * [CupertinoNavigationBar], an iOS navigation bar for use on non-scrolling
@@ -202,17 +271,19 @@
/// The [largeTitle] argument is required and must not be null.
const CupertinoSliverNavigationBar({
Key key,
- @required this.largeTitle,
+ this.largeTitle,
this.leading,
this.automaticallyImplyLeading = true,
+ this.automaticallyImplyTitle = true,
+ this.previousPageTitle,
this.middle,
this.trailing,
this.border = _kDefaultNavBarBorder,
this.backgroundColor = _kDefaultNavBarBackgroundColor,
this.padding,
this.actionsForegroundColor = CupertinoColors.activeBlue,
- }) : assert(largeTitle != null),
- assert(automaticallyImplyLeading != null),
+ }) : assert(automaticallyImplyLeading != null),
+ assert(automaticallyImplyTitle != null),
super(key: key);
/// The navigation bar's title.
@@ -229,21 +300,31 @@
/// any [GlobalKey]s, and that it not rely on maintaining state (for example,
/// animations will not survive the transition from one location to the other,
/// and may in fact be visible in two places at once during the transition).
+ ///
+ /// If null and [automaticallyImplyTitle] is true, an appropriate [Text]
+ /// title will be created if the current route is a [CupertinoPageRoute] and
+ /// has a `title`.
final Widget largeTitle;
- /// Widget to place at the start of the static navigation bar. Normally a back button
- /// for a normal page or a cancel button for full page dialogs.
+ /// {@macro flutter.cupertino.navBar.leading}
///
/// 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.
+ /// {@macro flutter.cupertino.navBar.automaticallyImplyLeading}
+ final bool automaticallyImplyLeading;
+
+ /// Controls whether we should try to imply the [largeTitle] 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.
+ /// If true and [largeTitle] is null, automatically fill in a [Text] widget
+ /// with the current route's `title` if the route is a [CupertinoPageRoute].
+ /// If [largeTitle] widget is not null, this parameter has no effect.
///
/// This value cannot be null.
- final bool automaticallyImplyLeading;
+ final bool automaticallyImplyTitle;
+
+ /// {@macro flutter.cupertino.navBar.previousPageTitle}
+ final String previousPageTitle;
/// A widget to place in the middle of the static navigation bar instead of
/// the [largeTitle].
@@ -253,39 +334,24 @@
/// [middle] widget is provided.
final Widget middle;
- /// Widget to place at the end of the static navigation bar. Normally
- /// additional actions taken on the page such as a search or edit function.
+ /// {@macro flutter.cupertino.navBar.trailing}
///
/// This widget is visible in both collapsed and expanded states.
final Widget trailing;
- /// Padding for the contents of the navigation bar.
- ///
- /// If null, the navigation bar will adopt the following defaults:
- ///
- /// * Vertically, contents will be sized to the same height as the navigation
- /// bar itself minus the status bar.
- /// * Horizontally, padding will be 16 pixels according to iOS specifications
- /// unless the leading widget is an automatically inserted back button, in
- /// which case the padding will be 0.
- ///
- /// Vertical padding won't change the height of the nav bar.
+ /// {@macro flutter.cupertino.navBar.backgroundColor}
+ final Color backgroundColor;
+
+ /// {@macro flutter.cupertino.navBar.padding}
final EdgeInsetsDirectional padding;
- /// The border of the navigation bar. By default renders a single pixel bottom border side.
- ///
- /// If a border is null, the navigation bar will not display a border.
+ /// {@macro flutter.cupertino.navBar.border}
final Border border;
- /// The background color of the navigation bar. If it contains transparency, the
- /// tab bar will automatically produce a blurring effect to the content
- /// behind it.
- final Color backgroundColor;
-
/// Default color used for text and icons of the [leading] and [trailing]
/// widgets in the navigation bar.
///
- /// The default color for text in the [middle] slot is always black, as per
+ /// The default color for text in the [largeTitle] slot is always black, as per
/// iOS standard design.
final Color actionsForegroundColor;
@@ -294,13 +360,20 @@
@override
Widget build(BuildContext context) {
+ final Widget effectiveTitle = _effectiveTitle(
+ title: largeTitle,
+ automaticallyImplyTitle: automaticallyImplyTitle,
+ currentRoute: ModalRoute.of(context),
+ );
+
return new SliverPersistentHeader(
pinned: true, // iOS navigation bars are always pinned.
delegate: new _CupertinoLargeTitleNavigationBarSliverDelegate(
persistentHeight: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
- title: largeTitle,
+ largeTitle: effectiveTitle,
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
+ previousPageTitle: previousPageTitle,
middle: middle,
trailing: trailing,
padding: padding,
@@ -312,6 +385,137 @@
}
}
+class _CupertinoLargeTitleNavigationBarSliverDelegate
+ extends SliverPersistentHeaderDelegate with DiagnosticableTreeMixin {
+ _CupertinoLargeTitleNavigationBarSliverDelegate({
+ @required this.persistentHeight,
+ @required this.largeTitle,
+ this.leading,
+ this.automaticallyImplyLeading,
+ this.previousPageTitle,
+ this.middle,
+ this.trailing,
+ this.padding,
+ this.border,
+ this.backgroundColor,
+ this.actionsForegroundColor,
+ }) : assert(persistentHeight != null);
+
+ final double persistentHeight;
+
+ final Widget largeTitle;
+
+ final Widget leading;
+
+ final bool automaticallyImplyLeading;
+
+ final String previousPageTitle;
+
+ final Widget middle;
+
+ final Widget trailing;
+
+ final EdgeInsetsDirectional padding;
+
+ final Color backgroundColor;
+
+ final Border border;
+
+ final Color actionsForegroundColor;
+
+ @override
+ double get minExtent => persistentHeight;
+
+ @override
+ double get maxExtent => persistentHeight + _kNavBarLargeTitleHeightExtension;
+
+ @override
+ Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
+ final bool showLargeTitle = shrinkOffset < maxExtent - minExtent - _kNavBarShowLargeTitleThreshold;
+
+ final _CupertinoPersistentNavigationBar persistentNavigationBar =
+ new _CupertinoPersistentNavigationBar(
+ leading: leading,
+ automaticallyImplyLeading: automaticallyImplyLeading,
+ previousPageTitle: previousPageTitle,
+ middle: middle ?? largeTitle,
+ trailing: trailing,
+ // If middle widget exists, always show it. Otherwise, show title
+ // when collapsed.
+ middleVisible: middle != null ? null : !showLargeTitle,
+ padding: padding,
+ actionsForegroundColor: actionsForegroundColor,
+ );
+
+ return _wrapWithBackground(
+ border: border,
+ backgroundColor: backgroundColor,
+ child: new Stack(
+ fit: StackFit.expand,
+ children: <Widget>[
+ new Positioned(
+ top: persistentHeight,
+ left: 0.0,
+ right: 0.0,
+ bottom: 0.0,
+ child: new ClipRect(
+ // The large title starts at the persistent bar.
+ // It's aligned with the bottom of the sliver and expands clipped
+ // and behind the persistent bar.
+ child: new OverflowBox(
+ minHeight: 0.0,
+ maxHeight: double.infinity,
+ alignment: AlignmentDirectional.bottomStart,
+ child: new Padding(
+ padding: const EdgeInsetsDirectional.only(
+ start: _kNavBarEdgePadding,
+ bottom: 8.0, // Bottom has a different padding.
+ ),
+ child: new DefaultTextStyle(
+ style: _kLargeTitleTextStyle,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ child: new AnimatedOpacity(
+ opacity: showLargeTitle ? 1.0 : 0.0,
+ duration: _kNavBarTitleFadeDuration,
+ child: new SafeArea(
+ top: false,
+ bottom: false,
+ child: new Semantics(
+ header: true,
+ child: largeTitle,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ new Positioned(
+ left: 0.0,
+ right: 0.0,
+ top: 0.0,
+ child: persistentNavigationBar,
+ ),
+ ],
+ ),
+ );
+ }
+
+ @override
+ bool shouldRebuild(_CupertinoLargeTitleNavigationBarSliverDelegate oldDelegate) {
+ return persistentHeight != oldDelegate.persistentHeight
+ || largeTitle != oldDelegate.largeTitle
+ || leading != oldDelegate.leading
+ || middle != oldDelegate.middle
+ || trailing != oldDelegate.trailing
+ || border != oldDelegate.border
+ || backgroundColor != oldDelegate.backgroundColor
+ || actionsForegroundColor != oldDelegate.actionsForegroundColor;
+ }
+}
+
/// Returns `child` wrapped with background and a bottom border if background color
/// is opaque. Otherwise, also blur with [BackdropFilter].
Widget _wrapWithBackground({
@@ -347,6 +551,22 @@
);
}
+Widget _effectiveTitle({
+ Widget title,
+ bool automaticallyImplyTitle,
+ ModalRoute<dynamic> currentRoute,
+}) {
+ // Auto use the CupertinoPageRoute's title if middle not provided.
+ if (title == null &&
+ automaticallyImplyTitle &&
+ currentRoute is CupertinoPageRoute &&
+ currentRoute.title != null) {
+ return new Text(currentRoute.title);
+ }
+
+ return title;
+}
+
/// The top part of the navigation bar that's never scrolled away.
///
/// Consists of the entire navigation bar without background and border when used
@@ -357,6 +577,7 @@
Key key,
this.leading,
this.automaticallyImplyLeading,
+ this.previousPageTitle,
this.middle,
this.trailing,
this.padding,
@@ -368,6 +589,8 @@
final bool automaticallyImplyLeading;
+ final String previousPageTitle;
+
final Widget middle;
final Widget trailing;
@@ -418,14 +641,16 @@
// Let the middle be black rather than `actionsForegroundColor` in case
// it's a plain text title.
- final Widget styledMiddle = middle == null ? null : new DefaultTextStyle(
- style: actionsStyle.copyWith(
- fontWeight: FontWeight.w600,
- letterSpacing: -0.08,
- color: CupertinoColors.black,
- ),
- child: middle,
- );
+ final Widget styledMiddle = middle == null
+ ? null
+ : new DefaultTextStyle(
+ style: actionsStyle.copyWith(
+ fontWeight: FontWeight.w600,
+ letterSpacing: -0.08,
+ color: CupertinoColors.black,
+ ),
+ child: new Semantics(child: middle, header: true),
+ );
final Widget animatedStyledMiddle = middleVisible == null
? styledMiddle
@@ -437,23 +662,26 @@
// 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: AlignmentDirectional.centerStart,
- child: const Icon(CupertinoIcons.back, size: 34.0,)
- )
- : const Text('Close'),
- padding: EdgeInsets.zero,
- onPressed: () { Navigator.maybePop(context); },
- );
+ if (currentRoute is PageRoute && currentRoute?.fullscreenDialog == true) {
+ backOrCloseButton = new CupertinoButton(
+ child: const Padding(
+ padding: EdgeInsetsDirectional.only(
+ start: _kNavBarEdgePadding,
+ ),
+ child: Text('Close'),
+ ),
+ padding: EdgeInsets.zero,
+ onPressed: () { Navigator.maybePop(context); },
+ );
+ } else {
+ backOrCloseButton = new CupertinoNavigationBarBackButton(
+ color: actionsForegroundColor,
+ previousPageTitle: previousPageTitle,
+ );
+ }
}
}
@@ -462,6 +690,7 @@
middle: animatedStyledMiddle,
trailing: styledTrailing,
centerMiddle: true,
+ middleSpacing: 6.0,
);
if (padding != null) {
@@ -476,143 +705,164 @@
return new SizedBox(
height: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
- child: IconTheme.merge(
- data: new IconThemeData(
- color: actionsForegroundColor,
- size: 22.0,
- ),
- child: new SafeArea(
- bottom: false,
- child: paddedToolbar,
- ),
+ child: new SafeArea(
+ bottom: false,
+ child: paddedToolbar,
),
);
}
}
-class _CupertinoLargeTitleNavigationBarSliverDelegate
- extends SliverPersistentHeaderDelegate with DiagnosticableTreeMixin {
- _CupertinoLargeTitleNavigationBarSliverDelegate({
- @required this.persistentHeight,
- @required this.title,
- this.leading,
- this.automaticallyImplyLeading,
- this.middle,
- this.trailing,
- this.padding,
- this.border,
- this.backgroundColor,
- this.actionsForegroundColor,
- }) : assert(persistentHeight != null);
+/// A nav bar back button typically used in [CupertinoNavigationBar].
+///
+/// This is automatically inserted into [CupertinoNavigationBar] and
+/// [CupertinoSliverNavigationBar]'s `leading` slot when
+/// `automaticallyImplyLeading` is true.
+///
+/// Shows a back chevron and the previous route's title when available from
+/// the previous [CupertinoPageRoute.title]. If [previousPageTitle] is specified,
+/// it will be shown instead.
+class CupertinoNavigationBarBackButton extends StatelessWidget {
+ /// Construct a [CupertinoNavigationBarBackButton] that can be used to pop
+ /// the current route.
+ ///
+ /// The [color] parameter must not be null.
+ const CupertinoNavigationBarBackButton({
+ @required this.color,
+ this.previousPageTitle,
+ }) : assert(color != null);
- final double persistentHeight;
+ /// The [Color] of the back chevron.
+ ///
+ /// Must not be null.
+ final Color color;
- final Widget title;
-
- final Widget leading;
-
- final bool automaticallyImplyLeading;
-
- final Widget middle;
-
- final Widget trailing;
-
- final EdgeInsetsDirectional padding;
-
- final Color backgroundColor;
-
- final Border border;
-
- final Color actionsForegroundColor;
+ /// An override for showing the previous route's title. If null, it will be
+ /// automatically derived from [CupertinoPageRoute.title] if the current and
+ /// previous routes are both [CupertinoPageRoute]s.
+ final String previousPageTitle;
@override
- double get minExtent => persistentHeight;
-
- @override
- double get maxExtent => persistentHeight + _kNavBarLargeTitleHeightExtension;
-
- @override
- Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
- final bool showLargeTitle = shrinkOffset < maxExtent - minExtent - _kNavBarShowLargeTitleThreshold;
-
- final _CupertinoPersistentNavigationBar persistentNavigationBar =
- new _CupertinoPersistentNavigationBar(
- leading: leading,
- automaticallyImplyLeading: automaticallyImplyLeading,
- middle: new Semantics(child: middle ?? title, header: true),
- trailing: trailing,
- // If middle widget exists, always show it. Otherwise, show title
- // when collapsed.
- middleVisible: middle != null ? null : !showLargeTitle,
- padding: padding,
- actionsForegroundColor: actionsForegroundColor,
+ Widget build(BuildContext context) {
+ final ModalRoute<dynamic> currentRoute = ModalRoute.of(context);
+ assert(
+ currentRoute.canPop,
+ 'CupertinoNavigationBarBackButton should only be used in routes that can be popped',
);
- return _wrapWithBackground(
- border: border,
- backgroundColor: backgroundColor,
- child: new Stack(
- fit: StackFit.expand,
- children: <Widget>[
- new Positioned(
- top: persistentHeight,
- left: 0.0,
- right: 0.0,
- bottom: 0.0,
- child: new ClipRect(
- // The large title starts at the persistent bar.
- // It's aligned with the bottom of the sliver and expands clipped
- // and behind the persistent bar.
- child: new OverflowBox(
- minHeight: 0.0,
- maxHeight: double.infinity,
- alignment: AlignmentDirectional.bottomStart,
- child: new Padding(
- padding: const EdgeInsetsDirectional.only(
- start: _kNavBarEdgePadding,
- bottom: 8.0, // Bottom has a different padding.
- ),
- child: new DefaultTextStyle(
- style: _kLargeTitleTextStyle,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- child: new AnimatedOpacity(
- opacity: showLargeTitle ? 1.0 : 0.0,
- duration: _kNavBarTitleFadeDuration,
- child: new SafeArea(
- top: false,
- bottom: false,
- child: new Semantics(
- header: true,
- child: title,
- ),
- ),
- ),
- ),
+ return new CupertinoButton(
+ child: new Semantics(
+ container: true,
+ excludeSemantics: true,
+ label: 'Back',
+ button: true,
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: _kNavBarBackButtonTapWidth),
+ child: new Row(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: <Widget>[
+ const Padding(padding: EdgeInsetsDirectional.only(start: 8.0)),
+ new _BackChevron(color: color),
+ const Padding(padding: EdgeInsetsDirectional.only(start: 6.0)),
+ new Flexible(
+ child: new _BackLabel(
+ specifiedPreviousTitle: previousPageTitle,
+ route: currentRoute,
),
),
- ),
+ ],
),
- new Positioned(
- left: 0.0,
- right: 0.0,
- top: 0.0,
- child: persistentNavigationBar,
- ),
- ],
+ ),
+ ),
+ padding: EdgeInsets.zero,
+ onPressed: () { Navigator.maybePop(context); },
+ );
+ }
+}
+
+class _BackChevron extends StatelessWidget {
+ const _BackChevron({
+ @required this.color,
+ }) : assert(color != null);
+
+ final Color color;
+
+ @override
+ Widget build(BuildContext context) {
+ final TextDirection textDirection = Directionality.of(context);
+
+ // Replicate the Icon logic here to get a tightly sized icon and add
+ // custom non-square padding.
+ Widget iconWidget = new Text.rich(
+ new TextSpan(
+ text: new String.fromCharCode(CupertinoIcons.back.codePoint),
+ style: new TextStyle(
+ inherit: false,
+ color: color,
+ fontSize: 34.0,
+ fontFamily: CupertinoIcons.back.fontFamily,
+ package: CupertinoIcons.back.fontPackage,
+ ),
),
);
+ switch (textDirection) {
+ case TextDirection.rtl:
+ iconWidget = new Transform(
+ transform: new Matrix4.identity()..scale(-1.0, 1.0, 1.0),
+ alignment: Alignment.center,
+ transformHitTests: false,
+ child: iconWidget,
+ );
+ break;
+ case TextDirection.ltr:
+ break;
+ }
+
+ return iconWidget;
+ }
+}
+
+/// A widget that shows next to the back chevron when `automaticallyImplyLeading`
+/// is true.
+class _BackLabel extends StatelessWidget {
+ const _BackLabel({
+ @required this.specifiedPreviousTitle,
+ @required this.route,
+ }) : assert(route != null);
+
+ final String specifiedPreviousTitle;
+ final ModalRoute<dynamic> route;
+
+ // `child` is never passed in into ValueListenableBuilder so it's always
+ // null here and unused.
+ Widget _buildPreviousTitleWidget(BuildContext context, String previousTitle, Widget child) {
+ if (previousTitle == null) {
+ return const SizedBox(height: 0.0, width: 0.0);
+ }
+
+ if (previousTitle.length > 10) {
+ return const Text('Back');
+ }
+
+ return new Text(previousTitle, maxLines: 1);
}
@override
- bool shouldRebuild(_CupertinoLargeTitleNavigationBarSliverDelegate oldDelegate) {
- return persistentHeight != oldDelegate.persistentHeight
- || title != oldDelegate.title
- || leading != oldDelegate.leading
- || middle != oldDelegate.middle
- || trailing != oldDelegate.trailing
- || border != oldDelegate.border
- || backgroundColor != oldDelegate.backgroundColor
- || actionsForegroundColor != oldDelegate.actionsForegroundColor;
+ Widget build(BuildContext context) {
+ if (specifiedPreviousTitle != null) {
+ return _buildPreviousTitleWidget(context, specifiedPreviousTitle, null);
+ } else if (route is CupertinoPageRoute<dynamic>) {
+ final CupertinoPageRoute<dynamic> cupertinoRoute = route;
+ // There is no timing issue because the previousTitle Listenable changes
+ // happen during route modifications before the ValueListenableBuilder
+ // is built.
+ return new ValueListenableBuilder<String>(
+ valueListenable: cupertinoRoute.previousTitle,
+ builder: _buildPreviousTitleWidget,
+ );
+ } else {
+ return const SizedBox(height: 0.0, width: 0.0);
+ }
}
}
diff --git a/packages/flutter/lib/src/cupertino/route.dart b/packages/flutter/lib/src/cupertino/route.dart
index 4db02e1..cbc2736 100644
--- a/packages/flutter/lib/src/cupertino/route.dart
+++ b/packages/flutter/lib/src/cupertino/route.dart
@@ -4,6 +4,7 @@
import 'dart:async';
+import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
@@ -87,6 +88,7 @@
/// be null.
CupertinoPageRoute({
@required this.builder,
+ this.title,
RouteSettings settings,
this.maintainState = true,
bool fullscreenDialog = false,
@@ -102,6 +104,50 @@
/// Builds the primary contents of the route.
final WidgetBuilder builder;
+ /// A title string for this route.
+ ///
+ /// Used to autopopulate [CupertinoNavigationBar] and
+ /// [CupertinoSliverNavigationBar]'s `middle`/`largeTitle` widgets when
+ /// one is not manually supplied.
+ final String title;
+
+ ValueNotifier<String> _previousTitle;
+
+ /// The title string of the previous [CupertinoPageRoute].
+ ///
+ /// The [ValueListenable]'s value is readable after the route is installed
+ /// onto a [Navigator]. The [ValueListenable] will also notify its listeners
+ /// if the value changes (such as by replacing the previous route).
+ ///
+ /// The [ValueListenable] itself will be null before the route is installed.
+ /// Its content value will be null if the previous route has no title or
+ /// is not a [CupertinoPageRoute].
+ ///
+ /// See also:
+ ///
+ /// * [ValueListenableBuilder], which can be used to listen and rebuild
+ /// widgets based on a ValueListenable.
+ ValueListenable<String> get previousTitle {
+ assert(
+ _previousTitle != null,
+ 'Cannot read the previousTitle for a route that has not yet been installed',
+ );
+ return _previousTitle;
+ }
+
+ @override
+ void didChangePrevious(Route<dynamic> previousRoute) {
+ final String previousTitleString = previousRoute is CupertinoPageRoute
+ ? previousRoute.title
+ : null;
+ if (_previousTitle == null) {
+ _previousTitle = new ValueNotifier<String>(previousTitleString);
+ } else {
+ _previousTitle.value = previousTitleString;
+ }
+ super.didChangePrevious(previousRoute);
+ }
+
@override
final bool maintainState;
@@ -511,7 +557,6 @@
}
}
-
/// A controller for an iOS-style back gesture.
///
/// This is created by a [CupertinoPageRoute] in response from a gesture caught
diff --git a/packages/flutter/lib/src/cupertino/tab_view.dart b/packages/flutter/lib/src/cupertino/tab_view.dart
index 781165a..16b42ed 100644
--- a/packages/flutter/lib/src/cupertino/tab_view.dart
+++ b/packages/flutter/lib/src/cupertino/tab_view.dart
@@ -42,6 +42,7 @@
const CupertinoTabView({
Key key,
this.builder,
+ this.defaultTitle,
this.routes,
this.onGenerateRoute,
this.onUnknownRoute,
@@ -56,6 +57,9 @@
/// as [builder] takes its place.
final WidgetBuilder builder;
+ /// The title of the default route.
+ final String defaultTitle;
+
/// This tab view's routing table.
///
/// When a named route is pushed with [Navigator.pushNamed] inside this tab view,
@@ -109,13 +113,17 @@
Route<dynamic> _onGenerateRoute(RouteSettings settings) {
final String name = settings.name;
WidgetBuilder routeBuilder;
- if (name == Navigator.defaultRouteName && builder != null)
+ String title;
+ if (name == Navigator.defaultRouteName && builder != null) {
routeBuilder = builder;
+ title = defaultTitle;
+ }
else if (routes != null)
routeBuilder = routes[name];
if (routeBuilder != null) {
return new CupertinoPageRoute<dynamic>(
builder: routeBuilder,
+ title: title,
settings: settings,
);
}
diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart
index fb0a280..65798a4 100644
--- a/packages/flutter/lib/src/widgets/navigator.dart
+++ b/packages/flutter/lib/src/widgets/navigator.dart
@@ -111,15 +111,15 @@
///
/// The returned value resolves when the push transition is complete.
///
- /// The [didChangeNext] method is typically called immediately after this
- /// method is called.
+ /// The [didChangeNext] and [didChangePrevious] methods are typically called
+ /// immediately after this method is called.
@protected
TickerFuture didPush() => new TickerFuture.complete();
/// Called after [install] when the route replaced another in the navigator.
///
- /// The [didChangeNext] method is typically called immediately after this
- /// method is called.
+ /// The [didChangeNext] and [didChangePrevious] methods are typically called
+ /// immediately after this method is called.
@protected
@mustCallSuper
void didReplace(Route<dynamic> oldRoute) { }
@@ -201,9 +201,8 @@
/// This route's previous route has changed to the given new route. This is
/// called on a route whenever the previous route changes for any reason, so
- /// long as it is in the history, except for immediately after the route has
- /// been pushed (in which case [didPush] or [didReplace] will be called
- /// instead). `previousRoute` will be null if there's no previous route.
+ /// long as it is in the history. `previousRoute` will be null if there's no
+ /// previous route.
@protected
@mustCallSuper
void didChangePrevious(Route<dynamic> previousRoute) { }
@@ -1539,8 +1538,10 @@
_history.add(route);
route.didPush();
route.didChangeNext(null);
- if (oldRoute != null)
+ if (oldRoute != null) {
oldRoute.didChangeNext(route);
+ route.didChangePrevious(oldRoute);
+ }
for (NavigatorObserver observer in widget.observers)
observer.didPush(route, oldRoute);
assert(() { _debugLocked = false; return true; }());
@@ -1589,8 +1590,10 @@
}
});
newRoute.didChangeNext(null);
- if (index > 0)
+ if (index > 0) {
_history[index - 1].didChangeNext(newRoute);
+ newRoute.didChangePrevious(_history[index - 1]);
+ }
for (NavigatorObserver observer in widget.observers)
observer.didReplace(newRoute: newRoute, oldRoute: oldRoute);
assert(() { _debugLocked = false; return true; }());
@@ -1684,8 +1687,10 @@
} else {
newRoute.didChangeNext(null);
}
- if (index > 0)
+ if (index > 0) {
_history[index - 1].didChangeNext(newRoute);
+ newRoute.didChangePrevious(_history[index - 1]);
+ }
for (NavigatorObserver observer in widget.observers)
observer.didReplace(newRoute: newRoute, oldRoute: oldRoute);
oldRoute.dispose();
diff --git a/packages/flutter/test/cupertino/nav_bar_test.dart b/packages/flutter/test/cupertino/nav_bar_test.dart
index 906d3d5..1508f5c 100644
--- a/packages/flutter/test/cupertino/nav_bar_test.dart
+++ b/packages/flutter/test/cupertino/nav_bar_test.dart
@@ -393,7 +393,7 @@
await tester.pump(const Duration(milliseconds: 200));
expect(find.byType(CupertinoButton), findsOneWidget);
- expect(find.byType(Icon), findsOneWidget);
+ expect(find.text(new String.fromCharCode(CupertinoIcons.back.codePoint)), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).push(new CupertinoPageRoute<void>(
fullscreenDialog: true,
@@ -418,7 +418,7 @@
expect(find.text('Page 2'), findsOneWidget);
- await tester.tap(find.byType(Icon));
+ await tester.tap(find.text(new String.fromCharCode(CupertinoIcons.back.codePoint)));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
@@ -426,6 +426,49 @@
expect(find.text('Home page'), findsOneWidget);
});
+ testWidgets('Long back label turns into "back"', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ new CupertinoApp(
+ home: const Placeholder(),
+ ),
+ );
+
+ tester.state<NavigatorState>(find.byType(Navigator)).push(
+ new CupertinoPageRoute<void>(
+ builder: (BuildContext context) {
+ return const CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(
+ previousPageTitle: '0123456789',
+ ),
+ child: Placeholder(),
+ );
+ }
+ )
+ );
+
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 500));
+
+ expect(find.widgetWithText(CupertinoButton, '0123456789'), findsOneWidget);
+
+ tester.state<NavigatorState>(find.byType(Navigator)).push(
+ new CupertinoPageRoute<void>(
+ builder: (BuildContext context) {
+ return const CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(
+ previousPageTitle: '01234567890',
+ ),
+ child: Placeholder(),
+ );
+ }
+ )
+ );
+
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 500));
+ expect(find.widgetWithText(CupertinoButton, 'Back'), findsOneWidget);
+ });
+
testWidgets('Border should be displayed by default', (WidgetTester tester) async {
await tester.pumpWidget(
new CupertinoApp(
diff --git a/packages/flutter/test/cupertino/route_test.dart b/packages/flutter/test/cupertino/route_test.dart
new file mode 100644
index 0000000..92197da
--- /dev/null
+++ b/packages/flutter/test/cupertino/route_test.dart
@@ -0,0 +1,190 @@
+// Copyright 2018 The Chromium 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_test/flutter_test.dart';
+
+void main() {
+ testWidgets('Middle auto-populates with title', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ new CupertinoApp(
+ home: const Placeholder(),
+ ),
+ );
+
+ tester.state<NavigatorState>(find.byType(Navigator)).push(
+ new CupertinoPageRoute<void>(
+ title: 'An iPod',
+ builder: (BuildContext context) {
+ return const CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(),
+ child: Placeholder(),
+ );
+ }
+ )
+ );
+
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 500));
+
+ // There should be a Text widget with the title in the nav bar even though
+ // we didn't specify anything in the nav bar constructor.
+ expect(find.widgetWithText(CupertinoNavigationBar, 'An iPod'), findsOneWidget);
+
+ // As a title, it should also be centered.
+ expect(tester.getCenter(find.text('An iPod')).dx, 400.0);
+ });
+
+ testWidgets('Leading auto-populates with back button with previous title', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ new CupertinoApp(
+ home: const Placeholder(),
+ ),
+ );
+
+ tester.state<NavigatorState>(find.byType(Navigator)).push(
+ new CupertinoPageRoute<void>(
+ title: 'An iPod',
+ builder: (BuildContext context) {
+ return const CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(),
+ child: Placeholder(),
+ );
+ }
+ )
+ );
+
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 500));
+
+ tester.state<NavigatorState>(find.byType(Navigator)).push(
+ new CupertinoPageRoute<void>(
+ title: 'A Phone',
+ builder: (BuildContext context) {
+ return const CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(),
+ child: Placeholder(),
+ );
+ }
+ )
+ );
+
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 500));
+
+ expect(find.widgetWithText(CupertinoNavigationBar, 'A Phone'), findsOneWidget);
+ expect(tester.getCenter(find.text('A Phone')).dx, 400.0);
+
+ // Also shows the previous page's title next to the back button.
+ expect(find.widgetWithText(CupertinoButton, 'An iPod'), findsOneWidget);
+ // 2 paddings + 1 ahem character at font size 34.0.
+ expect(tester.getTopLeft(find.text('An iPod')).dx, 8.0 + 34.0 + 6.0);
+ });
+
+ testWidgets('Previous title is correct on first transition frame', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ new CupertinoApp(
+ home: const Placeholder(),
+ ),
+ );
+
+ tester.state<NavigatorState>(find.byType(Navigator)).push(
+ new CupertinoPageRoute<void>(
+ title: 'An iPod',
+ builder: (BuildContext context) {
+ return const CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(),
+ child: Placeholder(),
+ );
+ }
+ )
+ );
+
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 500));
+
+ tester.state<NavigatorState>(find.byType(Navigator)).push(
+ new CupertinoPageRoute<void>(
+ title: 'A Phone',
+ builder: (BuildContext context) {
+ return const CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(),
+ child: Placeholder(),
+ );
+ }
+ )
+ );
+
+ // Trigger the route push
+ await tester.pump();
+ // Draw the first frame.
+ await tester.pump();
+
+ // Also shows the previous page's title next to the back button.
+ expect(find.widgetWithText(CupertinoButton, 'An iPod'), findsOneWidget);
+ });
+
+ testWidgets('Previous title stays up to date with changing routes', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ new CupertinoApp(
+ home: const Placeholder(),
+ ),
+ );
+
+ final CupertinoPageRoute<void> route2 = new CupertinoPageRoute<void>(
+ title: 'An iPod',
+ builder: (BuildContext context) {
+ return const CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(),
+ child: Placeholder(),
+ );
+ }
+ );
+
+ final CupertinoPageRoute<void> route3 = new CupertinoPageRoute<void>(
+ title: 'A Phone',
+ builder: (BuildContext context) {
+ return const CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(),
+ child: Placeholder(),
+ );
+ }
+ );
+
+ tester.state<NavigatorState>(find.byType(Navigator)).push(route2);
+
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 500));
+
+ tester.state<NavigatorState>(find.byType(Navigator)).push(route3);
+
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 500));
+
+ tester.state<NavigatorState>(find.byType(Navigator)).replace(
+ oldRoute: route2,
+ newRoute: new CupertinoPageRoute<void>(
+ title: 'An Internet communicator',
+ builder: (BuildContext context) {
+ return const CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(),
+ child: Placeholder(),
+ );
+ }
+ )
+ );
+
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 500));
+
+ expect(find.widgetWithText(CupertinoNavigationBar, 'A Phone'), findsOneWidget);
+ expect(tester.getCenter(find.text('A Phone')).dx, 400.0);
+
+ // After swapping the route behind the top one, the previous label changes
+ // from An iPod to Back (since An Internet communicator is too long to
+ // fit in the back button).
+ expect(find.widgetWithText(CupertinoButton, 'Back'), findsOneWidget);
+ expect(tester.getTopLeft(find.text('Back')).dx, 8.0 + 34.0 + 6.0);
+ });
+}
diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart
index 1b68b1b..ba31d9a 100644
--- a/packages/flutter_test/lib/src/widget_tester.dart
+++ b/packages/flutter_test/lib/src/widget_tester.dart
@@ -608,7 +608,7 @@
return TestAsyncUtils.guard(() async {
Finder backButton = find.byTooltip('Back');
if (backButton.evaluate().isEmpty) {
- backButton = find.widgetWithIcon(CupertinoButton, CupertinoIcons.back);
+ backButton = find.byType(CupertinoNavigationBarBackButton);
}
expectSync(backButton, findsOneWidget, reason: 'One back button expected on screen');