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);
const Color _kDefaultNavBarBorderColor = const Color(0x4C000000);
const TextStyle _kLargeTitleTextStyle = const TextStyle(
fontFamily: '.SF UI Text',
fontSize: 34.0,
fontWeight: FontWeight.bold,
letterSpacing: 0.41,
fontWeight: FontWeight.w700,
letterSpacing: -1.4,
color: CupertinoColors.black,
);
......@@ -49,13 +50,13 @@ const TextStyle _kLargeTitleTextStyle = const TextStyle(
/// 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.
///
/// Enabling [largeTitle] will create a scrollable second row showing the title
/// 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.
/// See also:
///
/// * [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): add sample code using icons.
// TODO(xster): document integration into a CupertinoScaffold.
class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWidget {
/// Creates a navigation bar in the iOS style.
const CupertinoNavigationBar({
......@@ -65,7 +66,6 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid
this.trailing,
this.backgroundColor: _kDefaultNavBarBackgroundColor,
this.actionsForegroundColor: CupertinoColors.activeBlue,
this.largeTitle: false,
}) : assert(middle != null, 'There must be a middle widget, usually a title.'),
super(key: key);
......@@ -98,50 +98,115 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid
/// True if the nav bar's background color has no transparency.
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
Size get preferredSize => const Size.fromHeight(_kNavBarPersistentHeight);
Size get preferredSize {
return opaque ? const Size.fromHeight(_kNavBarPersistentHeight) : Size.zero;
}
@override
Widget build(BuildContext context) {
assert(
!largeTitle || middle is Text,
"largeTitle mode is only possible when 'middle' is a Text widget"
return _wrapWithBackground(
backgroundColor: backgroundColor,
child: new _CupertinoPersistentNavigationBar(
leading: leading,
middle: middle,
trailing: trailing,
actionsForegroundColor: actionsForegroundColor,
),
);
}
}
if (!largeTitle) {
return _wrapWithBackground(
/// An iOS-styled navigation bar with iOS 11 style large titles using slivers.
///
/// 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,
child: new _CupertinoPersistentNavigationBar(
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,
),
);
}
actionsForegroundColor: actionsForegroundColor,
),
);
}
}
......@@ -206,6 +271,7 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
@override
Widget build(BuildContext context) {
final TextStyle actionsStyle = new TextStyle(
fontFamily: '.SF UI Text',
fontSize: 17.0,
letterSpacing: -0.24,
color: actionsForegroundColor,
......@@ -224,7 +290,11 @@ 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 : DefaultTextStyle.merge(
style: actionsStyle.copyWith(color: CupertinoColors.black),
style: actionsStyle.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.72,
color: CupertinoColors.black,
),
child: middle,
);
......@@ -268,8 +338,9 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHeaderDelegate {
const _CupertinoLargeTitleNavigationBarSliverDelegate({
@required this.persistentHeight,
@required this.title,
this.leading,
@required this.middle,
this.middle,
this.trailing,
this.backgroundColor,
this.actionsForegroundColor,
......@@ -277,9 +348,11 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe
final double persistentHeight;
final Text title;
final Widget leading;
final Text middle;
final Widget middle;
final Widget trailing;
......@@ -300,9 +373,11 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe
final _CupertinoPersistentNavigationBar persistentNavigationBar =
new _CupertinoPersistentNavigationBar(
leading: leading,
middle: middle,
middle: middle ?? title,
trailing: trailing,
middleVisible: !showLargeTitle,
// If middle widget exists, always show it. Otherwise, show title
// when collapsed.
middleVisible: middle != null ? null : !showLargeTitle,
actionsForegroundColor: actionsForegroundColor,
);
......@@ -336,7 +411,7 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe
child: new AnimatedOpacity(
opacity: showLargeTitle ? 1.0 : 0.0,
duration: _kNavBarTitleFadeDuration,
child: middle,
child: title,
)
),
),
......
......@@ -114,11 +114,9 @@ class _CupertinoScaffoldState extends State<CupertinoScaffold> {
/// on whether the middle widget should slide behind translucent bars.
Widget _padMiddle(Widget middle) {
double topPadding = 0.0;
if (widget.navigationBar is CupertinoNavigationBar) {
final CupertinoNavigationBar top = widget.navigationBar;
if (widget.navigationBar != null) {
topPadding += MediaQuery.of(context).padding.top;
if (top.opaque)
topPadding += top.preferredSize.height;
topPadding += widget.navigationBar.preferredSize.height;
}
double bottomPadding = 0.0;
......
......@@ -81,7 +81,7 @@ void main() {
builder: (BuildContext context) {
return const CupertinoNavigationBar(
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),
actionsForegroundColor: const Color(0xFF001122),
);
......@@ -129,9 +129,8 @@ void main() {
child: new CustomScrollView(
controller: scrollController,
slivers: <Widget>[
const CupertinoNavigationBar(
middle: const Text('Title'),
largeTitle: true,
const CupertinoSliverNavigationBar(
largeTitle: const Text('Title'),
),
new SliverToBoxAdapter(
child: new Container(
......@@ -204,12 +203,85 @@ void main() {
// The OverflowBox is squished with the text in it.
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 {
const _ExpectStyles({ this.color, this.index });
const _ExpectStyles({ this.color, this.letterSpacing, this.index });
final Color color;
final double letterSpacing;
final int index;
@override
......@@ -217,7 +289,7 @@ class _ExpectStyles extends StatelessWidget {
final TextStyle style = DefaultTextStyle.of(context).style;
expect(style.color, color);
expect(style.fontSize, 17.0);
expect(style.letterSpacing, -0.24);
expect(style.letterSpacing, letterSpacing ?? -0.24);
count += index;
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