@@ -9,12 +9,32 @@ import 'package:flutter/widgets.dart';
9
9
10
10
import 'colors.dart' ;
11
11
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 );
14
27
15
28
const Color _kDefaultNavBarBackgroundColor = const Color (0xCCF8F8F8 );
16
29
const Color _kDefaultNavBarBorderColor = const Color (0x4C000000 );
17
30
31
+ const TextStyle _kLargeTitleTextStyle = const TextStyle (
32
+ fontSize: 34.0 ,
33
+ fontWeight: FontWeight .bold,
34
+ letterSpacing: 0.41 ,
35
+ color: CupertinoColors .black,
36
+ );
37
+
18
38
/// An iOS-styled navigation bar.
19
39
///
20
40
/// The navigation bar is a toolbar that minimally consists of a widget, normally
@@ -28,6 +48,10 @@ const Color _kDefaultNavBarBorderColor = const Color(0x4C000000);
28
48
///
29
49
/// If the given [backgroundColor] 's opacity is not 1.0 (which is the case by
30
50
/// 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.
31
55
//
32
56
// TODO(xster): document automatic addition of a CupertinoBackButton.
33
57
// TODO(xster): add sample code using icons.
@@ -41,8 +65,9 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid
41
65
this .trailing,
42
66
this .backgroundColor: _kDefaultNavBarBackgroundColor,
43
67
this .actionsForegroundColor: CupertinoColors .activeBlue,
68
+ this .largeTitle: false ,
44
69
}) : assert (middle != null , 'There must be a middle widget, usually a title.' ),
45
- super (key: key);
70
+ super (key: key);
46
71
47
72
/// Widget to place at the start of the nav bar. Normally a back button
48
73
/// for a normal page or a cancel button for full page dialogs.
@@ -73,8 +98,110 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid
73
98
/// True if the nav bar's background color has no transparency.
74
99
bool get opaque => backgroundColor.alpha == 0xFF ;
75
100
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
+
76
203
@override
77
- Size get preferredSize => const Size .fromHeight (_kNavBarHeight );
204
+ Size get preferredSize => const Size .fromHeight (_kNavBarPersistentHeight );
78
205
79
206
@override
80
207
Widget build (BuildContext context) {
@@ -101,55 +228,139 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid
101
228
child: middle,
102
229
);
103
230
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
+
104
239
// TODO(xster): automatically build a CupertinoBackButton.
105
240
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 ,
114
247
),
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,
123
255
),
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 ,
138
261
),
139
262
),
140
263
),
141
264
);
265
+ }
266
+ }
142
267
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;
152
299
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;
154
365
}
155
366
}
0 commit comments