Skip to content

Commit fa0bcce

Browse files
authored
Add iOS 11 style large titles to cupertino nav bar (flutter#12002)
* Things lay out but the effects not right yet * Remaining functionalities and tests * one line large title only * Add more docs * review
1 parent e830c5e commit fa0bcce

File tree

4 files changed

+386
-66
lines changed

4 files changed

+386
-66
lines changed

packages/flutter/lib/src/cupertino/nav_bar.dart

Lines changed: 255 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,32 @@ import 'package:flutter/widgets.dart';
99

1010
import 'colors.dart';
1111

12-
// Standard iOS 10 nav bar height without the status bar.
13-
const double _kNavBarHeight = 44.0;
12+
/// Standard iOS nav bar height without the status bar.
13+
const double _kNavBarPersistentHeight = 44.0;
14+
15+
/// Size increase from expanding the nav bar into an iOS 11 style large title
16+
/// form in a [CustomScrollView].
17+
const double _kNavBarLargeTitleHeightExtension = 56.0;
18+
19+
/// Number of logical pixels scrolled down before the title text is transferred
20+
/// from the normal nav bar to a big title below the nav bar.
21+
const double _kNavBarShowLargeTitleThreshold = 10.0;
22+
23+
const double _kNavBarEdgePadding = 16.0;
24+
25+
/// Title text transfer fade.
26+
const Duration _kNavBarTitleFadeDuration = const Duration(milliseconds: 150);
1427

1528
const Color _kDefaultNavBarBackgroundColor = const Color(0xCCF8F8F8);
1629
const Color _kDefaultNavBarBorderColor = const Color(0x4C000000);
1730

31+
const TextStyle _kLargeTitleTextStyle = const TextStyle(
32+
fontSize: 34.0,
33+
fontWeight: FontWeight.bold,
34+
letterSpacing: 0.41,
35+
color: CupertinoColors.black,
36+
);
37+
1838
/// An iOS-styled navigation bar.
1939
///
2040
/// The navigation bar is a toolbar that minimally consists of a widget, normally
@@ -28,6 +48,10 @@ const Color _kDefaultNavBarBorderColor = const Color(0x4C000000);
2848
///
2949
/// If the given [backgroundColor]'s opacity is not 1.0 (which is the case by
3050
/// default), it will produce a blurring effect to the content behind it.
51+
///
52+
/// Enabling [largeTitle] will create a scrollable second row showing the title
53+
/// in a larger font introduced in iOS 11. The [middle] widget must be a text
54+
/// and the [CupertinoNavigationBar] must be placed in a sliver group in this case.
3155
//
3256
// TODO(xster): document automatic addition of a CupertinoBackButton.
3357
// TODO(xster): add sample code using icons.
@@ -41,8 +65,9 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid
4165
this.trailing,
4266
this.backgroundColor: _kDefaultNavBarBackgroundColor,
4367
this.actionsForegroundColor: CupertinoColors.activeBlue,
68+
this.largeTitle: false,
4469
}) : assert(middle != null, 'There must be a middle widget, usually a title.'),
45-
super(key: key);
70+
super(key: key);
4671

4772
/// Widget to place at the start of the nav bar. Normally a back button
4873
/// for a normal page or a cancel button for full page dialogs.
@@ -73,8 +98,110 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid
7398
/// True if the nav bar's background color has no transparency.
7499
bool get opaque => backgroundColor.alpha == 0xFF;
75100

