nav_bar.dart 80.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:math' as math;
6 7 8
import 'dart:ui' show ImageFilter;

import 'package:flutter/foundation.dart';
9
import 'package:flutter/rendering.dart';
10
import 'package:flutter/services.dart';
11 12
import 'package:flutter/widgets.dart';

13
import 'button.dart';
14
import 'colors.dart';
15
import 'constants.dart';
16
import 'icons.dart';
17
import 'page_scaffold.dart';
18
import 'route.dart';
xster's avatar
xster committed
19
import 'theme.dart';
20

21
/// Standard iOS navigation bar height without the status bar.
22 23
///
/// This height is constant and independent of accessibility as it is in iOS.
24
const double _kNavBarPersistentHeight = kMinInteractiveDimensionCupertino;
25

26
/// Size increase from expanding the navigation bar into an iOS-11-style large title
27
/// form in a [CustomScrollView].
28
const double _kNavBarLargeTitleHeightExtension = 52.0;
29 30

/// Number of logical pixels scrolled down before the title text is transferred
31
/// from the normal navigation bar to a big title below the navigation bar.
32 33 34 35
const double _kNavBarShowLargeTitleThreshold = 10.0;

const double _kNavBarEdgePadding = 16.0;

36 37
const double _kNavBarBackButtonTapWidth = 50.0;

38
/// Title text transfer fade.
39
const Duration _kNavBarTitleFadeDuration = Duration(milliseconds: 150);
40

41
const Color _kDefaultNavBarBorderColor = Color(0x4D000000);
42

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

51 52
// There's a single tag for all instances of navigation bars because they can
// all transition between each other (per Navigator) via Hero transitions.
53
const _HeroTag _defaultHeroTag = _HeroTag(null);
54

55
@immutable
56
class _HeroTag {
57 58
  const _HeroTag(this.navigator);

59
  final NavigatorState? navigator;
60

61 62
  // Let the Hero tag be described in tree dumps.
  @override
63 64 65 66 67 68 69 70 71 72
  String toString() => 'Default Hero tag for Cupertino navigation bars with navigator $navigator';

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    if (other.runtimeType != runtimeType) {
      return false;
    }
73 74
    return other is _HeroTag
        && other.navigator == navigator;
75 76 77 78 79 80
  }

  @override
  int get hashCode {
    return identityHashCode(navigator);
  }
81 82 83 84 85 86 87 88
}

/// Returns `child` wrapped with background and a bottom border if background color
/// is opaque. Otherwise, also blur with [BackdropFilter].
///
/// When `updateSystemUiOverlay` is true, the nav bar will update the OS
/// status bar's color theme based on the background color of the nav bar.
Widget _wrapWithBackground({
89 90 91 92
  Border? border,
  required Color backgroundColor,
  Brightness? brightness,
  required Widget child,
93 94 95 96
  bool updateSystemUiOverlay = true,
}) {
  Widget result = child;
  if (updateSystemUiOverlay) {
97 98
    final bool isDark = backgroundColor.computeLuminance() < 0.179;
    final Brightness newBrightness = brightness ?? (isDark ? Brightness.dark : Brightness.light);
99
    final SystemUiOverlayStyle overlayStyle;
100 101 102 103 104 105 106 107
    switch (newBrightness) {
      case Brightness.dark:
        overlayStyle = SystemUiOverlayStyle.light;
        break;
      case Brightness.light:
        overlayStyle = SystemUiOverlayStyle.dark;
        break;
    }
108
    result = AnnotatedRegion<SystemUiOverlayStyle>(
109 110 111 112 113
      value: overlayStyle,
      sized: true,
      child: result,
    );
  }
114 115
  final DecoratedBox childWithBackground = DecoratedBox(
    decoration: BoxDecoration(
116 117 118 119 120 121 122 123 124
      border: border,
      color: backgroundColor,
    ),
    child: result,
  );

  if (backgroundColor.alpha == 0xFF)
    return childWithBackground;

125 126 127
  return ClipRect(
    child: BackdropFilter(
      filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
128 129 130 131 132 133 134
      child: childWithBackground,
    ),
  );
}

// Whether the current route supports nav bar hero transitions from or to.
bool _isTransitionable(BuildContext context) {
135
  final ModalRoute<dynamic>? route = ModalRoute.of(context);
136 137 138 139 140 141 142

  // Fullscreen dialogs never transitions their nav bar with other push-style
  // pages' nav bars or with other fullscreen dialog pages on the way in or on
  // the way out.
  return route is PageRoute && !route.fullscreenDialog;
}

143
/// An iOS-styled navigation bar.
144 145 146 147 148 149 150
///
/// 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.
///
151 152 153 154
/// 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).
///
155
/// The [middle] widget will automatically be a title text from the current
156 157
/// [CupertinoPageRoute] if none is provided and [automaticallyImplyMiddle] is
/// true (true by default).
158
///
159 160 161 162 163
/// 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.
164
///
165
/// When [transitionBetweenRoutes] is true, this navigation bar will transition
166
/// on top of the routes instead of inside them if the route being transitioned
167 168 169 170 171
/// to also has a [CupertinoNavigationBar] or a [CupertinoSliverNavigationBar]
/// with [transitionBetweenRoutes] set to true. If [transitionBetweenRoutes] is
/// true, none of the [Widget] parameters can contain a key in its subtree since
/// that widget will exist in multiple places in the tree simultaneously.
///
172 173 174 175 176
/// By default, only one [CupertinoNavigationBar] or [CupertinoSliverNavigationBar]
/// should be present in each [PageRoute] to support the default transitions.
/// Use [transitionBetweenRoutes] or [heroTag] to customize the transition
/// behavior for multiple navigation bars per route.
///
177 178 179 180 181 182 183 184
/// When used in a [CupertinoPageScaffold], [CupertinoPageScaffold.navigationBar]
/// has its text scale factor set to 1.0 and does not respond to text scale factor
/// changes from the operating system, to match the native iOS behavior. To override
/// this behavior, wrap each of the `navigationBar`'s components inside a [MediaQuery]
/// with the desired [MediaQueryData.textScaleFactor] value. The text scale factor
/// value from the operating system can be retrieved in many ways, such as querying
/// [MediaQuery.textScaleFactorOf] against [CupertinoApp]'s [BuildContext].
///
185 186 187 188 189 190 191
/// {@tool dartpad --template=stateful_widget_cupertino}
/// This example shows a [CupertinoNavigationBar] placed in a [CupertinoPageScaffold].
/// Since [backgroundColor]'s opacity is not 1.0, there is a blur effect and
/// content slides underneath.
///
///
/// ```dart
192
/// @override
193 194 195 196 197 198 199 200
/// Widget build(BuildContext context) {
///   return CupertinoPageScaffold(
///     navigationBar: CupertinoNavigationBar(
///       // Try removing opacity to observe the lack of a blur effect and of sliding content.
///       backgroundColor: CupertinoColors.systemGrey.withOpacity(0.5),
///       middle: const Text('Sample Code'),
///     ),
///     child: Column(
201
///       children: <Widget>[
202 203 204 205 206 207 208 209 210 211 212
///         Container(height: 50, color: CupertinoColors.systemRed),
///         Container(height: 50, color: CupertinoColors.systemGreen),
///         Container(height: 50, color: CupertinoColors.systemBlue),
///         Container(height: 50, color: CupertinoColors.systemYellow),
///       ],
///     ),
///   );
/// }
/// ```
/// {@end-tool}
///
213 214
/// See also:
///
215 216 217 218
///  * [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.
219
class CupertinoNavigationBar extends StatefulWidget implements ObstructingPreferredSizeWidget {
220
  /// Creates a navigation bar in the iOS style.
221
  const CupertinoNavigationBar({
222
    Key? key,
223
    this.leading,
224
    this.automaticallyImplyLeading = true,
225 226
    this.automaticallyImplyMiddle = true,
    this.previousPageTitle,
227
    this.middle,
228
    this.trailing,
229
    this.border = _kDefaultNavBarBorder,
xster's avatar
xster committed
230
    this.backgroundColor,
231
    this.brightness,
232
    this.padding,
233 234
    this.transitionBetweenRoutes = true,
    this.heroTag = _defaultHeroTag,
235
  }) : assert(automaticallyImplyLeading != null),
236
       assert(automaticallyImplyMiddle != null),
237 238 239 240
       assert(transitionBetweenRoutes != null),
       assert(
         heroTag != null,
         'heroTag cannot be null. Use transitionBetweenRoutes = false to '
241
         'disable Hero transition on this navigation bar.',
242 243 244 245
       ),
       assert(
         !transitionBetweenRoutes || identical(heroTag, _defaultHeroTag),
         'Cannot specify a heroTag override if this navigation bar does not '
246
         'transition due to transitionBetweenRoutes = false.',
247
       ),
248
       super(key: key);
249

250
  /// {@template flutter.cupertino.CupertinoNavigationBar.leading}
251
  /// Widget to place at the start of the navigation bar. Normally a back button
252
  /// for a normal page or a cancel button for full page dialogs.
253 254 255 256
  ///
  /// If null and [automaticallyImplyLeading] is true, an appropriate button
  /// will be automatically created.
  /// {@endtemplate}
257
  final Widget? leading;
258

259
  /// {@template flutter.cupertino.CupertinoNavigationBar.automaticallyImplyLeading}
260 261 262 263 264
  /// 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.
  ///
265 266 267 268 269 270 271 272 273
  /// Specifically this navigation bar will:
  ///
  /// 1. Show a 'Close' button if the current route is a `fullscreenDialog`.
  /// 2. Show a back chevron with [previousPageTitle] if [previousPageTitle] is
  ///    not null.
  /// 3. Show a back chevron with the previous route's `title` if the current
  ///    route is a [CupertinoPageRoute] and the previous route is also a
  ///    [CupertinoPageRoute].
  ///
274
  /// This value cannot be null.
275
  /// {@endtemplate}
276 277
  final bool automaticallyImplyLeading;

278 279 280 281 282 283 284 285 286
  /// Controls whether we should try to imply the middle widget if null.
  ///
  /// If true and [middle] is null, automatically fill in a [Text] widget with
  /// the current route's `title` if the route is a [CupertinoPageRoute].
  /// If [middle] widget is not null, this parameter has no effect.
  ///
  /// This value cannot be null.
  final bool automaticallyImplyMiddle;

287
  /// {@template flutter.cupertino.CupertinoNavigationBar.previousPageTitle}
288 289 290 291 292 293 294 295 296 297
  /// Manually specify the previous route's title when automatically implying
  /// the leading back button.
  ///
  /// Overrides the text shown with the back chevron instead of automatically
  /// showing the previous [CupertinoPageRoute]'s `title` when
  /// [automaticallyImplyLeading] is true.
  ///
  /// Has no effect when [leading] is not null or if [automaticallyImplyLeading]
  /// is false.
  /// {@endtemplate}
298
  final String? previousPageTitle;
299

300
  /// Widget to place in the middle of the navigation bar. Normally a title or
301
  /// a segmented control.
302 303 304 305
  ///
  /// If null and [automaticallyImplyMiddle] is true, an appropriate [Text]
  /// title will be created if the current route is a [CupertinoPageRoute] and
  /// has a `title`.
306
  final Widget? middle;
307

308
  /// {@template flutter.cupertino.CupertinoNavigationBar.trailing}
309
  /// Widget to place at the end of the navigation bar. Normally additional actions
310
  /// taken on the page such as a search or edit function.
311
  /// {@endtemplate}
312
  final Widget? trailing;
313

314 315
  // TODO(xster): https://github.com/flutter/flutter/issues/10469 implement
  // support for double row navigation bars.
316

317
  /// {@template flutter.cupertino.CupertinoNavigationBar.backgroundColor}
318
  /// The background color of the navigation bar. If it contains transparency, the
319 320
  /// tab bar will automatically produce a blurring effect to the content
  /// behind it.
xster's avatar
xster committed
321 322
  ///
  /// Defaults to [CupertinoTheme]'s `barBackgroundColor` if null.
323
  /// {@endtemplate}
324
  final Color? backgroundColor;
325

326
  /// {@template flutter.cupertino.CupertinoNavigationBar.brightness}
327 328 329 330 331 332 333 334 335
  /// The brightness of the specified [backgroundColor].
  ///
  /// Setting this value changes the style of the system status bar. Typically
  /// used to increase the contrast ratio of the system status bar over
  /// [backgroundColor].
  ///
  /// If set to null, the value of the property will be inferred from the relative
  /// luminance of [backgroundColor].
  /// {@endtemplate}
336
  final Brightness? brightness;
337

338
  /// {@template flutter.cupertino.CupertinoNavigationBar.padding}
339 340 341 342 343 344 345 346 347 348 349
  /// Padding for the contents of the navigation bar.
  ///
  /// If null, the navigation bar will adopt the following defaults:
  ///
  ///  * Vertically, contents will be sized to the same height as the navigation
  ///    bar itself minus the status bar.
  ///  * Horizontally, padding will be 16 pixels according to iOS specifications
  ///    unless the leading widget is an automatically inserted back button, in
  ///    which case the padding will be 0.
  ///
  /// Vertical padding won't change the height of the nav bar.
350
  /// {@endtemplate}
351
  final EdgeInsetsDirectional? padding;
352

353
  /// {@template flutter.cupertino.CupertinoNavigationBar.border}
354 355 356
  /// 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.
357
  /// {@endtemplate}
358
  final Border? border;
359

360
  /// {@template flutter.cupertino.CupertinoNavigationBar.transitionBetweenRoutes}
361 362 363 364 365 366 367
  /// Whether to transition between navigation bars.
  ///
  /// When [transitionBetweenRoutes] is true, this navigation bar will transition
  /// on top of the routes instead of inside it if the route being transitioned
  /// to also has a [CupertinoNavigationBar] or a [CupertinoSliverNavigationBar]
  /// with [transitionBetweenRoutes] set to true.
  ///
xster's avatar
xster committed
368 369 370 371
  /// This transition will also occur on edge back swipe gestures like on iOS
  /// but only if the previous page below has `maintainState` set to true on the
  /// [PageRoute].
  ///
372 373 374 375 376 377 378
  /// When set to true, only one navigation bar can be present per route unless
  /// [heroTag] is also set.
  ///
  /// This value defaults to true and cannot be null.
  /// {@endtemplate}
  final bool transitionBetweenRoutes;

379
  /// {@template flutter.cupertino.CupertinoNavigationBar.heroTag}
380 381 382
  /// Tag for the navigation bar's Hero widget if [transitionBetweenRoutes] is true.
  ///
  /// Defaults to a common tag between all [CupertinoNavigationBar] and
383 384 385 386 387 388 389
  /// [CupertinoSliverNavigationBar] instances of the same [Navigator]. With the
  /// default tag, all navigation bars of the same navigator can transition
  /// between each other as long as there's only one navigation bar per route.
  ///
  /// This [heroTag] can be overridden to manually handle having multiple
  /// navigation bars per route or to transition between multiple
  /// [Navigator]s.
390 391 392 393 394 395
  ///
  /// Cannot be null. To disable Hero transitions for this navigation bar,
  /// set [transitionBetweenRoutes] to false.
  /// {@endtemplate}
  final Object heroTag;

396
  /// True if the navigation bar's background color has no transparency.
397
  @override
398
  bool shouldFullyObstruct(BuildContext context) {
399
    final Color backgroundColor = CupertinoDynamicColor.maybeResolve(this.backgroundColor, context)
400 401 402
                               ?? CupertinoTheme.of(context).barBackgroundColor;
    return backgroundColor.alpha == 0xFF;
  }
403

404
  @override
405
  Size get preferredSize {
406
    return const Size.fromHeight(_kNavBarPersistentHeight);
407
  }
408

409
  @override
410
  State<CupertinoNavigationBar> createState() => _CupertinoNavigationBarState();
411 412 413 414 415 416
}

