Unverified Commit c418b2f3 authored by xster's avatar xster Committed by GitHub

Auto populate nav bar title and previous from page route (#19637)

parent ce8ba6e8
......@@ -47,51 +47,58 @@ class CupertinoNavigationDemo extends StatelessWidget {
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 @@ class CupertinoDemoTab1 extends StatelessWidget {
child: new CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(
largeTitle: Text('Colors'),
trailing: ExitButton(),
),
new SliverPadding(
......@@ -174,6 +180,7 @@ class Tab1RowItem extends StatelessWidget {
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 @@ class Tab1ItemPageState extends State<Tab1ItemPage> {
@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 @@ class CupertinoDemoTab2 extends StatelessWidget {
Widget build(BuildContext context) {
return new CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Support Chat'),
trailing: ExitButton(),
),
child: new ListView(
......@@ -699,7 +704,6 @@ class CupertinoDemoTab3 extends StatelessWidget {
Widget build(BuildContext context) {
return new CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Account'),
trailing: ExitButton(),
),
child: new DecoratedBox(
......
......@@ -50,6 +50,7 @@ class _CupertinoRefreshControlDemoState extends State<CupertinoRefreshControlDem
slivers: <Widget>[
const CupertinoSliverNavigationBar(
largeTitle: Text('Cupertino Refresh'),
previousPageTitle: 'Cupertino',
),
new CupertinoSliverRefreshControl(
onRefresh: () {
......
......@@ -12,6 +12,7 @@ import 'button.dart';
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 @@ const TextStyle _kLargeTitleTextStyle = TextStyle(
/// 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 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe
Key key,
this.leading,
this.automaticallyImplyLeading = true,
this.automaticallyImplyMiddle = true,
this.previousPageTitle,
this.middle,
this.trailing,
this.border = _kDefaultNavBarBorder,
......@@ -87,35 +94,83 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe
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 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe
/// 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 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe
@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 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe
/// 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 @@ class CupertinoSliverNavigationBar extends StatelessWidget {
/// 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 @@ class CupertinoSliverNavigationBar extends StatelessWidget {
/// 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 @@ class CupertinoSliverNavigationBar extends StatelessWidget {
/// [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 @@ class CupertinoSliverNavigationBar extends StatelessWidget {
@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 CupertinoSliverNavigationBar extends StatelessWidget {
}
}
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 _wrapWithBackground({
);
}
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 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
Key key,
this.leading,
this.automaticallyImplyLeading,
this.previousPageTitle,
this.middle,
this.trailing,
this.padding,
......@@ -368,6 +589,8 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
final bool automaticallyImplyLeading;
final String previousPageTitle;
final Widget middle;
final Widget trailing;
......@@ -418,14 +641,16 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
// 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 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
// 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 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
middle: animatedStyledMiddle,
trailing: styledTrailing,
centerMiddle: true,
middleSpacing: 6.0,
);
if (padding != null) {
......@@ -476,143 +705,164 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
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);
final double persistentHeight;
final Widget title;
final Widget leading;
final bool automaticallyImplyLeading;
/// 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 Widget middle;
/// The [Color] of the back chevron.
///
/// Must not be null.
final Color color;
final Widget trailing;
/// 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;
final EdgeInsetsDirectional padding;
@override
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',
);
final Color backgroundColor;
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,
),
),
],
),
),
),
padding: EdgeInsets.zero,
onPressed: () { Navigator.maybePop(context); },
);
}
}
final Border border;
class _BackChevron extends StatelessWidget {
const _BackChevron({
@required this.color,
}) : assert(color != null);
final Color actionsForegroundColor;
final Color color;
@override
double get minExtent => persistentHeight;
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;
}
@override
double get maxExtent => persistentHeight + _kNavBarLargeTitleHeightExtension;
return iconWidget;
}
}
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final bool showLargeTitle = shrinkOffset < maxExtent - minExtent - _kNavBarShowLargeTitleThreshold;
/// 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);
}
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,
);
if (previousTitle.length > 10) {
return const Text('Back');
}
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,
),
),
),
),
),
),
),
),
new Positioned(
left: 0.0,
right: 0.0,
top: 0.0,
child: persistentNavigationBar,
),
],
),
);
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);
}
}
}
......@@ -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 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
/// be null.
CupertinoPageRoute({
@required this.builder,
this.title,
RouteSettings settings,
this.maintainState = true,
bool fullscreenDialog = false,
......@@ -102,6 +104,50 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
/// 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 @@ class _CupertinoBackGestureDetectorState<T> extends State<_CupertinoBackGestureD
}
}
/// A controller for an iOS-style back gesture.
///
/// This is created by a [CupertinoPageRoute] in response from a gesture caught
......
......@@ -42,6 +42,7 @@ class CupertinoTabView extends StatelessWidget {
const CupertinoTabView({
Key key,
this.builder,
this.defaultTitle,
this.routes,
this.onGenerateRoute,
this.onUnknownRoute,
......@@ -56,6 +57,9 @@ class CupertinoTabView extends StatelessWidget {
/// 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 @@ class CupertinoTabView extends StatelessWidget {
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,
);
}
......
......@@ -111,15 +111,15 @@ abstract class Route<T> {
///
/// 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 @@ abstract class Route<T> {
/// 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 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
_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 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
}
});
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 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
} 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();
......
......@@ -393,7 +393,7 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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(
......
// 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);
});
}
......@@ -608,7 +608,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
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');
......
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