using System; using System.Collections.Generic; using Unity.UIWidgets.animation; using Unity.UIWidgets.foundation; using Unity.UIWidgets.painting; using Unity.UIWidgets.rendering; using Unity.UIWidgets.service; using Unity.UIWidgets.ui; using Unity.UIWidgets.widgets; using UnityEngine; using Color = Unity.UIWidgets.ui.Color; using Rect = Unity.UIWidgets.ui.Rect; using TextStyle = Unity.UIWidgets.painting.TextStyle; using Brightness = Unity.UIWidgets.ui.Brightness; using Transform = Unity.UIWidgets.widgets.Transform; namespace Unity.UIWidgets.cupertino { class NavBarUtils { public const float _kNavBarPersistentHeight = 44.0f; public const float _kNavBarLargeTitleHeightExtension = 52.0f; public const float _kNavBarShowLargeTitleThreshold = 10.0f; public const float _kNavBarEdgePadding = 16.0f; public const float _kNavBarBackButtonTapWidth = 50.0f; public static readonly TimeSpan _kNavBarTitleFadeDuration = TimeSpan.FromMilliseconds(150); public static readonly Color _kDefaultNavBarBorderColor = new Color(0x4C000000); public static readonly Border _kDefaultNavBarBorder = new Border( bottom: new BorderSide( color: _kDefaultNavBarBorderColor, width: 0.0f, // One physical pixel. style: BorderStyle.solid ) ); public static readonly _HeroTag _defaultHeroTag = new _HeroTag(null); public static Widget _wrapWithBackground( Border border = null, Color backgroundColor = null, Brightness? brightness = null, Widget child = null, bool updateSystemUiOverlay = true ) { Widget result = child; if (updateSystemUiOverlay) { bool isDark = backgroundColor.computeLuminance() < 0.179; 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 = new AnnotatedRegion( value: overlayStyle, sized: true, child: result ); } DecoratedBox childWithBackground = new DecoratedBox( decoration: new BoxDecoration( border: border, color: backgroundColor ), child: result ); if (backgroundColor.alpha == 0xFF) { return childWithBackground; } return new ClipRect( child: new BackdropFilter( filter: ImageFilter.blur(sigmaX: 10.0f, sigmaY: 10.0f), child: childWithBackground ) ); } public static Widget _wrapActiveColor(Color color, BuildContext context, Widget child) { if (color == null) { return child; } return new CupertinoTheme( data: CupertinoTheme.of(context).copyWith(primaryColor: color), child: child ); } public static bool _isTransitionable(BuildContext context) { ModalRoute route = ModalRoute.of(context); return route is PageRoute && !(route as PageRoute).fullscreenDialog; } public static HeroFlightShuttleBuilder _navBarHeroFlightShuttleBuilder = ( BuildContext flightContext, Animation animation, HeroFlightDirection flightDirection, BuildContext fromHeroContext, BuildContext toHeroContext ) => { D.assert(animation != null); D.assert(fromHeroContext != null); D.assert(toHeroContext != null); D.assert(fromHeroContext.widget is Hero); D.assert(toHeroContext.widget is Hero); Hero fromHeroWidget = (Hero) fromHeroContext.widget; Hero toHeroWidget = (Hero) toHeroContext.widget; D.assert(fromHeroWidget.child is _TransitionableNavigationBar); D.assert(toHeroWidget.child is _TransitionableNavigationBar); _TransitionableNavigationBar fromNavBar = (_TransitionableNavigationBar) fromHeroWidget.child; _TransitionableNavigationBar toNavBar = (_TransitionableNavigationBar) toHeroWidget.child; D.assert(fromNavBar.componentsKeys != null); D.assert(toNavBar.componentsKeys != null); D.assert( fromNavBar.componentsKeys.navBarBoxKey.currentContext.owner != null, () => "The from nav bar to Hero must have been mounted in the previous frame" ); D.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 new _NavigationBarTransition( animation: animation, bottomNavBar: fromNavBar, topNavBar: toNavBar ); case HeroFlightDirection.pop: return new _NavigationBarTransition( animation: animation, bottomNavBar: toNavBar, topNavBar: fromNavBar ); } return null; }; public static CreateRectTween _linearTranslateWithLargestRectSizeTween = (Rect begin, Rect end) => { Size largestSize = new Size( Mathf.Max(begin.size.width, end.size.width), Mathf.Max(begin.size.height, end.size.height) ); return new RectTween( begin: begin.topLeft & largestSize, end: end.topLeft & largestSize ); }; public static HeroPlaceholderBuilder _navBarHeroLaunchPadBuilder = ( BuildContext context, Size heroSize, Widget child ) => { D.assert(child is _TransitionableNavigationBar); return new Visibility( maintainSize: true, maintainAnimation: true, maintainState: true, visible: false, child: child ); }; } class _HeroTag { public _HeroTag( NavigatorState navigator ) { this.navigator = navigator; } public readonly NavigatorState navigator; public override string ToString() { return $"Default Hero tag for Cupertino navigation bars with navigator {navigator}"; } public bool Equals(_HeroTag other) { if (ReferenceEquals(null, other)) { return false; } if (ReferenceEquals(this, other)) { return true; } return other is _HeroTag && navigator == other.navigator; } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } if (ReferenceEquals(this, obj)) { return true; } if (obj.GetType() != GetType()) { return false; } return Equals((_HeroTag) obj); } public override int GetHashCode() { return navigator.GetHashCode(); } public static bool operator ==(_HeroTag left, _HeroTag right) { return Equals(left, right); } public static bool operator !=(_HeroTag left, _HeroTag right) { return !Equals(left, right); } } public class CupertinoNavigationBar : ObstructingPreferredSizeWidget { public CupertinoNavigationBar( Key key = null, Widget leading = null, bool automaticallyImplyLeading = true, bool automaticallyImplyMiddle = true, string previousPageTitle = null, Widget middle = null, Widget trailing = null, Border border = null, Color backgroundColor = null, Brightness? brightness = null, EdgeInsets padding = null, Color actionsForegroundColor = null, bool transitionBetweenRoutes = true, object heroTag = null ) : base(key: key) { this.leading = leading; this.automaticallyImplyLeading = automaticallyImplyLeading; this.automaticallyImplyMiddle = automaticallyImplyMiddle; this.previousPageTitle = previousPageTitle; this.middle = middle; this.trailing = trailing; this.border = border ?? NavBarUtils._kDefaultNavBarBorder; this.backgroundColor = backgroundColor; this.brightness = brightness; this.padding = padding; this.actionsForegroundColor = actionsForegroundColor; this.transitionBetweenRoutes = transitionBetweenRoutes; this.heroTag = heroTag ?? NavBarUtils._defaultHeroTag; D.assert( this.heroTag != null, () => "heroTag cannot be null. Use transitionBetweenRoutes = false to " + "disable Hero transition on this navigation bar." ); D.assert( !transitionBetweenRoutes || ReferenceEquals(this.heroTag, NavBarUtils._defaultHeroTag), () => "Cannot specify a heroTag override if this navigation bar does not " + "transition due to transitionBetweenRoutes = false." ); } public readonly Widget leading; public readonly bool automaticallyImplyLeading; public readonly bool automaticallyImplyMiddle; public readonly string previousPageTitle; public readonly Widget middle; public readonly Widget trailing; public readonly Brightness? brightness; public readonly Color backgroundColor; public readonly EdgeInsets padding; public readonly Border border; public readonly Color actionsForegroundColor; public readonly bool transitionBetweenRoutes; public readonly object heroTag; public override bool shouldFullyObstruct(BuildContext context) { Color backgroundColor = CupertinoDynamicColor.resolve(this.backgroundColor, context) ?? CupertinoTheme.of(context).barBackgroundColor; return backgroundColor.alpha == 0xFF; } public override Size preferredSize { get { return Size.fromHeight(NavBarUtils._kNavBarPersistentHeight); } } public override State createState() { return new _CupertinoNavigationBarState(); } } class _CupertinoNavigationBarState : State { _NavigationBarStaticComponentsKeys keys; public override void initState() { base.initState(); keys = new _NavigationBarStaticComponentsKeys(); } public override Widget build(BuildContext context) { Color backgroundColor = CupertinoDynamicColor.resolve(widget.backgroundColor, context) ?? CupertinoTheme.of(context).barBackgroundColor; _NavigationBarStaticComponents components = new _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 ); Widget navBar = NavBarUtils._wrapWithBackground( border: widget.border, backgroundColor: backgroundColor, brightness:widget.brightness, child: new DefaultTextStyle( style: CupertinoTheme.of(context).textTheme.textStyle, child: new _PersistentNavigationBar( components: components, padding: widget.padding ) ) ); Color actionsForegroundColor = CupertinoDynamicColor.resolve( widget.actionsForegroundColor, // ignore: deprecated_member_use_from_same_package context ); if (!widget.transitionBetweenRoutes || !NavBarUtils._isTransitionable(context)) { return NavBarUtils._wrapActiveColor(actionsForegroundColor, context, navBar); } return NavBarUtils._wrapActiveColor( actionsForegroundColor, context, new Builder( builder: (BuildContext _context) => { return new Hero( tag: widget.heroTag as _HeroTag == NavBarUtils._defaultHeroTag ? new _HeroTag(Navigator.of(_context)) : widget.heroTag, createRectTween: NavBarUtils._linearTranslateWithLargestRectSizeTween, placeholderBuilder: NavBarUtils._navBarHeroLaunchPadBuilder, flightShuttleBuilder: NavBarUtils._navBarHeroFlightShuttleBuilder, transitionOnUserGestures: true, child: new _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 ) ); } ) ); } } public class CupertinoSliverNavigationBar : StatefulWidget { public CupertinoSliverNavigationBar( Key key = null, Widget largeTitle = null, Widget leading = null, bool automaticallyImplyLeading = true, bool automaticallyImplyTitle = true, string previousPageTitle = null, Widget middle = null, Widget trailing = null, Border border = null, Color backgroundColor = null, Brightness? brightness = null, EdgeInsets padding = null, Color actionsForegroundColor = null, bool transitionBetweenRoutes = true, object heroTag = null ) : base(key: key) { D.assert( automaticallyImplyTitle == true || largeTitle != null, () => "No largeTitle has been provided but automaticallyImplyTitle is also " + "false. Either provide a largeTitle or set automaticallyImplyTitle to " + "true." ); this.largeTitle = largeTitle; this.leading = leading; this.automaticallyImplyLeading = automaticallyImplyLeading; this.automaticallyImplyTitle = automaticallyImplyTitle; this.previousPageTitle = previousPageTitle; this.middle = middle; this.trailing = trailing; this.border = border ?? NavBarUtils._kDefaultNavBarBorder; this.backgroundColor = backgroundColor; this.brightness = brightness; this.padding = padding; this.actionsForegroundColor = actionsForegroundColor; this.transitionBetweenRoutes = transitionBetweenRoutes; this.heroTag = heroTag ?? NavBarUtils._defaultHeroTag; } public readonly Widget largeTitle; public readonly Widget leading; public readonly bool automaticallyImplyLeading; public readonly bool automaticallyImplyTitle; public readonly string previousPageTitle; public readonly Widget middle; public readonly Widget trailing; public readonly Color backgroundColor; public readonly Brightness? brightness; public readonly EdgeInsets padding; public readonly Border border; public readonly Color actionsForegroundColor; public readonly bool transitionBetweenRoutes; public readonly object heroTag; public bool opaque { get { return backgroundColor.alpha == 0xFF; } } public override State createState() { return new _CupertinoSliverNavigationBarState(); } } class _CupertinoSliverNavigationBarState : State { _NavigationBarStaticComponentsKeys keys; public override void initState() { base.initState(); keys = new _NavigationBarStaticComponentsKeys(); } public override Widget build(BuildContext context) { Color actionsForegroundColor = CupertinoDynamicColor.resolve(widget.actionsForegroundColor, context) // ignore: deprecated_member_use_from_same_package ?? CupertinoTheme.of(context).primaryColor; _NavigationBarStaticComponents components = new _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 NavBarUtils._wrapActiveColor( actionsForegroundColor, context, new MediaQuery( data: MediaQuery.of(context).copyWith(textScaleFactor: 1), child: new SliverPersistentHeader( pinned: true, // iOS navigation bars are always pinned. del: new _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: NavBarUtils._kNavBarPersistentHeight + MediaQuery.of(context).padding.top, alwaysShowMiddle: widget.middle != null ) ) ) ); } } class _LargeTitleNavigationBarSliverDelegate : SliverPersistentHeaderDelegate { public _LargeTitleNavigationBarSliverDelegate( _NavigationBarStaticComponentsKeys keys = null, _NavigationBarStaticComponents components = null, Widget userMiddle = null, Color backgroundColor = null, Brightness? brightness = null, Border border = null, EdgeInsets padding = null, Color actionsForegroundColor = null, bool transitionBetweenRoutes = false, object heroTag = null, float persistentHeight = 0.0f, bool alwaysShowMiddle =false ) { this.keys = keys; this.components = components; this.userMiddle = userMiddle; this.backgroundColor = backgroundColor; this.border = border; this.brightness = brightness; this.padding = padding; this.actionsForegroundColor = actionsForegroundColor; this.transitionBetweenRoutes = transitionBetweenRoutes; this.heroTag = heroTag; this.persistentHeight = persistentHeight; this.alwaysShowMiddle = alwaysShowMiddle; } public readonly _NavigationBarStaticComponentsKeys keys; public readonly _NavigationBarStaticComponents components; public readonly Widget userMiddle; public readonly Color backgroundColor; public readonly Border border; public readonly Brightness? brightness; public readonly EdgeInsets padding; public readonly Color actionsForegroundColor; public readonly bool transitionBetweenRoutes; public readonly object heroTag; public readonly float persistentHeight; public readonly bool alwaysShowMiddle; public override float? minExtent { get { return persistentHeight; } } public override float? maxExtent { get { return persistentHeight + NavBarUtils._kNavBarLargeTitleHeightExtension; } } public override Widget build(BuildContext context, float shrinkOffset, bool overlapsContent) { bool showLargeTitle = shrinkOffset < maxExtent - minExtent - NavBarUtils._kNavBarShowLargeTitleThreshold; _PersistentNavigationBar persistentNavigationBar = new _PersistentNavigationBar( components: components, padding: padding, middleVisible: alwaysShowMiddle ? null : (bool?) !showLargeTitle ); Widget navBar = NavBarUtils._wrapWithBackground( border: border, backgroundColor: CupertinoDynamicColor.resolve(backgroundColor, context), brightness: brightness, child: new DefaultTextStyle( style: CupertinoTheme.of(context).textTheme.textStyle, child: new Stack( fit: StackFit.expand, children: new List { new Positioned( top: persistentHeight, left: 0.0f, right: 0.0f, bottom: 0.0f, child: new ClipRect( child: new OverflowBox( minHeight: 0.0f, maxHeight: float.PositiveInfinity, alignment: AlignmentDirectional.bottomStart, child: new Padding( padding: EdgeInsets.only( left: NavBarUtils._kNavBarEdgePadding, bottom: 8.0f ), child: new SafeArea( top: false, bottom: false, child: new AnimatedOpacity( opacity: showLargeTitle ? 1.0f : 0.0f, duration: NavBarUtils._kNavBarTitleFadeDuration, child: new DefaultTextStyle( style: CupertinoTheme.of(context).textTheme .navLargeTitleTextStyle, maxLines: 1, overflow: TextOverflow.ellipsis, child: components.largeTitle ) ) ) ) ) ) ), new Positioned( left: 0.0f, right: 0.0f, top: 0.0f, child: persistentNavigationBar ) } ) ) ); if (!transitionBetweenRoutes || !NavBarUtils._isTransitionable(context)) { return navBar; } return new Hero( tag: heroTag as _HeroTag == NavBarUtils._defaultHeroTag ? new _HeroTag(Navigator.of(context)) : heroTag, createRectTween: NavBarUtils._linearTranslateWithLargestRectSizeTween, flightShuttleBuilder: NavBarUtils._navBarHeroFlightShuttleBuilder, placeholderBuilder: NavBarUtils._navBarHeroLaunchPadBuilder, transitionOnUserGestures: true, child: new _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 ) ); } public override bool shouldRebuild(SliverPersistentHeaderDelegate _oldDelegate) { _LargeTitleNavigationBarSliverDelegate oldDelegate = _oldDelegate as _LargeTitleNavigationBarSliverDelegate; 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; } } class _PersistentNavigationBar : StatelessWidget { public _PersistentNavigationBar( Key key = null, _NavigationBarStaticComponents components = null, EdgeInsets padding = null, bool? middleVisible = null ) : base(key: key) { this.components = components; this.padding = padding; this.middleVisible = middleVisible ?? true; } public readonly _NavigationBarStaticComponents components; public readonly EdgeInsets padding; public readonly bool middleVisible; public override Widget build(BuildContext context) { Widget middle = components.middle; if (middle != null) { middle = new DefaultTextStyle( style: CupertinoTheme.of(context).textTheme.navTitleTextStyle, child: middle ); middle = middleVisible == null ? middle : new AnimatedOpacity( opacity: middleVisible ? 1.0f : 0.0f, duration: NavBarUtils._kNavBarTitleFadeDuration, child: middle ); } Widget leading = components.leading; Widget backChevron = components.backChevron; Widget backLabel = components.backLabel; if (leading == null && backChevron != null && backLabel != null) { leading = CupertinoNavigationBarBackButton._assemble( backChevron, backLabel ); } Widget paddedToolbar = new NavigationToolbar( leading: leading, middle: middle, trailing: components.trailing, centerMiddle: true, middleSpacing: 6.0f ); if (padding != null) { paddedToolbar = new Padding( padding: EdgeInsets.only( top: padding.top, bottom: padding.bottom ), child: paddedToolbar ); } return new SizedBox( height: NavBarUtils._kNavBarPersistentHeight + MediaQuery.of(context).padding.top, child: new SafeArea( bottom: false, child: paddedToolbar ) ); } } class _NavigationBarStaticComponentsKeys { public _NavigationBarStaticComponentsKeys() { navBarBoxKey = GlobalKey.key(debugLabel: "Navigation bar render box"); leadingKey = GlobalKey.key(debugLabel: "Leading"); backChevronKey = GlobalKey.key(debugLabel: "Back chevron"); backLabelKey = GlobalKey.key(debugLabel: "Back label"); middleKey = GlobalKey.key(debugLabel: "Middle"); trailingKey = GlobalKey.key(debugLabel: "Trailing"); largeTitleKey = GlobalKey.key(debugLabel: "Large title"); } public readonly GlobalKey navBarBoxKey; public readonly GlobalKey leadingKey; public readonly GlobalKey backChevronKey; public readonly GlobalKey backLabelKey; public readonly GlobalKey middleKey; public readonly GlobalKey trailingKey; public readonly GlobalKey largeTitleKey; } class _NavigationBarStaticComponents { public _NavigationBarStaticComponents( bool automaticallyImplyLeading, bool automaticallyImplyTitle, bool large, _NavigationBarStaticComponentsKeys keys = null, ModalRoute route = null, Widget userLeading = null, string previousPageTitle = null, Widget userMiddle = null, Widget userTrailing = null, Widget userLargeTitle = null, EdgeInsets padding = null ) { 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 currentRoute = null ) { if (automaticallyImplyTitle && currentRoute is CupertinoPageRoute route && ((CupertinoPageRoute)route).title != null) { return new Text(route.title); } return null; } public readonly KeyedSubtree leading; static KeyedSubtree createLeading( bool automaticallyImplyLeading, GlobalKey leadingKey = null, Widget userLeading = null, ModalRoute route = null, EdgeInsets padding = null ) { Widget leadingContent = null; if (userLeading != null) { leadingContent = userLeading; } else if ( automaticallyImplyLeading && route is PageRoute pageRoute && route.canPop && pageRoute.fullscreenDialog ) { leadingContent = new CupertinoButton( child: new Text("Close"), padding: EdgeInsets.zero, onPressed: () => { route.navigator.maybePop(); } ); } if (leadingContent == null) { return null; } return new KeyedSubtree( key: leadingKey, child: new Padding( padding: EdgeInsets.only( left: padding?.left ?? NavBarUtils._kNavBarEdgePadding ), child: IconTheme.merge( data: new IconThemeData( size: 32.0f ), child: leadingContent ) ) ); } public readonly KeyedSubtree backChevron; static KeyedSubtree createBackChevron( bool automaticallyImplyLeading, GlobalKey backChevronKey = null, Widget userLeading = null, ModalRoute route = null ) { if ( userLeading != null || !automaticallyImplyLeading || route == null || !route.canPop || (route is PageRoute pageRoute && pageRoute.fullscreenDialog) ) { return null; } return new KeyedSubtree(key: backChevronKey, child: new _BackChevron()); } public readonly KeyedSubtree backLabel; static KeyedSubtree createBackLabel( bool automaticallyImplyLeading, GlobalKey backLabelKey = null, Widget userLeading = null, ModalRoute route = null, string previousPageTitle = null ) { if ( userLeading != null || !automaticallyImplyLeading || route == null || !route.canPop || (route is PageRoute pageRoute && pageRoute.fullscreenDialog) ) { return null; } return new KeyedSubtree( key: backLabelKey, child: new _BackLabel( specifiedPreviousTitle: previousPageTitle, route: route ) ); } public readonly KeyedSubtree middle; static KeyedSubtree createMiddle( bool large, bool automaticallyImplyTitle, GlobalKey middleKey = null, Widget userMiddle = null, Widget userLargeTitle = null, ModalRoute route = null ) { Widget middleContent = userMiddle; if (large) { middleContent = middleContent ?? userLargeTitle; } middleContent = middleContent ?? _derivedTitle(automaticallyImplyTitle: automaticallyImplyTitle, currentRoute: route); if (middleContent == null) { return null; } return new KeyedSubtree( key: middleKey, child: middleContent ); } public readonly KeyedSubtree trailing; static KeyedSubtree createTrailing( GlobalKey trailingKey = null, Widget userTrailing = null, EdgeInsets padding = null ) { if (userTrailing == null) { return null; } return new KeyedSubtree( key: trailingKey, child: new Padding( padding: EdgeInsets.only( right: padding?.right ?? NavBarUtils._kNavBarEdgePadding ), child: IconTheme.merge( data: new IconThemeData( size: 32.0f ), child: userTrailing ) ) ); } public readonly KeyedSubtree largeTitle; static KeyedSubtree createLargeTitle( bool large, bool automaticImplyTitle, GlobalKey largeTitleKey = null, Widget userLargeTitle = null, ModalRoute route = null ) { if (!large) { return null; } Widget largeTitleContent = userLargeTitle ?? _derivedTitle( automaticallyImplyTitle: automaticImplyTitle, currentRoute: route); D.assert( largeTitleContent != null, () => "largeTitle was not provided and there was no title from the route." ); return new KeyedSubtree( key: largeTitleKey, child: largeTitleContent ); } } public class CupertinoNavigationBarBackButton : StatelessWidget { public CupertinoNavigationBarBackButton( Key key = null, Color color = null, string previousPageTitle = null, VoidCallback onPressed = null ):base(key:key) { _backChevron = null; _backLabel = null; this.color = color; this.previousPageTitle = previousPageTitle; this.onPressed = onPressed; } internal CupertinoNavigationBarBackButton( Widget backChevron, Widget backLabel, Color color = null, string previousPageTitle = null, VoidCallback onPressed = null ) { _backChevron = backChevron; _backLabel = backLabel; this.color = color; this.previousPageTitle = previousPageTitle; this.onPressed = onPressed; } public static CupertinoNavigationBarBackButton _assemble( Widget _backChevron, Widget _backLabel ) { return new CupertinoNavigationBarBackButton( backChevron:_backChevron, backLabel : _backLabel ); } public readonly Color color; public readonly string previousPageTitle; public readonly Widget _backChevron; public readonly Widget _backLabel; public readonly VoidCallback onPressed; public override Widget build(BuildContext context) { ModalRoute currentRoute = ModalRoute.of(context); if (onPressed == null) { D.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 new CupertinoButton( child: new DefaultTextStyle( style: actionTextStyle, child: new ConstrainedBox( constraints: new BoxConstraints(minWidth: NavBarUtils._kNavBarBackButtonTapWidth), child: new Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, children: new List { new Padding(padding: EdgeInsets.only(left: 8.0f)), _backChevron ?? new _BackChevron(), new Padding(padding: EdgeInsets.only(left: 6.0f)), new Flexible( child: _backLabel ?? new _BackLabel( specifiedPreviousTitle: previousPageTitle, route: currentRoute ) ) } ) ) ), padding: EdgeInsets.zero, onPressed: () => { if (onPressed != null) { onPressed(); } else { Navigator.maybePop(context); } } ); } } class _BackChevron : StatelessWidget { public _BackChevron(Key key = null) : base(key: key) { } public override Widget build(BuildContext context) { TextDirection textDirection = Directionality.of(context); TextStyle textStyle = DefaultTextStyle.of(context).style; Widget iconWidget = Text.rich( new TextSpan( text:new string(new[] {(char) CupertinoIcons.back.codePoint}), style: new TextStyle( inherit: false, color: textStyle.color, fontSize: 34.0f, fontFamily: CupertinoIcons.back.fontFamily //package: CupertinoIcons.back.fontPackage ) ) ); var matrix = Matrix4.identity(); matrix.scale(-1.0f, 1.0f, 1.0f); switch (textDirection) { case TextDirection.rtl: iconWidget = new Transform( transform: matrix, alignment: Alignment.center, transformHitTests: false, child: iconWidget ) as Widget; break; case TextDirection.ltr: break; } return iconWidget; } } class _BackLabel : StatelessWidget { public _BackLabel( Key key = null, string specifiedPreviousTitle = null, ModalRoute route = null ) : base(key: key) { this.specifiedPreviousTitle = specifiedPreviousTitle; this.route = route; } public readonly string specifiedPreviousTitle; public readonly ModalRoute route; Widget _buildPreviousTitleWidget(BuildContext context, string previousTitle, Widget child) { if (previousTitle == null) { return new SizedBox(height: 0.0f, width: 0.0f); } Text textWidget = new Text( previousTitle, maxLines: 1, overflow: TextOverflow.ellipsis ); if (previousTitle.Length > 12) { textWidget = new Text("Back"); } return new Align( alignment: AlignmentDirectional.centerStart, widthFactor: 1.0f, child: textWidget ); } public override Widget build(BuildContext context) { if (specifiedPreviousTitle != null) { return _buildPreviousTitleWidget(context, specifiedPreviousTitle, null); } else if (route is CupertinoPageRoute && !route.isFirst) { CupertinoPageRoute cupertinoRoute = route as CupertinoPageRoute; return new ValueListenableBuilder( valueListenable: cupertinoRoute.previousTitle, builder: _buildPreviousTitleWidget ); } else { return new SizedBox(height: 0.0f, width: 0.0f); } } } class _TransitionableNavigationBar : StatelessWidget { public _TransitionableNavigationBar( _NavigationBarStaticComponentsKeys componentsKeys = null, Color backgroundColor = null, TextStyle backButtonTextStyle = null, TextStyle titleTextStyle = null, TextStyle largeTitleTextStyle = null, Border border = null, bool? hasUserMiddle = null, bool? largeExpanded = null, Widget child = null ) : base(key: componentsKeys.navBarBoxKey) { D.assert(largeExpanded != null); D.assert(!largeExpanded.Value || largeTitleTextStyle != null); this.componentsKeys = componentsKeys; this.backgroundColor = backgroundColor; this.backButtonTextStyle = backButtonTextStyle; this.titleTextStyle = titleTextStyle; this.largeTitleTextStyle = largeTitleTextStyle; this.border = border; this.hasUserMiddle = hasUserMiddle; this.largeExpanded = largeExpanded; this.child = child; } public readonly _NavigationBarStaticComponentsKeys componentsKeys; public readonly Color backgroundColor; public readonly TextStyle backButtonTextStyle; public readonly TextStyle titleTextStyle; public readonly TextStyle largeTitleTextStyle; public readonly Border border; public readonly bool? hasUserMiddle; public readonly bool? largeExpanded; public readonly Widget child; public RenderBox renderBox { get { RenderBox box = componentsKeys.navBarBoxKey.currentContext.findRenderObject() as RenderBox; D.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; } } public override Widget build(BuildContext context) { D.assert(() => { bool? inHero = null; context.visitAncestorElements((Element ancestor) => { if (ancestor is ComponentElement) { D.assert( ancestor.widget.GetType() != typeof(_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.GetType() == typeof(Hero)) { inHero = true; } } inHero = inHero ?? false; return true; }); D.assert( inHero == true, () => "_TransitionableNavigationBar should only be added as the immediate " + "child of Hero widgets." ); return true; }); return child; } } class _NavigationBarTransition : StatelessWidget { public _NavigationBarTransition( Animation animation = null, _TransitionableNavigationBar topNavBar = null, _TransitionableNavigationBar bottomNavBar = null ) { this.animation = animation; this.topNavBar = topNavBar; this.bottomNavBar = bottomNavBar; heightTween = new FloatTween( begin: bottomNavBar.renderBox.size.height, end: topNavBar.renderBox.size.height ); backgroundTween = new ColorTween( begin: bottomNavBar.backgroundColor, end: topNavBar.backgroundColor ); borderTween = new BorderTween( begin: bottomNavBar.border, end: topNavBar.border ); } public readonly Animation animation; public readonly _TransitionableNavigationBar topNavBar; public readonly _TransitionableNavigationBar bottomNavBar; public readonly FloatTween heightTween; public readonly ColorTween backgroundTween; public readonly BorderTween borderTween; public override Widget build(BuildContext context) { _NavigationBarComponentsTransition componentsTransition = new _NavigationBarComponentsTransition( animation: animation, bottomNavBar: bottomNavBar, topNavBar: topNavBar, directionality: Directionality.of(context) ); List children = new List { new AnimatedBuilder( animation: animation, builder: (BuildContext _context, Widget child) => { return NavBarUtils._wrapWithBackground( updateSystemUiOverlay: false, backgroundColor: backgroundTween.evaluate(animation), border: borderTween.evaluate(animation), child: new SizedBox( height: heightTween.evaluate(animation), width: float.PositiveInfinity ) ); } ), componentsTransition.bottomBackChevron, componentsTransition.bottomBackLabel, componentsTransition.bottomLeading, componentsTransition.bottomMiddle, componentsTransition.bottomLargeTitle, componentsTransition.bottomTrailing, componentsTransition.topLeading, componentsTransition.topBackChevron, componentsTransition.topBackLabel, componentsTransition.topMiddle, componentsTransition.topLargeTitle, componentsTransition.topTrailing }; children.RemoveAll((Widget child) => child == null); return new SizedBox( height: Mathf.Max(heightTween.begin, heightTween.end) + MediaQuery.of(context).padding.top, width: float.PositiveInfinity, child: new Stack( children: children ) ); } } class _NavigationBarComponentsTransition { public _NavigationBarComponentsTransition( Animation animation = null, _TransitionableNavigationBar bottomNavBar = null, _TransitionableNavigationBar topNavBar = null, TextDirection? directionality = null ) { this.animation = animation; 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 = bottomNavBar.renderBox.paintBounds.expandToInclude(topNavBar.renderBox.paintBounds); forwardDirection = directionality == TextDirection.ltr ? 1.0f : -1.0f; } public static Animatable fadeOut = new FloatTween( begin: 1.0f, end: 0.0f ); public static Animatable fadeIn = new FloatTween( begin: 0.0f, end: 1.0f ); public readonly Animation animation; public readonly _NavigationBarStaticComponentsKeys bottomComponents; public readonly _NavigationBarStaticComponentsKeys topComponents; public readonly RenderBox bottomNavBarBox; public readonly RenderBox topNavBarBox; public readonly TextStyle bottomBackButtonTextStyle; public readonly TextStyle topBackButtonTextStyle; public readonly TextStyle bottomTitleTextStyle; public readonly TextStyle topTitleTextStyle; public readonly TextStyle bottomLargeTitleTextStyle; public readonly TextStyle topLargeTitleTextStyle; public readonly bool? bottomHasUserMiddle; public readonly bool? topHasUserMiddle; public readonly bool? bottomLargeExpanded; public readonly bool? topLargeExpanded; public readonly Rect transitionBox; public readonly float forwardDirection; public RelativeRect positionInTransitionBox( GlobalKey key = null, RenderBox from = null ) { RenderBox componentBox = key.currentContext.findRenderObject() as RenderBox; D.assert(componentBox.attached); return RelativeRect.fromRect( componentBox.localToGlobal(Offset.zero, ancestor: from) & componentBox.size, transitionBox ); } public RelativeRectTween slideFromLeadingEdge( GlobalKey fromKey = null, RenderBox fromNavBarBox = null, GlobalKey toKey = null, RenderBox toNavBarBox = null ) { RelativeRect fromRect = positionInTransitionBox(fromKey, from: fromNavBarBox); RenderBox fromBox = fromKey.currentContext.findRenderObject() as RenderBox; RenderBox toBox = toKey.currentContext.findRenderObject() as RenderBox; Rect toRect = toBox.localToGlobal( Offset.zero, ancestor: toNavBarBox ).translate( 0.0f, - fromBox.size.height / 2 + toBox.size.height / 2 ) & fromBox.size; // Keep the from render object"s size. if (forwardDirection < 0) { toRect = toRect.translate(-fromBox.size.width + toBox.size.width, 0.0f); } return new RelativeRectTween( begin: fromRect, end: RelativeRect.fromRect(toRect, transitionBox) ); } public Animation fadeInFrom(float t, Curve curve = null) { return animation.drive(fadeIn.chain( new CurveTween(curve: new Interval(t, 1.0f, curve: curve ?? Curves.easeIn)) )); } public Animation fadeOutBy(float t, Curve curve = null) { return animation.drive(fadeOut.chain( new CurveTween(curve: new Interval(0.0f, t, curve: curve ?? Curves.easeOut)) )); } public Widget bottomLeading { get { KeyedSubtree bottomLeading = bottomComponents.leadingKey.currentWidget as KeyedSubtree; if (bottomLeading == null) { return null; } return Positioned.fromRelativeRect( rect: positionInTransitionBox(bottomComponents.leadingKey, from: bottomNavBarBox), child: new FadeTransition( opacity: fadeOutBy(0.4f), child: bottomLeading.child ) ); } } public Widget bottomBackChevron { get { KeyedSubtree bottomBackChevron = bottomComponents.backChevronKey.currentWidget as KeyedSubtree; if (bottomBackChevron == null) { return null; } return Positioned.fromRelativeRect( rect: positionInTransitionBox(bottomComponents.backChevronKey, from: bottomNavBarBox), child: new FadeTransition( opacity: fadeOutBy(0.6f), child: new DefaultTextStyle( style: bottomBackButtonTextStyle, child: bottomBackChevron.child ) ) ); } } public Widget bottomBackLabel { get { KeyedSubtree bottomBackLabel = bottomComponents.backLabelKey.currentWidget as KeyedSubtree; if (bottomBackLabel == null) { return null; } RelativeRect from = positionInTransitionBox(bottomComponents.backLabelKey, from: bottomNavBarBox); RelativeRectTween positionTween = new RelativeRectTween( begin: from, end: from.shift( new Offset(forwardDirection * (-bottomNavBarBox.size.width / 2.0f), 0.0f ) ) ); return new PositionedTransition( rect: animation.drive(positionTween), child: new FadeTransition( opacity: fadeOutBy(0.2f), child: new DefaultTextStyle( style: bottomBackButtonTextStyle, child: bottomBackLabel.child ) ) ); } } public Widget bottomMiddle { get { KeyedSubtree bottomMiddle = bottomComponents.middleKey.currentWidget as KeyedSubtree; KeyedSubtree topBackLabel = topComponents.backLabelKey.currentWidget as KeyedSubtree; KeyedSubtree topLeading = topComponents.leadingKey.currentWidget as KeyedSubtree; if (bottomHasUserMiddle != true && bottomLargeExpanded == true) { return null; } if (bottomMiddle != null && topBackLabel != null) { return new PositionedTransition( rect: animation.drive(slideFromLeadingEdge( fromKey: bottomComponents.middleKey, fromNavBarBox: bottomNavBarBox, toKey: topComponents.backLabelKey, toNavBarBox: topNavBarBox )), child: new FadeTransition( opacity: fadeOutBy(bottomHasUserMiddle == true ? 0.4f : 0.7f), child: new Align( alignment: AlignmentDirectional.centerStart, child: new DefaultTextStyleTransition( style: animation.drive(new TextStyleTween( begin: bottomTitleTextStyle, end: topBackButtonTextStyle )), child: bottomMiddle.child ) ) ) ); } if (bottomMiddle != null && topLeading != null) { return Positioned.fromRelativeRect( rect: positionInTransitionBox(bottomComponents.middleKey, from: bottomNavBarBox), child: new FadeTransition( opacity: fadeOutBy(bottomHasUserMiddle == true ? 0.4f : 0.7f), child: new DefaultTextStyle( style: bottomTitleTextStyle, child: bottomMiddle.child ) ) ); } return null; } } public Widget bottomLargeTitle { get { KeyedSubtree bottomLargeTitle = (KeyedSubtree) bottomComponents.largeTitleKey.currentWidget; KeyedSubtree topBackLabel = (KeyedSubtree) topComponents.backLabelKey.currentWidget; KeyedSubtree topLeading = (KeyedSubtree) topComponents.leadingKey.currentWidget; if (bottomLargeTitle == null || bottomLargeExpanded != true) { return null; } if (bottomLargeTitle != null && topBackLabel != null) { return new PositionedTransition( rect: animation.drive(slideFromLeadingEdge( fromKey: bottomComponents.largeTitleKey, fromNavBarBox: bottomNavBarBox, toKey: topComponents.backLabelKey, toNavBarBox: topNavBarBox )), child: new FadeTransition( opacity: fadeOutBy(0.6f), child: new Align( alignment: AlignmentDirectional.centerStart, child: new DefaultTextStyleTransition( style: animation.drive(new TextStyleTween( begin: bottomLargeTitleTextStyle, end: topBackButtonTextStyle )), maxLines: 1, overflow: TextOverflow.ellipsis, child: bottomLargeTitle.child ) ) ) ); } if (bottomLargeTitle != null && topLeading != null) { RelativeRect from = positionInTransitionBox(bottomComponents.largeTitleKey, from: bottomNavBarBox); RelativeRectTween positionTween = new RelativeRectTween( begin: from, end: from.shift( new Offset(forwardDirection * bottomNavBarBox.size.width / 4.0f, 0.0f ) ) ); return new PositionedTransition( rect: animation.drive(positionTween), child: new FadeTransition( opacity: fadeOutBy(0.4f), child: new DefaultTextStyle( style: bottomLargeTitleTextStyle, child: bottomLargeTitle.child ) ) ); } return null; } } public Widget bottomTrailing { get { KeyedSubtree bottomTrailing = (KeyedSubtree) bottomComponents.trailingKey.currentWidget; if (bottomTrailing == null) { return null; } return Positioned.fromRelativeRect( rect: positionInTransitionBox(bottomComponents.trailingKey, from: bottomNavBarBox), child: new FadeTransition( opacity: fadeOutBy(0.6f), child: bottomTrailing.child ) ); } } public Widget topLeading { get { KeyedSubtree topLeading = (KeyedSubtree) topComponents.leadingKey.currentWidget; if (topLeading == null) { return null; } return Positioned.fromRelativeRect( rect: positionInTransitionBox(topComponents.leadingKey, from: topNavBarBox), child: new FadeTransition( opacity: fadeInFrom(0.6f), child: topLeading.child ) ); } } public Widget topBackChevron { get { KeyedSubtree topBackChevron = (KeyedSubtree) topComponents.backChevronKey.currentWidget; KeyedSubtree bottomBackChevron = (KeyedSubtree) bottomComponents.backChevronKey.currentWidget; if (topBackChevron == null) { return null; } RelativeRect to = positionInTransitionBox(topComponents.backChevronKey, from: topNavBarBox); RelativeRect from = to; if (bottomBackChevron == null) { RenderBox topBackChevronBox = (RenderBox) topComponents.backChevronKey.currentContext.findRenderObject(); from = to.shift( new Offset(forwardDirection * topBackChevronBox.size.width * 2.0f, 0.0f ) ); } RelativeRectTween positionTween = new RelativeRectTween( begin: from, end: to ); return new PositionedTransition( rect: animation.drive(positionTween), child: new FadeTransition( opacity: fadeInFrom(bottomBackChevron == null ? 0.7f : 0.4f), child: new DefaultTextStyle( style: topBackButtonTextStyle, child: topBackChevron.child ) ) ); } } public Widget topBackLabel { get { KeyedSubtree bottomMiddle = (KeyedSubtree) bottomComponents.middleKey.currentWidget; KeyedSubtree bottomLargeTitle = (KeyedSubtree) bottomComponents.largeTitleKey.currentWidget; KeyedSubtree topBackLabel = (KeyedSubtree) topComponents.backLabelKey.currentWidget; if (topBackLabel == null) { return null; } RenderAnimatedOpacity topBackLabelOpacity = (RenderAnimatedOpacity) topComponents.backLabelKey.currentContext?.findAncestorRenderObjectOfType(); Animation midClickOpacity = null; if (topBackLabelOpacity != null && topBackLabelOpacity.opacity.value < 1.0f) { midClickOpacity = animation.drive(new FloatTween( begin: 0.0f, end: topBackLabelOpacity.opacity.value )); } if (bottomLargeTitle != null && topBackLabel != null && bottomLargeExpanded.Value) { return new PositionedTransition( rect: animation.drive(slideFromLeadingEdge( fromKey: bottomComponents.largeTitleKey, fromNavBarBox: bottomNavBarBox, toKey: topComponents.backLabelKey, toNavBarBox: topNavBarBox )), child: new FadeTransition( opacity: midClickOpacity ?? fadeInFrom(0.4f), child: new DefaultTextStyleTransition( style: animation.drive(new TextStyleTween( begin: bottomLargeTitleTextStyle, end: topBackButtonTextStyle )), maxLines: 1, overflow: TextOverflow.ellipsis, child: topBackLabel.child ) ) ); } if (bottomMiddle != null && topBackLabel != null) { return new PositionedTransition( rect: animation.drive(slideFromLeadingEdge( fromKey: bottomComponents.middleKey, fromNavBarBox: bottomNavBarBox, toKey: topComponents.backLabelKey, toNavBarBox: topNavBarBox )), child: new FadeTransition( opacity: midClickOpacity ?? fadeInFrom(0.3f), child: new DefaultTextStyleTransition( style: animation.drive(new TextStyleTween( begin: bottomTitleTextStyle, end: topBackButtonTextStyle )), child: topBackLabel.child ) ) ); } return null; } } public Widget topMiddle { get { KeyedSubtree topMiddle = (KeyedSubtree) topComponents.middleKey.currentWidget; if (topMiddle == null) { return null; } if (topHasUserMiddle != true && topLargeExpanded == true) { return null; } RelativeRect to = positionInTransitionBox(topComponents.middleKey, from: topNavBarBox); RelativeRectTween positionTween = new RelativeRectTween( begin: to.shift( new Offset(forwardDirection * topNavBarBox.size.width / 2.0f, 0.0f ) ), end: to ); return new PositionedTransition( rect: animation.drive(positionTween), child: new FadeTransition( opacity: fadeInFrom(0.25f), child: new DefaultTextStyle( style: topTitleTextStyle, child: topMiddle.child ) ) ); } } public Widget topTrailing { get { KeyedSubtree topTrailing = (KeyedSubtree) topComponents.trailingKey.currentWidget; if (topTrailing == null) { return null; } return Positioned.fromRelativeRect( rect: positionInTransitionBox(topComponents.trailingKey, from: topNavBarBox), child: new FadeTransition( opacity: fadeInFrom(0.4f), child: topTrailing.child ) ); } } public Widget topLargeTitle { get { KeyedSubtree topLargeTitle = (KeyedSubtree) topComponents.largeTitleKey.currentWidget; if (topLargeTitle == null || topLargeExpanded != true) { return null; } RelativeRect to = positionInTransitionBox(topComponents.largeTitleKey, from: topNavBarBox); RelativeRectTween positionTween = new RelativeRectTween( begin: to.shift( new Offset(forwardDirection * topNavBarBox.size.width, 0.0f ) ), end: to ); return new PositionedTransition( rect: animation.drive(positionTween), child: new FadeTransition( opacity: fadeInFrom(0.3f), child: new DefaultTextStyle( style: topLargeTitleTextStyle, maxLines: 1, overflow: TextOverflow.ellipsis, child: topLargeTitle.child ) ) ); } } } }