nav_bar.dart 19.4 KB
Newer Older
1 2 3 4 5 6 7
// Copyright 2017 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 'dart:ui' show ImageFilter;

import 'package:flutter/foundation.dart';
8
import 'package:flutter/services.dart';
9 10
import 'package:flutter/widgets.dart';

11
import 'button.dart';
12
import 'colors.dart';
13
import 'icons.dart';
14
import 'page_scaffold.dart';
15

16
/// Standard iOS navigation bar height without the status bar.
17 18
const double _kNavBarPersistentHeight = 44.0;

19
/// Size increase from expanding the navigation bar into an iOS-11-style large title
20
/// form in a [CustomScrollView].
21
const double _kNavBarLargeTitleHeightExtension = 52.0;
22 23

/// Number of logical pixels scrolled down before the title text is transferred
24
/// from the normal navigation bar to a big title below the navigation bar.
25 26 27 28
const double _kNavBarShowLargeTitleThreshold = 10.0;

const double _kNavBarEdgePadding = 16.0;

29 30 31 32 33
// The back chevron has a special padding in iOS.
const double _kNavBarBackButtonPadding = 0.0;

const double _kNavBarBackButtonTapWidth = 50.0;

34 35
/// Title text transfer fade.
const Duration _kNavBarTitleFadeDuration = const Duration(milliseconds: 150);
36 37 38 39

const Color _kDefaultNavBarBackgroundColor = const Color(0xCCF8F8F8);
const Color _kDefaultNavBarBorderColor = const Color(0x4C000000);

40 41 42 43 44 45 46 47
const Border _kDefaultNavBarBorder = const Border(
  bottom: const BorderSide(
    color: _kDefaultNavBarBorderColor,
    width: 0.0, // One physical pixel.
    style: BorderStyle.solid,
  ),
);

48
const TextStyle _kLargeTitleTextStyle = const TextStyle(
49
  fontFamily: '.SF Pro Display',
50
  fontSize: 34.0,
51
  fontWeight: FontWeight.w700,
52
  letterSpacing: 0.24,
53 54 55
  color: CupertinoColors.black,
);

56
/// An iOS-styled navigation bar.
57 58 59 60 61 62 63
///
/// The navigation bar is a toolbar that minimally consists of a widget, normally
/// a page title, in the [middle] of the toolbar.
///
/// It also supports a [leading] and [trailing] widget before and after the
/// [middle] widget while keeping the [middle] widget centered.
///
64 65 66 67
/// The [leading] widget will automatically be a back chevron icon button (or a
/// close button in case of a fullscreen dialog) to pop the current route if none
/// is provided and [automaticallyImplyLeading] is true (true by default).
///
68 69 70 71 72
/// It should be placed at top of the screen and automatically accounts for
/// the OS's status bar.
///
/// 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.
73
///
74 75
/// See also:
///
76 77 78 79
///  * [CupertinoPageScaffold], a page layout helper typically hosting the
///    [CupertinoNavigationBar].
///  * [CupertinoSliverNavigationBar] for a navigation bar to be placed in a
///    scrolling list and that supports iOS-11-style large titles.
80
class CupertinoNavigationBar extends StatelessWidget implements ObstructingPreferredSizeWidget {
81
  /// Creates a navigation bar in the iOS style.
82 83 84
  const CupertinoNavigationBar({
    Key key,
    this.leading,
85
    this.automaticallyImplyLeading = true,
86
    this.middle,
87
    this.trailing,
88 89 90
    this.border = _kDefaultNavBarBorder,
    this.backgroundColor = _kDefaultNavBarBackgroundColor,
    this.actionsForegroundColor = CupertinoColors.activeBlue,
91
  }) : assert(automaticallyImplyLeading != null),
92
       super(key: key);
93

94
  /// Widget to place at the start of the navigation bar. Normally a back button
95 96 97
  /// for a normal page or a cancel button for full page dialogs.
  final Widget leading;

98 99 100 101 102 103 104 105
  /// 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]
  /// widget should be. If [leading] widget is not null, this parameter has no effect.
  ///
  /// This value cannot be null.
  final bool automaticallyImplyLeading;

106
  /// Widget to place in the middle of the navigation bar. Normally a title or
107 108 109
  /// a segmented control.
  final Widget middle;

110
  /// Widget to place at the end of the navigation bar. Normally additional actions
111 112 113
  /// taken on the page such as a search or edit function.
  final Widget trailing;

114
  // TODO(xster): implement support for double row navigation bars.
115

116
  /// The background color of the navigation bar. If it contains transparency, the
117 118 119 120
  /// tab bar will automatically produce a blurring effect to the content
  /// behind it.
  final Color backgroundColor;

121 122 123 124 125
  /// 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.
  final Border border;

126
  /// Default color used for text and icons of the [leading] and [trailing]
127
  /// widgets in the navigation bar.
128
  ///
129 130
  /// The default color for text in the [middle] slot is always black, as per
  /// iOS standard design.
131 132
  final Color actionsForegroundColor;

133
  /// True if the navigation bar's background color has no transparency.
134 135
  @override
  bool get fullObstruction => backgroundColor.alpha == 0xFF;
136

137
  @override
138
  Size get preferredSize {
139
    return const Size.fromHeight(_kNavBarPersistentHeight);
140
  }
141 142 143