// A state class exists for the nav bar so that the keys of its sub-components
// don't change when rebuilding the nav bar, causing the sub-components to
// lose their own states.
class _CupertinoNavigationBarState extends State<CupertinoNavigationBar> {
417
  late _NavigationBarStaticComponentsKeys keys;
418 419 420 421

  @override
  void initState() {
    super.initState();
422
    keys = _NavigationBarStaticComponentsKeys();
423 424
  }

425 426
  @override
  Widget build(BuildContext context) {
xster's avatar
xster committed
427
    final Color backgroundColor =
428
      CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context) ?? CupertinoTheme.of(context).barBackgroundColor;
xster's avatar
xster committed
429

430
    final _NavigationBarStaticComponents components = _NavigationBarStaticComponents(
431 432 433 434 435 436 437 438 439 440 441
      keys: keys,
      route: ModalRoute.of(context),
      userLeading: widget.leading,
      automaticallyImplyLeading: widget.automaticallyImplyLeading,
      automaticallyImplyTitle: widget.automaticallyImplyMiddle,
      previousPageTitle: widget.previousPageTitle,
      userMiddle: widget.middle,
      userTrailing: widget.trailing,
      padding: widget.padding,
      userLargeTitle: null,
      large: false,
442 443
    );

444 445
    final Widget navBar = _wrapWithBackground(
      border: widget.border,
xster's avatar
xster committed
446
      backgroundColor: backgroundColor,
447
      brightness: widget.brightness,
xster's avatar
xster committed
448 449 450 451 452 453
      child: DefaultTextStyle(
        style: CupertinoTheme.of(context).textTheme.textStyle,
        child: _PersistentNavigationBar(
          components: components,
          padding: widget.padding,
        ),
454 455 456 457
      ),
    );

    if (!widget.transitionBetweenRoutes || !_isTransitionable(context)) {
xster's avatar
xster committed
458
      // Lint ignore to maintain backward compatibility.
459
      return navBar;
460 461
    }

462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485
    return Builder(
      // Get the context that might have a possibly changed CupertinoTheme.
      builder: (BuildContext context) {
        return Hero(
          tag: widget.heroTag == _defaultHeroTag
              ? _HeroTag(Navigator.of(context))
              : widget.heroTag,
          createRectTween: _linearTranslateWithLargestRectSizeTween,
          placeholderBuilder: _navBarHeroLaunchPadBuilder,
          flightShuttleBuilder: _navBarHeroFlightShuttleBuilder,
          transitionOnUserGestures: true,
          child: _TransitionableNavigationBar(
            componentsKeys: keys,
            backgroundColor: backgroundColor,
            backButtonTextStyle: CupertinoTheme.of(context).textTheme.navActionTextStyle,
            titleTextStyle: CupertinoTheme.of(context).textTheme.navTitleTextStyle,
            largeTitleTextStyle: null,
            border: widget.border,
            hasUserMiddle: widget.middle != null,
            largeExpanded: false,
            child: navBar,
          ),
        );
      },
486
    );
487 488
  }
}
489

490
/// An iOS-styled navigation bar with iOS-11-style large titles using slivers.
491 492 493 494
///
/// The [CupertinoSliverNavigationBar] must be placed in a sliver group such
/// as the [CustomScrollView].
///
495 496
/// 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.
497 498
///
/// It should be placed at top of the screen and automatically accounts for
499
/// the iOS status bar.
500
///
501 502
/// 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
503 504
/// when the sliver is expanded.
///
505 506
/// 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.
507
///
508 509 510 511 512 513 514
/// 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).
///
515
/// The [largeTitle] widget will automatically be a title text from the current
516 517
/// [CupertinoPageRoute] if none is provided and [automaticallyImplyTitle] is
/// true (true by default).
518
///
519
/// When [transitionBetweenRoutes] is true, this navigation bar will transition
520
/// on top of the routes instead of inside them if the route being transitioned
521 522 523 524 525 526
/// to also has a [CupertinoNavigationBar] or a [CupertinoSliverNavigationBar]
/// with [transitionBetweenRoutes] set to true. If [transitionBetweenRoutes] is
/// true, none of the [Widget] parameters can contain any [GlobalKey]s in their
/// subtrees since those widgets will exist in multiple places in the tree
/// simultaneously.
///
527 528 529 530 531
/// By default, only one [CupertinoNavigationBar] or [CupertinoSliverNavigationBar]
/// should be present in each [PageRoute] to support the default transitions.
/// Use [transitionBetweenRoutes] or [heroTag] to customize the transition
/// behavior for multiple navigation bars per route.
///
532 533 534 535 536 537 538 539
/// `CupertinoSliverNavigationBar` has its text scale factor set to 1.0 by default
/// and does not respond to text scale factor changes from the operating system,
/// to match the native iOS behavior. To override this behavior, wrap each of the
/// `CupertinoSliverNavigationBar`'s components inside a [MediaQuery] with the
/// desired [MediaQueryData.textScaleFactor] value. The text scale factor value
/// from the operating system can be retrieved in many ways, such as querying
/// [MediaQuery.textScaleFactorOf] against [CupertinoApp]'s [BuildContext].
///
540 541 542
/// The [stretch] parameter determines whether the nav bar should stretch to
/// fill the over-scroll area. The nav bar can still expand and contract as the
/// user scrolls, but it will also stretch when the user over-scrolls if the
543
/// [stretch] value is `true`. Defaults to `false`.
544
///
545 546
/// See also:
///
547 548
///  * [CupertinoNavigationBar], an iOS navigation bar for use on non-scrolling
///    pages.
549
class CupertinoSliverNavigationBar extends StatefulWidget {
550 551 552
  /// Creates a navigation bar for scrolling lists.
  ///
  /// The [largeTitle] argument is required and must not be null.
553
  const CupertinoSliverNavigationBar({
554
    Key? key,
555
    this.largeTitle,
556
    this.leading,
557
    this.automaticallyImplyLeading = true,
558 559
    this.automaticallyImplyTitle = true,
    this.previousPageTitle,
560 561
    this.middle,
    this.trailing,
562
    this.border = _kDefaultNavBarBorder,
xster's avatar
xster committed
563
    this.backgroundColor,
564
    this.brightness,
565
    this.padding,
566 567
    this.transitionBetweenRoutes = true,
    this.heroTag = _defaultHeroTag,
568
    this.stretch = false,
569 570
  }) : assert(automaticallyImplyLeading != null),
       assert(automaticallyImplyTitle != null),
571 572 573 574
       assert(
         automaticallyImplyTitle == true || largeTitle != null,
         'No largeTitle has been provided but automaticallyImplyTitle is also '
         'false. Either provide a largeTitle or set automaticallyImplyTitle to '
575
         'true.',
576
       ),
577 578 579 580 581
       super(key: key);