101+
/// Use iOS 11 style large title navigation bars.
102+
///
103+
/// When true, the navigation bar will split into 2 sections. The static
104+
/// top 44px section will be wrapped in a SliverPersistentHeader and a
105+
/// second scrollable section behind it will show and replace the `middle`
106+
/// text in a larger font when scrolled down.
107+
///
108+
/// Navigation bars with large titles must be used in a sliver group such
109+
/// as [CustomScrollView].
110+
final bool largeTitle;
111+
112+
@override
113+
Size get preferredSize => const Size.fromHeight(_kNavBarPersistentHeight);
114+
115+
@override
116+
Widget build(BuildContext context) {
117+
assert(
118+
!largeTitle || middle is Text,
119+
"largeTitle mode is only possible when 'middle' is a Text widget",
120+
);
121+
122+
if (!largeTitle) {
123+
return _wrapWithBackground(
124+
backgroundColor: backgroundColor,
125+
child: new _CupertinoPersistentNavigationBar(
126+
leading: leading,
127+
middle: middle,
128+
trailing: trailing,
129+
actionsForegroundColor: actionsForegroundColor,
130+
),
131+
);
132+
} else {
133+
return new SliverPersistentHeader(
134+
pinned: true, // iOS navigation bars are always pinned.
135+
delegate: new _CupertinoLargeTitleNavigationBarSliverDelegate(
136+
persistentHeight: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
137+
leading: leading,
138+
middle: middle,
139+
trailing: trailing,
140+
backgroundColor: backgroundColor,
141+
actionsForegroundColor: actionsForegroundColor,
142+
),
143+
);
144+
}
145+
}
146+
}
147+
148+
/// Returns `child` wrapped with background and a bottom border if background color
149+
/// is opaque. Otherwise, also blur with [BackdropFilter].
150+
Widget _wrapWithBackground({Color backgroundColor, Widget child}) {
151+
final DecoratedBox childWithBackground = new DecoratedBox(
152+
decoration: new BoxDecoration(
153+
border: const Border(
154+
bottom: const BorderSide(
155+
color: _kDefaultNavBarBorderColor,
156+
width: 0.0, // One physical pixel.
157+
style: BorderStyle.solid,
158+
),
159+
),
160+
color: backgroundColor,
161+
),
162+
child: child,
163+
);
164+
165+
if (backgroundColor.alpha == 0xFF)
166+
return childWithBackground;
167+
168+
return new ClipRect(
169+
child: new BackdropFilter(
170+
filter: new ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
171+
child: childWithBackground,
172+
),
173+
);
174+
}
175+
176+
/// The top part of the nav bar that's never scrolled away.
177+
///
178+
/// Consists of the entire nav bar without background and border when used
179+
/// without large titles. With large titles, it's the top static half that
180+
/// doesn't scroll.
181+
class _CupertinoPersistentNavigationBar extends StatelessWidget implements PreferredSizeWidget {
182+
const _CupertinoPersistentNavigationBar({
183+
Key key,
184+
this.leading,
185+
@required this.middle,
186+
this.trailing,
187+
this.actionsForegroundColor,
188+
this.middleVisible,
189+
}) : super(key: key);
190+
191+
final Widget leading;
192+
193+
final Widget middle;
194+
195+
final Widget trailing;
196+
197+
final Color actionsForegroundColor;
198+
199+
/// Whether the middle widget has a visible animated opacity. A null value
200+
/// means the middle opacity will not be animated.
201+
final bool middleVisible;
202+
76203
@override
77-
Size get preferredSize => const Size.fromHeight(_kNavBarHeight);
204+
Size get preferredSize => const Size.fromHeight(_kNavBarPersistentHeight);
78205

79206
@override
80207
Widget build(BuildContext context) {
@@ -101,55 +228,139 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid
101228
child: middle,
102229
);
103230

231+
final Widget animatedStyledMiddle = middleVisible == null
232+
? styledMiddle
233+
: new AnimatedOpacity(
234+
opacity: middleVisible ? 1.0 : 0.0,
235+
duration: _kNavBarTitleFadeDuration,
236+
child: styledMiddle,
237+
);
238+
104239
// TODO(xster): automatically build a CupertinoBackButton.
105240

106-
Widget result = new DecoratedBox(
107-
decoration: new BoxDecoration(
108-
border: const Border(
109-
bottom: const BorderSide(
110-
color: _kDefaultNavBarBorderColor,
111-
width: 0.0, // One physical pixel.
112-
style: BorderStyle.solid,
113-
),
241+
return new SizedBox(
242+
height: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
243+
child: IconTheme.merge(
244+
data: new IconThemeData(
245+
color: actionsForegroundColor,
246+
size: 22.0,
114247
),
115-
color: backgroundColor,
116-
),
117-
child: new SizedBox(
118-
height: _kNavBarHeight + MediaQuery.of(context).padding.top,
119-
child: IconTheme.merge(
120-
data: new IconThemeData(
121-
color: actionsForegroundColor,
122-
size: 22.0,
248+
child: new Padding(
249+
padding: new EdgeInsets.only(
250+
top: MediaQuery.of(context).padding.top,
251+
// TODO(xster): dynamically reduce padding when an automatic
252+
// CupertinoBackButton is present.
253+
left: _kNavBarEdgePadding,
254+
right: _kNavBarEdgePadding,
123255
),
124-
child: new Padding(
125-
padding: new EdgeInsets.only(
126-
top: MediaQuery.of(context).padding.top,
127-
// TODO(xster): dynamically reduce padding when an automatic
128-
// CupertinoBackButton is present.
129-
left: 16.0,
130-
right: 16.0,
131-
),
132-
child: new NavigationToolbar(
133-
leading: styledLeading,
134-
middle: styledMiddle,
135-
trailing: styledTrailing,
136-
centerMiddle: true,
137-
),
256+
child: new NavigationToolbar(
257+
leading: styledLeading,
258+
middle: animatedStyledMiddle,
259+
trailing: styledTrailing,
260+
centerMiddle: true,
138261
),
139262
),
140263
),
141264
);
265+
}
266+
}
142267

143-
if (!opaque) {
144-
// For non-opaque backgrounds, apply a blur effect.
145-
result = new ClipRect(
146-
child: new BackdropFilter(
147-
filter: new ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
148-
child: result,
149-
),
150-
);
151-
}
268+
class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHeaderDelegate {
269+
const _CupertinoLargeTitleNavigationBarSliverDelegate({
270+
@required this.persistentHeight,
271+
this.leading,
272+
@required this.middle,
273+
this.trailing,
274+
this.backgroundColor,
275+
this.actionsForegroundColor,
276+
}) : assert(persistentHeight != null);
277+
278+
final double persistentHeight;
279+
280+
final Widget leading;
281+
282+
final Text middle;
283+
284+
final Widget trailing;
285+
286+
final Color backgroundColor;
287+
288+
final Color actionsForegroundColor;
289+
290+
@override
291+
double get minExtent => persistentHeight;
292+
293+
@override
294+
double get maxExtent => persistentHeight + _kNavBarLargeTitleHeightExtension;
295+
296+
@override
297+
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
298+
final bool showLargeTitle = shrinkOffset < maxExtent - minExtent - _kNavBarShowLargeTitleThreshold;
152299

153-
return result;
300+
final _CupertinoPersistentNavigationBar persistentNavigationBar =
301+
new _CupertinoPersistentNavigationBar(
302+
leading: leading,
303+
middle: middle,
304+
trailing: trailing,
305+
middleVisible: !showLargeTitle,
306+
actionsForegroundColor: actionsForegroundColor,
307+
);
308+
309+
return _wrapWithBackground(
310+
backgroundColor: backgroundColor,
311+
child: new Stack(
312+
fit: StackFit.expand,
313+
children: <Widget>[
314+
new Positioned(
315+
top: persistentHeight,
316+
left: 0.0,
317+
right: 0.0,
318+
bottom: 0.0,
319+
child: new ClipRect(
320+
// The large title starts at the persistent bar.
321+
// It's aligned with the bottom of the sliver and expands clipped
322+
// and behind the persistent bar.
323+
child: new OverflowBox(
324+
minHeight: 0.0,
325+
maxHeight: double.INFINITY,
326+
alignment: FractionalOffsetDirectional.bottomStart,
327+
child: new Padding(
328+
padding: const EdgeInsetsDirectional.only(
329+
start: _kNavBarEdgePadding,
330+
bottom: 8.0, // Bottom has a different padding.
331+
),
332+
child: new DefaultTextStyle(
333+
style: _kLargeTitleTextStyle,
334+
maxLines: 1,
335+
overflow: TextOverflow.ellipsis,
336+
child: new AnimatedOpacity(
337+
opacity: showLargeTitle ? 1.0 : 0.0,
338+
duration: _kNavBarTitleFadeDuration,
339+
child: middle,
340+
)
341+
),
342+
),
343+
),
344+
),
345+
),
346+
new Positioned(
347+
left: 0.0,
348+
right: 0.0,
349+
top: 0.0,
350+
child: persistentNavigationBar,
351+
),
352+
],
353+
),
354+
);
355+
}
356+
357+
@override
358+
bool shouldRebuild(_CupertinoLargeTitleNavigationBarSliverDelegate oldDelegate) {
359+
return persistentHeight != oldDelegate.persistentHeight ||
360+
leading != oldDelegate.leading ||
361+
middle != oldDelegate.middle ||
362+
trailing != oldDelegate.trailing ||
363+
backgroundColor != oldDelegate.backgroundColor ||
364+
actionsForegroundColor != oldDelegate.actionsForegroundColor;
154365
}
155366
}

packages/flutter/lib/src/cupertino/scaffold.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,10 @@ class _CupertinoScaffoldState extends State<CupertinoScaffold> {
113113
/// Pad the given middle widget with or without top and bottom offsets depending
114114
/// on whether the middle widget should slide behind translucent bars.
115115
Widget _padMiddle(Widget middle) {
116-
double topPadding = MediaQuery.of(context).padding.top;
116+
double topPadding = 0.0;
117117
if (widget.navigationBar is CupertinoNavigationBar) {
118118
final CupertinoNavigationBar top = widget.navigationBar;
119+
topPadding += MediaQuery.of(context).padding.top;
119120
if (top.opaque)
120121
topPadding += top.preferredSize.height;
121122
}

0 commit comments

Comments
 (0)