  @override
  Widget build(BuildContext context) {
144
    return _wrapWithBackground(
145
      border: border,
146 147 148
      backgroundColor: backgroundColor,
      child: new _CupertinoPersistentNavigationBar(
        leading: leading,
149
        automaticallyImplyLeading: automaticallyImplyLeading,
150
        middle: new Semantics(child: middle, header: true),
151 152 153
        trailing: trailing,
        actionsForegroundColor: actionsForegroundColor,
      ),
154
    );
155 156
  }
}
157

158
/// An iOS-styled navigation bar with iOS-11-style large titles using slivers.
159 160 161 162
///
/// The [CupertinoSliverNavigationBar] must be placed in a sliver group such
/// as the [CustomScrollView].
///
163 164
/// This navigation bar consists of two sections, a pinned static section on top
/// and a sliding section containing iOS-11-style large title below it.
165 166
///
/// It should be placed at top of the screen and automatically accounts for
167
/// the iOS status bar.
168
///
169 170
/// Minimally, a [largeTitle] widget will appear in the middle of the app bar
/// when the sliver is collapsed and transfer to the area below in larger font
171 172
/// when the sliver is expanded.
///
173 174
/// For advanced uses, an optional [middle] widget can be supplied to show a
/// different widget in the middle of the navigation bar when the sliver is collapsed.
175
///
176 177 178 179 180 181 182
/// Like [CupertinoNavigationBar], it also supports a [leading] and [trailing]
/// widget on the static section on top that remains while scrolling.
///
/// The [leading] widget will automatically be a back chevron icon button (or a
/// close button in case of a fullscreen dialog) to pop the current route if none
/// is provided and [automaticallyImplyLeading] is true (true by default).
///
183 184
/// See also:
///
185 186
///  * [CupertinoNavigationBar], an iOS navigation bar for use on non-scrolling
///    pages.
187
class CupertinoSliverNavigationBar extends StatelessWidget {
188 189 190
  /// Creates a navigation bar for scrolling lists.
  ///
  /// The [largeTitle] argument is required and must not be null.
191 192 193 194
  const CupertinoSliverNavigationBar({
    Key key,
    @required this.largeTitle,
    this.leading,
195
    this.automaticallyImplyLeading = true,
196 197
    this.middle,
    this.trailing,
198
    this.border = _kDefaultNavBarBorder,
199 200
    this.backgroundColor = _kDefaultNavBarBackgroundColor,
    this.actionsForegroundColor = CupertinoColors.activeBlue,
201
  }) : assert(largeTitle != null),
202
       assert(automaticallyImplyLeading != null),
203 204 205 206 207
       super(key: key);

  /// The navigation bar's title.
  ///
  /// This text will appear in the top static navigation bar when collapsed and
208 209 210 211 212 213 214 215 216 217 218 219 220 221
  /// below the navigation bar, in a larger font, when expanded.
  ///
  /// A suitable [DefaultTextStyle] is provided around this widget as it is
  /// moved around, to change its font size.
  ///
  /// If [middle] is null, then the [largeTitle] widget will be inserted into
  /// the tree in two places when transitioning from the collapsed state to the
  /// expanded state. It is therefore imperative that this subtree not contain
  /// 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,
  /// and may in fact be visible in two places at once during the transition).
  final Widget largeTitle;