  /// The navigation bar's title.
  ///
  /// This text will appear in the top static navigation bar when collapsed and
582 583 584 585 586 587 588 589 590 591 592
  /// 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).
593 594 595 596
  ///
  /// If null and [automaticallyImplyTitle] is true, an appropriate [Text]
  /// title will be created if the current route is a [CupertinoPageRoute] and
  /// has a `title`.
597 598 599
  ///
  /// This parameter must either be non-null or the route must have a title
  /// ([CupertinoPageRoute.title]) and [automaticallyImplyTitle] must be true.
600
  final Widget? largeTitle;
601

602
  /// {@macro flutter.cupertino.CupertinoNavigationBar.leading}
603 604
  ///
  /// This widget is visible in both collapsed and expanded states.
605
  final Widget? leading;
606

607
  /// {@macro flutter.cupertino.CupertinoNavigationBar.automaticallyImplyLeading}
608 609 610
  final bool automaticallyImplyLeading;

  /// Controls whether we should try to imply the [largeTitle] widget if null.
611
  ///
612 613 614
  /// If true and [largeTitle] is null, automatically fill in a [Text] widget
  /// with the current route's `title` if the route is a [CupertinoPageRoute].
  /// If [largeTitle] widget is not null, this parameter has no effect.
615 616
  ///
  /// This value cannot be null.
617 618
  final bool automaticallyImplyTitle;

619
  /// {@macro flutter.cupertino.CupertinoNavigationBar.previousPageTitle}
620
  final String? previousPageTitle;
621

622 623
  /// A widget to place in the middle of the static navigation bar instead of
  /// the [largeTitle].
624 625
  ///
  /// This widget is visible in both collapsed and expanded states. The text
626 627
  /// supplied in [largeTitle] will no longer appear in collapsed state if a
  /// [middle] widget is provided.
628
  final Widget? middle;
629

630
  /// {@macro flutter.cupertino.CupertinoNavigationBar.trailing}
631 632
  ///
  /// This widget is visible in both collapsed and expanded states.
633
  final Widget? trailing;
634

635
  /// {@macro flutter.cupertino.CupertinoNavigationBar.backgroundColor}
636
  final Color? backgroundColor;
637

638
  /// {@macro flutter.cupertino.CupertinoNavigationBar.brightness}
639
  final Brightness? brightness;
640

641
  /// {@macro flutter.cupertino.CupertinoNavigationBar.padding}
642
  final EdgeInsetsDirectional? padding;
643

644
  /// {@macro flutter.cupertino.CupertinoNavigationBar.border}
645
  final Border? border;
646

647
  /// {@macro flutter.cupertino.CupertinoNavigationBar.transitionBetweenRoutes}
648 649
  final bool transitionBetweenRoutes;

650
  /// {@macro flutter.cupertino.CupertinoNavigationBar.heroTag}
651 652
  final Object heroTag;

653
  /// True if the navigation bar's background color has no transparency.
654
  bool get opaque => backgroundColor?.alpha == 0xFF;
655

656 657 658 659
  /// Whether the nav bar should stretch to fill the over-scroll area.
  ///
  /// The nav bar can still expand and contract as the user scrolls, but it will
  /// also stretch when the user over-scrolls if the [stretch] value is `true`.
660 661 662 663 664 665
  ///
  /// When set to `true`, the nav bar will prevent subsequent slivers from
  /// accessing overscrolls. This may be undesirable for using overscroll-based
  /// widgets like the [CupertinoSliverRefreshControl].
  ///
  /// Defaults to `false`.
666 667
  final bool stretch;

668
  @override
669
  State<CupertinoSliverNavigationBar> createState() => _CupertinoSliverNavigationBarState();
670 671 672 673 674 675
}

// A state class exists for the nav bar so that the keys of its sub-components
// don't change when rebuilding the nav bar, causing the sub-components to
// lose their own states.
class _CupertinoSliverNavigationBarState extends State<CupertinoSliverNavigationBar> {
676
  late _NavigationBarStaticComponentsKeys keys;
677 678 679 680

  @override
  void initState() {
    super.initState();
681
    keys = _NavigationBarStaticComponentsKeys();
682 683
  }

684 685
  @override
  Widget build(BuildContext context) {
686
    final _NavigationBarStaticComponents components = _NavigationBarStaticComponents(
687 688 689 690 691 692 693 694 695 696 697
      keys: keys,
      route: ModalRoute.of(context),
      userLeading: widget.leading,
      automaticallyImplyLeading: widget.automaticallyImplyLeading,
      automaticallyImplyTitle: widget.automaticallyImplyTitle,
      previousPageTitle: widget.previousPageTitle,
      userMiddle: widget.middle,
      userTrailing: widget.trailing,
      userLargeTitle: widget.largeTitle,
      padding: widget.padding,
      large: true,
698 699
    );

700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717
    return MediaQuery(
      data: MediaQuery.of(context).copyWith(textScaleFactor: 1),
      child: SliverPersistentHeader(
        pinned: true, // iOS navigation bars are always pinned.
        delegate: _LargeTitleNavigationBarSliverDelegate(
          keys: keys,
          components: components,
          userMiddle: widget.middle,
          backgroundColor: CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context) ?? CupertinoTheme.of(context).barBackgroundColor,
          brightness: widget.brightness,
          border: widget.border,
          padding: widget.padding,
          actionsForegroundColor: CupertinoTheme.of(context).primaryColor,
          transitionBetweenRoutes: widget.transitionBetweenRoutes,
          heroTag: widget.heroTag,
          persistentHeight: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
          alwaysShowMiddle: widget.middle != null,
          stretchConfiguration: widget.stretch ? OverScrollHeaderStretchConfiguration() : null,
xster's avatar
xster committed
718
        ),
719 720
      ),
    );
721 722 723
  }
}

