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 { ...@@ -47,51 +47,58 @@ class CupertinoNavigationDemo extends StatelessWidget {
return new WillPopScope( return new WillPopScope(
// Prevent swipe popping of this page. Use explicit exit buttons only. // Prevent swipe popping of this page. Use explicit exit buttons only.
onWillPop: () => new Future<bool>.value(true), onWillPop: () => new Future<bool>.value(true),
child: new CupertinoTabScaffold( child: new DefaultTextStyle(
tabBar: new CupertinoTabBar( style: const TextStyle(
items: const <BottomNavigationBarItem>[ fontFamily: '.SF UI Text',
BottomNavigationBarItem( fontSize: 17.0,
icon: Icon(CupertinoIcons.home), color: CupertinoColors.black,
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) { child: new CupertinoTabScaffold(
return new DefaultTextStyle( tabBar: new CupertinoTabBar(
style: const TextStyle( items: const <BottomNavigationBarItem>[
fontFamily: '.SF UI Text', BottomNavigationBarItem(
fontSize: 17.0, icon: Icon(CupertinoIcons.home),
color: CupertinoColors.black, title: Text('Home'),
), ),
child: new CupertinoTabView( BottomNavigationBarItem(
builder: (BuildContext context) { icon: Icon(CupertinoIcons.conversation_bubble),
switch (index) { title: Text('Support'),
case 0: ),
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( return new CupertinoDemoTab1(
colorItems: colorItems, colorItems: colorItems,
colorNameItems: colorNameItems colorNameItems: colorNameItems
); );
break; },
case 1: defaultTitle: 'Colors',
return new CupertinoDemoTab2(); );
break; break;
case 2: case 1:
return new CupertinoDemoTab3(); return new CupertinoTabView(
break; builder: (BuildContext context) => CupertinoDemoTab2(),
default: 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 { ...@@ -129,7 +136,6 @@ class CupertinoDemoTab1 extends StatelessWidget {
child: new CustomScrollView( child: new CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
const CupertinoSliverNavigationBar( const CupertinoSliverNavigationBar(
largeTitle: Text('Colors'),
trailing: ExitButton(), trailing: ExitButton(),
), ),
new SliverPadding( new SliverPadding(
...@@ -174,6 +180,7 @@ class Tab1RowItem extends StatelessWidget { ...@@ -174,6 +180,7 @@ class Tab1RowItem extends StatelessWidget {
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
Navigator.of(context).push(new CupertinoPageRoute<void>( Navigator.of(context).push(new CupertinoPageRoute<void>(
title: colorName,
builder: (BuildContext context) => new Tab1ItemPage( builder: (BuildContext context) => new Tab1ItemPage(
color: color, color: color,
colorName: colorName, colorName: colorName,
...@@ -285,9 +292,8 @@ class Tab1ItemPageState extends State<Tab1ItemPage> { ...@@ -285,9 +292,8 @@ class Tab1ItemPageState extends State<Tab1ItemPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new CupertinoPageScaffold( return new CupertinoPageScaffold(
navigationBar: new CupertinoNavigationBar( navigationBar: const CupertinoNavigationBar(
middle: new Text(widget.colorName), trailing: ExitButton(),
trailing: const ExitButton(),
), ),
child: new SafeArea( child: new SafeArea(
top: false, top: false,
...@@ -415,7 +421,6 @@ class CupertinoDemoTab2 extends StatelessWidget { ...@@ -415,7 +421,6 @@ class CupertinoDemoTab2 extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new CupertinoPageScaffold( return new CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar( navigationBar: const CupertinoNavigationBar(
middle: Text('Support Chat'),
trailing: ExitButton(), trailing: ExitButton(),
), ),
child: new ListView( child: new ListView(
...@@ -699,7 +704,6 @@ class CupertinoDemoTab3 extends StatelessWidget { ...@@ -699,7 +704,6 @@ class CupertinoDemoTab3 extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new CupertinoPageScaffold( return new CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar( navigationBar: const CupertinoNavigationBar(
middle: Text('Account'),
trailing: ExitButton(), trailing: ExitButton(),
), ),
child: new DecoratedBox( child: new DecoratedBox(
......
...@@ -50,6 +50,7 @@ class _CupertinoRefreshControlDemoState extends State<CupertinoRefreshControlDem ...@@ -50,6 +50,7 @@ class _CupertinoRefreshControlDemoState extends State<CupertinoRefreshControlDem
slivers: <Widget>[ slivers: <Widget>[
const CupertinoSliverNavigationBar( const CupertinoSliverNavigationBar(
largeTitle: Text('Cupertino Refresh'), largeTitle: Text('Cupertino Refresh'),
previousPageTitle: 'Cupertino',
), ),
new CupertinoSliverRefreshControl( new CupertinoSliverRefreshControl(
onRefresh: () { onRefresh: () {
......
...@@ -12,6 +12,7 @@ import 'button.dart'; ...@@ -12,6 +12,7 @@ import 'button.dart';
import 'colors.dart'; import 'colors.dart';
import 'icons.dart'; import 'icons.dart';
import 'page_scaffold.dart'; import 'page_scaffold.dart';
import 'route.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;
...@@ -62,6 +63,10 @@ const TextStyle _kLargeTitleTextStyle = TextStyle( ...@@ -62,6 +63,10 @@ const TextStyle _kLargeTitleTextStyle = TextStyle(
/// close button in case of a fullscreen dialog) to pop the current route if none /// close button in case of a fullscreen dialog) to pop the current route if none
/// is provided and [automaticallyImplyLeading] is true (true by default). /// 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 /// It should be placed at top of the screen and automatically accounts for
/// the OS's status bar. /// the OS's status bar.
/// ///
...@@ -80,6 +85,8 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe ...@@ -80,6 +85,8 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe
Key key, Key key,
this.leading, this.leading,
this.automaticallyImplyLeading = true, this.automaticallyImplyLeading = true,
this.automaticallyImplyMiddle = true,
this.previousPageTitle,
this.middle, this.middle,
this.trailing, this.trailing,
this.border = _kDefaultNavBarBorder, this.border = _kDefaultNavBarBorder,
...@@ -87,35 +94,83 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe ...@@ -87,35 +94,83 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe
this.padding, this.padding,
this.actionsForegroundColor = CupertinoColors.activeBlue, this.actionsForegroundColor = CupertinoColors.activeBlue,
}) : assert(automaticallyImplyLeading != null), }) : assert(automaticallyImplyLeading != null),
assert(automaticallyImplyMiddle != null),
super(key: key); super(key: key);
/// {@template flutter.cupertino.navBar.leading}
/// 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.
///
/// If null and [automaticallyImplyLeading] is true, an appropriate button
/// will be automatically created.
/// {@endtemplate}
final Widget leading; final Widget leading;
/// {@template flutter.cupertino.navBar.automaticallyImplyLeading}
/// Controls whether we should try to imply the leading widget if null. /// 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] /// 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. /// 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. /// This value cannot be null.
/// {@endtemplate}
final bool automaticallyImplyLeading; 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 /// Widget to place in the middle of the navigation bar. Normally a title or
/// a segmented control. /// 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; final Widget middle;
/// {@template flutter.cupertino.navBar.trailing}
/// Widget to place at the end of the navigation bar. Normally additional actions /// Widget to place at the end of the navigation bar. Normally additional actions
/// taken on the page such as a search or edit function. /// taken on the page such as a search or edit function.
/// {@endtemplate}
final Widget trailing; final Widget trailing;
// TODO(xster): implement support for double row navigation bars. // 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 /// The background color of the navigation bar. If it contains transparency, the
/// tab bar will automatically produce a blurring effect to the content /// tab bar will automatically produce a blurring effect to the content
/// behind it. /// behind it.
/// {@endtemplate}
final Color backgroundColor; final Color backgroundColor;
/// {@template flutter.cupertino.navBar.padding}
/// Padding for the contents of the navigation bar. /// Padding for the contents of the navigation bar.
/// ///
/// If null, the navigation bar will adopt the following defaults: /// If null, the navigation bar will adopt the following defaults:
...@@ -127,11 +182,14 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe ...@@ -127,11 +182,14 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe
/// which case the padding will be 0. /// which case the padding will be 0.
/// ///
/// Vertical padding won't change the height of the nav bar. /// Vertical padding won't change the height of the nav bar.
/// {@endtemplate}
final EdgeInsetsDirectional padding; final EdgeInsetsDirectional padding;
/// {@template flutter.cupertino.navBar.border}
/// The border of the navigation bar. By default renders a single pixel bottom border side. /// 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. /// If a border is null, the navigation bar will not display a border.
/// {@endtemplate}
final Border border; final Border border;
/// Default color used for text and icons of the [leading] and [trailing] /// Default color used for text and icons of the [leading] and [trailing]
...@@ -152,13 +210,20 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe ...@@ -152,13 +210,20 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Widget effectiveMiddle = _effectiveTitle(
title: middle,
automaticallyImplyTitle: automaticallyImplyMiddle,
currentRoute: ModalRoute.of(context),
);
return _wrapWithBackground( return _wrapWithBackground(
border: border, border: border,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
child: new _CupertinoPersistentNavigationBar( child: new _CupertinoPersistentNavigationBar(
leading: leading, leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading, automaticallyImplyLeading: automaticallyImplyLeading,
middle: new Semantics(child: middle, header: true), previousPageTitle: previousPageTitle,
middle: effectiveMiddle,
trailing: trailing, trailing: trailing,
padding: padding, padding: padding,
actionsForegroundColor: actionsForegroundColor, actionsForegroundColor: actionsForegroundColor,
...@@ -192,6 +257,10 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe ...@@ -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 /// close button in case of a fullscreen dialog) to pop the current route if none
/// is provided and [automaticallyImplyLeading] is true (true by default). /// 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: /// See also:
/// ///
/// * [CupertinoNavigationBar], an iOS navigation bar for use on non-scrolling /// * [CupertinoNavigationBar], an iOS navigation bar for use on non-scrolling
...@@ -202,17 +271,19 @@ class CupertinoSliverNavigationBar extends StatelessWidget { ...@@ -202,17 +271,19 @@ class CupertinoSliverNavigationBar extends StatelessWidget {
/// The [largeTitle] argument is required and must not be null. /// The [largeTitle] argument is required and must not be null.
const CupertinoSliverNavigationBar({ const CupertinoSliverNavigationBar({
Key key, Key key,
@required this.largeTitle, this.largeTitle,
this.leading, this.leading,
this.automaticallyImplyLeading = true, this.automaticallyImplyLeading = true,
this.automaticallyImplyTitle = true,
this.previousPageTitle,
this.middle, this.middle,
this.trailing, this.trailing,
this.border = _kDefaultNavBarBorder, this.border = _kDefaultNavBarBorder,
this.backgroundColor = _kDefaultNavBarBackgroundColor, this.backgroundColor = _kDefaultNavBarBackgroundColor,
this.padding, this.padding,
this.actionsForegroundColor = CupertinoColors.activeBlue, this.actionsForegroundColor = CupertinoColors.activeBlue,
}) : assert(largeTitle != null), }) : assert(automaticallyImplyLeading != null),
assert(automaticallyImplyLeading != null), assert(automaticallyImplyTitle != null),
super(key: key); super(key: key);
/// The navigation bar's title. /// The navigation bar's title.
...@@ -229,21 +300,31 @@ class CupertinoSliverNavigationBar extends StatelessWidget { ...@@ -229,21 +300,31 @@ class CupertinoSliverNavigationBar extends StatelessWidget {
/// any [GlobalKey]s, and that it not rely on maintaining state (for example, /// 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, /// 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). /// 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; final Widget largeTitle;
/// Widget to place at the start of the static navigation bar. Normally a back button /// {@macro flutter.cupertino.navBar.leading}
/// for a normal page or a cancel button for full page dialogs.
/// ///
/// 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. /// {@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] /// If true and [largeTitle] is null, automatically fill in a [Text] widget
/// widget should be. If [leading] widget is not null, this parameter has no effect. /// 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. /// 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 /// A widget to place in the middle of the static navigation bar instead of
/// the [largeTitle]. /// the [largeTitle].
...@@ -253,39 +334,24 @@ class CupertinoSliverNavigationBar extends StatelessWidget { ...@@ -253,39 +334,24 @@ class CupertinoSliverNavigationBar extends StatelessWidget {
/// [middle] widget is provided. /// [middle] widget is provided.
final Widget middle; final Widget middle;
/// Widget to place at the end of the static navigation bar. Normally /// {@macro flutter.cupertino.navBar.trailing}
/// additional actions taken on the page such as a search or edit function.
/// ///
/// This widget is visible in both collapsed and expanded states. /// This widget is visible in both collapsed and expanded states.
final Widget trailing; final Widget trailing;
/// Padding for the contents of the navigation bar. /// {@macro flutter.cupertino.navBar.backgroundColor}
/// final Color backgroundColor;
/// If null, the navigation bar will adopt the following defaults:
/// /// {@macro flutter.cupertino.navBar.padding}
/// * 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.
final EdgeInsetsDirectional padding; final EdgeInsetsDirectional padding;
/// The border of the navigation bar. By default renders a single pixel bottom border side. /// {@macro flutter.cupertino.navBar.border}
///
/// If a border is null, the navigation bar will not display a border.
final Border 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] /// Default color used for text and icons of the [leading] and [trailing]
/// widgets in the navigation bar. /// 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. /// iOS standard design.
final Color actionsForegroundColor; final Color actionsForegroundColor;
...@@ -294,13 +360,20 @@ class CupertinoSliverNavigationBar extends StatelessWidget { ...@@ -294,13 +360,20 @@ class CupertinoSliverNavigationBar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Widget effectiveTitle = _effectiveTitle(
title: largeTitle,
automaticallyImplyTitle: automaticallyImplyTitle,
currentRoute: ModalRoute.of(context),
);
return new SliverPersistentHeader( return new SliverPersistentHeader(
pinned: true, // iOS navigation bars are always pinned. pinned: true, // iOS navigation bars are always pinned.
delegate: new _CupertinoLargeTitleNavigationBarSliverDelegate( delegate: new _CupertinoLargeTitleNavigationBarSliverDelegate(
persistentHeight: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top, persistentHeight: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
title: largeTitle, largeTitle: effectiveTitle,
leading: leading, leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading, automaticallyImplyLeading: automaticallyImplyLeading,
previousPageTitle: previousPageTitle,
middle: middle, middle: middle,
trailing: trailing, trailing: trailing,
padding: padding, padding: padding,
...@@ -312,6 +385,137 @@ class CupertinoSliverNavigationBar extends StatelessWidget { ...@@ -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 /// Returns `child` wrapped with background and a bottom border if background color
/// is opaque. Otherwise, also blur with [BackdropFilter]. /// is opaque. Otherwise, also blur with [BackdropFilter].
Widget _wrapWithBackground({ Widget _wrapWithBackground({
...@@ -347,6 +551,22 @@ 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. /// The top part of the navigation bar that's never scrolled away.
/// ///
/// Consists of the entire navigation bar without background and border when used /// Consists of the entire navigation bar without background and border when used
...@@ -357,6 +577,7 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe ...@@ -357,6 +577,7 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
Key key, Key key,
this.leading, this.leading,
this.automaticallyImplyLeading, this.automaticallyImplyLeading,
this.previousPageTitle,
this.middle, this.middle,
this.trailing, this.trailing,
this.padding, this.padding,
...@@ -368,6 +589,8 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe ...@@ -368,6 +589,8 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
final bool automaticallyImplyLeading; final bool automaticallyImplyLeading;
final String previousPageTitle;
final Widget middle; final Widget middle;
final Widget trailing; final Widget trailing;
...@@ -418,14 +641,16 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe ...@@ -418,14 +641,16 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
// Let the middle be black rather than `actionsForegroundColor` in case // Let the middle be black rather than `actionsForegroundColor` in case
// it's a plain text title. // it's a plain text title.
final Widget styledMiddle = middle == null ? null : new DefaultTextStyle( final Widget styledMiddle = middle == null
style: actionsStyle.copyWith( ? null
fontWeight: FontWeight.w600, : new DefaultTextStyle(
letterSpacing: -0.08, style: actionsStyle.copyWith(
color: CupertinoColors.black, fontWeight: FontWeight.w600,
), letterSpacing: -0.08,
child: middle, color: CupertinoColors.black,
); ),
child: new Semantics(child: middle, header: true),
);
final Widget animatedStyledMiddle = middleVisible == null final Widget animatedStyledMiddle = middleVisible == null
? styledMiddle ? styledMiddle
...@@ -437,23 +662,26 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe ...@@ -437,23 +662,26 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
// Auto add back button if leading not provided. // Auto add back button if leading not provided.
Widget backOrCloseButton; Widget backOrCloseButton;
bool useBackButton = false;
if (styledLeading == null && automaticallyImplyLeading) { if (styledLeading == null && automaticallyImplyLeading) {
final ModalRoute<dynamic> currentRoute = ModalRoute.of(context); final ModalRoute<dynamic> currentRoute = ModalRoute.of(context);
if (currentRoute?.canPop == true) { if (currentRoute?.canPop == true) {
useBackButton = !(currentRoute is PageRoute && currentRoute?.fullscreenDialog == true); if (currentRoute is PageRoute && currentRoute?.fullscreenDialog == true) {
backOrCloseButton = new CupertinoButton( backOrCloseButton = new CupertinoButton(
child: useBackButton child: const Padding(
? new Container( padding: EdgeInsetsDirectional.only(
height: _kNavBarPersistentHeight, start: _kNavBarEdgePadding,
width: _kNavBarBackButtonTapWidth, ),
alignment: AlignmentDirectional.centerStart, child: Text('Close'),
child: const Icon(CupertinoIcons.back, size: 34.0,) ),
) padding: EdgeInsets.zero,
: const Text('Close'), onPressed: () { Navigator.maybePop(context); },
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 ...@@ -462,6 +690,7 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
middle: animatedStyledMiddle, middle: animatedStyledMiddle,
trailing: styledTrailing, trailing: styledTrailing,
centerMiddle: true, centerMiddle: true,
middleSpacing: 6.0,
); );
if (padding != null) { if (padding != null) {
...@@ -476,143 +705,164 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe ...@@ -476,143 +705,164 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
return new SizedBox( return new SizedBox(
height: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top, height: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
child: IconTheme.merge( child: new SafeArea(
data: new IconThemeData( bottom: false,
color: actionsForegroundColor, child: paddedToolbar,
size: 22.0,
),
child: new SafeArea(
bottom: false,
child: paddedToolbar,
),
), ),
); );
} }
} }
class _CupertinoLargeTitleNavigationBarSliverDelegate /// A nav bar back button typically used in [CupertinoNavigationBar].
extends SliverPersistentHeaderDelegate with DiagnosticableTreeMixin { ///
_CupertinoLargeTitleNavigationBarSliverDelegate({ /// This is automatically inserted into [CupertinoNavigationBar] and
@required this.persistentHeight, /// [CupertinoSliverNavigationBar]'s `leading` slot when
@required this.title, /// `automaticallyImplyLeading` is true.
this.leading, ///
this.automaticallyImplyLeading, /// Shows a back chevron and the previous route's title when available from
this.middle, /// the previous [CupertinoPageRoute.title]. If [previousPageTitle] is specified,
this.trailing, /// it will be shown instead.
this.padding, class CupertinoNavigationBarBackButton extends StatelessWidget {
this.border, /// Construct a [CupertinoNavigationBarBackButton] that can be used to pop
this.backgroundColor, /// the current route.
this.actionsForegroundColor, ///
}) : assert(persistentHeight != null); /// The [color] parameter must not be null.
const CupertinoNavigationBarBackButton({
final double persistentHeight; @required this.color,
this.previousPageTitle,
final Widget title; }) : assert(color != null);
final Widget leading;
final bool automaticallyImplyLeading;
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 @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 return iconWidget;
double get maxExtent => persistentHeight + _kNavBarLargeTitleHeightExtension; }
}
@override /// A widget that shows next to the back chevron when `automaticallyImplyLeading`
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { /// is true.
final bool showLargeTitle = shrinkOffset < maxExtent - minExtent - _kNavBarShowLargeTitleThreshold; 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 = if (previousTitle.length > 10) {
new _CupertinoPersistentNavigationBar( return const Text('Back');
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,
);
return _wrapWithBackground( return new Text(previousTitle, maxLines: 1);
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,
),
],
),
);
} }
@override @override
bool shouldRebuild(_CupertinoLargeTitleNavigationBarSliverDelegate oldDelegate) { Widget build(BuildContext context) {
return persistentHeight != oldDelegate.persistentHeight if (specifiedPreviousTitle != null) {
|| title != oldDelegate.title return _buildPreviousTitleWidget(context, specifiedPreviousTitle, null);
|| leading != oldDelegate.leading } else if (route is CupertinoPageRoute<dynamic>) {
|| middle != oldDelegate.middle final CupertinoPageRoute<dynamic> cupertinoRoute = route;
|| trailing != oldDelegate.trailing // There is no timing issue because the previousTitle Listenable changes
|| border != oldDelegate.border // happen during route modifications before the ValueListenableBuilder
|| backgroundColor != oldDelegate.backgroundColor // is built.
|| actionsForegroundColor != oldDelegate.actionsForegroundColor; return new ValueListenableBuilder<String>(
valueListenable: cupertinoRoute.previousTitle,
builder: _buildPreviousTitleWidget,
);
} else {
return const SizedBox(height: 0.0, width: 0.0);
}
} }
} }
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -87,6 +88,7 @@ class CupertinoPageRoute<T> extends PageRoute<T> { ...@@ -87,6 +88,7 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
/// be null. /// be null.
CupertinoPageRoute({ CupertinoPageRoute({
@required this.builder, @required this.builder,
this.title,
RouteSettings settings, RouteSettings settings,
this.maintainState = true, this.maintainState = true,
bool fullscreenDialog = false, bool fullscreenDialog = false,
...@@ -102,6 +104,50 @@ class CupertinoPageRoute<T> extends PageRoute<T> { ...@@ -102,6 +104,50 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
/// Builds the primary contents of the route. /// Builds the primary contents of the route.
final WidgetBuilder builder; 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 @override
final bool maintainState; final bool maintainState;
...@@ -511,7 +557,6 @@ class _CupertinoBackGestureDetectorState<T> extends State<_CupertinoBackGestureD ...@@ -511,7 +557,6 @@ class _CupertinoBackGestureDetectorState<T> extends State<_CupertinoBackGestureD
} }
} }
/// A controller for an iOS-style back gesture. /// A controller for an iOS-style back gesture.
/// ///
/// This is created by a [CupertinoPageRoute] in response from a gesture caught /// This is created by a [CupertinoPageRoute] in response from a gesture caught
......
...@@ -42,6 +42,7 @@ class CupertinoTabView extends StatelessWidget { ...@@ -42,6 +42,7 @@ class CupertinoTabView extends StatelessWidget {
const CupertinoTabView({ const CupertinoTabView({
Key key, Key key,
this.builder, this.builder,
this.defaultTitle,
this.routes, this.routes,
this.onGenerateRoute, this.onGenerateRoute,
this.onUnknownRoute, this.onUnknownRoute,
...@@ -56,6 +57,9 @@ class CupertinoTabView extends StatelessWidget { ...@@ -56,6 +57,9 @@ class CupertinoTabView extends StatelessWidget {
/// as [builder] takes its place. /// as [builder] takes its place.
final WidgetBuilder builder; final WidgetBuilder builder;
/// The title of the default route.
final String defaultTitle;
/// This tab view's routing table. /// This tab view's routing table.
/// ///
/// When a named route is pushed with [Navigator.pushNamed] inside this tab view, /// When a named route is pushed with [Navigator.pushNamed] inside this tab view,
...@@ -109,13 +113,17 @@ class CupertinoTabView extends StatelessWidget { ...@@ -109,13 +113,17 @@ class CupertinoTabView extends StatelessWidget {
Route<dynamic> _onGenerateRoute(RouteSettings settings) { Route<dynamic> _onGenerateRoute(RouteSettings settings) {
final String name = settings.name; final String name = settings.name;
WidgetBuilder routeBuilder; WidgetBuilder routeBuilder;
if (name == Navigator.defaultRouteName && builder != null) String title;
if (name == Navigator.defaultRouteName && builder != null) {
routeBuilder = builder; routeBuilder = builder;
title = defaultTitle;
}
else if (routes != null) else if (routes != null)
routeBuilder = routes[name]; routeBuilder = routes[name];
if (routeBuilder != null) { if (routeBuilder != null) {
return new CupertinoPageRoute<dynamic>( return new CupertinoPageRoute<dynamic>(
builder: routeBuilder, builder: routeBuilder,
title: title,
settings: settings, settings: settings,
); );
} }
......
...@@ -111,15 +111,15 @@ abstract class Route<T> { ...@@ -111,15 +111,15 @@ abstract class Route<T> {
/// ///
/// The returned value resolves when the push transition is complete. /// The returned value resolves when the push transition is complete.
/// ///
/// The [didChangeNext] method is typically called immediately after this /// The [didChangeNext] and [didChangePrevious] methods are typically called
/// method is called. /// immediately after this method is called.
@protected @protected
TickerFuture didPush() => new TickerFuture.complete(); TickerFuture didPush() => new TickerFuture.complete();
/// Called after [install] when the route replaced another in the navigator. /// Called after [install] when the route replaced another in the navigator.
/// ///
/// The [didChangeNext] method is typically called immediately after this /// The [didChangeNext] and [didChangePrevious] methods are typically called
/// method is called. /// immediately after this method is called.
@protected @protected
@mustCallSuper @mustCallSuper
void didReplace(Route<dynamic> oldRoute) { } void didReplace(Route<dynamic> oldRoute) { }
...@@ -201,9 +201,8 @@ abstract class Route<T> { ...@@ -201,9 +201,8 @@ abstract class Route<T> {
/// This route's previous route has changed to the given new route. This is /// 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 /// 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 /// long as it is in the history. `previousRoute` will be null if there's no
/// been pushed (in which case [didPush] or [didReplace] will be called /// previous route.
/// instead). `previousRoute` will be null if there's no previous route.
@protected @protected
@mustCallSuper @mustCallSuper
void didChangePrevious(Route<dynamic> previousRoute) { } void didChangePrevious(Route<dynamic> previousRoute) { }
...@@ -1539,8 +1538,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1539,8 +1538,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
_history.add(route); _history.add(route);
route.didPush(); route.didPush();
route.didChangeNext(null); route.didChangeNext(null);
if (oldRoute != null) if (oldRoute != null) {
oldRoute.didChangeNext(route); oldRoute.didChangeNext(route);
route.didChangePrevious(oldRoute);
}
for (NavigatorObserver observer in widget.observers) for (NavigatorObserver observer in widget.observers)
observer.didPush(route, oldRoute); observer.didPush(route, oldRoute);
assert(() { _debugLocked = false; return true; }()); assert(() { _debugLocked = false; return true; }());
...@@ -1589,8 +1590,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1589,8 +1590,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
} }
}); });
newRoute.didChangeNext(null); newRoute.didChangeNext(null);
if (index > 0) if (index > 0) {
_history[index - 1].didChangeNext(newRoute); _history[index - 1].didChangeNext(newRoute);
newRoute.didChangePrevious(_history[index - 1]);
}
for (NavigatorObserver observer in widget.observers) for (NavigatorObserver observer in widget.observers)
observer.didReplace(newRoute: newRoute, oldRoute: oldRoute); observer.didReplace(newRoute: newRoute, oldRoute: oldRoute);
assert(() { _debugLocked = false; return true; }()); assert(() { _debugLocked = false; return true; }());
...@@ -1684,8 +1687,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1684,8 +1687,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
} else { } else {
newRoute.didChangeNext(null); newRoute.didChangeNext(null);
} }
if (index > 0) if (index > 0) {
_history[index - 1].didChangeNext(newRoute); _history[index - 1].didChangeNext(newRoute);
newRoute.didChangePrevious(_history[index - 1]);
}
for (NavigatorObserver observer in widget.observers) for (NavigatorObserver observer in widget.observers)
observer.didReplace(newRoute: newRoute, oldRoute: oldRoute); observer.didReplace(newRoute: newRoute, oldRoute: oldRoute);
oldRoute.dispose(); oldRoute.dispose();
......
...@@ -393,7 +393,7 @@ void main() { ...@@ -393,7 +393,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 200)); await tester.pump(const Duration(milliseconds: 200));
expect(find.byType(CupertinoButton), findsOneWidget); 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>( tester.state<NavigatorState>(find.byType(Navigator)).push(new CupertinoPageRoute<void>(
fullscreenDialog: true, fullscreenDialog: true,
...@@ -418,7 +418,7 @@ void main() { ...@@ -418,7 +418,7 @@ void main() {
expect(find.text('Page 2'), findsOneWidget); 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();
await tester.pump(const Duration(milliseconds: 200)); await tester.pump(const Duration(milliseconds: 200));
...@@ -426,6 +426,49 @@ void main() { ...@@ -426,6 +426,49 @@ void main() {
expect(find.text('Home page'), findsOneWidget); 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 { testWidgets('Border should be displayed by default', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
new CupertinoApp( 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 ...@@ -608,7 +608,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
return TestAsyncUtils.guard(() async { return TestAsyncUtils.guard(() async {
Finder backButton = find.byTooltip('Back'); Finder backButton = find.byTooltip('Back');
if (backButton.evaluate().isEmpty) { if (backButton.evaluate().isEmpty) {
backButton = find.widgetWithIcon(CupertinoButton, CupertinoIcons.back); backButton = find.byType(CupertinoNavigationBarBackButton);
} }
expectSync(backButton, findsOneWidget, reason: 'One back button expected on screen'); 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