// Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:math' as math; import 'dart:ui' show ImageFilter; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'button.dart'; import 'constants.dart'; import 'icons.dart'; import 'page_scaffold.dart'; import 'route.dart'; import 'theme.dart'; /// Standard iOS navigation bar height without the status bar. /// /// This height is constant and independent of accessibility as it is in iOS. const double _kNavBarPersistentHeight = kMinInteractiveDimensionCupertino; /// Size increase from expanding the navigation bar into an iOS-11-style large title /// form in a [CustomScrollView]. const double _kNavBarLargeTitleHeightExtension = 52.0; /// Number of logical pixels scrolled down before the title text is transferred /// from the normal navigation bar to a big title below the navigation bar. const double _kNavBarShowLargeTitleThreshold = 10.0; const double _kNavBarEdgePadding = 16.0; const double _kNavBarBackButtonTapWidth = 50.0; /// Title text transfer fade. const Duration _kNavBarTitleFadeDuration = Duration(milliseconds: 150); const Color _kDefaultNavBarBorderColor = Color(0x4C000000); const Border _kDefaultNavBarBorder = Border( bottom: BorderSide( color: _kDefaultNavBarBorderColor, width: 0.0, // One physical pixel. style: BorderStyle.solid, ), ); // There's a single tag for all instances of navigation bars because they can // all transition between each other (per Navigator) via Hero transitions. const _HeroTag _defaultHeroTag = _HeroTag(null); class _HeroTag { const _HeroTag(this.navigator); final NavigatorState navigator; // Let the Hero tag be described in tree dumps. @override 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; } final _HeroTag otherTag = other; return navigator == otherTag.navigator; } @override int get hashCode { return identityHashCode(navigator); } } /// 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({ Border border, Color backgroundColor, Widget child, bool updateSystemUiOverlay = true, }) { Widget result = child; if (updateSystemUiOverlay) { final bool darkBackground = backgroundColor.computeLuminance() < 0.179; final SystemUiOverlayStyle overlayStyle = darkBackground ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark; result = AnnotatedRegion<SystemUiOverlayStyle>( value: overlayStyle, sized: true, child: result, ); } final DecoratedBox childWithBackground = DecoratedBox( decoration: BoxDecoration( border: border, color: backgroundColor, ), child: result, ); if (backgroundColor.alpha == 0xFF) return childWithBackground; return ClipRect( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), child: childWithBackground, ), ); } // This exists to support backward compatibility with arguments like // `actionsForegroundColor`. CupertinoThemes can be used to support these // scenarios now. To support `actionsForegroundColor`, the nav bar rewraps // its children with a CupertinoTheme. Widget _wrapActiveColor(Color color, BuildContext context, Widget child) { if (color == null) { return child; } return CupertinoTheme( data: CupertinoTheme.of(context).copyWith(primaryColor: color), child: child, ); } // Whether the current route supports nav bar hero transitions from or to. bool _isTransitionable(BuildContext context) { final ModalRoute<dynamic> route = ModalRoute.of(context); // 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; } /// An iOS-styled navigation bar. /// /// 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. /// /// 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). /// /// The [middle] widget will automatically be a title text from the current /// [CupertinoPageRoute] if none is provided and [automaticallyImplyMiddle] is /// true (true by default). /// /// 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. /// /// When [transitionBetweenRoutes] is true, this navigation bar will transition /// on top of the routes instead of inside them if the route being transitioned /// 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. /// /// 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. /// /// 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]. /// /// See also: /// /// * [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. class CupertinoNavigationBar extends StatefulWidget implements ObstructingPreferredSizeWidget { /// Creates a navigation bar in the iOS style. const CupertinoNavigationBar({ Key key, this.leading, this.automaticallyImplyLeading = true, this.automaticallyImplyMiddle = true, this.previousPageTitle, this.middle, this.trailing, this.border = _kDefaultNavBarBorder, this.backgroundColor, this.padding, this.actionsForegroundColor, this.transitionBetweenRoutes = true, this.heroTag = _defaultHeroTag, }) : assert(automaticallyImplyLeading != null), assert(automaticallyImplyMiddle != null), assert(transitionBetweenRoutes != null), assert( heroTag != null, 'heroTag cannot be null. Use transitionBetweenRoutes = false to ' 'disable Hero transition on this navigation bar.' ), assert( !transitionBetweenRoutes || identical(heroTag, _defaultHeroTag), 'Cannot specify a heroTag override if this navigation bar does not ' 'transition due to transitionBetweenRoutes = false.' ), super(key: key); /// {@template flutter.cupertino.navBar.leading} /// Widget to place at the start of the navigation bar. Normally a back button /// for a normal page or a cancel button for full page dialogs. /// /// If null and [automaticallyImplyLeading] is true, an appropriate button /// will be automatically created. /// {@endtemplate} final Widget leading; /// {@template flutter.cupertino.navBar.automaticallyImplyLeading} /// 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. /// /// 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]. /// /// This value cannot be null. /// {@endtemplate} final bool automaticallyImplyLeading; /// 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; /// {@template flutter.cupertino.navBar.previousPageTitle} /// 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} final String previousPageTitle; /// Widget to place in the middle of the navigation bar. Normally a title or /// a segmented control. /// /// If null and [automaticallyImplyMiddle] is true, an appropriate [Text] /// title will be created if the current route is a [CupertinoPageRoute] and /// has a `title`. final Widget middle; /// {@template flutter.cupertino.navBar.trailing} /// Widget to place at the end of the navigation bar. Normally additional actions /// taken on the page such as a search or edit function. /// {@endtemplate} final Widget trailing; // TODO(xster): implement support for double row navigation bars. /// {@template flutter.cupertino.navBar.backgroundColor} /// The background color of the navigation bar. If it contains transparency, the /// tab bar will automatically produce a blurring effect to the content /// behind it. /// /// Defaults to [CupertinoTheme]'s `barBackgroundColor` if null. /// {@endtemplate} final Color backgroundColor; /// {@template flutter.cupertino.navBar.padding} /// 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. /// {@endtemplate} final EdgeInsetsDirectional padding; /// {@template flutter.cupertino.navBar.border} /// 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. /// {@endtemplate} final Border border; /// {@template flutter.cupertino.navBar.actionsForegroundColor} /// Default color used for text and icons of the [leading] and [trailing] /// widgets in the navigation bar. /// /// Defaults to the `primaryColor` of the [CupertinoTheme] when null. /// {@endtemplate} /// /// The default color for text in the [middle] slot is always black, as per /// iOS standard design. @Deprecated('Use CupertinoTheme and primaryColor to propagate color') final Color actionsForegroundColor; /// {@template flutter.cupertino.navBar.transitionBetweenRoutes} /// 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. /// /// 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]. /// /// 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; /// {@template flutter.cupertino.navBar.heroTag} /// Tag for the navigation bar's Hero widget if [transitionBetweenRoutes] is true. /// /// Defaults to a common tag between all [CupertinoNavigationBar] and /// [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. /// /// Cannot be null. To disable Hero transitions for this navigation bar, /// set [transitionBetweenRoutes] to false. /// {@endtemplate} final Object heroTag; /// True if the navigation bar's background color has no transparency. @override bool get fullObstruction => backgroundColor == null ? null : backgroundColor.alpha == 0xFF; @override Size get preferredSize { return const Size.fromHeight(_kNavBarPersistentHeight); } @override _CupertinoNavigationBarState createState() { return _CupertinoNavigationBarState(); } } // 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> { _NavigationBarStaticComponentsKeys keys; @override void initState() { super.initState(); keys = _NavigationBarStaticComponentsKeys(); } @override Widget build(BuildContext context) { final Color backgroundColor = widget.backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor; final _NavigationBarStaticComponents components = _NavigationBarStaticComponents( 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, ); final Widget navBar = _wrapWithBackground( border: widget.border, backgroundColor: backgroundColor, child: DefaultTextStyle( style: CupertinoTheme.of(context).textTheme.textStyle, child: _PersistentNavigationBar( components: components, padding: widget.padding, ), ), ); if (!widget.transitionBetweenRoutes || !_isTransitionable(context)) { // Lint ignore to maintain backward compatibility. return _wrapActiveColor(widget.actionsForegroundColor, context, navBar); // ignore: deprecated_member_use_from_same_package } return _wrapActiveColor( // Lint ignore to maintain backward compatibility. widget.actionsForegroundColor, // ignore: deprecated_member_use_from_same_package context, 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, ), ); }, ), ); } } /// An iOS-styled navigation bar with iOS-11-style large titles using slivers. /// /// The [CupertinoSliverNavigationBar] must be placed in a sliver group such /// as the [CustomScrollView]. /// /// This navigation bar consists of two sections, a pinned static section on top /// and a sliding section containing iOS-11-style large title below it. /// /// It should be placed at top of the screen and automatically accounts for /// the iOS status bar. /// /// 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 /// when the sliver is expanded. /// /// For advanced uses, an optional [middle] widget can be supplied to show a /// different widget in the middle of the navigation bar when the sliver is collapsed. /// /// 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). /// /// The [largeTitle] widget will automatically be a title text from the current /// [CupertinoPageRoute] if none is provided and [automaticallyImplyTitle] is /// true (true by default). /// /// When [transitionBetweenRoutes] is true, this navigation bar will transition /// on top of the routes instead of inside them if the route being transitioned /// 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. /// /// 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. /// /// `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]. /// /// See also: /// /// * [CupertinoNavigationBar], an iOS navigation bar for use on non-scrolling /// pages. class CupertinoSliverNavigationBar extends StatefulWidget { /// Creates a navigation bar for scrolling lists. /// /// The [largeTitle] argument is required and must not be null. const CupertinoSliverNavigationBar({ Key key, this.largeTitle, this.leading, this.automaticallyImplyLeading = true, this.automaticallyImplyTitle = true, this.previousPageTitle, this.middle, this.trailing, this.border = _kDefaultNavBarBorder, this.backgroundColor, this.padding, this.actionsForegroundColor, this.transitionBetweenRoutes = true, this.heroTag = _defaultHeroTag, }) : assert(automaticallyImplyLeading != null), assert(automaticallyImplyTitle != null), assert( automaticallyImplyTitle == true || largeTitle != null, 'No largeTitle has been provided but automaticallyImplyTitle is also ' 'false. Either provide a largeTitle or set automaticallyImplyTitle to ' 'true.' ), super(key: key); /// The navigation bar's title. /// /// This text will appear in the top static navigation bar when collapsed and /// 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). /// /// If null and [automaticallyImplyTitle] is true, an appropriate [Text] /// title will be created if the current route is a [CupertinoPageRoute] and /// has a `title`. /// /// This parameter must either be non-null or the route must have a title /// ([CupertinoPageRoute.title]) and [automaticallyImplyTitle] must be true. final Widget largeTitle; /// {@macro flutter.cupertino.navBar.leading} /// /// This widget is visible in both collapsed and expanded states. final Widget leading; /// {@macro flutter.cupertino.navBar.automaticallyImplyLeading} final bool automaticallyImplyLeading; /// Controls whether we should try to imply the [largeTitle] widget if null. /// /// 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. /// /// This value cannot be null. final bool automaticallyImplyTitle; /// {@macro flutter.cupertino.navBar.previousPageTitle} final String previousPageTitle; /// A widget to place in the middle of the static navigation bar instead of /// the [largeTitle]. /// /// This widget is visible in both collapsed and expanded states. The text /// supplied in [largeTitle] will no longer appear in collapsed state if a /// [middle] widget is provided. final Widget middle; /// {@macro flutter.cupertino.navBar.trailing} /// /// This widget is visible in both collapsed and expanded states. final Widget trailing; /// {@macro flutter.cupertino.navBar.backgroundColor} final Color backgroundColor; /// {@macro flutter.cupertino.navBar.padding} final EdgeInsetsDirectional padding; /// {@macro flutter.cupertino.navBar.border} final Border border; /// {@macro flutter.cupertino.navBar.actionsForegroundColor} /// /// The default color for text in the [largeTitle] slot is always black, as per /// iOS standard design. @Deprecated('Use CupertinoTheme and primaryColor to propagate color') final Color actionsForegroundColor; /// {@macro flutter.cupertino.navBar.transitionBetweenRoutes} final bool transitionBetweenRoutes; /// {@macro flutter.cupertino.navBar.heroTag} final Object heroTag; /// True if the navigation bar's background color has no transparency. bool get opaque => backgroundColor.alpha == 0xFF; @override _CupertinoSliverNavigationBarState createState() => _CupertinoSliverNavigationBarState(); } // 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> { _NavigationBarStaticComponentsKeys keys; @override void initState() { super.initState(); keys = _NavigationBarStaticComponentsKeys(); } @override Widget build(BuildContext context) { // Lint ignore to maintain backward compatibility. final Color actionsForegroundColor = widget.actionsForegroundColor ?? CupertinoTheme.of(context).primaryColor; // ignore: deprecated_member_use_from_same_package final _NavigationBarStaticComponents components = _NavigationBarStaticComponents( 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, ); return _wrapActiveColor( // Lint ignore to maintain backward compatibility. widget.actionsForegroundColor, // ignore: deprecated_member_use_from_same_package context, 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: widget.backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor, border: widget.border, padding: widget.padding, actionsForegroundColor: actionsForegroundColor, transitionBetweenRoutes: widget.transitionBetweenRoutes, heroTag: widget.heroTag, persistentHeight: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top, alwaysShowMiddle: widget.middle != null, ), ), ), ); } } class _LargeTitleNavigationBarSliverDelegate extends SliverPersistentHeaderDelegate with DiagnosticableTreeMixin { _LargeTitleNavigationBarSliverDelegate({ @required this.keys, @required this.components, @required this.userMiddle, @required this.backgroundColor, @required this.border, @required this.padding, @required this.actionsForegroundColor, @required this.transitionBetweenRoutes, @required this.heroTag, @required this.persistentHeight, @required this.alwaysShowMiddle, }) : assert(persistentHeight != null), assert(alwaysShowMiddle != null), assert(transitionBetweenRoutes != null); final _NavigationBarStaticComponentsKeys keys; final _NavigationBarStaticComponents components; final Widget userMiddle; final Color backgroundColor; final Border border; final EdgeInsetsDirectional padding; final Color actionsForegroundColor; final bool transitionBetweenRoutes; final Object heroTag; final double persistentHeight; final bool alwaysShowMiddle; @override double get minExtent => persistentHeight; @override double get maxExtent => persistentHeight + _kNavBarLargeTitleHeightExtension; @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { final bool showLargeTitle = shrinkOffset < maxExtent - minExtent - _kNavBarShowLargeTitleThreshold; final _PersistentNavigationBar persistentNavigationBar = _PersistentNavigationBar( components: components, padding: padding, // If a user specified middle exists, always show it. Otherwise, show // title when sliver is collapsed. middleVisible: alwaysShowMiddle ? null : !showLargeTitle, ); final Widget navBar = _wrapWithBackground( border: border, backgroundColor: backgroundColor, 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, child: components.largeTitle, ), ), ), ), ), ), ), ), Positioned( left: 0.0, right: 0.0, top: 0.0, child: persistentNavigationBar, ), ], ), ), ); if (!transitionBetweenRoutes || !_isTransitionable(context)) { return navBar; } return Hero( tag: heroTag == _defaultHeroTag ? _HeroTag(Navigator.of(context)) : heroTag, createRectTween: _linearTranslateWithLargestRectSizeTween, flightShuttleBuilder: _navBarHeroFlightShuttleBuilder, placeholderBuilder: _navBarHeroLaunchPadBuilder, transitionOnUserGestures: true, // 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. child: _TransitionableNavigationBar( componentsKeys: keys, backgroundColor: backgroundColor, backButtonTextStyle: CupertinoTheme.of(context).textTheme.navActionTextStyle, titleTextStyle: CupertinoTheme.of(context).textTheme.navTitleTextStyle, largeTitleTextStyle: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle, border: border, hasUserMiddle: userMiddle != null, largeExpanded: showLargeTitle, child: navBar, ), ); } @override bool shouldRebuild(_LargeTitleNavigationBarSliverDelegate oldDelegate) { return components != oldDelegate.components || userMiddle != oldDelegate.userMiddle || backgroundColor != oldDelegate.backgroundColor || border != oldDelegate.border || padding != oldDelegate.padding || actionsForegroundColor != oldDelegate.actionsForegroundColor || transitionBetweenRoutes != oldDelegate.transitionBetweenRoutes || persistentHeight != oldDelegate.persistentHeight || alwaysShowMiddle != oldDelegate.alwaysShowMiddle || heroTag != oldDelegate.heroTag; } } /// The top part of the navigation bar that's never scrolled away. /// /// Consists of the entire navigation bar without background and border when used /// without large titles. With large titles, it's the top static half that /// doesn't scroll. class _PersistentNavigationBar extends StatelessWidget { const _PersistentNavigationBar({ Key key, this.components, this.padding, this.middleVisible, }) : super(key: key); final _NavigationBarStaticComponents components; final EdgeInsetsDirectional padding; /// Whether the middle widget has a visible animated opacity. A null value /// means the middle opacity will not be animated. final bool middleVisible; @override Widget build(BuildContext context) { Widget middle = components.middle; if (middle != null) { middle = DefaultTextStyle( style: CupertinoTheme.of(context).textTheme.navTitleTextStyle, child: Semantics(header: true, child: middle), ); // When the middle's visibility can change on the fly like with large title // slivers, wrap with animated opacity. middle = middleVisible == null ? middle : AnimatedOpacity( opacity: middleVisible ? 1.0 : 0.0, duration: _kNavBarTitleFadeDuration, child: middle, ); } Widget leading = components.leading; final Widget backChevron = components.backChevron; final Widget backLabel = components.backLabel; if (leading == null && backChevron != null && backLabel != null) { leading = CupertinoNavigationBarBackButton._assemble( backChevron, backLabel, ); } Widget paddedToolbar = NavigationToolbar( leading: leading, middle: middle, trailing: components.trailing, centerMiddle: true, middleSpacing: 6.0, ); if (padding != null) { paddedToolbar = Padding( padding: EdgeInsets.only( top: padding.top, bottom: padding.bottom, ), child: paddedToolbar, ); } return SizedBox( height: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top, child: SafeArea( bottom: false, child: paddedToolbar, ), ); } } // 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() : 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'); 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({ @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, }) : 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, ); static Widget _derivedTitle({ bool automaticallyImplyTitle, ModalRoute<dynamic> currentRoute, }) { // Auto use the CupertinoPageRoute's title if middle not provided. if (automaticallyImplyTitle && currentRoute is CupertinoPageRoute && currentRoute.title != null) { return Text(currentRoute.title); } return null; } final KeyedSubtree leading; static KeyedSubtree createLeading({ @required GlobalKey leadingKey, @required Widget userLeading, @required ModalRoute<dynamic> route, @required bool automaticallyImplyLeading, @required EdgeInsetsDirectional padding, }) { Widget leadingContent; if (userLeading != null) { leadingContent = userLeading; } else if ( automaticallyImplyLeading && route is PageRoute && route.canPop && route.fullscreenDialog ) { leadingContent = CupertinoButton( child: const Text('Close'), padding: EdgeInsets.zero, onPressed: () { route.navigator.maybePop(); }, ); } if (leadingContent == null) { return null; } return KeyedSubtree( key: leadingKey, child: Padding( padding: EdgeInsetsDirectional.only( start: padding?.start ?? _kNavBarEdgePadding, ), child: IconTheme.merge( data: const IconThemeData( size: 32.0, ), child: leadingContent, ), ), ); } final KeyedSubtree backChevron; static KeyedSubtree createBackChevron({ @required GlobalKey backChevronKey, @required Widget userLeading, @required ModalRoute<dynamic> route, @required bool automaticallyImplyLeading, }) { if ( userLeading != null || !automaticallyImplyLeading || route == null || !route.canPop || (route is PageRoute && route.fullscreenDialog) ) { return null; } return KeyedSubtree(key: backChevronKey, child: const _BackChevron()); } /// This widget is not decorated with a font since the font style could /// animate during transitions. final KeyedSubtree backLabel; static KeyedSubtree createBackLabel({ @required GlobalKey backLabelKey, @required Widget userLeading, @required ModalRoute<dynamic> route, @required bool automaticallyImplyLeading, @required String previousPageTitle, }) { if ( userLeading != null || !automaticallyImplyLeading || route == null || !route.canPop || (route is PageRoute && route.fullscreenDialog) ) { return null; } return KeyedSubtree( key: backLabelKey, child: _BackLabel( specifiedPreviousTitle: previousPageTitle, route: route, ), ); } /// This widget is not decorated with a font since the font style could /// animate during transitions. 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, }) { Widget middleContent = userMiddle; if (large) { middleContent ??= userLargeTitle; } middleContent ??= _derivedTitle( automaticallyImplyTitle: automaticallyImplyTitle, currentRoute: route, ); if (middleContent == null) { return null; } return KeyedSubtree( key: middleKey, child: middleContent, ); } final KeyedSubtree trailing; static KeyedSubtree createTrailing({ @required GlobalKey trailingKey, @required Widget userTrailing, @required EdgeInsetsDirectional padding, }) { if (userTrailing == null) { return null; } return KeyedSubtree( key: trailingKey, child: Padding( padding: EdgeInsetsDirectional.only( end: padding?.end ?? _kNavBarEdgePadding, ), child: IconTheme.merge( data: const IconThemeData( size: 32.0, ), child: userTrailing, ), ), ); } /// This widget is not decorated with a font since the font style could /// animate during transitions. final KeyedSubtree largeTitle; static KeyedSubtree createLargeTitle({ @required GlobalKey largeTitleKey, @required Widget userLargeTitle, @required bool large, @required bool automaticImplyTitle, @required ModalRoute<dynamic> route, }) { if (!large) { return null; } final Widget largeTitleContent = userLargeTitle ?? _derivedTitle( automaticallyImplyTitle: automaticImplyTitle, currentRoute: route, ); assert( largeTitleContent != null, 'largeTitle was not provided and there was no title from the route.', ); return KeyedSubtree( key: largeTitleKey, child: largeTitleContent, ); } } /// A nav bar back button typically used in [CupertinoNavigationBar]. /// /// This is automatically inserted into [CupertinoNavigationBar] and /// [CupertinoSliverNavigationBar]'s `leading` slot when /// `automaticallyImplyLeading` is true. /// /// When manually inserted, the [CupertinoNavigationBarBackButton] should only /// be used in routes that can be popped unless a custom [onPressed] is /// provided. /// /// 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({ this.color, this.previousPageTitle, this.onPressed, }) : _backChevron = null, _backLabel = null; // 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, color = null, onPressed = null; /// The [Color] of the back button. /// /// Can be used to override the color of the back button chevron and label. /// /// Defaults to [CupertinoTheme]'s `primaryColor` if null. final Color color; /// 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. final String previousPageTitle; /// 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 /// instead of Flutter's [Navigator]. /// /// Defaults to null. final VoidCallback onPressed; final Widget _backChevron; final Widget _backLabel; @override Widget build(BuildContext context) { final ModalRoute<dynamic> currentRoute = ModalRoute.of(context); if (onPressed == null) { assert( currentRoute?.canPop == true, 'CupertinoNavigationBarBackButton should only be used in routes that can be popped', ); } TextStyle actionTextStyle = CupertinoTheme.of(context).textTheme.navActionTextStyle; if (color != null) { actionTextStyle = actionTextStyle.copyWith(color: color); } return CupertinoButton( child: Semantics( container: true, excludeSemantics: true, label: 'Back', button: true, child: DefaultTextStyle( style: actionTextStyle, child: ConstrainedBox( constraints: const BoxConstraints(minWidth: _kNavBarBackButtonTapWidth), child: Row( 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)), Flexible( child: _backLabel ?? _BackLabel( specifiedPreviousTitle: previousPageTitle, route: currentRoute, ), ), ], ), ), ), ), padding: EdgeInsets.zero, onPressed: () { if (onPressed != null) { onPressed(); } else { Navigator.maybePop(context); } }, ); } } class _BackChevron extends StatelessWidget { const _BackChevron({ Key key }) : super(key: key); @override Widget build(BuildContext context) { final TextDirection textDirection = Directionality.of(context); final TextStyle textStyle = DefaultTextStyle.of(context).style; // Replicate the Icon logic here to get a tightly sized icon and add // custom non-square padding. Widget iconWidget = Text.rich( TextSpan( text: String.fromCharCode(CupertinoIcons.back.codePoint), style: TextStyle( inherit: false, color: textStyle.color, fontSize: 34.0, fontFamily: CupertinoIcons.back.fontFamily, package: CupertinoIcons.back.fontPackage, ), ), ); switch (textDirection) { case TextDirection.rtl: iconWidget = Transform( transform: Matrix4.identity()..scale(-1.0, 1.0, 1.0), alignment: Alignment.center, transformHitTests: false, child: iconWidget, ); break; case TextDirection.ltr: break; } return iconWidget; } } /// A widget that shows next to the back chevron when `automaticallyImplyLeading` /// is true. class _BackLabel extends StatelessWidget { const _BackLabel({ Key key, @required this.specifiedPreviousTitle, @required this.route, }) : super(key: key); final String specifiedPreviousTitle; final ModalRoute<dynamic> route; // `child` is never passed in into ValueListenableBuilder so it's always // null here and unused. Widget _buildPreviousTitleWidget(BuildContext context, String previousTitle, Widget child) { if (previousTitle == null) { return const SizedBox(height: 0.0, width: 0.0); } Text textWidget = Text( previousTitle, maxLines: 1, overflow: TextOverflow.ellipsis, ); if (previousTitle.length > 12) { textWidget = const Text('Back'); } return Align( alignment: AlignmentDirectional.centerStart, widthFactor: 1.0, child: textWidget, ); } @override Widget build(BuildContext context) { if (specifiedPreviousTitle != null) { return _buildPreviousTitleWidget(context, specifiedPreviousTitle, null); } else if (route is CupertinoPageRoute<dynamic> && !route.isFirst) { final CupertinoPageRoute<dynamic> cupertinoRoute = route; // There is no timing issue because the previousTitle Listenable changes // happen during route modifications before the ValueListenableBuilder // is built. return ValueListenableBuilder<String>( valueListenable: cupertinoRoute.previousTitle, builder: _buildPreviousTitleWidget, ); } else { return const SizedBox(height: 0.0, width: 0.0); } } } /// 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({ @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, }) : assert(componentsKeys != null), assert(largeExpanded != null), assert(!largeExpanded || largeTitleTextStyle != null), super(key: componentsKeys.navBarBoxKey); final _NavigationBarStaticComponentsKeys componentsKeys; final Color backgroundColor; final TextStyle backButtonTextStyle; final TextStyle titleTextStyle; final TextStyle largeTitleTextStyle; final Border border; final bool hasUserMiddle; final bool largeExpanded; final Widget child; RenderBox get renderBox { final RenderBox box = componentsKeys.navBarBoxKey.currentContext.findRenderObject(); 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(() { bool inHero; 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; } } inHero ??= false; return true; }); assert( inHero == true, '_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({ @required this.animation, @required this.topNavBar, @required this.bottomNavBar, }) : heightTween = Tween<double>( begin: bottomNavBar.renderBox.size.height, end: topNavBar.renderBox.size.height, ), backgroundTween = ColorTween( begin: bottomNavBar.backgroundColor, end: topNavBar.backgroundColor, ), borderTween = BorderTween( begin: bottomNavBar.border, end: topNavBar.border, ); final Animation<double> animation; final _TransitionableNavigationBar topNavBar; final _TransitionableNavigationBar bottomNavBar; final Tween<double> heightTween; final ColorTween backgroundTween; final BorderTween borderTween; @override Widget build(BuildContext context) { final _NavigationBarComponentsTransition componentsTransition = _NavigationBarComponentsTransition( animation: animation, bottomNavBar: bottomNavBar, topNavBar: topNavBar, directionality: Directionality.of(context), ); 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, builder: (BuildContext context, Widget child) { return _wrapWithBackground( // Don't update the system status bar color mid-flight. updateSystemUiOverlay: false, backgroundColor: backgroundTween.evaluate(animation), border: borderTween.evaluate(animation), child: SizedBox( height: heightTween.evaluate(animation), width: double.infinity, ), ); }, ), // Draw all the components on top of the empty bar box. componentsTransition.bottomBackChevron, componentsTransition.bottomBackLabel, componentsTransition.bottomLeading, componentsTransition.bottomMiddle, componentsTransition.bottomLargeTitle, componentsTransition.bottomTrailing, // Draw top components on top of the bottom components. componentsTransition.topLeading, componentsTransition.topBackChevron, componentsTransition.topBackLabel, componentsTransition.topMiddle, componentsTransition.topLargeTitle, componentsTransition.topTrailing, ]; children.removeWhere((Widget child) => child == null); // 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. return SizedBox( height: math.max(heightTween.begin, heightTween.end) + MediaQuery.of(context).padding.top, width: double.infinity, child: Stack( 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({ @required this.animation, @required _TransitionableNavigationBar bottomNavBar, @required _TransitionableNavigationBar topNavBar, @required TextDirection directionality, }) : bottomComponents = bottomNavBar.componentsKeys, topComponents = topNavBar.componentsKeys, bottomNavBarBox = bottomNavBar.renderBox, topNavBarBox = topNavBar.renderBox, bottomBackButtonTextStyle = bottomNavBar.backButtonTextStyle, topBackButtonTextStyle = topNavBar.backButtonTextStyle, bottomTitleTextStyle = bottomNavBar.titleTextStyle, topTitleTextStyle = topNavBar.titleTextStyle, bottomLargeTitleTextStyle = bottomNavBar.largeTitleTextStyle, topLargeTitleTextStyle = topNavBar.largeTitleTextStyle, 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. bottomNavBar.renderBox.paintBounds.expandToInclude(topNavBar.renderBox.paintBounds), forwardDirection = directionality == TextDirection.ltr ? 1.0 : -1.0; static final Animatable<double> fadeOut = Tween<double>( begin: 1.0, end: 0.0, ); static final Animatable<double> fadeIn = Tween<double>( 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; final TextStyle bottomBackButtonTextStyle; final TextStyle topBackButtonTextStyle; final TextStyle bottomTitleTextStyle; final TextStyle topTitleTextStyle; final TextStyle bottomLargeTitleTextStyle; final TextStyle topLargeTitleTextStyle; 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; // x-axis unity number representing the direction of growth for text. final double forwardDirection; // 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, { @required RenderBox from, }) { final RenderBox componentBox = key.currentContext.findRenderObject(); assert(componentBox.attached); return RelativeRect.fromRect( 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. // // Anchor their positions based on the vertical middle of their respective // render boxes' leading edge. // // 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({ @required GlobalKey fromKey, @required RenderBox fromNavBarBox, @required GlobalKey toKey, @required RenderBox toNavBarBox, }) { final RelativeRect fromRect = positionInTransitionBox(fromKey, from: fromNavBarBox); final RenderBox fromBox = fromKey.currentContext.findRenderObject(); final RenderBox toBox = toKey.currentContext.findRenderObject(); // 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 = toBox.localToGlobal( Offset.zero, ancestor: toNavBarBox, ).translate( 0.0, - fromBox.size.height / 2 + toBox.size.height / 2, ) & fromBox.size; // Keep the from render object's size. 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); } return RelativeRectTween( begin: fromRect, end: RelativeRect.fromRect(toRect, transitionBox), ); } Animation<double> fadeInFrom(double t, { Curve curve = Curves.easeIn }) { return animation.drive(fadeIn.chain( CurveTween(curve: Interval(t, 1.0, curve: curve)), )); } Animation<double> fadeOutBy(double t, { Curve curve = Curves.easeOut }) { return animation.drive(fadeOut.chain( CurveTween(curve: Interval(0.0, t, curve: curve)), )); } Widget get bottomLeading { final KeyedSubtree bottomLeading = bottomComponents.leadingKey.currentWidget; if (bottomLeading == null) { return null; } return Positioned.fromRelativeRect( rect: positionInTransitionBox(bottomComponents.leadingKey, from: bottomNavBarBox), child: FadeTransition( opacity: fadeOutBy(0.4), child: bottomLeading.child, ), ); } Widget get bottomBackChevron { final KeyedSubtree bottomBackChevron = bottomComponents.backChevronKey.currentWidget; if (bottomBackChevron == null) { return null; } return Positioned.fromRelativeRect( rect: positionInTransitionBox(bottomComponents.backChevronKey, from: bottomNavBarBox), child: FadeTransition( opacity: fadeOutBy(0.6), child: DefaultTextStyle( style: bottomBackButtonTextStyle, child: bottomBackChevron.child, ), ), ); } Widget get bottomBackLabel { final KeyedSubtree bottomBackLabel = bottomComponents.backLabelKey.currentWidget; if (bottomBackLabel == null) { return null; } final RelativeRect from = positionInTransitionBox(bottomComponents.backLabelKey, from: bottomNavBarBox); // Transition away by sliding horizontally to the leading edge off of the screen. final RelativeRectTween positionTween = RelativeRectTween( begin: from, end: from.shift( Offset( forwardDirection * (-bottomNavBarBox.size.width / 2.0), 0.0, ), ), ); return PositionedTransition( rect: animation.drive(positionTween), child: FadeTransition( opacity: fadeOutBy(0.2), child: DefaultTextStyle( style: bottomBackButtonTextStyle, child: bottomBackLabel.child, ), ), ); } Widget get bottomMiddle { final KeyedSubtree bottomMiddle = bottomComponents.middleKey.currentWidget; final KeyedSubtree topBackLabel = topComponents.backLabelKey.currentWidget; final KeyedSubtree topLeading = topComponents.leadingKey.currentWidget; // 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) { // Move from current position to the top page's back label position. return PositionedTransition( rect: animation.drive(slideFromLeadingEdge( fromKey: bottomComponents.middleKey, fromNavBarBox: bottomNavBarBox, toKey: topComponents.backLabelKey, toNavBarBox: topNavBarBox, )), child: FadeTransition( // A custom middle widget like a segmented control fades away faster. opacity: fadeOutBy(bottomHasUserMiddle ? 0.4 : 0.7), child: Align( // As the text shrinks, make sure it's still anchored to the leading // edge of a constantly sized outer box. alignment: AlignmentDirectional.centerStart, child: DefaultTextStyleTransition( style: animation.drive(TextStyleTween( begin: bottomTitleTextStyle, end: topBackButtonTextStyle, )), child: bottomMiddle.child, ), ), ), ); } // 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. if (bottomMiddle != null && topLeading != null) { return Positioned.fromRelativeRect( rect: positionInTransitionBox(bottomComponents.middleKey, from: bottomNavBarBox), child: FadeTransition( opacity: fadeOutBy(bottomHasUserMiddle ? 0.4 : 0.7), // Keep the font when transitioning into a non-back label leading. child: DefaultTextStyle( style: bottomTitleTextStyle, child: bottomMiddle.child, ), ), ); } return null; } Widget get bottomLargeTitle { final KeyedSubtree bottomLargeTitle = bottomComponents.largeTitleKey.currentWidget; final KeyedSubtree topBackLabel = topComponents.backLabelKey.currentWidget; final KeyedSubtree topLeading = topComponents.leadingKey.currentWidget; if (bottomLargeTitle == null || !bottomLargeExpanded) { return null; } if (bottomLargeTitle != null && topBackLabel != null) { // Move from current position to the top page's back label position. return PositionedTransition( rect: animation.drive(slideFromLeadingEdge( fromKey: bottomComponents.largeTitleKey, fromNavBarBox: bottomNavBarBox, toKey: topComponents.backLabelKey, toNavBarBox: topNavBarBox, )), child: FadeTransition( opacity: fadeOutBy(0.6), child: Align( // As the text shrinks, make sure it's still anchored to the leading // edge of a constantly sized outer box. alignment: AlignmentDirectional.centerStart, child: DefaultTextStyleTransition( style: animation.drive(TextStyleTween( begin: bottomLargeTitleTextStyle, end: topBackButtonTextStyle, )), maxLines: 1, overflow: TextOverflow.ellipsis, child: bottomLargeTitle.child, ), ), ), ); } if (bottomLargeTitle != null && topLeading != null) { // Unlike bottom middle, the bottom large title moves when it can't // transition to the top back label position. final RelativeRect from = positionInTransitionBox(bottomComponents.largeTitleKey, from: bottomNavBarBox); final RelativeRectTween positionTween = RelativeRectTween( begin: from, end: from.shift( Offset( forwardDirection * bottomNavBarBox.size.width / 4.0, 0.0, ), ), ); // Just shift slightly towards the trailing edge instead of moving to the // back label position. return PositionedTransition( rect: animation.drive(positionTween), child: FadeTransition( opacity: fadeOutBy(0.4), // Keep the font when transitioning into a non-back-label leading. child: DefaultTextStyle( style: bottomLargeTitleTextStyle, child: bottomLargeTitle.child, ), ), ); } return null; } Widget get bottomTrailing { final KeyedSubtree bottomTrailing = bottomComponents.trailingKey.currentWidget; if (bottomTrailing == null) { return null; } return Positioned.fromRelativeRect( rect: positionInTransitionBox(bottomComponents.trailingKey, from: bottomNavBarBox), child: FadeTransition( opacity: fadeOutBy(0.6), child: bottomTrailing.child, ), ); } Widget get topLeading { final KeyedSubtree topLeading = topComponents.leadingKey.currentWidget; if (topLeading == null) { return null; } return Positioned.fromRelativeRect( rect: positionInTransitionBox(topComponents.leadingKey, from: topNavBarBox), child: FadeTransition( opacity: fadeInFrom(0.6), child: topLeading.child, ), ); } Widget get topBackChevron { final KeyedSubtree topBackChevron = topComponents.backChevronKey.currentWidget; final KeyedSubtree bottomBackChevron = bottomComponents.backChevronKey.currentWidget; 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) { final RenderBox topBackChevronBox = topComponents.backChevronKey.currentContext.findRenderObject(); from = to.shift( Offset( forwardDirection * topBackChevronBox.size.width * 2.0, 0.0, ), ); } final RelativeRectTween positionTween = RelativeRectTween( begin: from, end: to, ); return PositionedTransition( rect: animation.drive(positionTween), child: FadeTransition( opacity: fadeInFrom(bottomBackChevron == null ? 0.7 : 0.4), child: DefaultTextStyle( style: topBackButtonTextStyle, child: topBackChevron.child, ), ), ); } Widget get topBackLabel { final KeyedSubtree bottomMiddle = bottomComponents.middleKey.currentWidget; final KeyedSubtree bottomLargeTitle = bottomComponents.largeTitleKey.currentWidget; final KeyedSubtree topBackLabel = topComponents.backLabelKey.currentWidget; if (topBackLabel == null) { return null; } final RenderAnimatedOpacity topBackLabelOpacity = topComponents.backLabelKey.currentContext?.ancestorRenderObjectOfType( const TypeMatcher<RenderAnimatedOpacity>() ); Animation<double> midClickOpacity; if (topBackLabelOpacity != null && topBackLabelOpacity.opacity.value < 1.0) { midClickOpacity = animation.drive(Tween<double>( begin: 0.0, end: topBackLabelOpacity.opacity.value, )); } // 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 && bottomLargeExpanded) { return PositionedTransition( rect: animation.drive(slideFromLeadingEdge( fromKey: bottomComponents.largeTitleKey, fromNavBarBox: bottomNavBarBox, toKey: topComponents.backLabelKey, toNavBarBox: topNavBarBox, )), child: FadeTransition( opacity: midClickOpacity ?? fadeInFrom(0.4), child: DefaultTextStyleTransition( style: animation.drive(TextStyleTween( begin: bottomLargeTitleTextStyle, end: topBackButtonTextStyle, )), 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) { return PositionedTransition( rect: animation.drive(slideFromLeadingEdge( fromKey: bottomComponents.middleKey, fromNavBarBox: bottomNavBarBox, toKey: topComponents.backLabelKey, toNavBarBox: topNavBarBox, )), child: FadeTransition( opacity: midClickOpacity ?? fadeInFrom(0.3), child: DefaultTextStyleTransition( style: animation.drive(TextStyleTween( begin: bottomTitleTextStyle, end: topBackButtonTextStyle, )), child: topBackLabel.child, ), ), ); } return null; } Widget get topMiddle { final KeyedSubtree topMiddle = topComponents.middleKey.currentWidget; 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. final RelativeRectTween positionTween = RelativeRectTween( begin: to.shift( Offset( forwardDirection * topNavBarBox.size.width / 2.0, 0.0, ), ), end: to, ); return PositionedTransition( rect: animation.drive(positionTween), child: FadeTransition( opacity: fadeInFrom(0.25), child: DefaultTextStyle( style: topTitleTextStyle, child: topMiddle.child, ), ), ); } Widget get topTrailing { final KeyedSubtree topTrailing = topComponents.trailingKey.currentWidget; if (topTrailing == null) { return null; } return Positioned.fromRelativeRect( rect: positionInTransitionBox(topComponents.trailingKey, from: topNavBarBox), child: FadeTransition( opacity: fadeInFrom(0.4), child: topTrailing.child, ), ); } Widget get topLargeTitle { final KeyedSubtree topLargeTitle = topComponents.largeTitleKey.currentWidget; if (topLargeTitle == null || !topLargeExpanded) { return null; } final RelativeRect to = positionInTransitionBox(topComponents.largeTitleKey, from: topNavBarBox); // Shift in from the trailing edge of the screen. final RelativeRectTween positionTween = RelativeRectTween( begin: to.shift( Offset( forwardDirection * topNavBarBox.size.width, 0.0, ), ), end: to, ); return PositionedTransition( rect: animation.drive(positionTween), child: FadeTransition( opacity: fadeInFrom(0.3), child: DefaultTextStyle( style: topLargeTitleTextStyle, 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. CreateRectTween _linearTranslateWithLargestRectSizeTween = (Rect begin, Rect end) { final Size largestSize = Size( math.max(begin.size.width, end.size.width), math.max(begin.size.height, end.size.height), ); return RectTween( begin: begin.topLeft & largestSize, end: end.topLeft & largestSize, ); }; final HeroPlaceholderBuilder _navBarHeroLaunchPadBuilder = ( BuildContext context, Size heroSize, 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. return Visibility( maintainSize: true, maintainAnimation: true, maintainState: true, visible: false, child: child, ); }; /// Navigation bars' hero flight shuttle builder. final HeroFlightShuttleBuilder _navBarHeroFlightShuttleBuilder = ( 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); final Hero fromHeroWidget = fromHeroContext.widget; final Hero toHeroWidget = toHeroContext.widget; assert(fromHeroWidget.child is _TransitionableNavigationBar); assert(toHeroWidget.child is _TransitionableNavigationBar); final _TransitionableNavigationBar fromNavBar = fromHeroWidget.child; final _TransitionableNavigationBar toNavBar = toHeroWidget.child; assert(fromNavBar.componentsKeys != null); assert(toNavBar.componentsKeys != null); assert( fromNavBar.componentsKeys.navBarBoxKey.currentContext.owner != null, 'The from nav bar to Hero must have been mounted in the previous frame', ); assert( toNavBar.componentsKeys.navBarBoxKey.currentContext.owner != null, 'The to nav bar to Hero must have been mounted in the previous frame', ); switch (flightDirection) { case HeroFlightDirection.push: return _NavigationBarTransition( animation: animation, bottomNavBar: fromNavBar, topNavBar: toNavBar, ); break; case HeroFlightDirection.pop: return _NavigationBarTransition( animation: animation, bottomNavBar: toNavBar, topNavBar: fromNavBar, ); } return null; };