724
class _LargeTitleNavigationBarSliverDelegate
725
    extends SliverPersistentHeaderDelegate with DiagnosticableTreeMixin {
726
  _LargeTitleNavigationBarSliverDelegate({
727 728 729 730 731 732 733 734 735 736 737 738
    required this.keys,
    required this.components,
    required this.userMiddle,
    required this.backgroundColor,
    required this.brightness,
    required this.border,
    required this.padding,
    required this.actionsForegroundColor,
    required this.transitionBetweenRoutes,
    required this.heroTag,
    required this.persistentHeight,
    required this.alwaysShowMiddle,
739
    required this.stretchConfiguration,
740 741 742 743 744 745
  }) : assert(persistentHeight != null),
       assert(alwaysShowMiddle != null),
       assert(transitionBetweenRoutes != null);

  final _NavigationBarStaticComponentsKeys keys;
  final _NavigationBarStaticComponents components;
746
  final Widget? userMiddle;
747
  final Color backgroundColor;
748 749 750
  final Brightness? brightness;
  final Border? border;
  final EdgeInsetsDirectional? padding;
751
  final Color actionsForegroundColor;
752 753 754 755
  final bool transitionBetweenRoutes;
  final Object heroTag;
  final double persistentHeight;
  final bool alwaysShowMiddle;
756 757 758 759 760 761 762

  @override
  double get minExtent => persistentHeight;

  @override
  double get maxExtent => persistentHeight + _kNavBarLargeTitleHeightExtension;

763 764 765
  @override
  OverScrollHeaderStretchConfiguration? stretchConfiguration;

766 767 768 769
  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    final bool showLargeTitle = shrinkOffset < maxExtent - minExtent - _kNavBarShowLargeTitleThreshold;

770
    final _PersistentNavigationBar persistentNavigationBar =
771
        _PersistentNavigationBar(
772
      components: components,
773
      padding: padding,
774 775 776
      // If a user specified middle exists, always show it. Otherwise, show
      // title when sliver is collapsed.
      middleVisible: alwaysShowMiddle ? null : !showLargeTitle,
777 778
    );

779
    final Widget navBar = _wrapWithBackground(
780
      border: border,
781
      backgroundColor: CupertinoDynamicColor.resolve(backgroundColor, context),
782
      brightness: brightness,
xster's avatar
xster committed
783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817
      child: DefaultTextStyle(
        style: CupertinoTheme.of(context).textTheme.textStyle,
        child: Stack(
          fit: StackFit.expand,
          children: <Widget>[
            Positioned(
              top: persistentHeight,
              left: 0.0,
              right: 0.0,
              bottom: 0.0,
              child: 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: OverflowBox(
                  minHeight: 0.0,
                  maxHeight: double.infinity,
                  alignment: AlignmentDirectional.bottomStart,
                  child: Padding(
                    padding: const EdgeInsetsDirectional.only(
                      start: _kNavBarEdgePadding,
                      bottom: 8.0, // Bottom has a different padding.
                    ),
                    child: SafeArea(
                      top: false,
                      bottom: false,
                      child: AnimatedOpacity(
                        opacity: showLargeTitle ? 1.0 : 0.0,
                        duration: _kNavBarTitleFadeDuration,
                        child: Semantics(
                          header: true,
                          child: DefaultTextStyle(
                            style: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle,
                            maxLines: 1,
                            overflow: TextOverflow.ellipsis,
818
                            child: components.largeTitle!,
xster's avatar
xster committed
819
                          ),
820 821 822 823 824 825 826
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ),
xster's avatar
xster committed
827 828 829 830 831 832 833 834
            Positioned(
              left: 0.0,
              right: 0.0,
              top: 0.0,
              child: persistentNavigationBar,
            ),
          ],
        ),
835 836
      ),
    );
837 838 839 840 841

    if (!transitionBetweenRoutes || !_isTransitionable(context)) {
      return navBar;
    }

842
    return Hero(
843 844 845
      tag: heroTag == _defaultHeroTag
          ? _HeroTag(Navigator.of(context))
          : heroTag,
846 847 848
      createRectTween: _linearTranslateWithLargestRectSizeTween,
      flightShuttleBuilder: _navBarHeroFlightShuttleBuilder,
      placeholderBuilder: _navBarHeroLaunchPadBuilder,
xster's avatar
xster committed
849
      transitionOnUserGestures: true,
850 851 852
      // This is all the way down here instead of being at the top level of
      // CupertinoSliverNavigationBar like CupertinoNavigationBar because it
      // needs to wrap the top level RenderBox rather than a RenderSliver.
853
      child: _TransitionableNavigationBar(
854
        componentsKeys: keys,
855
        backgroundColor: CupertinoDynamicColor.resolve(backgroundColor, context),
xster's avatar
xster committed
856 857 858
        backButtonTextStyle: CupertinoTheme.of(context).textTheme.navActionTextStyle,
        titleTextStyle: CupertinoTheme.of(context).textTheme.navTitleTextStyle,
        largeTitleTextStyle: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle,
859 860 861 862 863 864
        border: border,
        hasUserMiddle: userMiddle != null,
        largeExpanded: showLargeTitle,
        child: navBar,
      ),
    );
865 866 867
  }

  @override
868
  bool shouldRebuild(_LargeTitleNavigationBarSliverDelegate oldDelegate) {
869
    return components != oldDelegate.components
870
        || userMiddle != oldDelegate.userMiddle
871
        || backgroundColor != oldDelegate.backgroundColor
872 873 874 875 876 877 878
        || border != oldDelegate.border
        || padding != oldDelegate.padding
        || actionsForegroundColor != oldDelegate.actionsForegroundColor
        || transitionBetweenRoutes != oldDelegate.transitionBetweenRoutes
        || persistentHeight != oldDelegate.persistentHeight
        || alwaysShowMiddle != oldDelegate.alwaysShowMiddle
        || heroTag != oldDelegate.heroTag;
879 880 881
  }
}

882
/// The top part of the navigation bar that's never scrolled away.
883
///
884
/// Consists of the entire navigation bar without background and border when used
885 886
/// without large titles. With large titles, it's the top static half that
/// doesn't scroll.
887 888
class _PersistentNavigationBar extends StatelessWidget {
  const _PersistentNavigationBar({
889 890
    Key? key,
    required this.components,
891
    this.padding,
892 893 894
    this.middleVisible,
  }) : super(key: key);

895
  final _NavigationBarStaticComponents components;
896

897
  final EdgeInsetsDirectional? padding;
898 899
  /// Whether the middle widget has a visible animated opacity. A null value
  /// means the middle opacity will not be animated.
900
  final bool? middleVisible;
901

902 903
  @override
  Widget build(BuildContext context) {
904
    Widget? middle = components.middle;
905

906
    if (middle != null) {
907
      middle = DefaultTextStyle(
xster's avatar
xster committed
908
        style: CupertinoTheme.of(context).textTheme.navTitleTextStyle,
909
        child: Semantics(header: true, child: middle),
910 911 912 913 914
      );
      // When the middle's visibility can change on the fly like with large title
      // slivers, wrap with animated opacity.
      middle = middleVisible == null
        ? middle
915
        : AnimatedOpacity(
916
          opacity: middleVisible! ? 1.0 : 0.0,
917 918
          duration: _kNavBarTitleFadeDuration,
          child: middle,
919
        );
920
    }
921

922 923 924
    Widget? leading = components.leading;
    final Widget? backChevron = components.backChevron;
    final Widget? backLabel = components.backLabel;
925

926
    if (leading == null && backChevron != null && backLabel != null) {
927
      leading = CupertinoNavigationBarBackButton._assemble(
928 929
        backChevron,
        backLabel,
930
      );
931
    }
932

933
    Widget paddedToolbar = NavigationToolbar(
934 935 936
      leading: leading,
      middle: middle,
      trailing: components.trailing,
937
      centerMiddle: true,
938
      middleSpacing: 6.0,
939 940 941
    );

    if (padding != null) {
942
      paddedToolbar = Padding(
943
        padding: EdgeInsets.only(
944 945
          top: padding!.top,
          bottom: padding!.bottom,
946 947 948 949 950
        ),
        child: paddedToolbar,
      );
    }

951
    return SizedBox(
952
      height: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
953
      child: SafeArea(
954 955
        bottom: false,
        child: paddedToolbar,
956 957
      ),
    );
958 959
  }
}
960

961 962 963 964 965 966 967 968 969
// A collection of keys always used when building static routes' nav bars's
// components with _NavigationBarStaticComponents and read in
// _NavigationBarTransition in Hero flights in order to reference the components'
// RenderBoxes for their positions.
//
// These keys should never re-appear inside the Hero flights.
@immutable
class _NavigationBarStaticComponentsKeys {
  _NavigationBarStaticComponentsKeys()
970 971 972 973 974 975 976
    : navBarBoxKey = GlobalKey(debugLabel: 'Navigation bar render box'),
      leadingKey = GlobalKey(debugLabel: 'Leading'),
      backChevronKey = GlobalKey(debugLabel: 'Back chevron'),
      backLabelKey = GlobalKey(debugLabel: 'Back label'),
      middleKey = GlobalKey(debugLabel: 'Middle'),
      trailingKey = GlobalKey(debugLabel: 'Trailing'),
      largeTitleKey = GlobalKey(debugLabel: 'Large title');
977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993

  final GlobalKey navBarBoxKey;
  final GlobalKey leadingKey;
  final GlobalKey backChevronKey;
  final GlobalKey backLabelKey;
  final GlobalKey middleKey;
  final GlobalKey trailingKey;
  final GlobalKey largeTitleKey;
}

// Based on various user Widgets and other parameters, construct KeyedSubtree
// components that are used in common by the CupertinoNavigationBar and
// CupertinoSliverNavigationBar. The KeyedSubtrees are inserted into static
// routes and the KeyedSubtrees' child are reused in the Hero flights.
@immutable
class _NavigationBarStaticComponents {
  _NavigationBarStaticComponents({
994 995 996 997 998 999 1000 1001 1002 1003 1004
    required _NavigationBarStaticComponentsKeys keys,
    required ModalRoute<dynamic>? route,
    required Widget? userLeading,
    required bool automaticallyImplyLeading,
    required bool automaticallyImplyTitle,
    required String? previousPageTitle,
    required Widget? userMiddle,
    required Widget? userTrailing,
    required Widget? userLargeTitle,
    required EdgeInsetsDirectional? padding,
    required bool large,
1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045
  }) : leading = createLeading(
         leadingKey: keys.leadingKey,
         userLeading: userLeading,
         route: route,
         automaticallyImplyLeading: automaticallyImplyLeading,
         padding: padding,
       ),
       backChevron = createBackChevron(
         backChevronKey: keys.backChevronKey,
         userLeading: userLeading,
         route: route,
         automaticallyImplyLeading: automaticallyImplyLeading,
       ),
       backLabel = createBackLabel(
         backLabelKey: keys.backLabelKey,
         userLeading: userLeading,
         route: route,
         previousPageTitle: previousPageTitle,
         automaticallyImplyLeading: automaticallyImplyLeading,
       ),
       middle = createMiddle(
         middleKey: keys.middleKey,
         userMiddle: userMiddle,
         userLargeTitle: userLargeTitle,
         route: route,
         automaticallyImplyTitle: automaticallyImplyTitle,
         large: large,
       ),
       trailing = createTrailing(
         trailingKey: keys.trailingKey,
         userTrailing: userTrailing,
         padding: padding,
       ),
       largeTitle = createLargeTitle(
         largeTitleKey: keys.largeTitleKey,
         userLargeTitle: userLargeTitle,
         route: route,
         automaticImplyTitle: automaticallyImplyTitle,
         large: large,
       );

1046 1047 1048
  static Widget? _derivedTitle({
    required bool automaticallyImplyTitle,
    ModalRoute<dynamic>? currentRoute,
1049 1050 1051
  }) {
    // Auto use the CupertinoPageRoute's title if middle not provided.
    if (automaticallyImplyTitle &&
1052
        currentRoute is CupertinoRouteTransitionMixin &&
1053
        currentRoute.title != null) {
1054
      return Text(currentRoute.title!);
1055 1056 1057 1058 1059
    }

    return null;
  }

1060 1061 1062 1063 1064 1065 1066
  final KeyedSubtree? leading;
  static KeyedSubtree? createLeading({
    required GlobalKey leadingKey,
    required Widget? userLeading,
    required ModalRoute<dynamic>? route,
    required bool automaticallyImplyLeading,
    required EdgeInsetsDirectional? padding,
1067
  }) {
1068
    Widget? leadingContent;
1069 1070 1071 1072 1073 1074 1075 1076 1077

    if (userLeading != null) {
      leadingContent = userLeading;
    } else if (
      automaticallyImplyLeading &&
      route is PageRoute &&
      route.canPop &&
      route.fullscreenDialog
    ) {
1078
      leadingContent = CupertinoButton(
1079
        padding: EdgeInsets.zero,
1080
        onPressed: () { route.navigator!.maybePop(); },
1081
        child: const Text('Close'),
1082 1083 1084 1085 1086 1087 1088
      );
    }

    if (leadingContent == null) {
      return null;
    }

1089
    return KeyedSubtree(
1090
      key: leadingKey,
1091 1092
      child: Padding(
        padding: EdgeInsetsDirectional.only(
1093 1094
          start: padding?.start ?? _kNavBarEdgePadding,
        ),
xster's avatar
xster committed
1095 1096 1097
        child: IconTheme.merge(
          data: const IconThemeData(
            size: 32.0,
1098
          ),
xster's avatar
xster committed
1099
          child: leadingContent,
1100 1101 1102 1103 1104
        ),
      ),
    );
  }

1105 1106 1107 1108 1109 1110
  final KeyedSubtree? backChevron;
  static KeyedSubtree? createBackChevron({
    required GlobalKey backChevronKey,
    required Widget? userLeading,
    required ModalRoute<dynamic>? route,
    required bool automaticallyImplyLeading,
1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121
  }) {
    if (
      userLeading != null ||
      !automaticallyImplyLeading ||
      route == null ||
      !route.canPop ||
      (route is PageRoute && route.fullscreenDialog)
    ) {
      return null;
    }

1122
    return KeyedSubtree(key: backChevronKey, child: const _BackChevron());
1123 1124 1125 1126
  }

  /// This widget is not decorated with a font since the font style could
  /// animate during transitions.
1127 1128 1129 1130 1131 1132 1133
  final KeyedSubtree? backLabel;
  static KeyedSubtree? createBackLabel({
    required GlobalKey backLabelKey,
    required Widget? userLeading,
    required ModalRoute<dynamic>? route,
    required bool automaticallyImplyLeading,
    required String? previousPageTitle,
1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144
  }) {
    if (
      userLeading != null ||
      !automaticallyImplyLeading ||
      route == null ||
      !route.canPop ||
      (route is PageRoute && route.fullscreenDialog)
    ) {
      return null;
    }

1145
    return KeyedSubtree(
1146
      key: backLabelKey,
1147
      child: _BackLabel(
1148 1149 1150 1151 1152 1153 1154 1155
        specifiedPreviousTitle: previousPageTitle,
        route: route,
      ),
    );
  }

  /// This widget is not decorated with a font since the font style could
  /// animate during transitions.
1156 1157 1158 1159 1160 1161 1162 1163
  final KeyedSubtree? middle;
  static KeyedSubtree? createMiddle({
    required GlobalKey middleKey,
    required Widget? userMiddle,
    required Widget? userLargeTitle,
    required bool large,
    required bool automaticallyImplyTitle,
    required ModalRoute<dynamic>? route,
1164
  }) {
1165
    Widget? middleContent = userMiddle;
1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179

    if (large) {
      middleContent ??= userLargeTitle;
    }

    middleContent ??= _derivedTitle(
      automaticallyImplyTitle: automaticallyImplyTitle,
      currentRoute: route,
    );

    if (middleContent == null) {
      return null;
    }

1180
    return KeyedSubtree(
1181 1182 1183 1184 1185
      key: middleKey,
      child: middleContent,
    );
  }

1186 1187 1188 1189 1190
  final KeyedSubtree? trailing;
  static KeyedSubtree? createTrailing({
    required GlobalKey trailingKey,
    required Widget? userTrailing,
    required EdgeInsetsDirectional? padding,
1191 1192 1193 1194 1195
  }) {
    if (userTrailing == null) {
      return null;
    }

1196
    return KeyedSubtree(
1197
      key: trailingKey,
1198 1199
      child: Padding(
        padding: EdgeInsetsDirectional.only(
1200 1201
          end: padding?.end ?? _kNavBarEdgePadding,
        ),
xster's avatar
xster committed
1202 1203 1204
        child: IconTheme.merge(
          data: const IconThemeData(
            size: 32.0,
1205
          ),
xster's avatar
xster committed
1206
          child: userTrailing,
1207 1208 1209 1210 1211 1212 1213
        ),
      ),
    );
  }

  /// This widget is not decorated with a font since the font style could
  /// animate during transitions.
1214 1215 1216 1217 1218 1219 1220
  final KeyedSubtree? largeTitle;
  static KeyedSubtree? createLargeTitle({
    required GlobalKey largeTitleKey,
    required Widget? userLargeTitle,
    required bool large,
    required bool automaticImplyTitle,
    required ModalRoute<dynamic>? route,
1221 1222 1223 1224 1225
  }) {
    if (!large) {
      return null;
    }

1226
    final Widget? largeTitleContent = userLargeTitle ?? _derivedTitle(
1227 1228 1229 1230 1231 1232 1233 1234 1235
      automaticallyImplyTitle: automaticImplyTitle,
      currentRoute: route,
    );

    assert(
      largeTitleContent != null,
      'largeTitle was not provided and there was no title from the route.',
    );

1236
    return KeyedSubtree(
1237
      key: largeTitleKey,
1238
      child: largeTitleContent!,
1239 1240 1241 1242
    );
  }
}

1243 1244 1245 1246 1247 1248
/// A nav bar back button typically used in [CupertinoNavigationBar].
///
/// This is automatically inserted into [CupertinoNavigationBar] and
/// [CupertinoSliverNavigationBar]'s `leading` slot when
/// `automaticallyImplyLeading` is true.
///
1249 1250 1251 1252
/// When manually inserted, the [CupertinoNavigationBarBackButton] should only
/// be used in routes that can be popped unless a custom [onPressed] is
/// provided.
///
1253 1254 1255 1256 1257 1258 1259 1260 1261
/// Shows a back chevron and the previous route's title when available from
/// the previous [CupertinoPageRoute.title]. If [previousPageTitle] is specified,
/// it will be shown instead.
class CupertinoNavigationBarBackButton extends StatelessWidget {
  /// Construct a [CupertinoNavigationBarBackButton] that can be used to pop
  /// the current route.
  ///
  /// The [color] parameter must not be null.
  const CupertinoNavigationBarBackButton({
1262
    Key? key,
xster's avatar
xster committed
1263
    this.color,
1264
    this.previousPageTitle,
1265
    this.onPressed,
1266
  }) : _backChevron = null,
1267 1268
       _backLabel = null,
       super(key: key);
1269 1270 1271 1272 1273 1274 1275

  // Allow the back chevron and label to be separately created (and keyed)
  // because they animate separately during page transitions.
  const CupertinoNavigationBarBackButton._assemble(
    this._backChevron,
    this._backLabel,
  ) : previousPageTitle = null,
1276 1277
      color = null,
      onPressed = null;
1278 1279

  /// The [Color] of the back button.
1280
  ///
xster's avatar
xster committed
1281 1282 1283
  /// Can be used to override the color of the back button chevron and label.
  ///
  /// Defaults to [CupertinoTheme]'s `primaryColor` if null.
1284
  final Color? color;
1285

1286 1287 1288
  /// An override for showing the previous route's title. If null, it will be
  /// automatically derived from [CupertinoPageRoute.title] if the current and
  /// previous routes are both [CupertinoPageRoute]s.
1289
  final String? previousPageTitle;
1290

1291 1292 1293 1294
  /// An override callback to perform instead of the default behavior which is
  /// to pop the [Navigator].
  ///
  /// It can, for instance, be used to pop the platform's navigation stack
1295 1296
  /// via [SystemNavigator] instead of Flutter's [Navigator] in add-to-app
  /// situations.
1297 1298
  ///
  /// Defaults to null.
1299
  final VoidCallback? onPressed;
1300

1301
  final Widget? _backChevron;
1302

1303
  final Widget? _backLabel;
1304

1305 1306
  @override
  Widget build(BuildContext context) {
1307
    final ModalRoute<dynamic>? currentRoute = ModalRoute.of(context);
1308 1309 1310 1311 1312 1313
    if (onPressed == null) {
      assert(
        currentRoute?.canPop == true,
        'CupertinoNavigationBarBackButton should only be used in routes that can be popped',
      );
    }
1314

xster's avatar
xster committed
1315 1316
    TextStyle actionTextStyle = CupertinoTheme.of(context).textTheme.navActionTextStyle;
    if (color != null) {
1317
      actionTextStyle = actionTextStyle.copyWith(color: CupertinoDynamicColor.maybeResolve(color, context));
xster's avatar
xster committed
1318 1319
    }

1320
    return CupertinoButton(
1321
      padding: EdgeInsets.zero,
1322
      child: Semantics(
1323 1324 1325 1326
        container: true,
        excludeSemantics: true,
        label: 'Back',
        button: true,
xster's avatar
xster committed
1327 1328 1329 1330
        child: DefaultTextStyle(
          style: actionTextStyle,
          child: ConstrainedBox(
            constraints: const BoxConstraints(minWidth: _kNavBarBackButtonTapWidth),
1331
            child: Row(
1332 1333 1334 1335 1336 1337
              mainAxisSize: MainAxisSize.min,
              mainAxisAlignment: MainAxisAlignment.start,
              children: <Widget>[
                const Padding(padding: EdgeInsetsDirectional.only(start: 8.0)),
                _backChevron ?? const _BackChevron(),
                const Padding(padding: EdgeInsetsDirectional.only(start: 6.0)),
1338 1339
                Flexible(
                  child: _backLabel ?? _BackLabel(
1340 1341 1342
                    specifiedPreviousTitle: previousPageTitle,
                    route: currentRoute,
                  ),
1343
                ),
1344 1345
              ],
            ),
1346 1347 1348
          ),
        ),
      ),
1349 1350
      onPressed: () {
        if (onPressed != null) {
1351
          onPressed!();
1352 1353 1354 1355
        } else {
          Navigator.maybePop(context);
        }
      },
1356 1357 1358
    );
  }
}
1359

1360

1361
class _BackChevron extends StatelessWidget {
1362
  const _BackChevron({ Key? key }) : super(key: key);
1363 1364

  @override
1365
  Widget build(BuildContext context) {
1366
    final TextDirection textDirection = Directionality.of(context);
1367
    final TextStyle textStyle = DefaultTextStyle.of(context).style;
1368 1369 1370

    // Replicate the Icon logic here to get a tightly sized icon and add
    // custom non-square padding.
1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382
    Widget iconWidget = Padding(
      padding: const EdgeInsetsDirectional.only(start: 6, end: 2),
      child: Text.rich(
        TextSpan(
          text: String.fromCharCode(CupertinoIcons.back.codePoint),
          style: TextStyle(
            inherit: false,
            color: textStyle.color,
            fontSize: 30.0,
            fontFamily: CupertinoIcons.back.fontFamily,
            package: CupertinoIcons.back.fontPackage,
          ),
1383 1384 1385 1386 1387
        ),
      ),
    );
    switch (textDirection) {
      case TextDirection.rtl:
1388 1389
        iconWidget = Transform(
          transform: Matrix4.identity()..scale(-1.0, 1.0, 1.0),
1390 1391 1392 1393 1394 1395 1396 1397
          alignment: Alignment.center,
          transformHitTests: false,
          child: iconWidget,
        );
        break;
      case TextDirection.ltr:
        break;
    }
1398

1399 1400 1401
    return iconWidget;
  }
}
1402

1403 1404 1405 1406
/// A widget that shows next to the back chevron when `automaticallyImplyLeading`
/// is true.
class _BackLabel extends StatelessWidget {
  const _BackLabel({
1407 1408 1409
    Key? key,
    required this.specifiedPreviousTitle,
    required this.route,
1410
  }) : super(key: key);
1411

1412 1413
  final String? specifiedPreviousTitle;
  final ModalRoute<dynamic>? route;
1414 1415 1416

  // `child` is never passed in into ValueListenableBuilder so it's always
  // null here and unused.
1417
  Widget _buildPreviousTitleWidget(BuildContext context, String? previousTitle, Widget? child) {
1418 1419 1420
    if (previousTitle == null) {
      return const SizedBox(height: 0.0, width: 0.0);
    }
1421

1422
    Text textWidget = Text(
1423 1424 1425 1426 1427 1428 1429
      previousTitle,
      maxLines: 1,
      overflow: TextOverflow.ellipsis,
    );

    if (previousTitle.length > 12) {
      textWidget = const Text('Back');
1430
    }
1431

1432
    return Align(
1433 1434 1435 1436
      alignment: AlignmentDirectional.centerStart,
      widthFactor: 1.0,
      child: textWidget,
    );
1437 1438 1439
  }

  @override
1440 1441 1442
  Widget build(BuildContext context) {
    if (specifiedPreviousTitle != null) {
      return _buildPreviousTitleWidget(context, specifiedPreviousTitle, null);
1443
    } else if (route is CupertinoRouteTransitionMixin<dynamic> && !route!.isFirst) {
1444
      final CupertinoRouteTransitionMixin<dynamic> cupertinoRoute = route! as CupertinoRouteTransitionMixin<dynamic>;
1445 1446 1447
      // There is no timing issue because the previousTitle Listenable changes
      // happen during route modifications before the ValueListenableBuilder
      // is built.
1448
      return ValueListenableBuilder<String?>(
1449 1450 1451 1452 1453 1454
        valueListenable: cupertinoRoute.previousTitle,
        builder: _buildPreviousTitleWidget,
      );
    } else {
      return const SizedBox(height: 0.0, width: 0.0);
    }
1455 1456
  }
}
1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467

/// This should always be the first child of Hero widgets.
///
/// This class helps each Hero transition obtain the start or end navigation
/// bar's box size and the inner components of the navigation bar that will
/// move around.
///
/// It should be wrapped around the biggest [RenderBox] of the static
/// navigation bar in each route.
class _TransitionableNavigationBar extends StatelessWidget {
  _TransitionableNavigationBar({
1468 1469 1470 1471 1472 1473 1474 1475 1476
    required this.componentsKeys,
    required this.backgroundColor,
    required this.backButtonTextStyle,
    required this.titleTextStyle,
    required this.largeTitleTextStyle,
    required this.border,
    required this.hasUserMiddle,
    required this.largeExpanded,
    required this.child,
1477 1478
  }) : assert(componentsKeys != null),
       assert(largeExpanded != null),
xster's avatar
xster committed
1479
       assert(!largeExpanded || largeTitleTextStyle != null),
1480 1481 1482
       super(key: componentsKeys.navBarBoxKey);

  final _NavigationBarStaticComponentsKeys componentsKeys;
1483
  final Color? backgroundColor;
xster's avatar
xster committed
1484 1485
  final TextStyle backButtonTextStyle;
  final TextStyle titleTextStyle;
1486 1487
  final TextStyle? largeTitleTextStyle;
  final Border? border;
1488 1489 1490 1491 1492
  final bool hasUserMiddle;
  final bool largeExpanded;
  final Widget child;

  RenderBox get renderBox {
1493
    final RenderBox box = componentsKeys.navBarBoxKey.currentContext!.findRenderObject()! as RenderBox;
1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505
    assert(
      box.attached,
      '_TransitionableNavigationBar.renderBox should be called when building '
      'hero flight shuttles when the from and the to nav bar boxes are already '
      'laid out and painted.',
    );
    return box;
  }

  @override
  Widget build(BuildContext context) {
    assert(() {
1506
      bool inHero = false;
1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522
      context.visitAncestorElements((Element ancestor) {
        if (ancestor is ComponentElement) {
          assert(
            ancestor.widget.runtimeType != _NavigationBarTransition,
            '_TransitionableNavigationBar should never re-appear inside '
            '_NavigationBarTransition. Keyed _TransitionableNavigationBar should '
            'only serve as anchor points in routes rather than appearing inside '
            'Hero flights themselves.',
          );
          if (ancestor.widget.runtimeType == Hero) {
            inHero = true;
          }
        }
        return true;
      });
      assert(
1523
        inHero,
1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549
        '_TransitionableNavigationBar should only be added as the immediate '
        'child of Hero widgets.',
      );
      return true;
    }());
    return child;
  }
}

/// This class represents the widget that will be in the Hero flight instead of
/// the 2 static navigation bars by taking inner components from both.
///
/// The `topNavBar` parameter is the nav bar that was on top regardless of
/// push/pop direction.
///
/// Similarly, the `bottomNavBar` parameter is the nav bar that was at the
/// bottom regardless of the push/pop direction.
///
/// If [MediaQuery.padding] is still present in this widget's [BuildContext],
/// that padding will become part of the transitional navigation bar as well.
///
/// [MediaQuery.padding] should be consistent between the from/to routes and
/// the Hero overlay. Inconsistent [MediaQuery.padding] will produce undetermined
/// results.
class _NavigationBarTransition extends StatelessWidget {
  _NavigationBarTransition({
1550 1551 1552
    required this.animation,
    required this.topNavBar,
    required this.bottomNavBar,
1553
  }) : heightTween = Tween<double>(
1554 1555 1556
         begin: bottomNavBar.renderBox.size.height,
         end: topNavBar.renderBox.size.height,
       ),
1557
       backgroundTween = ColorTween(
1558 1559 1560
         begin: bottomNavBar.backgroundColor,
         end: topNavBar.backgroundColor,
       ),
1561
       borderTween = BorderTween(
1562 1563 1564 1565 1566
         begin: bottomNavBar.border,
         end: topNavBar.border,
       );

  final Animation<double> animation;
1567 1568
  final _TransitionableNavigationBar topNavBar;
  final _TransitionableNavigationBar bottomNavBar;
1569 1570 1571 1572 1573 1574 1575

  final Tween<double> heightTween;
  final ColorTween backgroundTween;
  final BorderTween borderTween;

  @override
  Widget build(BuildContext context) {
1576 1577 1578 1579
    final _NavigationBarComponentsTransition componentsTransition = _NavigationBarComponentsTransition(
      animation: animation,
      bottomNavBar: bottomNavBar,
      topNavBar: topNavBar,
1580
      directionality: Directionality.of(context),
1581 1582
    );

1583 1584 1585 1586 1587
    final List<Widget> children = <Widget>[
      // Draw an empty navigation bar box with changing shape behind all the
      // moving components without any components inside it itself.
      AnimatedBuilder(
        animation: animation,
1588
        builder: (BuildContext context, Widget? child) {
1589 1590 1591
          return _wrapWithBackground(
            // Don't update the system status bar color mid-flight.
            updateSystemUiOverlay: false,
1592
            backgroundColor: backgroundTween.evaluate(animation)!,
1593
            border: borderTween.evaluate(animation),
1594
            child: SizedBox(
1595 1596 1597 1598 1599 1600 1601
              height: heightTween.evaluate(animation),
              width: double.infinity,
            ),
          );
        },
      ),
      // Draw all the components on top of the empty bar box.
1602 1603 1604 1605 1606 1607
      if (componentsTransition.bottomBackChevron != null) componentsTransition.bottomBackChevron!,
      if (componentsTransition.bottomBackLabel != null) componentsTransition.bottomBackLabel!,
      if (componentsTransition.bottomLeading != null) componentsTransition.bottomLeading!,
      if (componentsTransition.bottomMiddle != null) componentsTransition.bottomMiddle!,
      if (componentsTransition.bottomLargeTitle != null) componentsTransition.bottomLargeTitle!,
      if (componentsTransition.bottomTrailing != null) componentsTransition.bottomTrailing!,
1608
      // Draw top components on top of the bottom components.
1609 1610 1611 1612 1613 1614
      if (componentsTransition.topLeading != null) componentsTransition.topLeading!,
      if (componentsTransition.topBackChevron != null) componentsTransition.topBackChevron!,
      if (componentsTransition.topBackLabel != null) componentsTransition.topBackLabel!,
      if (componentsTransition.topMiddle != null) componentsTransition.topMiddle!,
      if (componentsTransition.topLargeTitle != null) componentsTransition.topLargeTitle!,
      if (componentsTransition.topTrailing != null) componentsTransition.topTrailing!,
1615 1616 1617 1618 1619 1620 1621
    ];


    // The actual outer box is big enough to contain both the bottom and top
    // navigation bars. It's not a direct Rect lerp because some components
    // can actually be outside the linearly lerp'ed Rect in the middle of
    // the animation, such as the topLargeTitle.
1622
    return SizedBox(
1623
      height: math.max(heightTween.begin!, heightTween.end!) + MediaQuery.of(context).padding.top,
1624
      width: double.infinity,
1625
      child: Stack(
1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655
        children: children,
      ),
    );
  }
}

/// This class helps create widgets that are in transition based on static
/// components from the bottom and top navigation bars.
///
/// It animates these transitional components both in terms of position and
/// their appearance.
///
/// Instead of running the transitional components through their normal static
/// navigation bar layout logic, this creates transitional widgets that are based
/// on these widgets' existing render objects' layout and position.
///
/// This is possible because this widget is only used during Hero transitions
/// where both the from and to routes are already built and laid out.
///
/// The components' existing layout constraints and positions are then
/// replicated using [Positioned] or [PositionedTransition] wrappers.
///
/// This class should never return [KeyedSubtree]s created by
/// _NavigationBarStaticComponents directly. Since widgets from
/// _NavigationBarStaticComponents are still present in the widget tree during the
/// hero transitions, it would cause global key duplications. Instead, return
/// only the [KeyedSubtree]s' child.
@immutable
class _NavigationBarComponentsTransition {
  _NavigationBarComponentsTransition({
1656 1657 1658 1659
    required this.animation,
    required _TransitionableNavigationBar bottomNavBar,
    required _TransitionableNavigationBar topNavBar,
    required TextDirection directionality,
1660 1661 1662 1663
  }) : bottomComponents = bottomNavBar.componentsKeys,
       topComponents = topNavBar.componentsKeys,
       bottomNavBarBox = bottomNavBar.renderBox,
       topNavBarBox = topNavBar.renderBox,
xster's avatar
xster committed
1664 1665 1666 1667 1668 1669
       bottomBackButtonTextStyle = bottomNavBar.backButtonTextStyle,
       topBackButtonTextStyle = topNavBar.backButtonTextStyle,
       bottomTitleTextStyle = bottomNavBar.titleTextStyle,
       topTitleTextStyle = topNavBar.titleTextStyle,
       bottomLargeTitleTextStyle = bottomNavBar.largeTitleTextStyle,
       topLargeTitleTextStyle = topNavBar.largeTitleTextStyle,
1670 1671 1672 1673 1674 1675
       bottomHasUserMiddle = bottomNavBar.hasUserMiddle,
       topHasUserMiddle = topNavBar.hasUserMiddle,
       bottomLargeExpanded = bottomNavBar.largeExpanded,
       topLargeExpanded = topNavBar.largeExpanded,
       transitionBox =
           // paintBounds are based on offset zero so it's ok to expand the Rects.
1676 1677
           bottomNavBar.renderBox.paintBounds.expandToInclude(topNavBar.renderBox.paintBounds),
       forwardDirection = directionality == TextDirection.ltr ? 1.0 : -1.0;
1678

1679
  static final Animatable<double> fadeOut = Tween<double>(
1680 1681 1682
    begin: 1.0,
    end: 0.0,
  );
1683
  static final Animatable<double> fadeIn = Tween<double>(
1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697
    begin: 0.0,
    end: 1.0,
  );

  final Animation<double> animation;
  final _NavigationBarStaticComponentsKeys bottomComponents;
  final _NavigationBarStaticComponentsKeys topComponents;

  // These render boxes that are the ancestors of all the bottom and top
  // components are used to determine the components' relative positions inside
  // their respective navigation bars.
  final RenderBox bottomNavBarBox;
  final RenderBox topNavBarBox;

xster's avatar
xster committed
1698 1699 1700 1701
  final TextStyle bottomBackButtonTextStyle;
  final TextStyle topBackButtonTextStyle;
  final TextStyle bottomTitleTextStyle;
  final TextStyle topTitleTextStyle;
1702 1703
  final TextStyle? bottomLargeTitleTextStyle;
  final TextStyle? topLargeTitleTextStyle;
xster's avatar
xster committed
1704

1705 1706 1707 1708 1709 1710 1711 1712 1713
  final bool bottomHasUserMiddle;
  final bool topHasUserMiddle;
  final bool bottomLargeExpanded;
  final bool topLargeExpanded;

  // This is the outer box in which all the components will be fitted. The
  // sizing component of RelativeRects will be based on this rect's size.
  final Rect transitionBox;

1714 1715 1716
  // x-axis unity number representing the direction of growth for text.
  final double forwardDirection;

1717 1718 1719 1720
  // Take a widget it its original ancestor navigation bar render box and
  // translate it into a RelativeBox in the transition navigation bar box.
  RelativeRect positionInTransitionBox(
    GlobalKey key, {
1721
    required RenderBox from,
1722
  }) {
1723
    final RenderBox componentBox = key.currentContext!.findRenderObject()! as RenderBox;
1724 1725
    assert(componentBox.attached);

1726
    return RelativeRect.fromRect(
1727 1728 1729 1730 1731 1732 1733 1734 1735
      componentBox.localToGlobal(Offset.zero, ancestor: from) & componentBox.size,
      transitionBox,
    );
  }

  // Create a Tween that moves a widget between its original position in its
  // ancestor navigation bar to another widget's position in that widget's
  // navigation bar.
  //
1736 1737
  // Anchor their positions based on the vertical middle of their respective
  // render boxes' leading edge.
1738 1739 1740 1741 1742
  //
  // Also produce RelativeRects with sizes that would preserve the constant
  // BoxConstraints of the 'from' widget so that animating font sizes etc don't
  // produce rounding error artifacts with a linearly resizing rect.
  RelativeRectTween slideFromLeadingEdge({
1743 1744 1745 1746
    required GlobalKey fromKey,
    required RenderBox fromNavBarBox,
    required GlobalKey toKey,
    required RenderBox toNavBarBox,
1747 1748 1749
  }) {
    final RelativeRect fromRect = positionInTransitionBox(fromKey, from: fromNavBarBox);

1750 1751
    final RenderBox fromBox = fromKey.currentContext!.findRenderObject()! as RenderBox;
    final RenderBox toBox = toKey.currentContext!.findRenderObject()! as RenderBox;
1752 1753 1754 1755 1756

    // We move a box with the size of the 'from' render object such that its
    // upper left corner is at the upper left corner of the 'to' render object.
    // With slight y axis adjustment for those render objects' height differences.
    Rect toRect =
1757 1758 1759 1760 1761
        toBox.localToGlobal(
          Offset.zero,
          ancestor: toNavBarBox,
        ).translate(
          0.0,
1762
          - fromBox.size.height / 2 + toBox.size.height / 2,
1763 1764
        ) & fromBox.size; // Keep the from render object's size.

1765 1766 1767 1768 1769 1770
    if (forwardDirection < 0) {
      // If RTL, move the center right to the center right instead of matching
      // the center lefts.
      toRect = toRect.translate(- fromBox.size.width + toBox.size.width, 0.0);
    }

1771
    return RelativeRectTween(
1772
        begin: fromRect,
1773
        end: RelativeRect.fromRect(toRect, transitionBox),
1774 1775 1776 1777
      );
  }

  Animation<double> fadeInFrom(double t, { Curve curve = Curves.easeIn }) {
1778 1779 1780
    return animation.drive(fadeIn.chain(
      CurveTween(curve: Interval(t, 1.0, curve: curve)),
    ));
1781 1782 1783
  }

  Animation<double> fadeOutBy(double t, { Curve curve = Curves.easeOut }) {
1784 1785 1786
    return animation.drive(fadeOut.chain(
      CurveTween(curve: Interval(0.0, t, curve: curve)),
    ));
1787 1788
  }

1789 1790
  Widget? get bottomLeading {
    final KeyedSubtree? bottomLeading = bottomComponents.leadingKey.currentWidget as KeyedSubtree?;
1791 1792 1793 1794 1795

    if (bottomLeading == null) {
      return null;
    }

1796
    return Positioned.fromRelativeRect(
1797
      rect: positionInTransitionBox(bottomComponents.leadingKey, from: bottomNavBarBox),
1798
      child: FadeTransition(
1799 1800 1801 1802 1803 1804
        opacity: fadeOutBy(0.4),
        child: bottomLeading.child,
      ),
    );
  }

1805 1806
  Widget? get bottomBackChevron {
    final KeyedSubtree? bottomBackChevron = bottomComponents.backChevronKey.currentWidget as KeyedSubtree?;
1807 1808 1809 1810 1811

    if (bottomBackChevron == null) {
      return null;
    }

1812
    return Positioned.fromRelativeRect(
1813
      rect: positionInTransitionBox(bottomComponents.backChevronKey, from: bottomNavBarBox),
1814
      child: FadeTransition(
1815
        opacity: fadeOutBy(0.6),
1816
        child: DefaultTextStyle(
xster's avatar
xster committed
1817
          style: bottomBackButtonTextStyle,
1818 1819 1820 1821 1822 1823
          child: bottomBackChevron.child,
        ),
      ),
    );
  }

1824 1825
  Widget? get bottomBackLabel {
    final KeyedSubtree? bottomBackLabel = bottomComponents.backLabelKey.currentWidget as KeyedSubtree?;
1826 1827 1828 1829 1830 1831 1832

    if (bottomBackLabel == null) {
      return null;
    }

    final RelativeRect from = positionInTransitionBox(bottomComponents.backLabelKey, from: bottomNavBarBox);

1833
    // Transition away by sliding horizontally to the leading edge off of the screen.
1834
    final RelativeRectTween positionTween = RelativeRectTween(
1835
      begin: from,
1836 1837 1838 1839 1840 1841
      end: from.shift(
        Offset(
          forwardDirection * (-bottomNavBarBox.size.width / 2.0),
          0.0,
        ),
      ),
1842 1843
    );

1844
    return PositionedTransition(
1845
      rect: animation.drive(positionTween),
1846
      child: FadeTransition(
1847
        opacity: fadeOutBy(0.2),
1848
        child: DefaultTextStyle(
xster's avatar
xster committed
1849
          style: bottomBackButtonTextStyle,
1850 1851 1852 1853 1854 1855
          child: bottomBackLabel.child,
        ),
      ),
    );
  }

1856 1857 1858 1859
  Widget? get bottomMiddle {
    final KeyedSubtree? bottomMiddle = bottomComponents.middleKey.currentWidget as KeyedSubtree?;
    final KeyedSubtree? topBackLabel = topComponents.backLabelKey.currentWidget as KeyedSubtree?;
    final KeyedSubtree? topLeading = topComponents.leadingKey.currentWidget as KeyedSubtree?;
1860 1861 1862 1863 1864 1865 1866 1867

    // The middle component is non-null when the nav bar is a large title
    // nav bar but would be invisible when expanded, therefore don't show it here.
    if (!bottomHasUserMiddle && bottomLargeExpanded) {
      return null;
    }

    if (bottomMiddle != null && topBackLabel != null) {
1868
      // Move from current position to the top page's back label position.
1869
      return PositionedTransition(
1870
        rect: animation.drive(slideFromLeadingEdge(
1871 1872 1873 1874
          fromKey: bottomComponents.middleKey,
          fromNavBarBox: bottomNavBarBox,
          toKey: topComponents.backLabelKey,
          toNavBarBox: topNavBarBox,
1875
        )),
1876
        child: FadeTransition(
1877 1878
          // A custom middle widget like a segmented control fades away faster.
          opacity: fadeOutBy(bottomHasUserMiddle ? 0.4 : 0.7),
1879
          child: Align(
1880 1881 1882
            // As the text shrinks, make sure it's still anchored to the leading
            // edge of a constantly sized outer box.
            alignment: AlignmentDirectional.centerStart,
1883
            child: DefaultTextStyleTransition(
1884
              style: animation.drive(TextStyleTween(
xster's avatar
xster committed
1885 1886
                begin: bottomTitleTextStyle,
                end: topBackButtonTextStyle,
1887
              )),
1888 1889 1890 1891 1892 1893 1894
              child: bottomMiddle.child,
            ),
          ),
        ),
      );
    }

1895 1896 1897
    // When the top page has a leading widget override (one of the few ways to
    // not have a top back label), don't move the bottom middle widget and just
    // fade.
1898
    if (bottomMiddle != null && topLeading != null) {
1899
      return Positioned.fromRelativeRect(
1900
        rect: positionInTransitionBox(bottomComponents.middleKey, from: bottomNavBarBox),
1901
        child: FadeTransition(
1902 1903
          opacity: fadeOutBy(bottomHasUserMiddle ? 0.4 : 0.7),
          // Keep the font when transitioning into a non-back label leading.
1904
          child: DefaultTextStyle(
xster's avatar
xster committed
1905
            style: bottomTitleTextStyle,
1906 1907 1908 1909 1910 1911 1912 1913 1914
            child: bottomMiddle.child,
          ),
        ),
      );
    }

    return null;
  }

1915 1916 1917 1918
  Widget? get bottomLargeTitle {
    final KeyedSubtree? bottomLargeTitle = bottomComponents.largeTitleKey.currentWidget as KeyedSubtree?;
    final KeyedSubtree? topBackLabel = topComponents.backLabelKey.currentWidget as KeyedSubtree?;
    final KeyedSubtree? topLeading = topComponents.leadingKey.currentWidget as KeyedSubtree?;
1919 1920 1921 1922 1923 1924

    if (bottomLargeTitle == null || !bottomLargeExpanded) {
      return null;
    }

    if (bottomLargeTitle != null && topBackLabel != null) {
1925
      // Move from current position to the top page's back label position.
1926
      return PositionedTransition(
1927
        rect: animation.drive(slideFromLeadingEdge(
1928 1929 1930 1931
          fromKey: bottomComponents.largeTitleKey,
          fromNavBarBox: bottomNavBarBox,
          toKey: topComponents.backLabelKey,
          toNavBarBox: topNavBarBox,
1932
        )),
1933
        child: FadeTransition(
1934
          opacity: fadeOutBy(0.6),
1935
          child: Align(
1936 1937 1938
            // As the text shrinks, make sure it's still anchored to the leading
            // edge of a constantly sized outer box.
            alignment: AlignmentDirectional.centerStart,
1939
            child: DefaultTextStyleTransition(
1940
              style: animation.drive(TextStyleTween(
xster's avatar
xster committed
1941 1942
                begin: bottomLargeTitleTextStyle,
                end: topBackButtonTextStyle,
1943
              )),
1944 1945 1946 1947 1948 1949 1950 1951 1952 1953
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
              child: bottomLargeTitle.child,
            ),
          ),
        ),
      );
    }

    if (bottomLargeTitle != null && topLeading != null) {
1954 1955
      // Unlike bottom middle, the bottom large title moves when it can't
      // transition to the top back label position.
1956 1957
      final RelativeRect from = positionInTransitionBox(bottomComponents.largeTitleKey, from: bottomNavBarBox);

1958
      final RelativeRectTween positionTween = RelativeRectTween(
1959
        begin: from,
1960 1961 1962 1963 1964 1965
        end: from.shift(
          Offset(
            forwardDirection * bottomNavBarBox.size.width / 4.0,
            0.0,
          ),
        ),
1966 1967
      );

1968 1969
      // Just shift slightly towards the trailing edge instead of moving to the
      // back label position.
1970
      return PositionedTransition(
1971
        rect: animation.drive(positionTween),
1972
        child: FadeTransition(
1973 1974
          opacity: fadeOutBy(0.4),
          // Keep the font when transitioning into a non-back-label leading.
1975
          child: DefaultTextStyle(
1976
            style: bottomLargeTitleTextStyle!,
1977 1978 1979 1980 1981 1982 1983 1984 1985
            child: bottomLargeTitle.child,
          ),
        ),
      );
    }

    return null;
  }

1986 1987
  Widget? get bottomTrailing {
    final KeyedSubtree? bottomTrailing = bottomComponents.trailingKey.currentWidget as KeyedSubtree?;
1988 1989 1990 1991 1992

    if (bottomTrailing == null) {
      return null;
    }

1993
    return Positioned.fromRelativeRect(
1994
      rect: positionInTransitionBox(bottomComponents.trailingKey, from: bottomNavBarBox),
1995
      child: FadeTransition(
1996 1997 1998 1999 2000 2001
        opacity: fadeOutBy(0.6),
        child: bottomTrailing.child,
      ),
    );
  }

2002 2003
  Widget? get topLeading {
    final KeyedSubtree? topLeading = topComponents.leadingKey.currentWidget as KeyedSubtree?;
2004 2005 2006 2007 2008

    if (topLeading == null) {
      return null;
    }

2009
    return Positioned.fromRelativeRect(
2010
      rect: positionInTransitionBox(topComponents.leadingKey, from: topNavBarBox),
2011
      child: FadeTransition(
2012 2013 2014 2015 2016 2017
        opacity: fadeInFrom(0.6),
        child: topLeading.child,
      ),
    );
  }

2018 2019 2020
  Widget? get topBackChevron {
    final KeyedSubtree? topBackChevron = topComponents.backChevronKey.currentWidget as KeyedSubtree?;
    final KeyedSubtree? bottomBackChevron = bottomComponents.backChevronKey.currentWidget as KeyedSubtree?;
2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031

    if (topBackChevron == null) {
      return null;
    }

    final RelativeRect to = positionInTransitionBox(topComponents.backChevronKey, from: topNavBarBox);
    RelativeRect from = to;

    // If it's the first page with a back chevron, shift in slightly from the
    // right.
    if (bottomBackChevron == null) {
2032
      final RenderBox topBackChevronBox = topComponents.backChevronKey.currentContext!.findRenderObject()! as RenderBox;
2033 2034 2035 2036 2037 2038
      from = to.shift(
        Offset(
          forwardDirection * topBackChevronBox.size.width * 2.0,
          0.0,
        ),
      );
2039 2040
    }

2041
    final RelativeRectTween positionTween = RelativeRectTween(
2042 2043 2044 2045
      begin: from,
      end: to,
    );

2046
    return PositionedTransition(
2047
      rect: animation.drive(positionTween),
2048
      child: FadeTransition(
2049
        opacity: fadeInFrom(bottomBackChevron == null ? 0.7 : 0.4),
2050
        child: DefaultTextStyle(
xster's avatar
xster committed
2051
          style: topBackButtonTextStyle,
2052 2053 2054 2055 2056 2057
          child: topBackChevron.child,
        ),
      ),
    );
  }

2058 2059 2060 2061
  Widget? get topBackLabel {
    final KeyedSubtree? bottomMiddle = bottomComponents.middleKey.currentWidget as KeyedSubtree?;
    final KeyedSubtree? bottomLargeTitle = bottomComponents.largeTitleKey.currentWidget as KeyedSubtree?;
    final KeyedSubtree? topBackLabel = topComponents.backLabelKey.currentWidget as KeyedSubtree?;
2062 2063 2064 2065 2066

    if (topBackLabel == null) {
      return null;
    }

2067
    final RenderAnimatedOpacity? topBackLabelOpacity =
2068
        topComponents.backLabelKey.currentContext?.findAncestorRenderObjectOfType<RenderAnimatedOpacity>();
2069

2070
    Animation<double>? midClickOpacity;
2071
    if (topBackLabelOpacity != null && topBackLabelOpacity.opacity.value < 1.0) {
2072
      midClickOpacity = animation.drive(Tween<double>(
2073 2074
        begin: 0.0,
        end: topBackLabelOpacity.opacity.value,
2075
      ));
2076 2077 2078 2079 2080 2081 2082 2083 2084
    }

    // Pick up from an incoming transition from the large title. This is
    // duplicated here from the bottomLargeTitle transition widget because the
    // content text might be different. For instance, if the bottomLargeTitle
    // text is too long, the topBackLabel will say 'Back' instead of the original
    // text.
    if (bottomLargeTitle != null &&
        topBackLabel != null &&
2085
        bottomLargeExpanded) {
2086
      return PositionedTransition(
2087
        rect: animation.drive(slideFromLeadingEdge(
2088 2089 2090 2091
          fromKey: bottomComponents.largeTitleKey,
          fromNavBarBox: bottomNavBarBox,
          toKey: topComponents.backLabelKey,
          toNavBarBox: topNavBarBox,
2092
        )),
2093
        child: FadeTransition(
2094
          opacity: midClickOpacity ?? fadeInFrom(0.4),
2095
          child: DefaultTextStyleTransition(
2096
            style: animation.drive(TextStyleTween(
xster's avatar
xster committed
2097 2098
              begin: bottomLargeTitleTextStyle,
              end: topBackButtonTextStyle,
2099
            )),
2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
            child: topBackLabel.child,
          ),
        ),
      );
    }

    // The topBackLabel always comes from the large title first if available
    // and expanded instead of middle.
    if (bottomMiddle != null && topBackLabel != null) {
2111
      return PositionedTransition(
2112
        rect: animation.drive(slideFromLeadingEdge(
2113 2114 2115 2116
          fromKey: bottomComponents.middleKey,
          fromNavBarBox: bottomNavBarBox,
          toKey: topComponents.backLabelKey,
          toNavBarBox: topNavBarBox,
2117
        )),
2118
        child: FadeTransition(
2119
          opacity: midClickOpacity ?? fadeInFrom(0.3),
2120
          child: DefaultTextStyleTransition(
2121
            style: animation.drive(TextStyleTween(
xster's avatar
xster committed
2122 2123
              begin: bottomTitleTextStyle,
              end: topBackButtonTextStyle,
2124
            )),
2125 2126 2127 2128 2129 2130 2131 2132 2133
            child: topBackLabel.child,
          ),
        ),
      );
    }

    return null;
  }

2134 2135
  Widget? get topMiddle {
    final KeyedSubtree? topMiddle = topComponents.middleKey.currentWidget as KeyedSubtree?;
2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149

    if (topMiddle == null) {
      return null;
    }

    // The middle component is non-null when the nav bar is a large title
    // nav bar but would be invisible when expanded, therefore don't show it here.
    if (!topHasUserMiddle && topLargeExpanded) {
      return null;
    }

    final RelativeRect to = positionInTransitionBox(topComponents.middleKey, from: topNavBarBox);

    // Shift in from the trailing edge of the screen.
2150
    final RelativeRectTween positionTween = RelativeRectTween(
2151 2152 2153 2154 2155 2156
      begin: to.shift(
        Offset(
          forwardDirection * topNavBarBox.size.width / 2.0,
          0.0,
        ),
      ),
2157 2158 2159
      end: to,
    );

2160
    return PositionedTransition(
2161
      rect: animation.drive(positionTween),
2162
      child: FadeTransition(
2163
        opacity: fadeInFrom(0.25),
2164
        child: DefaultTextStyle(
xster's avatar
xster committed
2165
          style: topTitleTextStyle,
2166 2167 2168 2169 2170 2171
          child: topMiddle.child,
        ),
      ),
    );
  }

2172 2173
  Widget? get topTrailing {
    final KeyedSubtree? topTrailing = topComponents.trailingKey.currentWidget as KeyedSubtree?;
2174 2175 2176 2177 2178

    if (topTrailing == null) {
      return null;
    }

2179
    return Positioned.fromRelativeRect(
2180
      rect: positionInTransitionBox(topComponents.trailingKey, from: topNavBarBox),
2181
      child: FadeTransition(
2182 2183 2184 2185 2186 2187
        opacity: fadeInFrom(0.4),
        child: topTrailing.child,
      ),
    );
  }

2188 2189
  Widget? get topLargeTitle {
    final KeyedSubtree? topLargeTitle = topComponents.largeTitleKey.currentWidget as KeyedSubtree?;
2190 2191 2192 2193 2194 2195 2196 2197

    if (topLargeTitle == null || !topLargeExpanded) {
      return null;
    }

    final RelativeRect to = positionInTransitionBox(topComponents.largeTitleKey, from: topNavBarBox);

    // Shift in from the trailing edge of the screen.
2198
    final RelativeRectTween positionTween = RelativeRectTween(
2199 2200 2201 2202 2203 2204
      begin: to.shift(
        Offset(
          forwardDirection * topNavBarBox.size.width,
          0.0,
        ),
      ),
2205 2206 2207
      end: to,
    );

2208
    return PositionedTransition(
2209
      rect: animation.drive(positionTween),
2210
      child: FadeTransition(
2211
        opacity: fadeInFrom(0.3),
2212
        child: DefaultTextStyle(
2213
          style: topLargeTitleTextStyle!,
2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
          child: topLargeTitle.child,
        ),
      ),
    );
  }
}

/// Navigation bars' hero rect tween that will move between the static bars
/// but keep a constant size that's the bigger of both navigation bars.
2225
RectTween _linearTranslateWithLargestRectSizeTween(Rect? begin, Rect? end) {
2226
  final Size largestSize = Size(
2227
    math.max(begin!.size.width, end!.size.width),
2228 2229
    math.max(begin.size.height, end.size.height),
  );
2230
  return RectTween(
2231 2232 2233
    begin: begin.topLeft & largestSize,
    end: end.topLeft & largestSize,
  );
2234
}
2235

2236
Widget _navBarHeroLaunchPadBuilder(
2237
  BuildContext context,
2238
  Size heroSize,
2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252
  Widget child,
) {
  assert(child is _TransitionableNavigationBar);
  // Tree reshaping is fine here because the Heroes' child is always a
  // _TransitionableNavigationBar which has a GlobalKey.

  // Keeping the Hero subtree here is needed (instead of just swapping out the
  // anchor nav bars for fixed size boxes during flights) because the nav bar
  // and their specific component children may serve as anchor points again if
  // another mid-transition flight diversion is triggered.

  // This is ok performance-wise because static nav bars are generally cheap to
  // build and layout but expensive to GPU render (due to clips and blurs) which
  // we're skipping here.
2253
  return Visibility(
2254 2255 2256 2257 2258 2259
    maintainSize: true,
    maintainAnimation: true,
    maintainState: true,
    visible: false,
    child: child,
  );
2260
}
2261 2262

/// Navigation bars' hero flight shuttle builder.
2263
Widget _navBarHeroFlightShuttleBuilder(
2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276
  BuildContext flightContext,
  Animation<double> animation,
  HeroFlightDirection flightDirection,
  BuildContext fromHeroContext,
  BuildContext toHeroContext,
) {
  assert(animation != null);
  assert(flightDirection != null);
  assert(fromHeroContext != null);
  assert(toHeroContext != null);
  assert(fromHeroContext.widget is Hero);
  assert(toHeroContext.widget is Hero);

2277 2278
  final Hero fromHeroWidget = fromHeroContext.widget as Hero;
  final Hero toHeroWidget = toHeroContext.widget as Hero;
2279 2280 2281 2282

  assert(fromHeroWidget.child is _TransitionableNavigationBar);
  assert(toHeroWidget.child is _TransitionableNavigationBar);

2283 2284
  final _TransitionableNavigationBar fromNavBar = fromHeroWidget.child as _TransitionableNavigationBar;
  final _TransitionableNavigationBar toNavBar = toHeroWidget.child as _TransitionableNavigationBar;
2285 2286 2287 2288 2289

  assert(fromNavBar.componentsKeys != null);
  assert(toNavBar.componentsKeys != null);

  assert(
2290
    fromNavBar.componentsKeys.navBarBoxKey.currentContext!.owner != null,
2291 2292 2293
    'The from nav bar to Hero must have been mounted in the previous frame',
  );
  assert(
2294
    toNavBar.componentsKeys.navBarBoxKey.currentContext!.owner != null,
2295 2296 2297 2298 2299
    'The to nav bar to Hero must have been mounted in the previous frame',
  );

  switch (flightDirection) {
    case HeroFlightDirection.push:
2300
      return _NavigationBarTransition(
2301 2302 2303 2304 2305
        animation: animation,
        bottomNavBar: fromNavBar,
        topNavBar: toNavBar,
      );
    case HeroFlightDirection.pop:
2306
      return _NavigationBarTransition(
2307 2308 2309 2310 2311
        animation: animation,
        bottomNavBar: toNavBar,
        topNavBar: fromNavBar,
      );
  }
2312
}