| // Copyright 2014 The Flutter 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 'colors.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(0x4D000000); |
| |
| 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); |
| |
| @immutable |
| 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; |
| } |
| return other is _HeroTag |
| && other.navigator == 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, |
| Brightness brightness, |
| Widget child, |
| bool updateSystemUiOverlay = true, |
| }) { |
| Widget result = child; |
| if (updateSystemUiOverlay) { |
| final bool isDark = backgroundColor.computeLuminance() < 0.179; |
| final Brightness newBrightness = brightness ?? (isDark ? Brightness.dark : Brightness.light); |
| SystemUiOverlayStyle overlayStyle; |
| switch (newBrightness) { |
| case Brightness.dark: |
| overlayStyle = SystemUiOverlayStyle.light; |
| break; |
| case Brightness.light: |
| default: |
| overlayStyle = SystemUiOverlayStyle.dark; |
| break; |
| } |
| 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.brightness, |
| 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): https://github.com/flutter/flutter/issues/10469 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.brightness} |
| /// 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} |
| final Brightness brightness; |
| |
| /// {@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. ' |
| 'This feature was deprecated after v1.1.2.' |
| ) |
| 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 shouldFullyObstruct(BuildContext context) { |
| final Color backgroundColor = CupertinoDynamicColor.resolve(this.backgroundColor, context) |
| ?? CupertinoTheme.of(context).barBackgroundColor; |
| return 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 = |
| CupertinoDynamicColor.resolve(widget.backgroundColor, context) ?? 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, |
| brightness: widget.brightness, |
| child: DefaultTextStyle( |
| style: CupertinoTheme.of(context).textTheme.textStyle, |
| child: _PersistentNavigationBar( |
| components: components, |
| padding: widget.padding, |
| ), |
| ), |
| ); |
| |
| final Color actionsForegroundColor = CupertinoDynamicColor.resolve( |
| widget.actionsForegroundColor, // ignore: deprecated_member_use_from_same_package |
| context, |
| ); |
| if (!widget.transitionBetweenRoutes || !_isTransitionable(context)) { |
| // Lint ignore to maintain backward compatibility. |
| return _wrapActiveColor(actionsForegroundColor, context, navBar); |
| } |
| |
| return _wrapActiveColor( |
| // Lint ignore to maintain backward compatibility. |
| actionsForegroundColor, |
| 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.brightness, |
| 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.brightness} |
| final Brightness brightness; |
| |
| /// {@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. ' |
| 'This feature was deprecated after v1.1.2.' |
| ) |
| 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 = CupertinoDynamicColor.resolve(widget.actionsForegroundColor, context) // ignore: deprecated_member_use_from_same_package |
| ?? CupertinoTheme.of(context).primaryColor; |
| |
| 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. |
| actionsForegroundColor, |
| 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: CupertinoDynamicColor.resolve(widget.backgroundColor, context) ?? CupertinoTheme.of(context).barBackgroundColor, |
| brightness: widget.brightness, |
| 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.brightness, |
| @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 Brightness brightness; |
| 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: CupertinoDynamicColor.resolve(backgroundColor, context), |
| brightness: brightness, |
| 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: CupertinoDynamicColor.resolve(backgroundColor, context), |
| 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({ |
| Key key, |
| this.color, |
| this.previousPageTitle, |
| this.onPressed, |
| }) : _backChevron = null, |
| _backLabel = null, |
| super(key: key); |
| |
| // 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 |
| /// via [SystemNavigator] instead of Flutter's [Navigator] in add-to-app |
| /// situations. |
| /// |
| /// 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: CupertinoDynamicColor.resolve(color, context)); |
| } |
| |
| 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 as CupertinoPageRoute<dynamic>; |
| // 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() as RenderBox; |
| 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() as RenderBox; |
| 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() as RenderBox; |
| final RenderBox toBox = toKey.currentContext.findRenderObject() as RenderBox; |
| |
| // 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 as KeyedSubtree; |
| |
| 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 as KeyedSubtree; |
| |
| 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 as KeyedSubtree; |
| |
| 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 as KeyedSubtree; |
| final KeyedSubtree topBackLabel = topComponents.backLabelKey.currentWidget as KeyedSubtree; |
| final KeyedSubtree topLeading = topComponents.leadingKey.currentWidget as KeyedSubtree; |
| |
| // 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 as KeyedSubtree; |
| final KeyedSubtree topBackLabel = topComponents.backLabelKey.currentWidget as KeyedSubtree; |
| final KeyedSubtree topLeading = topComponents.leadingKey.currentWidget as KeyedSubtree; |
| |
| 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 as KeyedSubtree; |
| |
| 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 as KeyedSubtree; |
| |
| 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 as KeyedSubtree; |
| final KeyedSubtree bottomBackChevron = bottomComponents.backChevronKey.currentWidget as KeyedSubtree; |
| |
| 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() as RenderBox; |
| 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 as KeyedSubtree; |
| final KeyedSubtree bottomLargeTitle = bottomComponents.largeTitleKey.currentWidget as KeyedSubtree; |
| final KeyedSubtree topBackLabel = topComponents.backLabelKey.currentWidget as KeyedSubtree; |
| |
| if (topBackLabel == null) { |
| return null; |
| } |
| |
| final RenderAnimatedOpacity topBackLabelOpacity = |
| topComponents.backLabelKey.currentContext?.findAncestorRenderObjectOfType<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 as KeyedSubtree; |
| |
| 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 as KeyedSubtree; |
| |
| 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 as KeyedSubtree; |
| |
| 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 as Hero; |
| final Hero toHeroWidget = toHeroContext.widget as Hero; |
| |
| assert(fromHeroWidget.child is _TransitionableNavigationBar); |
| assert(toHeroWidget.child is _TransitionableNavigationBar); |
| |
| final _TransitionableNavigationBar fromNavBar = fromHeroWidget.child as _TransitionableNavigationBar; |
| final _TransitionableNavigationBar toNavBar = toHeroWidget.child as _TransitionableNavigationBar; |
| |
| 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; |
| }; |