  /// Widget to place at the start of the static navigation bar. Normally a back button
222 223 224 225 226
  /// 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;

227 228 229 230 231 232 233 234
  /// 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]
  /// widget should be. If [leading] widget is not null, this parameter has no effect.
  ///
  /// This value cannot be null.
  final bool automaticallyImplyLeading;

235 236
  /// A widget to place in the middle of the static navigation bar instead of
  /// the [largeTitle].
237 238
  ///
  /// This widget is visible in both collapsed and expanded states. The text
239 240
  /// supplied in [largeTitle] will no longer appear in collapsed state if a
  /// [middle] widget is provided.
241 242
  final Widget middle;

243 244
  /// Widget to place at the end of the static navigation bar. Normally
  /// additional actions taken on the page such as a search or edit function.
245 246 247 248
  ///
  /// This widget is visible in both collapsed and expanded states.
  final Widget trailing;

249 250 251 252 253
  /// 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.
  final Border border;

254
  /// The background color of the navigation bar. If it contains transparency, the
255 256 257 258 259
  /// 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]
260
  /// widgets in the navigation bar.
261 262 263 264 265
  ///
  /// The default color for text in the [middle] slot is always black, as per
  /// iOS standard design.
  final Color actionsForegroundColor;

266
  /// True if the navigation bar's background color has no transparency.
267 268 269 270 271 272 273 274 275 276
  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,
277
        automaticallyImplyLeading: automaticallyImplyLeading,
278 279
        middle: middle,
        trailing: trailing,
280
        border: border,
281
        backgroundColor: backgroundColor,
282 283 284
        actionsForegroundColor: actionsForegroundColor,
      ),
    );
285 286 287 288 289
  }
}

/// Returns `child` wrapped with background and a bottom border if background color
/// is opaque. Otherwise, also blur with [BackdropFilter].
290 291 292 293 294
Widget _wrapWithBackground({
  Border border,
  Color backgroundColor,
  Widget child,
}) {
295 296
  final DecoratedBox childWithBackground = new DecoratedBox(
    decoration: new BoxDecoration(
297
      border: border,
298 299 300 301 302
      color: backgroundColor,
    ),
    child: child,
  );

303
  final bool darkBackground = backgroundColor.computeLuminance() < 0.179;
304 305 306 307 308 309 310 311 312 313 314 315
  // TODO(jonahwilliams): remove once we have platform themes.
  switch (defaultTargetPlatform) {
    case TargetPlatform.iOS:
      SystemChrome.setSystemUIOverlayStyle(
        darkBackground ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark
      );
      break;
    case TargetPlatform.android:
    case TargetPlatform.fuchsia:
      SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark);
  }

316

317 318 319 320 321 322 323 324 325 326 327
  if (backgroundColor.alpha == 0xFF)
    return childWithBackground;

  return new ClipRect(
    child: new BackdropFilter(
      filter: new ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
      child: childWithBackground,
    ),
  );
}

328
/// The top part of the navigation bar that's never scrolled away.
329
///
330
/// Consists of the entire navigation bar without background and border when used
331 332 333 334 335 336
/// without large titles. With large titles, it's the top static half that
/// doesn't scroll.
class _CupertinoPersistentNavigationBar extends StatelessWidget implements PreferredSizeWidget {
  const _CupertinoPersistentNavigationBar({
    Key key,
    this.leading,
337 338
    this.automaticallyImplyLeading,
    this.middle,
339 340 341 342 343 344 345
    this.trailing,
    this.actionsForegroundColor,
    this.middleVisible,
  }) : super(key: key);

  final Widget leading;

346 347
  final bool automaticallyImplyLeading;

348 349 350 351 352 353 354 355 356 357
  final Widget middle;

  final Widget trailing;

  final Color actionsForegroundColor;

  /// Whether the middle widget has a visible animated opacity. A null value
  /// means the middle opacity will not be animated.
  final bool middleVisible;

358
  @override
359
  Size get preferredSize => const Size.fromHeight(_kNavBarPersistentHeight);
360 361 362

