Commit 8475e0b2 authored by xster's avatar xster Committed by GitHub

Split Cupertino nav bar into a static one and a slivers one (#12102)

parent 987b2056
...@@ -29,9 +29,10 @@ const Color _kDefaultNavBarBackgroundColor = const Color(0xCCF8F8F8); ...@@ -29,9 +29,10 @@ const Color _kDefaultNavBarBackgroundColor = const Color(0xCCF8F8F8);
const Color _kDefaultNavBarBorderColor = const Color(0x4C000000); const Color _kDefaultNavBarBorderColor = const Color(0x4C000000);
const TextStyle _kLargeTitleTextStyle = const TextStyle( const TextStyle _kLargeTitleTextStyle = const TextStyle(
fontFamily: '.SF UI Text',
fontSize: 34.0, fontSize: 34.0,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w700,
letterSpacing: 0.41, letterSpacing: -1.4,
color: CupertinoColors.black, color: CupertinoColors.black,
); );
...@@ -49,13 +50,13 @@ const TextStyle _kLargeTitleTextStyle = const TextStyle( ...@@ -49,13 +50,13 @@ const TextStyle _kLargeTitleTextStyle = const TextStyle(
/// If the given [backgroundColor]'s opacity is not 1.0 (which is the case by /// If the given [backgroundColor]'s opacity is not 1.0 (which is the case by
/// default), it will produce a blurring effect to the content behind it. /// default), it will produce a blurring effect to the content behind it.
/// ///
/// Enabling [largeTitle] will create a scrollable second row showing the title /// See also:
/// in a larger font introduced in iOS 11. The [middle] widget must be a text ///
/// and the [CupertinoNavigationBar] must be placed in a sliver group in this case. /// * [CupertinoSliverNavigationBar] for a nav bar to be placed in a sliver and
/// that supports iOS 11 style large titles.
// //
// TODO(xster): document automatic addition of a CupertinoBackButton. // TODO(xster): document automatic addition of a CupertinoBackButton.
// TODO(xster): add sample code using icons. // TODO(xster): add sample code using icons.
// TODO(xster): document integration into a CupertinoScaffold.
class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWidget { class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWidget {
/// Creates a navigation bar in the iOS style. /// Creates a navigation bar in the iOS style.
const CupertinoNavigationBar({ const CupertinoNavigationBar({
...@@ -65,7 +66,6 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid ...@@ -65,7 +66,6 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid
this.trailing, this.trailing,
this.backgroundColor: _kDefaultNavBarBackgroundColor, this.backgroundColor: _kDefaultNavBarBackgroundColor,
this.actionsForegroundColor: CupertinoColors.activeBlue, this.actionsForegroundColor: CupertinoColors.activeBlue,
this.largeTitle: false,
}) : assert(middle != null, 'There must be a middle widget, usually a title.'), }) : assert(middle != null, 'There must be a middle widget, usually a title.'),
super(key: key); super(key: key);
...@@ -98,50 +98,115 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid ...@@ -98,50 +98,115 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid
/// True if the nav bar's background color has no transparency. /// True if the nav bar's background color has no transparency.
bool get opaque => backgroundColor.alpha == 0xFF; bool get opaque => backgroundColor.alpha == 0xFF;
/// Use iOS 11 style large title navigation bars.
///
/// When true, the navigation bar will split into 2 sections. The static
/// top 44px section will be wrapped in a SliverPersistentHeader and a
/// second scrollable section behind it will show and replace the `middle`
/// text in a larger font when scrolled down.
///
/// Navigation bars with large titles must be used in a sliver group such
/// as [CustomScrollView].
final bool largeTitle;
@override @override
Size get preferredSize => const Size.fromHeight(_kNavBarPersistentHeight); Size get preferredSize {
return opaque ? const Size.fromHeight(_kNavBarPersistentHeight) : Size.zero;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert( return _wrapWithBackground(
!largeTitle || middle is Text, backgroundColor: backgroundColor,
"largeTitle mode is only possible when 'middle' is a Text widget" child: new _CupertinoPersistentNavigationBar(
leading: leading,
middle: middle,
trailing: trailing,
actionsForegroundColor: actionsForegroundColor,
),
); );
}
}
if (!largeTitle) { /// An iOS-styled navigation bar with iOS 11 style large titles using slivers.
return _wrapWithBackground( ///
/// The [CupertinoSliverNavigationBar] must be placed in a sliver group such
/// as the [CustomScrollView].
///
/// This navigation bar consists of 2 sections, a pinned static section built
/// using [CupertinoNavigationBar] on top and a sliding section containing
/// iOS 11 style large titles below it.
///
/// It should be placed at top of the screen and automatically accounts for
/// the OS's status bar.
///
/// Minimally, a [largeTitle] [Text] will appear in the middle of the app bar
/// when the sliver is collapsed and transfer to the slidable below in larger font
/// when the sliver is expanded.
///
/// For advanced uses, an optional [middle] widget can be supplied to show a different
/// widget in the middle of the nav bar when the sliver is collapsed.
///
/// See also:
///
/// * [CupertinoNavigationBar] a static iOS nav bar that doesn't have a slidable
/// large title section.
class CupertinoSliverNavigationBar extends StatelessWidget {
const CupertinoSliverNavigationBar({
Key key,
@required this.largeTitle,
this.leading,
this.middle,
this.trailing,
this.backgroundColor: _kDefaultNavBarBackgroundColor,
this.actionsForegroundColor: CupertinoColors.activeBlue,
}) : assert(largeTitle != null, 'There must be a largeTitle Text'),
super(key: key);
/// The navigation bar's title.
///
/// This text will appear in the top static navigation bar when collapsed and
/// in the bottom slidable in a larger font when expanded.
final Text largeTitle;
/// Widget to place at the start of the static nav bar. Normally a back button
/// for a normal page or a cancel button for full page dialogs.
///
/// This widget is visible in both collapsed and expanded states.
final Widget leading;
/// An override widget to place in the middle of the static nav bar.
///
/// This widget is visible in both collapsed and expanded states. The text
/// supplied in [largeTitle] will no longer appear in collapsed state.
final Widget middle;
/// Widget to place at the end of the static nav bar. Normally additional actions
/// taken on the page such as a search or edit function.
///
/// This widget is visible in both collapsed and expanded states.
final Widget trailing;
// TODO(xster): implement support for double row nav bars.
/// The background color of the nav 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 nav bar.
///
/// The default color for text in the [middle] slot is always black, as per
/// iOS standard design.
final Color actionsForegroundColor;
/// True if the nav bar's background color has no transparency.
bool get opaque => backgroundColor.alpha == 0xFF;
@override
Widget build(BuildContext context) {
return new SliverPersistentHeader(
pinned: true, // iOS navigation bars are always pinned.
delegate: new _CupertinoLargeTitleNavigationBarSliverDelegate(
persistentHeight: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
title: largeTitle,
leading: leading,
middle: middle,
trailing: trailing,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
child: new _CupertinoPersistentNavigationBar( actionsForegroundColor: actionsForegroundColor,
leading: leading, ),
middle: middle, );
trailing: trailing,
actionsForegroundColor: actionsForegroundColor,
),
);
} else {
return new SliverPersistentHeader(
pinned: true, // iOS navigation bars are always pinned.
delegate: new _CupertinoLargeTitleNavigationBarSliverDelegate(
persistentHeight: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
leading: leading,
middle: middle,
trailing: trailing,
backgroundColor: backgroundColor,
actionsForegroundColor: actionsForegroundColor,
),
);
}
} }
} }
...@@ -206,6 +271,7 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe ...@@ -206,6 +271,7 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final TextStyle actionsStyle = new TextStyle( final TextStyle actionsStyle = new TextStyle(
fontFamily: '.SF UI Text',
fontSize: 17.0, fontSize: 17.0,
letterSpacing: -0.24, letterSpacing: -0.24,
color: actionsForegroundColor, color: actionsForegroundColor,
...@@ -224,7 +290,11 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe ...@@ -224,7 +290,11 @@ 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 : DefaultTextStyle.merge( final Widget styledMiddle = middle == null ? null : DefaultTextStyle.merge(
style: actionsStyle.copyWith(color: CupertinoColors.black), style: actionsStyle.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.72,
color: CupertinoColors.black,
),
child: middle, child: middle,
); );
...@@ -268,8 +338,9 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe ...@@ -268,8 +338,9 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHeaderDelegate { class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHeaderDelegate {
const _CupertinoLargeTitleNavigationBarSliverDelegate({ const _CupertinoLargeTitleNavigationBarSliverDelegate({
@required this.persistentHeight, @required this.persistentHeight,
@required this.title,
this.leading, this.leading,
@required this.middle, this.middle,
this.trailing, this.trailing,
this.backgroundColor, this.backgroundColor,
this.actionsForegroundColor, this.actionsForegroundColor,
...@@ -277,9 +348,11 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe ...@@ -277,9 +348,11 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe
final double persistentHeight; final double persistentHeight;
final Text title;
final Widget leading; final Widget leading;
final Text middle; final Widget middle;
final Widget trailing; final Widget trailing;
...@@ -300,9 +373,11 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe ...@@ -300,9 +373,11 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe
final _CupertinoPersistentNavigationBar persistentNavigationBar = final _CupertinoPersistentNavigationBar persistentNavigationBar =
new _CupertinoPersistentNavigationBar( new _CupertinoPersistentNavigationBar(
leading: leading, leading: leading,
middle: middle, middle: middle ?? title,
trailing: trailing, trailing: trailing,
middleVisible: !showLargeTitle, // If middle widget exists, always show it. Otherwise, show title
// when collapsed.
middleVisible: middle != null ? null : !showLargeTitle,
actionsForegroundColor: actionsForegroundColor, actionsForegroundColor: actionsForegroundColor,
); );
...@@ -336,7 +411,7 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe ...@@ -336,7 +411,7 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe
child: new AnimatedOpacity( child: new AnimatedOpacity(
opacity: showLargeTitle ? 1.0 : 0.0, opacity: showLargeTitle ? 1.0 : 0.0,
duration: _kNavBarTitleFadeDuration, duration: _kNavBarTitleFadeDuration,
child: middle, child: title,
) )
), ),
), ),
......
...@@ -114,11 +114,9 @@ class _CupertinoScaffoldState extends State<CupertinoScaffold> { ...@@ -114,11 +114,9 @@ class _CupertinoScaffoldState extends State<CupertinoScaffold> {
/// on whether the middle widget should slide behind translucent bars. /// on whether the middle widget should slide behind translucent bars.
Widget _padMiddle(Widget middle) { Widget _padMiddle(Widget middle) {
double topPadding = 0.0; double topPadding = 0.0;
if (widget.navigationBar is CupertinoNavigationBar) { if (widget.navigationBar != null) {
final CupertinoNavigationBar top = widget.navigationBar;
topPadding += MediaQuery.of(context).padding.top; topPadding += MediaQuery.of(context).padding.top;
if (top.opaque) topPadding += widget.navigationBar.preferredSize.height;
topPadding += top.preferredSize.height;
} }
double bottomPadding = 0.0; double bottomPadding = 0.0;
......
...@@ -81,7 +81,7 @@ void main() { ...@@ -81,7 +81,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return const CupertinoNavigationBar( return const CupertinoNavigationBar(
leading: const _ExpectStyles(color: const Color(0xFF001122), index: 0x000001), leading: const _ExpectStyles(color: const Color(0xFF001122), index: 0x000001),
middle: const _ExpectStyles(color: const Color(0xFF000000), index: 0x000100), middle: const _ExpectStyles(color: const Color(0xFF000000), letterSpacing: -0.72, index: 0x000100),
trailing: const _ExpectStyles(color: const Color(0xFF001122), index: 0x010000), trailing: const _ExpectStyles(color: const Color(0xFF001122), index: 0x010000),
actionsForegroundColor: const Color(0xFF001122), actionsForegroundColor: const Color(0xFF001122),
); );
...@@ -129,9 +129,8 @@ void main() { ...@@ -129,9 +129,8 @@ void main() {
child: new CustomScrollView( child: new CustomScrollView(
controller: scrollController, controller: scrollController,
slivers: <Widget>[ slivers: <Widget>[
const CupertinoNavigationBar( const CupertinoSliverNavigationBar(
middle: const Text('Title'), largeTitle: const Text('Title'),
largeTitle: true,
), ),
new SliverToBoxAdapter( new SliverToBoxAdapter(
child: new Container( child: new Container(
...@@ -204,12 +203,85 @@ void main() { ...@@ -204,12 +203,85 @@ void main() {
// The OverflowBox is squished with the text in it. // The OverflowBox is squished with the text in it.
expect(tester.getSize(find.widgetWithText(OverflowBox, 'Title')).height, 0.0); expect(tester.getSize(find.widgetWithText(OverflowBox, 'Title')).height, 0.0);
}); });
testWidgets('Small title can be overriden', (WidgetTester tester) async {
final ScrollController scrollController = new ScrollController();
await tester.pumpWidget(
new WidgetsApp(
color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>(
settings: settings,
builder: (BuildContext context) {
return new CupertinoScaffold(
child: new CustomScrollView(
controller: scrollController,
slivers: <Widget>[
const CupertinoSliverNavigationBar(
middle: const Text('Different title'),
largeTitle: const Text('Title'),
),
new SliverToBoxAdapter(
child: new Container(
height: 1200.0,
),
),
],
),
);
},
);
},
),
);
expect(scrollController.offset, 0.0);
expect(tester.getTopLeft(find.byType(NavigationToolbar)).dy, 0.0);
expect(tester.getSize(find.byType(NavigationToolbar)).height, 44.0);
expect(find.text('Title'), findsOneWidget);
expect(find.text('Different title'), findsOneWidget);
RenderOpacity largeTitleOpacity =
tester.element(find.text('Title')).ancestorRenderObjectOfType(const TypeMatcher<RenderOpacity>());
// Large title initially visible.
expect(
largeTitleOpacity.opacity,
1.0
);
// Middle widget not even wrapped with RenderOpacity, i.e. is always visible.
expect(
tester.element(find.text('Different title')).ancestorRenderObjectOfType(const TypeMatcher<RenderOpacity>()),
isNull,
);
expect(tester.getBottomLeft(find.text('Title')).dy, 44.0 + 56.0 - 8.0); // Static part + extension - padding.
scrollController.jumpTo(600.0);
await tester.pump(); // Once to trigger the opacity animation.
await tester.pump(const Duration(milliseconds: 300));
largeTitleOpacity =
tester.element(find.text('Title')).ancestorRenderObjectOfType(const TypeMatcher<RenderOpacity>());
// Large title no longer visible.
expect(
largeTitleOpacity.opacity,
0.0
);
// The persistent toolbar doesn't move or change size.
expect(tester.getTopLeft(find.byType(NavigationToolbar)).dy, 0.0);
expect(tester.getSize(find.byType(NavigationToolbar)).height, 44.0);
expect(tester.getBottomLeft(find.text('Title')).dy, 44.0 - 8.0); // Extension gone, (static part - padding) left.
});
} }
class _ExpectStyles extends StatelessWidget { class _ExpectStyles extends StatelessWidget {
const _ExpectStyles({ this.color, this.index }); const _ExpectStyles({ this.color, this.letterSpacing, this.index });
final Color color; final Color color;
final double letterSpacing;
final int index; final int index;
@override @override
...@@ -217,7 +289,7 @@ class _ExpectStyles extends StatelessWidget { ...@@ -217,7 +289,7 @@ class _ExpectStyles extends StatelessWidget {
final TextStyle style = DefaultTextStyle.of(context).style; final TextStyle style = DefaultTextStyle.of(context).style;
expect(style.color, color); expect(style.color, color);
expect(style.fontSize, 17.0); expect(style.fontSize, 17.0);
expect(style.letterSpacing, -0.24); expect(style.letterSpacing, letterSpacing ?? -0.24);
count += index; count += index;
return new Container(); return new Container();
} }
......
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