  @override
  Widget build(BuildContext context) {
363
    final TextStyle actionsStyle = new TextStyle(
364
      fontFamily: '.SF UI Text',
365 366 367 368 369
      fontSize: 17.0,
      letterSpacing: -0.24,
      color: actionsForegroundColor,
    );

370
    final Widget styledLeading = leading == null ? null : new DefaultTextStyle(
371 372 373 374
      style: actionsStyle,
      child: leading,
    );

375
    final Widget styledTrailing = trailing == null ? null : new DefaultTextStyle(
376 377 378 379 380 381
      style: actionsStyle,
      child: trailing,
    );

    // Let the middle be black rather than `actionsForegroundColor` in case
    // it's a plain text title.
382
    final Widget styledMiddle = middle == null ? null : new DefaultTextStyle(
383 384
      style: actionsStyle.copyWith(
        fontWeight: FontWeight.w600,
385
        letterSpacing: -0.08,
386 387
        color: CupertinoColors.black,
      ),
388 389
      child: middle,
    );
390

391 392 393 394 395 396 397 398
    final Widget animatedStyledMiddle = middleVisible == null
      ? styledMiddle
      : new AnimatedOpacity(
        opacity: middleVisible ? 1.0 : 0.0,
        duration: _kNavBarTitleFadeDuration,
        child: styledMiddle,
      );

399 400 401 402 403 404 405 406 407 408 409 410
    // Auto add back button if leading not provided.
    Widget backOrCloseButton;
    bool useBackButton = false;
    if (styledLeading == null && automaticallyImplyLeading) {
      final ModalRoute<dynamic> currentRoute = ModalRoute.of(context);
      if (currentRoute?.canPop == true) {
        useBackButton = !(currentRoute is PageRoute && currentRoute?.fullscreenDialog == true);
        backOrCloseButton = new CupertinoButton(
          child: useBackButton
              ? new Container(
                height: _kNavBarPersistentHeight,
                width: _kNavBarBackButtonTapWidth,
Ian Hickson's avatar
Ian Hickson committed
411
                alignment: AlignmentDirectional.centerStart,
412 413 414 415
                child: const Icon(CupertinoIcons.back, size: 34.0,)
              )
              : const Text('Close'),
          padding: EdgeInsets.zero,
416
          onPressed: () { Navigator.maybePop(context); },
417 418 419
        );
      }
    }
420

421 422 423 424 425 426
    return new SizedBox(
      height: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
      child: IconTheme.merge(
        data: new IconThemeData(
          color: actionsForegroundColor,
          size: 22.0,
427
        ),
428 429 430 431 432 433 434
        child: new SafeArea(
          bottom: false,
          child: new Padding(
            padding: new EdgeInsetsDirectional.only(
              start: useBackButton ? _kNavBarBackButtonPadding : _kNavBarEdgePadding,
              end: _kNavBarEdgePadding,
            ),
Ian Hickson's avatar
Ian Hickson committed
435
            child: new NavigationToolbar(
436
              leading: styledLeading ?? backOrCloseButton,
Ian Hickson's avatar
Ian Hickson committed
437 438 439 440
              middle: animatedStyledMiddle,
              trailing: styledTrailing,
              centerMiddle: true,
            ),
441 442 443 444
          ),
        ),
      ),
    );
445 446
  }
}
447

448 449 450
class _CupertinoLargeTitleNavigationBarSliverDelegate
    extends SliverPersistentHeaderDelegate with DiagnosticableTreeMixin {
  _CupertinoLargeTitleNavigationBarSliverDelegate({
451
    @required this.persistentHeight,
452
    @required this.title,
453
    this.leading,
454
    this.automaticallyImplyLeading,
455
    this.middle,
456
    this.trailing,
457 458
    this.border,
    this.backgroundColor,
459 460 461 462 463
    this.actionsForegroundColor,
  }) : assert(persistentHeight != null);

  final double persistentHeight;

464
  final Widget title;
465

466 467
  final Widget leading;

468 469
  final bool automaticallyImplyLeading;

470
  final Widget middle;
471 472 473 474 475

  final Widget trailing;

  final Color backgroundColor;

476 477
  final Border border;

478 479 480 481 482 483 484 485 486 487 488
  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;
489

490 491 492
    final _CupertinoPersistentNavigationBar persistentNavigationBar =
        new _CupertinoPersistentNavigationBar(
      leading: leading,
493
      automaticallyImplyLeading: automaticallyImplyLeading,
494
      middle: new Semantics(child: middle ?? title, header: true),
495
      trailing: trailing,
496 497 498
      // If middle widget exists, always show it. Otherwise, show title
      // when collapsed.
      middleVisible: middle != null ? null : !showLargeTitle,
499 500 501 502
      actionsForegroundColor: actionsForegroundColor,
    );

    return _wrapWithBackground(
503
      border: border,
504 505 506 507 508 509 510 511 512 513 514 515 516 517 518
      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,
519
                maxHeight: double.infinity,
520
                alignment: AlignmentDirectional.bottomStart,
521 522 523 524 525 526 527 528 529 530 531 532
                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,
533 534 535
                      child: new SafeArea(
                        top: false,
                        bottom: false,
536 537 538 539
                        child: new Semantics(
                          header: true,
                          child: title,
                        ),
540 541
                      ),
                    ),
542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559
                  ),
                ),
              ),
            ),
          ),
          new Positioned(
            left: 0.0,
            right: 0.0,
            top: 0.0,
            child: persistentNavigationBar,
          ),
        ],
      ),
    );
  }

  @override
  bool shouldRebuild(_CupertinoLargeTitleNavigationBarSliverDelegate oldDelegate) {
560 561 562 563 564
    return persistentHeight != oldDelegate.persistentHeight
        || title != oldDelegate.title
        || leading != oldDelegate.leading
        || middle != oldDelegate.middle
        || trailing != oldDelegate.trailing
565
        || border != oldDelegate.border
566 567
        || backgroundColor != oldDelegate.backgroundColor
        || actionsForegroundColor != oldDelegate.actionsForegroundColor;
568 569
  }
}