Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
f23c9ae5
Unverified
Commit
f23c9ae5
authored
Aug 28, 2018
by
xster
Committed by
GitHub
Aug 28, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Cupertino nav bar transitions between routes (#20322)
parent
05b4bd74
Changes
13
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
2827 additions
and
342 deletions
+2827
-342
cupertino_refresh_demo.dart
...er_gallery/lib/demo/cupertino/cupertino_refresh_demo.dart
+5
-1
app.dart
packages/flutter/lib/src/cupertino/app.dart
+1
-7
nav_bar.dart
packages/flutter/lib/src/cupertino/nav_bar.dart
+1510
-267
tab_view.dart
packages/flutter/lib/src/cupertino/tab_view.dart
+40
-11
heroes.dart
packages/flutter/lib/src/widgets/heroes.dart
+155
-44
implicit_animations.dart
packages/flutter/lib/src/widgets/implicit_animations.dart
+18
-0
sliver_persistent_header.dart
...ges/flutter/lib/src/widgets/sliver_persistent_header.dart
+1
-0
transitions.dart
packages/flutter/lib/src/widgets/transitions.dart
+59
-0
nav_bar_test.dart
packages/flutter/test/cupertino/nav_bar_test.dart
+67
-9
nav_bar_transition_test.dart
packages/flutter/test/cupertino/nav_bar_transition_test.dart
+811
-0
route_test.dart
packages/flutter/test/cupertino/route_test.dart
+66
-0
scaffold_test.dart
packages/flutter/test/cupertino/scaffold_test.dart
+3
-3
heroes_test.dart
packages/flutter/test/widgets/heroes_test.dart
+91
-0
No files found.
examples/flutter_gallery/lib/demo/cupertino/cupertino_refresh_demo.dart
View file @
f23c9ae5
...
...
@@ -55,7 +55,11 @@ class _CupertinoRefreshControlDemoState extends State<CupertinoRefreshControlDem
new
CupertinoSliverRefreshControl
(
onRefresh:
()
{
return
new
Future
<
void
>.
delayed
(
const
Duration
(
seconds:
2
))
..
then
((
_
)
=>
setState
(()
=>
repopulateList
()));
..
then
((
_
)
{
if
(
mounted
)
{
setState
(()
=>
repopulateList
());
}
});
},
),
new
SliverSafeArea
(
...
...
packages/flutter/lib/src/cupertino/app.dart
View file @
f23c9ae5
...
...
@@ -320,13 +320,10 @@ class _AlwaysCupertinoScrollBehavior extends ScrollBehavior {
}
class
_CupertinoAppState
extends
State
<
CupertinoApp
>
{
HeroController
_heroController
;
List
<
NavigatorObserver
>
_navigatorObservers
;
@override
void
initState
()
{
super
.
initState
();
_heroController
=
new
HeroController
();
// Linear tweening.
_updateNavigator
();
}
...
...
@@ -342,9 +339,6 @@ class _CupertinoAppState extends State<CupertinoApp> {
widget
.
routes
.
isNotEmpty
||
widget
.
onGenerateRoute
!=
null
||
widget
.
onUnknownRoute
!=
null
;
_navigatorObservers
=
new
List
<
NavigatorObserver
>.
from
(
widget
.
navigatorObservers
)
..
add
(
_heroController
);
}
Widget
defaultBuilder
(
BuildContext
context
,
Widget
child
)
{
...
...
@@ -361,7 +355,7 @@ class _CupertinoAppState extends State<CupertinoApp> {
routes:
widget
.
routes
,
onGenerateRoute:
widget
.
onGenerateRoute
,
onUnknownRoute:
widget
.
onUnknownRoute
,
navigatorObservers:
_
navigatorObservers
,
navigatorObservers:
widget
.
navigatorObservers
,
);
if
(
widget
.
builder
!=
null
)
{
return
widget
.
builder
(
context
,
navigator
);
...
...
packages/flutter/lib/src/cupertino/nav_bar.dart
View file @
f23c9ae5
...
...
@@ -2,9 +2,11 @@
// 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'
;
...
...
@@ -15,6 +17,8 @@ import 'page_scaffold.dart';
import
'route.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
=
44.0
;
/// Size increase from expanding the navigation bar into an iOS-11-style large title
...
...
@@ -43,6 +47,14 @@ const Border _kDefaultNavBarBorder = Border(
),
);
const
TextStyle
_kMiddleTitleTextStyle
=
TextStyle
(
fontFamily:
'.SF UI Text'
,
fontSize:
17.0
,
fontWeight:
FontWeight
.
w600
,
letterSpacing:
-
0.08
,
color:
CupertinoColors
.
black
,
);
const
TextStyle
_kLargeTitleTextStyle
=
TextStyle
(
fontFamily:
'.SF Pro Display'
,
fontSize:
34.0
,
...
...
@@ -51,6 +63,78 @@ const TextStyle _kLargeTitleTextStyle = TextStyle(
color:
CupertinoColors
.
black
,
);
// 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
();
class
_HeroTag
{
const
_HeroTag
();
// Let the Hero tag be described in tree dumps.
@override
String
toString
()
=>
'Default Hero tag for Cupertino navigation bars'
;
}
TextStyle
_navBarItemStyle
(
Color
color
)
{
return
new
TextStyle
(
fontFamily:
'.SF UI Text'
,
fontSize:
17.0
,
letterSpacing:
-
0.24
,
color:
color
,
);
}
/// Returns `child` wrapped with background and a bottom border if background color
/// is opaque. Otherwise, also blur with [BackdropFilter].
///
/// When `updateSystemUiOverlay` is true, the nav bar will update the OS
/// status bar's color theme based on the background color of the nav bar.
Widget
_wrapWithBackground
(
{
Border
border
,
Color
backgroundColor
,
Widget
child
,
bool
updateSystemUiOverlay
=
true
,
})
{
Widget
result
=
child
;
if
(
updateSystemUiOverlay
)
{
final
bool
darkBackground
=
backgroundColor
.
computeLuminance
()
<
0.179
;
final
SystemUiOverlayStyle
overlayStyle
=
darkBackground
?
SystemUiOverlayStyle
.
light
:
SystemUiOverlayStyle
.
dark
;
result
=
new
AnnotatedRegion
<
SystemUiOverlayStyle
>(
value:
overlayStyle
,
sized:
true
,
child:
result
,
);
}
final
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:
new
ImageFilter
.
blur
(
sigmaX:
10.0
,
sigmaY:
10.0
),
child:
childWithBackground
,
),
);
}
// 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
...
...
@@ -73,13 +157,20 @@ const TextStyle _kLargeTitleTextStyle = TextStyle(
/// 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 it 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.
///
/// 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
State
less
Widget
implements
ObstructingPreferredSizeWidget
{
class
CupertinoNavigationBar
extends
State
ful
Widget
implements
ObstructingPreferredSizeWidget
{
/// Creates a navigation bar in the iOS style.
const
CupertinoNavigationBar
({
Key
key
,
...
...
@@ -93,8 +184,21 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe
this
.
backgroundColor
=
_kDefaultNavBarBackgroundColor
,
this
.
padding
,
this
.
actionsForegroundColor
=
CupertinoColors
.
activeBlue
,
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}
...
...
@@ -199,6 +303,34 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe
/// iOS standard design.
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.
///
/// 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 so they can all transition
/// between each other as long as there's only one per route. Use this tag
/// override with different tags to have multiple navigation bars per route.
///
/// Cannot be null. To disable Hero transitions for this navigation bar,
/// set [transitionBetweenRoutes] to false.
/// {@endtemplate}
final
Object
heroTag
;
/// True if the navigation bar's background color has no transparency.
@override
bool
get
fullObstruction
=>
backgroundColor
.
alpha
==
0xFF
;
...
...
@@ -208,25 +340,67 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe
return
const
Size
.
fromHeight
(
_kNavBarPersistentHeight
);
}
@override
_CupertinoNavigationBarState
createState
()
{
return
new
_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
=
new
_NavigationBarStaticComponentsKeys
();
}
@override
Widget
build
(
BuildContext
context
)
{
final
Widget
effectiveMiddle
=
_effectiveTitle
(
title:
middle
,
automaticallyImplyTitle:
automaticallyImplyMiddle
,
currentRoute:
ModalRoute
.
of
(
context
),
final
_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
,
actionsForegroundColor:
widget
.
actionsForegroundColor
,
userLargeTitle:
null
,
large:
false
,
);
return
_wrapWithBackground
(
border:
border
,
backgroundColor:
backgroundColor
,
child:
new
_CupertinoPersistentNavigationBar
(
leading:
leading
,
automaticallyImplyLeading:
automaticallyImplyLeading
,
previousPageTitle:
previousPageTitle
,
middle:
effectiveMiddle
,
trailing:
trailing
,
padding:
padding
,
actionsForegroundColor:
actionsForegroundColor
,
final
Widget
navBar
=
_wrapWithBackground
(
border:
widget
.
border
,
backgroundColor:
widget
.
backgroundColor
,
child:
new
_PersistentNavigationBar
(
components:
components
,
padding:
widget
.
padding
,
),
);
if
(!
widget
.
transitionBetweenRoutes
||
!
_isTransitionable
(
context
))
{
return
navBar
;
}
return
new
Hero
(
tag:
widget
.
heroTag
,
createRectTween:
_linearTranslateWithLargestRectSizeTween
,
placeholderBuilder:
_navBarHeroLaunchPadBuilder
,
flightShuttleBuilder:
_navBarHeroFlightShuttleBuilder
,
child:
new
_TransitionableNavigationBar
(
componentsKeys:
keys
,
backgroundColor:
widget
.
backgroundColor
,
actionsForegroundColor:
widget
.
actionsForegroundColor
,
border:
widget
.
border
,
hasUserMiddle:
widget
.
middle
!=
null
,
largeExpanded:
false
,
child:
navBar
,
),
);
}
...
...
@@ -261,11 +435,19 @@ class CupertinoNavigationBar extends StatelessWidget implements ObstructingPrefe
/// route 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 it 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.
///
/// See also:
///
/// * [CupertinoNavigationBar], an iOS navigation bar for use on non-scrolling
/// pages.
class
CupertinoSliverNavigationBar
extends
State
less
Widget
{
class
CupertinoSliverNavigationBar
extends
State
ful
Widget
{
/// Creates a navigation bar for scrolling lists.
///
/// The [largeTitle] argument is required and must not be null.
...
...
@@ -282,8 +464,16 @@ class CupertinoSliverNavigationBar extends StatelessWidget {
this
.
backgroundColor
=
_kDefaultNavBarBackgroundColor
,
this
.
padding
,
this
.
actionsForegroundColor
=
CupertinoColors
.
activeBlue
,
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.
...
...
@@ -304,6 +494,9 @@ class CupertinoSliverNavigationBar extends StatelessWidget {
/// 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}
...
...
@@ -355,73 +548,96 @@ class CupertinoSliverNavigationBar extends StatelessWidget {
/// iOS standard design.
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
()
=>
new
_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
=
new
_NavigationBarStaticComponentsKeys
();
}
@override
Widget
build
(
BuildContext
context
)
{
final
Widget
effectiveTitle
=
_effectiveTitle
(
title:
largeTitle
,
automaticallyImplyTitle:
automaticallyImplyTitle
,
currentRoute:
ModalRoute
.
of
(
context
),
final
_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
,
actionsForegroundColor:
widget
.
actionsForegroundColor
,
large:
true
,
);
return
new
SliverPersistentHeader
(
pinned:
true
,
// iOS navigation bars are always pinned.
delegate:
new
_CupertinoLargeTitleNavigationBarSliverDelegate
(
delegate:
new
_LargeTitleNavigationBarSliverDelegate
(
keys:
keys
,
components:
components
,
userMiddle:
widget
.
middle
,
backgroundColor:
widget
.
backgroundColor
,
border:
widget
.
border
,
padding:
widget
.
padding
,
actionsForegroundColor:
widget
.
actionsForegroundColor
,
transitionBetweenRoutes:
widget
.
transitionBetweenRoutes
,
heroTag:
widget
.
heroTag
,
persistentHeight:
_kNavBarPersistentHeight
+
MediaQuery
.
of
(
context
).
padding
.
top
,
largeTitle:
effectiveTitle
,
leading:
leading
,
automaticallyImplyLeading:
automaticallyImplyLeading
,
previousPageTitle:
previousPageTitle
,
middle:
middle
,
trailing:
trailing
,
padding:
padding
,
border:
border
,
backgroundColor:
backgroundColor
,
actionsForegroundColor:
actionsForegroundColor
,
alwaysShowMiddle:
widget
.
middle
!=
null
,
),
);
}
}
class
_
Cupertino
LargeTitleNavigationBarSliverDelegate
class
_LargeTitleNavigationBarSliverDelegate
extends
SliverPersistentHeaderDelegate
with
DiagnosticableTreeMixin
{
_CupertinoLargeTitleNavigationBarSliverDelegate
({
_LargeTitleNavigationBarSliverDelegate
({
@required
this
.
keys
,
@required
this
.
components
,
@required
this
.
userMiddle
,
@required
this
.
backgroundColor
,
@required
this
.
border
,
@required
this
.
padding
,
@required
this
.
actionsForegroundColor
,
@required
this
.
transitionBetweenRoutes
,
@required
this
.
heroTag
,
@required
this
.
persistentHeight
,
@required
this
.
largeTitle
,
this
.
leading
,
this
.
automaticallyImplyLeading
,
this
.
previousPageTitle
,
this
.
middle
,
this
.
trailing
,
this
.
padding
,
this
.
border
,
this
.
backgroundColor
,
this
.
actionsForegroundColor
,
})
:
assert
(
persistentHeight
!=
null
);
final
double
persistentHeight
;
final
Widget
largeTitle
;
final
Widget
leading
;
final
bool
automaticallyImplyLeading
;
final
String
previousPageTitle
;
final
Widget
middle
;
final
Widget
trailing
;
final
EdgeInsetsDirectional
padding
;
@required
this
.
alwaysShowMiddle
,
})
:
assert
(
persistentHeight
!=
null
),
assert
(
alwaysShowMiddle
!=
null
),
assert
(
transitionBetweenRoutes
!=
null
);
final
_NavigationBarStaticComponentsKeys
keys
;
final
_NavigationBarStaticComponents
components
;
final
Widget
userMiddle
;
final
Color
backgroundColor
;
final
Border
border
;
final
EdgeInsetsDirectional
padding
;
final
Color
actionsForegroundColor
;
final
bool
transitionBetweenRoutes
;
final
Object
heroTag
;
final
double
persistentHeight
;
final
bool
alwaysShowMiddle
;
@override
double
get
minExtent
=>
persistentHeight
;
...
...
@@ -433,21 +649,16 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate
Widget
build
(
BuildContext
context
,
double
shrinkOffset
,
bool
overlapsContent
)
{
final
bool
showLargeTitle
=
shrinkOffset
<
maxExtent
-
minExtent
-
_kNavBarShowLargeTitleThreshold
;
final
_CupertinoPersistentNavigationBar
persistentNavigationBar
=
new
_CupertinoPersistentNavigationBar
(
leading:
leading
,
automaticallyImplyLeading:
automaticallyImplyLeading
,
previousPageTitle:
previousPageTitle
,
middle:
middle
??
largeTitle
,
trailing:
trailing
,
// If middle widget exists, always show it. Otherwise, show title
// when collapsed.
middleVisible:
middle
!=
null
?
null
:
!
showLargeTitle
,
final
_PersistentNavigationBar
persistentNavigationBar
=
new
_PersistentNavigationBar
(
components:
components
,
padding:
padding
,
actionsForegroundColor:
actionsForegroundColor
,
// If a user specified middle exists, always show it. Otherwise, show
// title when sliver is collapsed.
middleVisible:
alwaysShowMiddle
?
null
:
!
showLargeTitle
,
);
return
_wrapWithBackground
(
final
Widget
navBar
=
_wrapWithBackground
(
border:
border
,
backgroundColor:
backgroundColor
,
child:
new
Stack
(
...
...
@@ -471,19 +682,19 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate
start:
_kNavBarEdgePadding
,
bottom:
8.0
,
// Bottom has a different padding.
),
child:
new
DefaultTextStyle
(
style:
_kLargeTitleTextStyle
,
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
child:
new
SafeArea
(
top:
false
,
bottom:
false
,
child:
new
AnimatedOpacity
(
opacity:
showLargeTitle
?
1.0
:
0.0
,
duration:
_kNavBarTitleFadeDuration
,
child:
new
SafeArea
(
top:
false
,
bottom:
false
,
child:
new
Semantics
(
header:
true
,
child:
largeTitle
,
child:
new
Semantics
(
header:
true
,
child:
new
DefaultTextStyle
(
style:
_kLargeTitleTextStyle
,
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
child:
components
.
largeTitle
,
),
),
),
...
...
@@ -501,70 +712,44 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate
],
),
);
if
(!
transitionBetweenRoutes
||
!
_isTransitionable
(
context
))
{
return
navBar
;
}
return
new
Hero
(
tag:
heroTag
,
createRectTween:
_linearTranslateWithLargestRectSizeTween
,
flightShuttleBuilder:
_navBarHeroFlightShuttleBuilder
,
placeholderBuilder:
_navBarHeroLaunchPadBuilder
,
// 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:
new
_TransitionableNavigationBar
(
componentsKeys:
keys
,
backgroundColor:
backgroundColor
,
actionsForegroundColor:
actionsForegroundColor
,
border:
border
,
hasUserMiddle:
userMiddle
!=
null
,
largeExpanded:
showLargeTitle
,
child:
navBar
,
),
);
}
@override
bool
shouldRebuild
(
_CupertinoLargeTitleNavigationBarSliverDelegate
oldDelegate
)
{
return
persistentHeight
!=
oldDelegate
.
persistentHeight
||
largeTitle
!=
oldDelegate
.
largeTitle
||
leading
!=
oldDelegate
.
leading
||
middle
!=
oldDelegate
.
middle
||
trailing
!=
oldDelegate
.
trailing
||
border
!=
oldDelegate
.
border
bool
shouldRebuild
(
_LargeTitleNavigationBarSliverDelegate
oldDelegate
)
{
return
components
!=
oldDelegate
.
components
||
userMiddle
!=
oldDelegate
.
userMiddle
||
backgroundColor
!=
oldDelegate
.
backgroundColor
||
actionsForegroundColor
!=
oldDelegate
.
actionsForegroundColor
;
}
}
/// Returns `child` wrapped with background and a bottom border if background color
/// is opaque. Otherwise, also blur with [BackdropFilter].
Widget
_wrapWithBackground
(
{
Border
border
,
Color
backgroundColor
,
Widget
child
,
})
{
final
bool
darkBackground
=
backgroundColor
.
computeLuminance
()
<
0.179
;
final
SystemUiOverlayStyle
overlayStyle
=
darkBackground
?
SystemUiOverlayStyle
.
light
:
SystemUiOverlayStyle
.
dark
;
final
DecoratedBox
childWithBackground
=
new
DecoratedBox
(
decoration:
new
BoxDecoration
(
border:
border
,
color:
backgroundColor
,
),
child:
new
AnnotatedRegion
<
SystemUiOverlayStyle
>(
value:
overlayStyle
,
sized:
true
,
child:
child
,
),
);
if
(
backgroundColor
.
alpha
==
0xFF
)
return
childWithBackground
;
return
new
ClipRect
(
child:
new
BackdropFilter
(
filter:
new
ImageFilter
.
blur
(
sigmaX:
10.0
,
sigmaY:
10.0
),
child:
childWithBackground
,
),
);
}
Widget
_effectiveTitle
(
{
Widget
title
,
bool
automaticallyImplyTitle
,
ModalRoute
<
dynamic
>
currentRoute
,
})
{
// Auto use the CupertinoPageRoute's title if middle not provided.
if
(
title
==
null
&&
automaticallyImplyTitle
&&
currentRoute
is
CupertinoPageRoute
&&
currentRoute
.
title
!=
null
)
{
return
new
Text
(
currentRoute
.
title
);
||
border
!=
oldDelegate
.
border
||
padding
!=
oldDelegate
.
padding
||
actionsForegroundColor
!=
oldDelegate
.
actionsForegroundColor
||
transitionBetweenRoutes
!=
oldDelegate
.
transitionBetweenRoutes
||
persistentHeight
!=
oldDelegate
.
persistentHeight
||
alwaysShowMiddle
!=
oldDelegate
.
alwaysShowMiddle
||
heroTag
!=
oldDelegate
.
heroTag
;
}
return
title
;
}
/// The top part of the navigation bar that's never scrolled away.
...
...
@@ -572,123 +757,57 @@ Widget _effectiveTitle({
/// 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
_
CupertinoPersistentNavigationBar
extends
StatelessWidget
implements
PreferredSize
Widget
{
const
_
Cupertino
PersistentNavigationBar
({
class
_
PersistentNavigationBar
extends
Stateless
Widget
{
const
_PersistentNavigationBar
({
Key
key
,
this
.
leading
,
this
.
automaticallyImplyLeading
,
this
.
previousPageTitle
,
this
.
middle
,
this
.
trailing
,
this
.
components
,
this
.
padding
,
this
.
actionsForegroundColor
,
this
.
middleVisible
,
})
:
super
(
key:
key
);
final
Widget
leading
;
final
bool
automaticallyImplyLeading
;
final
String
previousPageTitle
;
final
Widget
middle
;
final
Widget
trailing
;
final
_NavigationBarStaticComponents
components
;
final
EdgeInsetsDirectional
padding
;
final
Color
actionsForegroundColor
;
/// Whether the middle widget has a visible animated opacity. A null value
/// means the middle opacity will not be animated.
final
bool
middleVisible
;
@override
Size
get
preferredSize
=>
const
Size
.
fromHeight
(
_kNavBarPersistentHeight
);
@override
Widget
build
(
BuildContext
context
)
{
final
TextStyle
actionsStyle
=
new
TextStyle
(
fontFamily:
'.SF UI Text'
,
fontSize:
17.0
,
letterSpacing:
-
0.24
,
color:
actionsForegroundColor
,
);
final
Widget
styledLeading
=
leading
==
null
?
null
:
new
Padding
(
padding:
new
EdgeInsetsDirectional
.
only
(
start:
padding
?.
start
??
_kNavBarEdgePadding
,
),
child:
new
DefaultTextStyle
(
style:
actionsStyle
,
child:
leading
,
),
);
Widget
middle
=
components
.
middle
;
final
Widget
styledTrailing
=
trailing
==
null
?
null
:
Padding
(
padding:
new
EdgeInsetsDirectional
.
only
(
end:
padding
?.
end
??
_kNavBarEdgePadding
,
),
child:
new
DefaultTextStyle
(
style:
actionsStyle
,
child:
trailing
,
),
if
(
middle
!=
null
)
{
middle
=
new
DefaultTextStyle
(
style:
_kMiddleTitleTextStyle
,
child:
new
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
:
new
AnimatedOpacity
(
opacity:
middleVisible
?
1.0
:
0.0
,
duration:
_kNavBarTitleFadeDuration
,
child:
middle
,
);
}
// Let the middle be black rather than `actionsForegroundColor` in case
// it's a plain text title.
final
Widget
styledMiddle
=
middle
==
null
?
null
:
new
DefaultTextStyle
(
style:
actionsStyle
.
copyWith
(
fontWeight:
FontWeight
.
w600
,
letterSpacing:
-
0.08
,
color:
CupertinoColors
.
black
,
),
child:
new
Semantics
(
child:
middle
,
header:
true
),
);
Widget
leading
=
components
.
leading
;
final
Widget
backChevron
=
components
.
backChevron
;
final
Widget
backLabel
=
components
.
backLabel
;
final
Widget
animatedStyledMiddle
=
middleVisible
==
null
?
styledMiddle
:
new
AnimatedOpacity
(
opacity:
middleVisible
?
1.0
:
0.0
,
duration:
_kNavBarTitleFadeDuration
,
child:
styledMiddle
,
if
(
leading
==
null
&&
backChevron
!=
null
&&
backLabel
!=
null
)
{
leading
=
new
CupertinoNavigationBarBackButton
.
_assemble
(
backChevron
,
backLabel
,
components
.
actionsForegroundColor
,
);
// Auto add back button if leading not provided.
Widget
backOrCloseButton
;
if
(
styledLeading
==
null
&&
automaticallyImplyLeading
)
{
final
ModalRoute
<
dynamic
>
currentRoute
=
ModalRoute
.
of
(
context
);
if
(
currentRoute
?.
canPop
==
true
)
{
if
(
currentRoute
is
PageRoute
&&
currentRoute
?.
fullscreenDialog
==
true
)
{
backOrCloseButton
=
new
CupertinoButton
(
child:
const
Padding
(
padding:
EdgeInsetsDirectional
.
only
(
start:
_kNavBarEdgePadding
,
),
child:
Text
(
'Close'
),
),
padding:
EdgeInsets
.
zero
,
onPressed:
()
{
Navigator
.
maybePop
(
context
);
},
);
}
else
{
backOrCloseButton
=
new
CupertinoNavigationBarBackButton
(
color:
actionsForegroundColor
,
previousPageTitle:
previousPageTitle
,
);
}
}
}
Widget
paddedToolbar
=
new
NavigationToolbar
(
leading:
styledLeading
??
backOrCloseButton
,
middle:
animatedStyledM
iddle
,
trailing:
styledT
railing
,
leading:
leading
,
middle:
m
iddle
,
trailing:
components
.
t
railing
,
centerMiddle:
true
,
middleSpacing:
6.0
,
);
...
...
@@ -713,6 +832,303 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
}
}
// 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
=
new
GlobalKey
(
debugLabel:
'Navigation bar render box'
),
leadingKey
=
new
GlobalKey
(
debugLabel:
'Leading'
),
backChevronKey
=
new
GlobalKey
(
debugLabel:
'Back chevron'
),
backLabelKey
=
new
GlobalKey
(
debugLabel:
'Back label'
),
middleKey
=
new
GlobalKey
(
debugLabel:
'Middle'
),
trailingKey
=
new
GlobalKey
(
debugLabel:
'Trailing'
),
largeTitleKey
=
new
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
this
.
actionsForegroundColor
,
@required
bool
large
,
})
:
leading
=
createLeading
(
leadingKey:
keys
.
leadingKey
,
userLeading:
userLeading
,
route:
route
,
automaticallyImplyLeading:
automaticallyImplyLeading
,
padding:
padding
,
actionsForegroundColor:
actionsForegroundColor
,
),
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
,
actionsForegroundColor:
actionsForegroundColor
,
),
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
new
Text
(
currentRoute
.
title
);
}
return
null
;
}
final
Color
actionsForegroundColor
;
final
KeyedSubtree
leading
;
static
KeyedSubtree
createLeading
({
@required
GlobalKey
leadingKey
,
@required
Widget
userLeading
,
@required
ModalRoute
<
dynamic
>
route
,
@required
bool
automaticallyImplyLeading
,
@required
EdgeInsetsDirectional
padding
,
@required
Color
actionsForegroundColor
})
{
Widget
leadingContent
;
if
(
userLeading
!=
null
)
{
leadingContent
=
userLeading
;
}
else
if
(
automaticallyImplyLeading
&&
route
is
PageRoute
&&
route
.
canPop
&&
route
.
fullscreenDialog
)
{
leadingContent
=
new
CupertinoButton
(
child:
const
Text
(
'Close'
),
padding:
EdgeInsets
.
zero
,
onPressed:
()
{
route
.
navigator
.
maybePop
();
},
);
}
if
(
leadingContent
==
null
)
{
return
null
;
}
return
new
KeyedSubtree
(
key:
leadingKey
,
child:
new
Padding
(
padding:
new
EdgeInsetsDirectional
.
only
(
start:
padding
?.
start
??
_kNavBarEdgePadding
,
),
child:
new
DefaultTextStyle
(
style:
_navBarItemStyle
(
actionsForegroundColor
),
child:
IconTheme
.
merge
(
data:
new
IconThemeData
(
color:
actionsForegroundColor
,
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
new
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
new
KeyedSubtree
(
key:
backLabelKey
,
child:
new
_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
new
KeyedSubtree
(
key:
middleKey
,
child:
middleContent
,
);
}
final
KeyedSubtree
trailing
;
static
KeyedSubtree
createTrailing
({
@required
GlobalKey
trailingKey
,
@required
Widget
userTrailing
,
@required
EdgeInsetsDirectional
padding
,
@required
Color
actionsForegroundColor
,
})
{
if
(
userTrailing
==
null
)
{
return
null
;
}
return
new
KeyedSubtree
(
key:
trailingKey
,
child:
new
Padding
(
padding:
new
EdgeInsetsDirectional
.
only
(
end:
padding
?.
end
??
_kNavBarEdgePadding
,
),
child:
new
DefaultTextStyle
(
style:
_navBarItemStyle
(
actionsForegroundColor
),
child:
IconTheme
.
merge
(
data:
new
IconThemeData
(
color:
actionsForegroundColor
,
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
new
KeyedSubtree
(
key:
largeTitleKey
,
child:
largeTitleContent
,
);
}
}
/// A nav bar back button typically used in [CupertinoNavigationBar].
///
/// This is automatically inserted into [CupertinoNavigationBar] and
...
...
@@ -730,9 +1146,20 @@ class CupertinoNavigationBarBackButton extends StatelessWidget {
const
CupertinoNavigationBarBackButton
({
@required
this
.
color
,
this
.
previousPageTitle
,
})
:
assert
(
color
!=
null
);
/// The [Color] of the back chevron.
})
:
_backChevron
=
null
,
_backLabel
=
null
,
assert
(
color
!=
null
);
// Allow the back chevron and label to be separately created (and keyed)
// because they animate separately during page transitions.
const
CupertinoNavigationBarBackButton
.
_assemble
(
this
.
_backChevron
,
this
.
_backLabel
,
this
.
color
,
)
:
previousPageTitle
=
null
,
assert
(
color
!=
null
);
/// The [Color] of the back button.
///
/// Must not be null.
final
Color
color
;
...
...
@@ -742,6 +1169,10 @@ class CupertinoNavigationBarBackButton extends StatelessWidget {
/// previous routes are both [CupertinoPageRoute]s.
final
String
previousPageTitle
;
final
Widget
_backChevron
;
final
Widget
_backLabel
;
@override
Widget
build
(
BuildContext
context
)
{
final
ModalRoute
<
dynamic
>
currentRoute
=
ModalRoute
.
of
(
context
);
...
...
@@ -758,20 +1189,23 @@ class CupertinoNavigationBarBackButton extends StatelessWidget {
button:
true
,
child:
ConstrainedBox
(
constraints:
const
BoxConstraints
(
minWidth:
_kNavBarBackButtonTapWidth
),
child:
new
Row
(
mainAxisSize:
MainAxisSize
.
min
,
mainAxisAlignment:
MainAxisAlignment
.
start
,
children:
<
Widget
>[
const
Padding
(
padding:
EdgeInsetsDirectional
.
only
(
start:
8.0
)),
new
_BackChevron
(
color:
color
),
const
Padding
(
padding:
EdgeInsetsDirectional
.
only
(
start:
6.0
)),
new
Flexible
(
child:
new
_BackLabel
(
specifiedPreviousTitle:
previousPageTitle
,
route:
currentRoute
,
child:
new
DefaultTextStyle
(
style:
_navBarItemStyle
(
color
),
child:
new
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
)),
new
Flexible
(
child:
_backLabel
??
new
_BackLabel
(
specifiedPreviousTitle:
previousPageTitle
,
route:
currentRoute
,
),
),
)
,
]
,
]
,
)
,
),
),
),
...
...
@@ -781,16 +1215,14 @@ class CupertinoNavigationBarBackButton extends StatelessWidget {
}
}
class
_BackChevron
extends
StatelessWidget
{
const
_BackChevron
({
@required
this
.
color
,
})
:
assert
(
color
!=
null
);
final
Color
color
;
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.
...
...
@@ -799,7 +1231,7 @@ class _BackChevron extends StatelessWidget {
text:
new
String
.
fromCharCode
(
CupertinoIcons
.
back
.
codePoint
),
style:
new
TextStyle
(
inherit:
false
,
color:
color
,
color:
textStyle
.
color
,
fontSize:
34.0
,
fontFamily:
CupertinoIcons
.
back
.
fontFamily
,
package:
CupertinoIcons
.
back
.
fontPackage
,
...
...
@@ -827,9 +1259,11 @@ class _BackChevron extends StatelessWidget {
/// is true.
class
_BackLabel
extends
StatelessWidget
{
const
_BackLabel
({
Key
key
,
@required
this
.
specifiedPreviousTitle
,
@required
this
.
route
,
})
:
assert
(
route
!=
null
);
})
:
assert
(
route
!=
null
),
super
(
key:
key
);
final
String
specifiedPreviousTitle
;
final
ModalRoute
<
dynamic
>
route
;
...
...
@@ -841,11 +1275,21 @@ class _BackLabel extends StatelessWidget {
return
const
SizedBox
(
height:
0.0
,
width:
0.0
);
}
if
(
previousTitle
.
length
>
10
)
{
return
const
Text
(
'Back'
);
Text
textWidget
=
new
Text
(
previousTitle
,
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
);
if
(
previousTitle
.
length
>
12
)
{
textWidget
=
const
Text
(
'Back'
);
}
return
new
Text
(
previousTitle
,
maxLines:
1
);
return
new
Align
(
alignment:
AlignmentDirectional
.
centerStart
,
widthFactor:
1.0
,
child:
textWidget
,
);
}
@override
...
...
@@ -866,3 +1310,802 @@ class _BackLabel extends StatelessWidget {
}
}
}
/// 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
.
actionsForegroundColor
,
@required
this
.
border
,
@required
this
.
hasUserMiddle
,
@required
this
.
largeExpanded
,
@required
this
.
child
,
})
:
assert
(
componentsKeys
!=
null
),
assert
(
largeExpanded
!=
null
),
super
(
key:
componentsKeys
.
navBarBoxKey
);
final
_NavigationBarStaticComponentsKeys
componentsKeys
;
final
Color
backgroundColor
;
final
Color
actionsForegroundColor
;
final
Border
border
;
final
bool
hasUserMiddle
;
final
bool
largeExpanded
;
final
Widget
child
;
RenderBox
get
renderBox
{
final
RenderBox
box
=
componentsKeys
.
navBarBoxKey
.
currentContext
.
findRenderObject
();
assert
(
box
.
attached
,
'_TransitionableNavigationBar.renderBox should be called when building '
'hero flight shuttles when the from and the to nav bar boxes are already '
'laid out and painted.'
,
);
return
box
;
}
@override
Widget
build
(
BuildContext
context
)
{
assert
(()
{
bool
inHero
;
context
.
visitAncestorElements
((
Element
ancestor
)
{
if
(
ancestor
is
ComponentElement
)
{
assert
(
ancestor
.
widget
.
runtimeType
!=
_NavigationBarTransition
,
'_TransitionableNavigationBar should never re-appear inside '
'_NavigationBarTransition. Keyed _TransitionableNavigationBar should '
'only serve as anchor points in routes rather than appearing inside '
'Hero flights themselves.'
,
);
if
(
ancestor
.
widget
.
runtimeType
==
Hero
)
{
inHero
=
true
;
}
}
inHero
??=
false
;
return
true
;
});
assert
(
inHero
==
true
,
'_TransitionableNavigationBar should only be added as the immediate '
'child of Hero widgets.'
,
);
return
true
;
}());
return
child
;
}
}
/// This class represents the widget that will be in the Hero flight instead of
/// the 2 static navigation bars by taking inner components from both.
///
/// The `topNavBar` parameter is the nav bar that was on top regardless of
/// push/pop direction.
///
/// Similarly, the `bottomNavBar` parameter is the nav bar that was at the
/// bottom regardless of the push/pop direction.
///
/// If [MediaQuery.padding] is still present in this widget's [BuildContext],
/// that padding will become part of the transitional navigation bar as well.
///
/// [MediaQuery.padding] should be consistent between the from/to routes and
/// the Hero overlay. Inconsistent [MediaQuery.padding] will produce undetermined
/// results.
class
_NavigationBarTransition
extends
StatelessWidget
{
_NavigationBarTransition
({
@required
this
.
animation
,
@required
_TransitionableNavigationBar
topNavBar
,
@required
_TransitionableNavigationBar
bottomNavBar
,
})
:
heightTween
=
new
Tween
<
double
>(
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
,
),
componentsTransition
=
new
_NavigationBarComponentsTransition
(
animation:
animation
,
bottomNavBar:
bottomNavBar
,
topNavBar:
topNavBar
,
);
final
Animation
<
double
>
animation
;
final
_NavigationBarComponentsTransition
componentsTransition
;
final
Tween
<
double
>
heightTween
;
final
ColorTween
backgroundTween
;
final
BorderTween
borderTween
;
@override
Widget
build
(
BuildContext
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:
new
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
new
SizedBox
(
height:
math
.
max
(
heightTween
.
begin
,
heightTween
.
end
)
+
MediaQuery
.
of
(
context
).
padding
.
top
,
width:
double
.
infinity
,
child:
new
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
,
})
:
bottomComponents
=
bottomNavBar
.
componentsKeys
,
topComponents
=
topNavBar
.
componentsKeys
,
bottomNavBarBox
=
bottomNavBar
.
renderBox
,
topNavBarBox
=
topNavBar
.
renderBox
,
bottomActionsStyle
=
_navBarItemStyle
(
bottomNavBar
.
actionsForegroundColor
),
topActionsStyle
=
_navBarItemStyle
(
topNavBar
.
actionsForegroundColor
),
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
);
static
final
Tween
<
double
>
fadeOut
=
new
Tween
<
double
>(
begin:
1.0
,
end:
0.0
,
);
static
final
Tween
<
double
>
fadeIn
=
new
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
bottomActionsStyle
;
final
TextStyle
topActionsStyle
;
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
;
// Take a widget it its original ancestor navigation bar render box and
// translate it into a RelativeBox in the transition navigation bar box.
RelativeRect
positionInTransitionBox
(
GlobalKey
key
,
{
@required
RenderBox
from
,
})
{
final
RenderBox
componentBox
=
key
.
currentContext
.
findRenderObject
();
assert
(
componentBox
.
attached
);
return
new
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 center of their respective render
// boxes' leading edge.
//
// Also produce RelativeRects with sizes that would preserve the constant
// BoxConstraints of the 'from' widget so that animating font sizes etc don't
// produce rounding error artifacts with a linearly resizing rect.
RelativeRectTween
slideFromLeadingEdge
({
@required
GlobalKey
fromKey
,
@required
RenderBox
fromNavBarBox
,
@required
GlobalKey
toKey
,
@required
RenderBox
toNavBarBox
,
})
{
final
RelativeRect
fromRect
=
positionInTransitionBox
(
fromKey
,
from:
fromNavBarBox
);
final
RenderBox
fromBox
=
fromKey
.
currentContext
.
findRenderObject
();
final
RenderBox
toBox
=
toKey
.
currentContext
.
findRenderObject
();
final
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.
return
new
RelativeRectTween
(
begin:
fromRect
,
end:
new
RelativeRect
.
fromRect
(
toRect
,
transitionBox
),
);
}
Animation
<
double
>
fadeInFrom
(
double
t
,
{
Curve
curve
=
Curves
.
easeIn
})
{
return
fadeIn
.
animate
(
new
CurvedAnimation
(
curve:
new
Interval
(
t
,
1.0
,
curve:
curve
),
parent:
animation
),
);
}
Animation
<
double
>
fadeOutBy
(
double
t
,
{
Curve
curve
=
Curves
.
easeOut
})
{
return
fadeOut
.
animate
(
new
CurvedAnimation
(
curve:
new
Interval
(
0.0
,
t
,
curve:
curve
),
parent:
animation
),
);
}
Widget
get
bottomLeading
{
final
KeyedSubtree
bottomLeading
=
bottomComponents
.
leadingKey
.
currentWidget
;
if
(
bottomLeading
==
null
)
{
return
null
;
}
return
new
Positioned
.
fromRelativeRect
(
rect:
positionInTransitionBox
(
bottomComponents
.
leadingKey
,
from:
bottomNavBarBox
),
child:
new
FadeTransition
(
opacity:
fadeOutBy
(
0.4
),
child:
bottomLeading
.
child
,
),
);
}
Widget
get
bottomBackChevron
{
final
KeyedSubtree
bottomBackChevron
=
bottomComponents
.
backChevronKey
.
currentWidget
;
if
(
bottomBackChevron
==
null
)
{
return
null
;
}
return
new
Positioned
.
fromRelativeRect
(
rect:
positionInTransitionBox
(
bottomComponents
.
backChevronKey
,
from:
bottomNavBarBox
),
child:
new
FadeTransition
(
opacity:
fadeOutBy
(
0.6
),
child:
new
DefaultTextStyle
(
style:
bottomActionsStyle
,
child:
bottomBackChevron
.
child
,
),
),
);
}
Widget
get
bottomBackLabel
{
final
KeyedSubtree
bottomBackLabel
=
bottomComponents
.
backLabelKey
.
currentWidget
;
if
(
bottomBackLabel
==
null
)
{
return
null
;
}
final
RelativeRect
from
=
positionInTransitionBox
(
bottomComponents
.
backLabelKey
,
from:
bottomNavBarBox
);
// Transition away by sliding horizontally to the left off of the screen.
final
RelativeRectTween
positionTween
=
new
RelativeRectTween
(
begin:
from
,
end:
from
.
shift
(
new
Offset
(-
bottomNavBarBox
.
size
.
width
/
2.0
,
0.0
)),
);
return
new
PositionedTransition
(
rect:
positionTween
.
animate
(
animation
),
child:
new
FadeTransition
(
opacity:
fadeOutBy
(
0.2
),
child:
new
DefaultTextStyle
(
style:
bottomActionsStyle
,
child:
bottomBackLabel
.
child
,
),
),
);
}
Widget
get
bottomMiddle
{
final
KeyedSubtree
bottomMiddle
=
bottomComponents
.
middleKey
.
currentWidget
;
final
KeyedSubtree
topBackLabel
=
topComponents
.
backLabelKey
.
currentWidget
;
final
KeyedSubtree
topLeading
=
topComponents
.
leadingKey
.
currentWidget
;
// The middle component is non-null when the nav bar is a large title
// nav bar but would be invisible when expanded, therefore don't show it here.
if
(!
bottomHasUserMiddle
&&
bottomLargeExpanded
)
{
return
null
;
}
if
(
bottomMiddle
!=
null
&&
topBackLabel
!=
null
)
{
return
new
PositionedTransition
(
rect:
slideFromLeadingEdge
(
fromKey:
bottomComponents
.
middleKey
,
fromNavBarBox:
bottomNavBarBox
,
toKey:
topComponents
.
backLabelKey
,
toNavBarBox:
topNavBarBox
,
).
animate
(
animation
),
child:
new
FadeTransition
(
// A custom middle widget like a segmented control fades away faster.
opacity:
fadeOutBy
(
bottomHasUserMiddle
?
0.4
:
0.7
),
child:
new
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:
new
DefaultTextStyleTransition
(
style:
TextStyleTween
(
begin:
_kMiddleTitleTextStyle
,
end:
topActionsStyle
,
).
animate
(
animation
),
child:
bottomMiddle
.
child
,
),
),
),
);
}
// When the top page has a leading widget override, don't move the bottom
// middle widget.
if
(
bottomMiddle
!=
null
&&
topLeading
!=
null
)
{
return
new
Positioned
.
fromRelativeRect
(
rect:
positionInTransitionBox
(
bottomComponents
.
middleKey
,
from:
bottomNavBarBox
),
child:
new
FadeTransition
(
opacity:
fadeOutBy
(
bottomHasUserMiddle
?
0.4
:
0.7
),
// Keep the font when transitioning into a non-back label leading.
child:
new
DefaultTextStyle
(
style:
_kMiddleTitleTextStyle
,
child:
bottomMiddle
.
child
,
),
),
);
}
return
null
;
}
Widget
get
bottomLargeTitle
{
final
KeyedSubtree
bottomLargeTitle
=
bottomComponents
.
largeTitleKey
.
currentWidget
;
final
KeyedSubtree
topBackLabel
=
topComponents
.
backLabelKey
.
currentWidget
;
final
KeyedSubtree
topLeading
=
topComponents
.
leadingKey
.
currentWidget
;
if
(
bottomLargeTitle
==
null
||
!
bottomLargeExpanded
)
{
return
null
;
}
if
(
bottomLargeTitle
!=
null
&&
topBackLabel
!=
null
)
{
return
new
PositionedTransition
(
rect:
slideFromLeadingEdge
(
fromKey:
bottomComponents
.
largeTitleKey
,
fromNavBarBox:
bottomNavBarBox
,
toKey:
topComponents
.
backLabelKey
,
toNavBarBox:
topNavBarBox
,
).
animate
(
animation
),
child:
new
FadeTransition
(
opacity:
fadeOutBy
(
0.6
),
child:
new
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:
new
DefaultTextStyleTransition
(
style:
TextStyleTween
(
begin:
_kLargeTitleTextStyle
,
end:
topActionsStyle
,
).
animate
(
animation
),
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
child:
bottomLargeTitle
.
child
,
),
),
),
);
}
if
(
bottomLargeTitle
!=
null
&&
topLeading
!=
null
)
{
final
RelativeRect
from
=
positionInTransitionBox
(
bottomComponents
.
largeTitleKey
,
from:
bottomNavBarBox
);
final
RelativeRectTween
positionTween
=
new
RelativeRectTween
(
begin:
from
,
end:
from
.
shift
(
new
Offset
(
bottomNavBarBox
.
size
.
width
/
4.0
,
0.0
)),
);
// Just shift slightly towards the right instead of moving to the back
// label position.
return
new
PositionedTransition
(
rect:
positionTween
.
animate
(
animation
),
child:
new
FadeTransition
(
opacity:
fadeOutBy
(
0.4
),
// Keep the font when transitioning into a non-back-label leading.
child:
new
DefaultTextStyle
(
style:
_kLargeTitleTextStyle
,
child:
bottomLargeTitle
.
child
,
),
),
);
}
return
null
;
}
Widget
get
bottomTrailing
{
final
KeyedSubtree
bottomTrailing
=
bottomComponents
.
trailingKey
.
currentWidget
;
if
(
bottomTrailing
==
null
)
{
return
null
;
}
return
new
Positioned
.
fromRelativeRect
(
rect:
positionInTransitionBox
(
bottomComponents
.
trailingKey
,
from:
bottomNavBarBox
),
child:
new
FadeTransition
(
opacity:
fadeOutBy
(
0.6
),
child:
bottomTrailing
.
child
,
),
);
}
Widget
get
topLeading
{
final
KeyedSubtree
topLeading
=
topComponents
.
leadingKey
.
currentWidget
;
if
(
topLeading
==
null
)
{
return
null
;
}
return
new
Positioned
.
fromRelativeRect
(
rect:
positionInTransitionBox
(
topComponents
.
leadingKey
,
from:
topNavBarBox
),
child:
new
FadeTransition
(
opacity:
fadeInFrom
(
0.6
),
child:
topLeading
.
child
,
),
);
}
Widget
get
topBackChevron
{
final
KeyedSubtree
topBackChevron
=
topComponents
.
backChevronKey
.
currentWidget
;
final
KeyedSubtree
bottomBackChevron
=
bottomComponents
.
backChevronKey
.
currentWidget
;
if
(
topBackChevron
==
null
)
{
return
null
;
}
final
RelativeRect
to
=
positionInTransitionBox
(
topComponents
.
backChevronKey
,
from:
topNavBarBox
);
RelativeRect
from
=
to
;
// If it's the first page with a back chevron, shift in slightly from the
// right.
if
(
bottomBackChevron
==
null
)
{
final
RenderBox
topBackChevronBox
=
topComponents
.
backChevronKey
.
currentContext
.
findRenderObject
();
from
=
to
.
shift
(
new
Offset
(
topBackChevronBox
.
size
.
width
*
2.0
,
0.0
));
}
final
RelativeRectTween
positionTween
=
new
RelativeRectTween
(
begin:
from
,
end:
to
,
);
return
new
PositionedTransition
(
rect:
positionTween
.
animate
(
animation
),
child:
new
FadeTransition
(
opacity:
fadeInFrom
(
bottomBackChevron
==
null
?
0.7
:
0.4
),
child:
new
DefaultTextStyle
(
style:
topActionsStyle
,
child:
topBackChevron
.
child
,
),
),
);
}
Widget
get
topBackLabel
{
final
KeyedSubtree
bottomMiddle
=
bottomComponents
.
middleKey
.
currentWidget
;
final
KeyedSubtree
bottomLargeTitle
=
bottomComponents
.
largeTitleKey
.
currentWidget
;
final
KeyedSubtree
topBackLabel
=
topComponents
.
backLabelKey
.
currentWidget
;
if
(
topBackLabel
==
null
)
{
return
null
;
}
final
RenderAnimatedOpacity
topBackLabelOpacity
=
topComponents
.
backLabelKey
.
currentContext
?.
ancestorRenderObjectOfType
(
const
TypeMatcher
<
RenderAnimatedOpacity
>()
);
Animation
<
double
>
midClickOpacity
;
if
(
topBackLabelOpacity
!=
null
&&
topBackLabelOpacity
.
opacity
.
value
<
1.0
)
{
midClickOpacity
=
new
Tween
<
double
>(
begin:
0.0
,
end:
topBackLabelOpacity
.
opacity
.
value
,
).
animate
(
animation
);
}
// 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
new
PositionedTransition
(
rect:
slideFromLeadingEdge
(
fromKey:
bottomComponents
.
largeTitleKey
,
fromNavBarBox:
bottomNavBarBox
,
toKey:
topComponents
.
backLabelKey
,
toNavBarBox:
topNavBarBox
,
).
animate
(
animation
),
child:
new
FadeTransition
(
opacity:
midClickOpacity
??
fadeInFrom
(
0.4
),
child:
new
DefaultTextStyleTransition
(
style:
TextStyleTween
(
begin:
_kLargeTitleTextStyle
,
end:
topActionsStyle
,
).
animate
(
animation
),
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
new
PositionedTransition
(
rect:
slideFromLeadingEdge
(
fromKey:
bottomComponents
.
middleKey
,
fromNavBarBox:
bottomNavBarBox
,
toKey:
topComponents
.
backLabelKey
,
toNavBarBox:
topNavBarBox
,
).
animate
(
animation
),
child:
new
FadeTransition
(
opacity:
midClickOpacity
??
fadeInFrom
(
0.3
),
child:
new
DefaultTextStyleTransition
(
style:
TextStyleTween
(
begin:
_kMiddleTitleTextStyle
,
end:
topActionsStyle
,
).
animate
(
animation
),
child:
topBackLabel
.
child
,
),
),
);
}
return
null
;
}
Widget
get
topMiddle
{
final
KeyedSubtree
topMiddle
=
topComponents
.
middleKey
.
currentWidget
;
if
(
topMiddle
==
null
)
{
return
null
;
}
// The middle component is non-null when the nav bar is a large title
// nav bar but would be invisible when expanded, therefore don't show it here.
if
(!
topHasUserMiddle
&&
topLargeExpanded
)
{
return
null
;
}
final
RelativeRect
to
=
positionInTransitionBox
(
topComponents
.
middleKey
,
from:
topNavBarBox
);
// Shift in from the trailing edge of the screen.
final
RelativeRectTween
positionTween
=
new
RelativeRectTween
(
begin:
to
.
shift
(
new
Offset
(
topNavBarBox
.
size
.
width
/
2.0
,
0.0
)),
end:
to
,
);
return
new
PositionedTransition
(
rect:
positionTween
.
animate
(
animation
),
child:
new
FadeTransition
(
opacity:
fadeInFrom
(
0.25
),
child:
new
DefaultTextStyle
(
style:
_kMiddleTitleTextStyle
,
child:
topMiddle
.
child
,
),
),
);
}
Widget
get
topTrailing
{
final
KeyedSubtree
topTrailing
=
topComponents
.
trailingKey
.
currentWidget
;
if
(
topTrailing
==
null
)
{
return
null
;
}
return
new
Positioned
.
fromRelativeRect
(
rect:
positionInTransitionBox
(
topComponents
.
trailingKey
,
from:
topNavBarBox
),
child:
new
FadeTransition
(
opacity:
fadeInFrom
(
0.4
),
child:
topTrailing
.
child
,
),
);
}
Widget
get
topLargeTitle
{
final
KeyedSubtree
topLargeTitle
=
topComponents
.
largeTitleKey
.
currentWidget
;
if
(
topLargeTitle
==
null
||
!
topLargeExpanded
)
{
return
null
;
}
final
RelativeRect
to
=
positionInTransitionBox
(
topComponents
.
largeTitleKey
,
from:
topNavBarBox
);
// Shift in from the trailing edge of the screen.
final
RelativeRectTween
positionTween
=
new
RelativeRectTween
(
begin:
to
.
shift
(
new
Offset
(
topNavBarBox
.
size
.
width
,
0.0
)),
end:
to
,
);
return
new
PositionedTransition
(
rect:
positionTween
.
animate
(
animation
),
child:
new
FadeTransition
(
opacity:
fadeInFrom
(
0.3
),
child:
new
DefaultTextStyle
(
style:
_kLargeTitleTextStyle
,
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
=
new
Size
(
math
.
max
(
begin
.
size
.
width
,
end
.
size
.
width
),
math
.
max
(
begin
.
size
.
height
,
end
.
size
.
height
),
);
return
new
RectTween
(
begin:
begin
.
topLeft
&
largestSize
,
end:
end
.
topLeft
&
largestSize
,
);
};
final
TransitionBuilder
_navBarHeroLaunchPadBuilder
=
(
BuildContext
context
,
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
new
Visibility
(
maintainSize:
true
,
maintainAnimation:
true
,
maintainState:
true
,
visible:
false
,
child:
child
,
);
};
/// Navigation bars' hero flight shuttle builder.
final
HeroFlightShuttleBuilder
_navBarHeroFlightShuttleBuilder
=
(
BuildContext
flightContext
,
Animation
<
double
>
animation
,
HeroFlightDirection
flightDirection
,
BuildContext
fromHeroContext
,
BuildContext
toHeroContext
,
)
{
assert
(
animation
!=
null
);
assert
(
flightDirection
!=
null
);
assert
(
fromHeroContext
!=
null
);
assert
(
toHeroContext
!=
null
);
assert
(
fromHeroContext
.
widget
is
Hero
);
assert
(
toHeroContext
.
widget
is
Hero
);
final
Hero
fromHeroWidget
=
fromHeroContext
.
widget
;
final
Hero
toHeroWidget
=
toHeroContext
.
widget
;
assert
(
fromHeroWidget
.
child
is
_TransitionableNavigationBar
);
assert
(
toHeroWidget
.
child
is
_TransitionableNavigationBar
);
final
_TransitionableNavigationBar
fromNavBar
=
fromHeroWidget
.
child
;
final
_TransitionableNavigationBar
toNavBar
=
toHeroWidget
.
child
;
assert
(
fromNavBar
.
componentsKeys
!=
null
);
assert
(
toNavBar
.
componentsKeys
!=
null
);
assert
(
fromNavBar
.
componentsKeys
.
navBarBoxKey
.
currentContext
.
owner
!=
null
,
'The from nav bar to Hero must have been mounted in the previous frame'
,
);
assert
(
toNavBar
.
componentsKeys
.
navBarBoxKey
.
currentContext
.
owner
!=
null
,
'The to nav bar to Hero must have been mounted in the previous frame'
,
);
switch
(
flightDirection
)
{
case
HeroFlightDirection
.
push
:
return
new
_NavigationBarTransition
(
animation:
animation
,
bottomNavBar:
fromNavBar
,
topNavBar:
toNavBar
,
);
break
;
case
HeroFlightDirection
.
pop
:
return
new
_NavigationBarTransition
(
animation:
animation
,
bottomNavBar:
toNavBar
,
topNavBar:
fromNavBar
,
);
}
};
packages/flutter/lib/src/cupertino/tab_view.dart
View file @
f23c9ae5
...
...
@@ -37,7 +37,7 @@ import 'route.dart';
/// * [CupertinoTabScaffold], a typical host that supports switching between tabs.
/// * [CupertinoPageRoute], a typical modal page route pushed onto the
/// [CupertinoTabView]'s [Navigator].
class
CupertinoTabView
extends
State
less
Widget
{
class
CupertinoTabView
extends
State
ful
Widget
{
/// Creates the content area for a tab in a [CupertinoTabScaffold].
const
CupertinoTabView
({
Key
key
,
...
...
@@ -101,12 +101,41 @@ class CupertinoTabView extends StatelessWidget {
/// This list of observers is not shared with ancestor or descendant [Navigator]s.
final
List
<
NavigatorObserver
>
navigatorObservers
;
@override
_CupertinoTabViewState
createState
()
{
return
new
_CupertinoTabViewState
();
}
}
class
_CupertinoTabViewState
extends
State
<
CupertinoTabView
>
{
HeroController
_heroController
;
List
<
NavigatorObserver
>
_navigatorObservers
;
@override
void
initState
()
{
super
.
initState
();
_heroController
=
new
HeroController
();
// Linear tweening.
_updateObservers
();
}
@override
void
didUpdateWidget
(
CupertinoTabView
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
_updateObservers
();
}
void
_updateObservers
()
{
_navigatorObservers
=
new
List
<
NavigatorObserver
>.
from
(
widget
.
navigatorObservers
)
..
add
(
_heroController
);
}
@override
Widget
build
(
BuildContext
context
)
{
return
new
Navigator
(
onGenerateRoute:
_onGenerateRoute
,
onUnknownRoute:
_onUnknownRoute
,
observers:
navigatorObservers
,
observers:
_
navigatorObservers
,
);
}
...
...
@@ -114,12 +143,12 @@ class CupertinoTabView extends StatelessWidget {
final
String
name
=
settings
.
name
;
WidgetBuilder
routeBuilder
;
String
title
;
if
(
name
==
Navigator
.
defaultRouteName
&&
builder
!=
null
)
{
routeBuilder
=
builder
;
title
=
defaultTitle
;
if
(
name
==
Navigator
.
defaultRouteName
&&
widget
.
builder
!=
null
)
{
routeBuilder
=
widget
.
builder
;
title
=
widget
.
defaultTitle
;
}
else
if
(
routes
!=
null
)
routeBuilder
=
routes
[
name
];
else
if
(
widget
.
routes
!=
null
)
routeBuilder
=
widget
.
routes
[
name
];
if
(
routeBuilder
!=
null
)
{
return
new
CupertinoPageRoute
<
dynamic
>(
builder:
routeBuilder
,
...
...
@@ -127,14 +156,14 @@ class CupertinoTabView extends StatelessWidget {
settings:
settings
,
);
}
if
(
onGenerateRoute
!=
null
)
return
onGenerateRoute
(
settings
);
if
(
widget
.
onGenerateRoute
!=
null
)
return
widget
.
onGenerateRoute
(
settings
);
return
null
;
}
Route
<
dynamic
>
_onUnknownRoute
(
RouteSettings
settings
)
{
assert
(()
{
if
(
onUnknownRoute
==
null
)
{
if
(
widget
.
onUnknownRoute
==
null
)
{
throw
new
FlutterError
(
'Could not find a generator for route
$settings
in the
$runtimeType
.
\n
'
'Generators for routes are searched for in the following order:
\n
'
...
...
@@ -149,7 +178,7 @@ class CupertinoTabView extends StatelessWidget {
}
return
true
;
}());
final
Route
<
dynamic
>
result
=
onUnknownRoute
(
settings
);
final
Route
<
dynamic
>
result
=
widget
.
onUnknownRoute
(
settings
);
assert
(()
{
if
(
result
==
null
)
{
throw
new
FlutterError
(
...
...
packages/flutter/lib/src/widgets/heroes.dart
View file @
f23c9ae5
...
...
@@ -20,11 +20,36 @@ import 'transitions.dart';
/// [MaterialRectArcTween].
typedef
Tween
<
Rect
>
CreateRectTween
(
Rect
begin
,
Rect
end
);
/// A function that lets [Hero]s self supply a [Widget] that is shown during the
/// hero's flight from one route to another instead of default (which is to
/// show the destination route's instance of the Hero).
typedef
Widget
HeroFlightShuttleBuilder
(
BuildContext
flightContext
,
Animation
<
double
>
animation
,
HeroFlightDirection
flightDirection
,
BuildContext
fromHeroContext
,
BuildContext
toHeroContext
,
);
typedef
void
_OnFlightEnded
(
_HeroFlight
flight
);
enum
_HeroFlightType
{
push
,
// Fly the "to" hero and animate with the "to" route.
pop
,
// Fly the "to" hero and animate with the "from" route.
/// Direction of the hero's flight based on the navigation operation.
enum
HeroFlightDirection
{
/// A flight triggered by a route push.
///
/// The animation goes from 0 to 1.
///
/// If no custom [HeroFlightShuttleBuilder] is supplied, the top route's
/// [Hero] child is shown in flight.
push
,
/// A flight triggered by a route pop.
///
/// The animation goes from 1 to 0.
///
/// If no custom [HeroFlightShuttleBuilder] is supplied, the bottom route's
/// [Hero] child is shown in flight.
pop
,
}
// The bounding box for context in global coordinates.
...
...
@@ -42,8 +67,8 @@ Rect _globalBoundingBoxFor(BuildContext context) {
/// be helpful for orienting the user for the feature to physically move from
/// one page to the other during the routes' transition. Such an animation
/// is called a *hero animation*. The hero widgets "fly" in the Navigator's
/// overlay during the transition and while they're in-flight they're
/// not shown in their original locations in the old and new routes.
/// overlay during the transition and while they're in-flight they're
, by
///
default,
not shown in their original locations in the old and new routes.
///
/// To label a widget as such a feature, wrap it in a [Hero] widget. When
/// navigation happens, the [Hero] widgets on each route are identified
...
...
@@ -52,6 +77,9 @@ Rect _globalBoundingBoxFor(BuildContext context) {
///
/// If a [Hero] is already in flight when navigation occurs, its
/// flight animation will be redirected to its new destination.
/// The widget shown in-flight during the transition is, by default, the
/// destination route's [Hero]'s child.
///
/// Routes must not contain more than one [Hero] for each [tag].
///
...
...
@@ -67,12 +95,24 @@ Rect _globalBoundingBoxFor(BuildContext context) {
///
/// To make the animations look good, it's critical that the widget tree for the
/// hero in both locations be essentially identical. The widget of the *target*
/// is used to do the transition: when going from route A to route B, route B's
/// hero's widget is placed over route A's hero's widget, and route A's hero is
/// hidden. Then the widget is animated to route B's hero's position, and then
/// the widget is inserted into route B. When going back from B to A, route A's
/// hero's widget is placed over where route B's hero's widget was, and then the
/// animation goes the other way.
/// is, by default, used to do the transition: when going from route A to route
/// B, route B's hero's widget is placed over route A's hero's widget. If a
/// [flightShuttleBuilder] is supplied, its output widget is shown during the
/// flight transition instead.
///
/// By default, both route A and route B's heroes are hidden while the
/// transitioning widget is animating in-flight above the 2 routes.
/// [placeholderBuilder] can be used to show a custom widget in their place
/// instead once the transition has taken flight.
///
/// During the transition, the transition widget is animated to route B's hero's
/// position, and then the widget is inserted into route B. When going back from
/// B to A, route A's hero's widget is, by default, placed over where route B's
/// hero's widget was, and then the animation goes the other way.
///
/// ## Parts of a Hero Transition
///
/// 
class
Hero
extends
StatefulWidget
{
/// Create a hero.
///
...
...
@@ -81,6 +121,8 @@ class Hero extends StatefulWidget {
Key
key
,
@required
this
.
tag
,
this
.
createRectTween
,
this
.
flightShuttleBuilder
,
this
.
placeholderBuilder
,
@required
this
.
child
,
})
:
assert
(
tag
!=
null
),
assert
(
child
!=
null
),
...
...
@@ -115,6 +157,25 @@ class Hero extends StatefulWidget {
/// {@macro flutter.widgets.child}
final
Widget
child
;
/// Optional override to supply a widget that's shown during the hero's flight.
///
/// This in-flight widget can depend on the route transition's animation as
/// well as the incoming and outgoing routes' [Hero] descendants' widgets and
/// layout.
///
/// When both the source and destination [Hero]s provide a [flightShuttleBuilder],
/// the destination's [flightShuttleBuilder] takes precedence.
///
/// If none is provided, the destination route's Hero child is shown in-flight
/// by default.
final
HeroFlightShuttleBuilder
flightShuttleBuilder
;
/// Placeholder widget left in place as the Hero's child once the flight takes off.
///
/// By default, an empty SizedBox keeping the Hero child's original size is
/// left in place once the Hero shuttle has taken flight.
final
TransitionBuilder
placeholderBuilder
;
// Returns a map of all of the heroes in context, indexed by hero tag.
static
Map
<
Object
,
_HeroState
>
_allHeroesFor
(
BuildContext
context
)
{
assert
(
context
!=
null
);
...
...
@@ -141,6 +202,10 @@ class Hero extends StatefulWidget {
final
_HeroState
heroState
=
hero
.
state
;
result
[
tag
]
=
heroState
;
}
// Don't perform transitions across different Navigators.
if
(
element
.
widget
is
Navigator
)
{
return
;
}
element
.
visitChildren
(
visitor
);
}
context
.
visitChildElements
(
visitor
);
...
...
@@ -181,10 +246,14 @@ class _HeroState extends State<Hero> {
@override
Widget
build
(
BuildContext
context
)
{
if
(
_placeholderSize
!=
null
)
{
return
new
SizedBox
(
width:
_placeholderSize
.
width
,
height:
_placeholderSize
.
height
);
if
(
widget
.
placeholderBuilder
==
null
)
{
return
new
SizedBox
(
width:
_placeholderSize
.
width
,
height:
_placeholderSize
.
height
);
}
else
{
return
widget
.
placeholderBuilder
(
context
,
widget
.
child
);
}
}
return
new
KeyedSubtree
(
key:
_key
,
...
...
@@ -204,9 +273,10 @@ class _HeroFlightManifest {
@required
this
.
fromHero
,
@required
this
.
toHero
,
@required
this
.
createRectTween
,
@required
this
.
shuttleBuilder
,
})
:
assert
(
fromHero
.
widget
.
tag
==
toHero
.
widget
.
tag
);
final
_HeroFlightType
type
;
final
HeroFlightDirection
type
;
final
OverlayState
overlay
;
final
Rect
navigatorRect
;
final
PageRoute
<
dynamic
>
fromRoute
;
...
...
@@ -214,19 +284,21 @@ class _HeroFlightManifest {
final
_HeroState
fromHero
;
final
_HeroState
toHero
;
final
CreateRectTween
createRectTween
;
final
HeroFlightShuttleBuilder
shuttleBuilder
;
Object
get
tag
=>
fromHero
.
widget
.
tag
;
Animation
<
double
>
get
animation
{
return
new
CurvedAnimation
(
parent:
(
type
==
_HeroFlightType
.
push
)
?
toRoute
.
animation
:
fromRoute
.
animation
,
parent:
(
type
==
HeroFlightDirection
.
push
)
?
toRoute
.
animation
:
fromRoute
.
animation
,
curve:
Curves
.
fastOutSlowIn
,
);
}
@override
String
toString
()
{
return
'_HeroFlightManifest(
$type
hero:
$tag
from:
${fromRoute.settings}
to:
${toRoute.settings}
)'
;
return
'_HeroFlightManifest(
$type
tag:
$tag
from route:
${fromRoute.settings}
'
'to route:
${toRoute.settings}
with hero:
$fromHero
to
$toHero
)'
;
}
}
...
...
@@ -238,7 +310,9 @@ class _HeroFlight {
final
_OnFlightEnded
onFlightEnded
;
Tween
<
Rect
>
heroRect
;
Tween
<
Rect
>
heroRectTween
;
Widget
shuttle
;
Animation
<
double
>
_heroOpacity
=
kAlwaysCompleteAnimation
;
ProxyAnimation
_proxyAnimation
;
_HeroFlightManifest
manifest
;
...
...
@@ -255,9 +329,18 @@ class _HeroFlight {
// The OverlayEntry WidgetBuilder callback for the hero's overlay.
Widget
_buildOverlay
(
BuildContext
context
)
{
assert
(
manifest
!=
null
);
shuttle
??=
manifest
.
shuttleBuilder
(
context
,
manifest
.
animation
,
manifest
.
type
,
manifest
.
fromHero
.
context
,
manifest
.
toHero
.
context
,
);
assert
(
shuttle
!=
null
);
return
new
AnimatedBuilder
(
animation:
_proxyAnimation
,
child:
manifest
.
toHero
.
widget
,
child:
shuttle
,
builder:
(
BuildContext
context
,
Widget
child
)
{
final
RenderBox
toHeroBox
=
manifest
.
toHero
.
context
?.
findRenderObject
();
if
(
_aborted
||
toHeroBox
==
null
||
!
toHeroBox
.
attached
)
{
...
...
@@ -273,13 +356,13 @@ class _HeroFlight {
// supposed to end up then recreate the heroRect tween.
final
RenderBox
finalRouteBox
=
manifest
.
toRoute
.
subtreeContext
?.
findRenderObject
();
final
Offset
toHeroOrigin
=
toHeroBox
.
localToGlobal
(
Offset
.
zero
,
ancestor:
finalRouteBox
);
if
(
toHeroOrigin
!=
heroRect
.
end
.
topLeft
)
{
final
Rect
heroRectEnd
=
toHeroOrigin
&
heroRect
.
end
.
size
;
heroRect
=
_doCreateRectTween
(
heroRect
.
begin
,
heroRectEnd
);
if
(
toHeroOrigin
!=
heroRect
Tween
.
end
.
topLeft
)
{
final
Rect
heroRectEnd
=
toHeroOrigin
&
heroRect
Tween
.
end
.
size
;
heroRect
Tween
=
_doCreateRectTween
(
heroRectTween
.
begin
,
heroRectEnd
);
}
}
final
Rect
rect
=
heroRect
.
evaluate
(
_proxyAnimation
);
final
Rect
rect
=
heroRect
Tween
.
evaluate
(
_proxyAnimation
);
final
Size
size
=
manifest
.
navigatorRect
.
size
;
final
RelativeRect
offsets
=
new
RelativeRect
.
fromSize
(
rect
,
size
);
...
...
@@ -291,7 +374,6 @@ class _HeroFlight {
child:
new
IgnorePointer
(
child:
new
RepaintBoundary
(
child:
new
Opacity
(
key:
manifest
.
toHero
.
_key
,
opacity:
_heroOpacity
.
value
,
child:
child
,
),
...
...
@@ -322,12 +404,12 @@ class _HeroFlight {
assert
(()
{
final
Animation
<
double
>
initial
=
initialManifest
.
animation
;
assert
(
initial
!=
null
);
final
_HeroFlightType
type
=
initialManifest
.
type
;
final
HeroFlightDirection
type
=
initialManifest
.
type
;
assert
(
type
!=
null
);
switch
(
type
)
{
case
_HeroFlightType
.
pop
:
case
HeroFlightDirection
.
pop
:
return
initial
.
value
==
1.0
&&
initial
.
status
==
AnimationStatus
.
reverse
;
case
_HeroFlightType
.
push
:
case
HeroFlightDirection
.
push
:
return
initial
.
value
==
0.0
&&
initial
.
status
==
AnimationStatus
.
forward
;
}
return
null
;
...
...
@@ -335,7 +417,7 @@ class _HeroFlight {
manifest
=
initialManifest
;
if
(
manifest
.
type
==
_HeroFlightType
.
pop
)
if
(
manifest
.
type
==
HeroFlightDirection
.
pop
)
_proxyAnimation
.
parent
=
new
ReverseAnimation
(
manifest
.
animation
);
else
_proxyAnimation
.
parent
=
manifest
.
animation
;
...
...
@@ -343,7 +425,7 @@ class _HeroFlight {
manifest
.
fromHero
.
startFlight
();
manifest
.
toHero
.
startFlight
();
heroRect
=
_doCreateRectTween
(
heroRect
Tween
=
_doCreateRectTween
(
_globalBoundingBoxFor
(
manifest
.
fromHero
.
context
),
_globalBoundingBoxFor
(
manifest
.
toHero
.
context
),
);
...
...
@@ -357,7 +439,7 @@ class _HeroFlight {
void
divert
(
_HeroFlightManifest
newManifest
)
{
assert
(
manifest
.
tag
==
newManifest
.
tag
);
if
(
manifest
.
type
==
_HeroFlightType
.
push
&&
newManifest
.
type
==
_HeroFlightType
.
pop
)
{
if
(
manifest
.
type
==
HeroFlightDirection
.
push
&&
newManifest
.
type
==
HeroFlightDirection
.
pop
)
{
// A push flight was interrupted by a pop.
assert
(
newManifest
.
animation
.
status
==
AnimationStatus
.
reverse
);
assert
(
manifest
.
fromHero
==
newManifest
.
toHero
);
...
...
@@ -371,8 +453,8 @@ class _HeroFlight {
// path for swapped begin and end parameters. We want the pop flight
// path to be the same (in reverse) as the push flight path.
_proxyAnimation
.
parent
=
new
ReverseAnimation
(
newManifest
.
animation
);
heroRect
=
new
ReverseTween
<
Rect
>(
heroRect
);
}
else
if
(
manifest
.
type
==
_HeroFlightType
.
pop
&&
newManifest
.
type
==
_HeroFlightType
.
push
)
{
heroRect
Tween
=
new
ReverseTween
<
Rect
>(
heroRectTween
);
}
else
if
(
manifest
.
type
==
HeroFlightDirection
.
pop
&&
newManifest
.
type
==
HeroFlightDirection
.
push
)
{
// A pop flight was interrupted by a push.
assert
(
newManifest
.
animation
.
status
==
AnimationStatus
.
forward
);
assert
(
manifest
.
toHero
==
newManifest
.
fromHero
);
...
...
@@ -386,10 +468,10 @@ class _HeroFlight {
if
(
manifest
.
fromHero
!=
newManifest
.
toHero
)
{
manifest
.
fromHero
.
endFlight
();
newManifest
.
toHero
.
startFlight
();
heroRect
=
_doCreateRectTween
(
heroRect
.
end
,
_globalBoundingBoxFor
(
newManifest
.
toHero
.
context
));
heroRect
Tween
=
_doCreateRectTween
(
heroRectTween
.
end
,
_globalBoundingBoxFor
(
newManifest
.
toHero
.
context
));
}
else
{
// TODO(hansmuller): Use ReverseTween here per github.com/flutter/flutter/pull/12203.
heroRect
=
_doCreateRectTween
(
heroRect
.
end
,
heroRect
.
begin
);
heroRect
Tween
=
_doCreateRectTween
(
heroRectTween
.
end
,
heroRectTween
.
begin
);
}
}
else
{
// A push or a pop flight is heading to a new route, i.e.
...
...
@@ -398,17 +480,24 @@ class _HeroFlight {
assert
(
manifest
.
fromHero
!=
newManifest
.
fromHero
);
assert
(
manifest
.
toHero
!=
newManifest
.
toHero
);
heroRect
=
_doCreateRectTween
(
heroRect
.
evaluate
(
_proxyAnimation
),
_globalBoundingBoxFor
(
newManifest
.
toHero
.
context
));
heroRectTween
=
_doCreateRectTween
(
heroRectTween
.
evaluate
(
_proxyAnimation
),
_globalBoundingBoxFor
(
newManifest
.
toHero
.
context
));
shuttle
=
null
;
if
(
newManifest
.
type
==
_HeroFlightType
.
pop
)
if
(
newManifest
.
type
==
HeroFlightDirection
.
pop
)
_proxyAnimation
.
parent
=
new
ReverseAnimation
(
newManifest
.
animation
);
else
_proxyAnimation
.
parent
=
newManifest
.
animation
;
manifest
.
fromHero
.
endFlight
();
manifest
.
toHero
.
endFlight
();
// Let the heroes in each of the routes rebuild with their placeholders.
newManifest
.
fromHero
.
startFlight
();
newManifest
.
toHero
.
startFlight
();
// Let the transition overlay on top of the routes also rebuild since
// we cleared the old shuttle.
overlayEntry
.
markNeedsBuild
();
}
_aborted
=
false
;
...
...
@@ -455,14 +544,14 @@ class HeroController extends NavigatorObserver {
void
didPush
(
Route
<
dynamic
>
route
,
Route
<
dynamic
>
previousRoute
)
{
assert
(
navigator
!=
null
);
assert
(
route
!=
null
);
_maybeStartHeroTransition
(
previousRoute
,
route
,
_HeroFlightType
.
push
);
_maybeStartHeroTransition
(
previousRoute
,
route
,
HeroFlightDirection
.
push
);
}
@override
void
didPop
(
Route
<
dynamic
>
route
,
Route
<
dynamic
>
previousRoute
)
{
assert
(
navigator
!=
null
);
assert
(
route
!=
null
);
_maybeStartHeroTransition
(
route
,
previousRoute
,
_HeroFlightType
.
pop
);
_maybeStartHeroTransition
(
route
,
previousRoute
,
HeroFlightDirection
.
pop
);
}
@override
...
...
@@ -477,14 +566,14 @@ class HeroController extends NavigatorObserver {
// If we're transitioning between different page routes, start a hero transition
// after the toRoute has been laid out with its animation's value at 1.0.
void
_maybeStartHeroTransition
(
Route
<
dynamic
>
fromRoute
,
Route
<
dynamic
>
toRoute
,
_HeroFlightType
flightType
)
{
void
_maybeStartHeroTransition
(
Route
<
dynamic
>
fromRoute
,
Route
<
dynamic
>
toRoute
,
HeroFlightDirection
flightType
)
{
if
(
_questsEnabled
&&
toRoute
!=
fromRoute
&&
toRoute
is
PageRoute
<
dynamic
>
&&
fromRoute
is
PageRoute
<
dynamic
>)
{
final
PageRoute
<
dynamic
>
from
=
fromRoute
;
final
PageRoute
<
dynamic
>
to
=
toRoute
;
final
Animation
<
double
>
animation
=
(
flightType
==
_HeroFlightType
.
push
)
?
to
.
animation
:
from
.
animation
;
final
Animation
<
double
>
animation
=
(
flightType
==
HeroFlightDirection
.
push
)
?
to
.
animation
:
from
.
animation
;
// A user gesture may have already completed the pop.
if
(
flightType
==
_HeroFlightType
.
pop
&&
animation
.
status
==
AnimationStatus
.
dismissed
)
if
(
flightType
==
HeroFlightDirection
.
pop
&&
animation
.
status
==
AnimationStatus
.
dismissed
)
return
;
// Putting a route offstage changes its animation value to 1.0. Once this
...
...
@@ -493,14 +582,19 @@ class HeroController extends NavigatorObserver {
to
.
offstage
=
to
.
animation
.
value
==
0.0
;
WidgetsBinding
.
instance
.
addPostFrameCallback
((
Duration
value
)
{
_startHeroTransition
(
from
,
to
,
flightType
);
_startHeroTransition
(
from
,
to
,
animation
,
flightType
);
});
}
}
// Find the matching pairs of heros in from and to and either start or a new
// hero flight, or divert an existing one.
void
_startHeroTransition
(
PageRoute
<
dynamic
>
from
,
PageRoute
<
dynamic
>
to
,
_HeroFlightType
flightType
)
{
void
_startHeroTransition
(
PageRoute
<
dynamic
>
from
,
PageRoute
<
dynamic
>
to
,
Animation
<
double
>
animation
,
HeroFlightDirection
flightType
,
)
{
// If the navigator or one of the routes subtrees was removed before this
// end-of-frame callback was called, then don't actually start a transition.
if
(
navigator
==
null
||
from
.
subtreeContext
==
null
||
to
.
subtreeContext
==
null
)
{
...
...
@@ -520,6 +614,9 @@ class HeroController extends NavigatorObserver {
for
(
Object
tag
in
fromHeroes
.
keys
)
{
if
(
toHeroes
[
tag
]
!=
null
)
{
final
HeroFlightShuttleBuilder
fromShuttleBuilder
=
fromHeroes
[
tag
].
widget
.
flightShuttleBuilder
;
final
HeroFlightShuttleBuilder
toShuttleBuilder
=
toHeroes
[
tag
].
widget
.
flightShuttleBuilder
;
final
_HeroFlightManifest
manifest
=
new
_HeroFlightManifest
(
type:
flightType
,
overlay:
navigator
.
overlay
,
...
...
@@ -529,7 +626,10 @@ class HeroController extends NavigatorObserver {
fromHero:
fromHeroes
[
tag
],
toHero:
toHeroes
[
tag
],
createRectTween:
createRectTween
,
shuttleBuilder:
toShuttleBuilder
??
fromShuttleBuilder
??
_defaultHeroFlightShuttleBuilder
,
);
if
(
_flights
[
tag
]
!=
null
)
_flights
[
tag
].
divert
(
manifest
);
else
...
...
@@ -543,4 +643,15 @@ class HeroController extends NavigatorObserver {
void
_handleFlightEnded
(
_HeroFlight
flight
)
{
_flights
.
remove
(
flight
.
manifest
.
tag
);
}
static
final
HeroFlightShuttleBuilder
_defaultHeroFlightShuttleBuilder
=
(
BuildContext
flightContext
,
Animation
<
double
>
animation
,
HeroFlightDirection
flightDirection
,
BuildContext
fromHeroContext
,
BuildContext
toHeroContext
,
)
{
final
Hero
toHero
=
toHeroContext
.
widget
;
return
toHero
.
child
;
};
}
packages/flutter/lib/src/widgets/implicit_animations.dart
View file @
f23c9ae5
...
...
@@ -127,6 +127,24 @@ class BorderRadiusTween extends Tween<BorderRadius> {
BorderRadius
lerp
(
double
t
)
=>
BorderRadius
.
lerp
(
begin
,
end
,
t
);
}
/// An interpolation between two [Border]s.
///
/// This class specializes the interpolation of [Tween<Border>] to use
/// [Border.lerp].
///
/// See [Tween] for a discussion on how to use interpolation objects.
class
BorderTween
extends
Tween
<
Border
>
{
/// Creates a [Border] tween.
///
/// The [begin] and [end] properties may be null; the null value
/// is treated as having no border.
BorderTween
({
Border
begin
,
Border
end
})
:
super
(
begin:
begin
,
end:
end
);
/// Returns the value this variable has at the given animation clock value.
@override
Border
lerp
(
double
t
)
=>
Border
.
lerp
(
begin
,
end
,
t
);
}
/// An interpolation between two [Matrix4]s.
///
/// This class specializes the interpolation of [Tween<Matrix4>] to be
...
...
packages/flutter/lib/src/widgets/sliver_persistent_header.dart
View file @
f23c9ae5
...
...
@@ -188,6 +188,7 @@ class _SliverPersistentHeaderElement extends RenderObjectElement {
@override
void
performRebuild
()
{
super
.
performRebuild
();
renderObject
.
triggerRebuild
();
}
...
...
packages/flutter/lib/src/widgets/transitions.dart
View file @
f23c9ae5
...
...
@@ -10,6 +10,7 @@ import 'package:vector_math/vector_math_64.dart' show Matrix4;
import
'basic.dart'
;
import
'container.dart'
;
import
'framework.dart'
;
import
'text.dart'
;
export
'package:flutter/rendering.dart'
show
RelativeRect
;
...
...
@@ -671,6 +672,64 @@ class AlignTransition extends AnimatedWidget {
}
}
/// Animated version of a [DefaultTextStyle] that animates the different properties
/// of its [TextStyle].
///
/// See also:
///
/// * [DefaultTextStyle], which also defines a [TextStyle] for its descendants
/// but is not animated.
class
DefaultTextStyleTransition
extends
AnimatedWidget
{
/// Creates an animated [DefaultTextStyle] whose [TextStyle] animation updates
/// the widget.
const
DefaultTextStyleTransition
({
Key
key
,
@required
Animation
<
TextStyle
>
style
,
@required
this
.
child
,
this
.
textAlign
,
this
.
softWrap
=
true
,
this
.
overflow
=
TextOverflow
.
clip
,
this
.
maxLines
,
})
:
super
(
key:
key
,
listenable:
style
);
/// The animation that controls the descendants' text style.
Animation
<
TextStyle
>
get
style
=>
listenable
;
/// How the text should be aligned horizontally.
final
TextAlign
textAlign
;
/// Whether the text should break at soft line breaks.
///
/// See [DefaultTextStyle.softWrap] for more details.
final
bool
softWrap
;
/// How visual overflow should be handled.
///
final
TextOverflow
overflow
;
/// An optional maximum number of lines for the text to span, wrapping if necessary.
///
/// See [DefaultTextStyle.maxLines] for more details.
final
int
maxLines
;
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.child}
final
Widget
child
;
@override
Widget
build
(
BuildContext
context
)
{
return
new
DefaultTextStyle
(
style:
style
.
value
,
textAlign:
textAlign
,
softWrap:
softWrap
,
overflow:
overflow
,
maxLines:
maxLines
,
child:
child
,
);
}
}
/// A general-purpose widget for building animations.
///
/// AnimatedBuilder is useful for more complex widgets that wish to include
...
...
packages/flutter/test/cupertino/nav_bar_test.dart
View file @
f23c9ae5
...
...
@@ -306,6 +306,65 @@ void main() {
expect
(
tester
.
getSize
(
find
.
widgetWithText
(
OverflowBox
,
'Title'
)).
height
,
0.0
);
});
testWidgets
(
'User specified middle is always visible in sliver'
,
(
WidgetTester
tester
)
async
{
final
ScrollController
scrollController
=
new
ScrollController
();
final
Key
segmentedControlsKey
=
new
UniqueKey
();
await
tester
.
pumpWidget
(
new
CupertinoApp
(
home:
new
CupertinoPageScaffold
(
child:
new
CustomScrollView
(
controller:
scrollController
,
slivers:
<
Widget
>[
new
CupertinoSliverNavigationBar
(
middle:
new
ConstrainedBox
(
constraints:
const
BoxConstraints
(
maxWidth:
200.0
),
child:
new
CupertinoSegmentedControl
<
int
>(
key:
segmentedControlsKey
,
children:
const
<
int
,
Widget
>{
0
:
Text
(
'Option A'
),
1
:
Text
(
'Option B'
),
},
onValueChanged:
(
int
selected
)
{
},
groupValue:
0
,
),
),
largeTitle:
const
Text
(
'Title'
),
),
new
SliverToBoxAdapter
(
child:
new
Container
(
height:
1200.0
,
),
),
],
),
),
),
);
expect
(
scrollController
.
offset
,
0.0
);
expect
(
tester
.
getTopLeft
(
find
.
byType
(
NavigationToolbar
)).
dy
,
0.0
);
expect
(
tester
.
getSize
(
find
.
byType
(
NavigationToolbar
)).
height
,
44.0
);
expect
(
find
.
text
(
'Title'
),
findsOneWidget
);
expect
(
tester
.
getCenter
(
find
.
byKey
(
segmentedControlsKey
)).
dx
,
400.0
);
expect
(
tester
.
getTopLeft
(
find
.
widgetWithText
(
OverflowBox
,
'Title'
)).
dy
,
44.0
);
expect
(
tester
.
getSize
(
find
.
widgetWithText
(
OverflowBox
,
'Title'
)).
height
,
52.0
);
scrollController
.
jumpTo
(
600.0
);
await
tester
.
pump
();
// Once to trigger the opacity animation.
await
tester
.
pump
(
const
Duration
(
milliseconds:
300
));
expect
(
tester
.
getCenter
(
find
.
byKey
(
segmentedControlsKey
)).
dx
,
400.0
);
// The large title is invisible now.
expect
(
tester
.
renderObject
<
RenderAnimatedOpacity
>(
find
.
widgetWithText
(
AnimatedOpacity
,
'Title'
)
).
opacity
.
value
,
0.0
,
);
});
testWidgets
(
'Small title can be overridden'
,
(
WidgetTester
tester
)
async
{
final
ScrollController
scrollController
=
new
ScrollController
();
await
tester
.
pumpWidget
(
...
...
@@ -390,7 +449,7 @@ void main() {
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
2
00
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
5
00
));
expect
(
find
.
byType
(
CupertinoButton
),
findsOneWidget
);
expect
(
find
.
text
(
new
String
.
fromCharCode
(
CupertinoIcons
.
back
.
codePoint
)),
findsOneWidget
);
...
...
@@ -405,23 +464,22 @@ void main() {
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
2
00
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
5
00
));
expect
(
find
.
byType
(
CupertinoButton
),
findsNWidgets
(
2
));
expect
(
find
.
text
(
'Close'
),
findsOneWidget
);
expect
(
find
.
widgetWithText
(
CupertinoButton
,
'Close'
),
findsOneWidget
);
// Test popping goes back correctly.
await
tester
.
tap
(
find
.
text
(
'Close'
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
2
00
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
5
00
));
expect
(
find
.
text
(
'Page 2'
),
findsOneWidget
);
await
tester
.
tap
(
find
.
text
(
new
String
.
fromCharCode
(
CupertinoIcons
.
back
.
codePoint
)));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
2
00
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
5
00
));
expect
(
find
.
text
(
'Home page'
),
findsOneWidget
);
});
...
...
@@ -438,7 +496,7 @@ void main() {
builder:
(
BuildContext
context
)
{
return
const
CupertinoPageScaffold
(
navigationBar:
CupertinoNavigationBar
(
previousPageTitle:
'0123456789'
,
previousPageTitle:
'0123456789
01
'
,
),
child:
Placeholder
(),
);
...
...
@@ -449,14 +507,14 @@ void main() {
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
expect
(
find
.
widgetWithText
(
CupertinoButton
,
'0123456789'
),
findsOneWidget
);
expect
(
find
.
widgetWithText
(
CupertinoButton
,
'0123456789
01
'
),
findsOneWidget
);
tester
.
state
<
NavigatorState
>(
find
.
byType
(
Navigator
)).
push
(
new
CupertinoPageRoute
<
void
>(
builder:
(
BuildContext
context
)
{
return
const
CupertinoPageScaffold
(
navigationBar:
CupertinoNavigationBar
(
previousPageTitle:
'01234567890'
,
previousPageTitle:
'01234567890
12
'
,
),
child:
Placeholder
(),
);
...
...
packages/flutter/test/cupertino/nav_bar_transition_test.dart
0 → 100644
View file @
f23c9ae5
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
Future
<
void
>
startTransitionBetween
(
WidgetTester
tester
,
{
Widget
from
,
Widget
to
,
String
fromTitle
,
String
toTitle
,
})
async
{
await
tester
.
pumpWidget
(
new
CupertinoApp
(
home:
const
Placeholder
(),
),
);
tester
.
state
<
NavigatorState
>(
find
.
byType
(
Navigator
))
.
push
(
new
CupertinoPageRoute
<
void
>(
title:
fromTitle
,
builder:
(
BuildContext
context
)
=>
scaffoldForNavBar
(
from
),
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
tester
.
state
<
NavigatorState
>(
find
.
byType
(
Navigator
))
.
push
(
new
CupertinoPageRoute
<
void
>(
title:
toTitle
,
builder:
(
BuildContext
context
)
=>
scaffoldForNavBar
(
to
),
));
await
tester
.
pump
();
}
CupertinoPageScaffold
scaffoldForNavBar
(
Widget
navBar
)
{
if
(
navBar
is
CupertinoNavigationBar
||
navBar
==
null
)
{
return
new
CupertinoPageScaffold
(
navigationBar:
navBar
??
const
CupertinoNavigationBar
(),
child:
const
Placeholder
(),
);
}
else
if
(
navBar
is
CupertinoSliverNavigationBar
)
{
return
new
CupertinoPageScaffold
(
child:
new
CustomScrollView
(
slivers:
<
Widget
>[
navBar
,
// Add filler so it's scrollable.
const
SliverToBoxAdapter
(
child:
Placeholder
(
fallbackHeight:
1000.0
),
),
],
),
);
}
assert
(
false
,
'Unexpected nav bar type
${navBar.runtimeType}
'
);
return
null
;
}
Finder
flying
(
WidgetTester
tester
,
Finder
finder
)
{
final
RenderObjectWithChildMixin
<
RenderStack
>
theater
=
tester
.
renderObject
(
find
.
byType
(
Overlay
));
final
RenderStack
theaterStack
=
theater
.
child
;
final
Finder
lastOverlayFinder
=
find
.
byElementPredicate
((
Element
element
)
{
return
element
is
RenderObjectElement
&&
element
.
renderObject
==
theaterStack
.
lastChild
;
});
assert
(
find
.
descendant
(
of:
lastOverlayFinder
,
matching:
find
.
byWidgetPredicate
(
(
Widget
widget
)
=>
widget
.
runtimeType
.
toString
()
==
'_NavigationBarTransition'
,
),
)
.
evaluate
()
.
length
==
1
,
'The last overlay in the navigator was not a flying hero'
,);
return
find
.
descendant
(
of:
lastOverlayFinder
,
matching:
finder
,
);
}
void
checkBackgroundBoxHeight
(
WidgetTester
tester
,
double
height
)
{
final
Widget
transitionBackgroundBox
=
tester
.
widget
<
Stack
>(
flying
(
tester
,
find
.
byType
(
Stack
))).
children
[
0
];
expect
(
tester
.
widget
<
SizedBox
>(
find
.
descendant
(
of:
find
.
byWidget
(
transitionBackgroundBox
),
matching:
find
.
byType
(
SizedBox
),
),
)
.
height
,
height
,
);
}
void
checkOpacity
(
WidgetTester
tester
,
Finder
finder
,
double
opacity
)
{
expect
(
tester
.
renderObject
<
RenderAnimatedOpacity
>(
find
.
ancestor
(
of:
finder
,
matching:
find
.
byType
(
FadeTransition
),
))
.
opacity
.
value
,
opacity
,
);
}
void
main
(
)
{
testWidgets
(
'Bottom middle moves between middle and back label'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
fromTitle:
'Page 1'
);
// Be mid-transition.
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
// There's 2 of them. One from the top route's back label and one from the
// bottom route's middle widget.
expect
(
flying
(
tester
,
find
.
text
(
'Page 1'
)),
findsNWidgets
(
2
));
// Since they have the same text, they should be more or less at the same
// place.
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'Page 1'
)).
first
),
const
Offset
(
331.0724935531616
,
13.5
),
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'Page 1'
)).
last
),
const
Offset
(
331.0724935531616
,
13.5
),
);
});
testWidgets
(
'Bottom middle and top back label transitions their font'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
fromTitle:
'Page 1'
);
// Be mid-transition.
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
// The transition's stack is ordered. The bottom middle is inserted first.
final
RenderParagraph
bottomMiddle
=
tester
.
renderObject
(
flying
(
tester
,
find
.
text
(
'Page 1'
)).
first
);
expect
(
bottomMiddle
.
text
.
style
.
color
,
const
Color
(
0xFF00070F
));
expect
(
bottomMiddle
.
text
.
style
.
fontWeight
,
FontWeight
.
w600
);
expect
(
bottomMiddle
.
text
.
style
.
fontFamily
,
'.SF UI Text'
);
expect
(
bottomMiddle
.
text
.
style
.
letterSpacing
,
-
0.08952957153320312
);
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Page 1'
)).
first
,
0.8609542846679688
);
// The top back label is styled exactly the same way. But the opacity tweens
// are flipped.
final
RenderParagraph
topBackLabel
=
tester
.
renderObject
(
flying
(
tester
,
find
.
text
(
'Page 1'
)).
last
);
expect
(
topBackLabel
.
text
.
style
.
color
,
const
Color
(
0xFF00070F
));
expect
(
topBackLabel
.
text
.
style
.
fontWeight
,
FontWeight
.
w600
);
expect
(
topBackLabel
.
text
.
style
.
fontFamily
,
'.SF UI Text'
);
expect
(
topBackLabel
.
text
.
style
.
letterSpacing
,
-
0.08952957153320312
);
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Page 1'
)).
last
,
0.0
);
// Move animation further a bit.
await
tester
.
pump
(
const
Duration
(
milliseconds:
200
));
expect
(
bottomMiddle
.
text
.
style
.
color
,
const
Color
(
0xFF0073F0
));
expect
(
bottomMiddle
.
text
.
style
.
fontWeight
,
FontWeight
.
w400
);
expect
(
bottomMiddle
.
text
.
style
.
fontFamily
,
'.SF UI Text'
);
expect
(
bottomMiddle
.
text
.
style
.
letterSpacing
,
-
0.231169798374176
);
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Page 1'
)).
first
,
0.0
);
expect
(
topBackLabel
.
text
.
style
.
color
,
const
Color
(
0xFF0073F0
));
expect
(
topBackLabel
.
text
.
style
.
fontWeight
,
FontWeight
.
w400
);
expect
(
topBackLabel
.
text
.
style
.
fontFamily
,
'.SF UI Text'
);
expect
(
topBackLabel
.
text
.
style
.
letterSpacing
,
-
0.231169798374176
);
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Page 1'
)).
last
,
0.8733493089675903
);
});
testWidgets
(
'Fullscreen dialogs do not create heroes'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
new
CupertinoApp
(
home:
const
Placeholder
(),
),
);
tester
.
state
<
NavigatorState
>(
find
.
byType
(
Navigator
))
.
push
(
new
CupertinoPageRoute
<
void
>(
title:
'Page 1'
,
builder:
(
BuildContext
context
)
=>
scaffoldForNavBar
(
null
),
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
tester
.
state
<
NavigatorState
>(
find
.
byType
(
Navigator
))
.
push
(
new
CupertinoPageRoute
<
void
>(
title:
'Page 2'
,
fullscreenDialog:
true
,
builder:
(
BuildContext
context
)
=>
scaffoldForNavBar
(
null
),
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
100
));
// Only the first (non-fullscreen-dialog) page has a Hero.
expect
(
find
.
byType
(
Hero
),
findsOneWidget
);
// No Hero transition happened.
expect
(()
=>
flying
(
tester
,
find
.
text
(
'Page 2'
)),
throwsAssertionError
);
});
testWidgets
(
'Turning off transition works'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
from:
const
CupertinoNavigationBar
(
transitionBetweenRoutes:
false
,
middle:
Text
(
'Page 1'
),
),
toTitle:
'Page 2'
,
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
// Only the second page that doesn't have the transitionBetweenRoutes
// override off has a Hero.
expect
(
find
.
byType
(
Hero
),
findsOneWidget
);
expect
(
find
.
descendant
(
of:
find
.
byType
(
Hero
),
matching:
find
.
text
(
'Page 2'
)),
findsOneWidget
,
);
// No Hero transition happened.
expect
(()
=>
flying
(
tester
,
find
.
text
(
'Page 2'
)),
throwsAssertionError
);
});
testWidgets
(
'Popping mid-transition is symmetrical'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
fromTitle:
'Page 1'
);
// Be mid-transition.
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
void
checkColorAndPositionAt50ms
()
{
// The transition's stack is ordered. The bottom middle is inserted first.
final
RenderParagraph
bottomMiddle
=
tester
.
renderObject
(
flying
(
tester
,
find
.
text
(
'Page 1'
)).
first
);
expect
(
bottomMiddle
.
text
.
style
.
color
,
const
Color
(
0xFF00070F
));
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'Page 1'
)).
first
),
const
Offset
(
331.0724935531616
,
13.5
),
);
// The top back label is styled exactly the same way. But the opacity tweens
// are flipped.
final
RenderParagraph
topBackLabel
=
tester
.
renderObject
(
flying
(
tester
,
find
.
text
(
'Page 1'
)).
last
);
expect
(
topBackLabel
.
text
.
style
.
color
,
const
Color
(
0xFF00070F
));
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'Page 1'
)).
last
),
const
Offset
(
331.0724935531616
,
13.5
),
);
}
checkColorAndPositionAt50ms
();
// Advance more.
await
tester
.
pump
(
const
Duration
(
milliseconds:
100
));
// Pop and reverse the same amount of time.
tester
.
state
<
NavigatorState
>(
find
.
byType
(
Navigator
)).
pop
();
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
100
));
// Check that everything's the same as on the way in.
checkColorAndPositionAt50ms
();
});
testWidgets
(
'There should be no global keys in the hero flight'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
fromTitle:
'Page 1'
);
// Be mid-transition.
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
flying
(
tester
,
find
.
byWidgetPredicate
((
Widget
widget
)
=>
widget
.
key
!=
null
),
),
findsNothing
,
);
});
testWidgets
(
'Transition box grows to large title size'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
fromTitle:
'Page 1'
,
to:
const
CupertinoSliverNavigationBar
(),
toTitle:
'Page 2'
,
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
checkBackgroundBoxHeight
(
tester
,
47.097110748291016
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
checkBackgroundBoxHeight
(
tester
,
61.0267448425293
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
checkBackgroundBoxHeight
(
tester
,
78.68475294113159
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
checkBackgroundBoxHeight
(
tester
,
88.32722091674805
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
checkBackgroundBoxHeight
(
tester
,
93.13018447160721
);
});
testWidgets
(
'Large transition box shrinks to standard nav bar size'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
from:
const
CupertinoSliverNavigationBar
(),
fromTitle:
'Page 1'
,
toTitle:
'Page 2'
,
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
checkBackgroundBoxHeight
(
tester
,
92.90288925170898
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
checkBackgroundBoxHeight
(
tester
,
78.9732551574707
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
checkBackgroundBoxHeight
(
tester
,
61.31524705886841
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
checkBackgroundBoxHeight
(
tester
,
51.67277908325195
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
checkBackgroundBoxHeight
(
tester
,
46.86981552839279
);
});
testWidgets
(
'Hero flight removed at the end of page transition'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
fromTitle:
'Page 1'
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
// There's 2 of them. One from the top route's back label and one from the
// bottom route's middle widget.
expect
(
flying
(
tester
,
find
.
text
(
'Page 1'
)),
findsNWidgets
(
2
));
// End the transition.
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
expect
(()
=>
flying
(
tester
,
find
.
text
(
'Page 1'
)),
throwsAssertionError
);
});
testWidgets
(
'Exact widget is reused to build inside the transition'
,
(
WidgetTester
tester
)
async
{
const
Widget
userMiddle
=
Placeholder
();
await
startTransitionBetween
(
tester
,
from:
const
CupertinoSliverNavigationBar
(
middle:
userMiddle
,
),
fromTitle:
'Page 1'
,
toTitle:
'Page 2'
,
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
flying
(
tester
,
find
.
byWidget
(
userMiddle
)),
findsOneWidget
);
});
testWidgets
(
'First appearance of back chevron fades in from the right'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
new
CupertinoApp
(
home:
scaffoldForNavBar
(
null
),
),
);
tester
.
state
<
NavigatorState
>(
find
.
byType
(
Navigator
))
.
push
(
new
CupertinoPageRoute
<
void
>(
title:
'Page 1'
,
builder:
(
BuildContext
context
)
=>
scaffoldForNavBar
(
null
),
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
final
Finder
backChevron
=
flying
(
tester
,
find
.
text
(
new
String
.
fromCharCode
(
CupertinoIcons
.
back
.
codePoint
)));
expect
(
backChevron
,
// Only one exists from the top page. The bottom page has no back chevron.
findsOneWidget
,
);
// Come in from the right and fade in.
checkOpacity
(
tester
,
backChevron
,
0.0
);
expect
(
tester
.
getTopLeft
(
backChevron
),
const
Offset
(
71.94993209838867
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
150
));
checkOpacity
(
tester
,
backChevron
,
0.32467134296894073
);
expect
(
tester
.
getTopLeft
(
backChevron
),
const
Offset
(
18.033634185791016
,
5.0
));
});
testWidgets
(
'Back chevron fades out and in when both pages have it'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
fromTitle:
'Page 1'
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
final
Finder
backChevrons
=
flying
(
tester
,
find
.
text
(
new
String
.
fromCharCode
(
CupertinoIcons
.
back
.
codePoint
)));
expect
(
backChevrons
,
findsNWidgets
(
2
),
);
checkOpacity
(
tester
,
backChevrons
.
first
,
0.8393326997756958
);
checkOpacity
(
tester
,
backChevrons
.
last
,
0.0
);
// Both overlap at the same place.
expect
(
tester
.
getTopLeft
(
backChevrons
.
first
),
const
Offset
(
8.0
,
5.0
));
expect
(
tester
.
getTopLeft
(
backChevrons
.
last
),
const
Offset
(
8.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
150
));
checkOpacity
(
tester
,
backChevrons
.
first
,
0.0
);
checkOpacity
(
tester
,
backChevrons
.
last
,
0.6276369094848633
);
// Still in the same place.
expect
(
tester
.
getTopLeft
(
backChevrons
.
first
),
const
Offset
(
8.0
,
5.0
));
expect
(
tester
.
getTopLeft
(
backChevrons
.
last
),
const
Offset
(
8.0
,
5.0
));
});
testWidgets
(
'Bottom middle just fades if top page has a custom leading'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
fromTitle:
'Page 1'
,
to:
const
CupertinoSliverNavigationBar
(
leading:
Text
(
'custom'
),
),
toTitle:
'Page 2'
,
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
// There's just 1 in flight because there's no back label on the top page.
expect
(
flying
(
tester
,
find
.
text
(
'Page 1'
)),
findsOneWidget
);
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Page 1'
)),
0.8609542846679688
);
// The middle widget doesn't move.
expect
(
tester
.
getCenter
(
flying
(
tester
,
find
.
text
(
'Page 1'
))),
const
Offset
(
400.0
,
22.0
),
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
150
));
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Page 1'
)),
0.0
);
expect
(
tester
.
getCenter
(
flying
(
tester
,
find
.
text
(
'Page 1'
))),
const
Offset
(
400.0
,
22.0
),
);
});
testWidgets
(
'Bottom leading fades in place'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
from:
const
CupertinoSliverNavigationBar
(
leading:
Text
(
'custom'
)),
fromTitle:
'Page 1'
,
toTitle:
'Page 2'
,
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
flying
(
tester
,
find
.
text
(
'custom'
)),
findsOneWidget
);
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'custom'
)),
0.7655444294214249
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'custom'
))),
const
Offset
(
16.0
,
0.0
),
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
150
));
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'custom'
)),
0.0
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'custom'
))),
const
Offset
(
16.0
,
0.0
),
);
});
testWidgets
(
'Bottom trailing fades in place'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
from:
const
CupertinoSliverNavigationBar
(
trailing:
Text
(
'custom'
)),
fromTitle:
'Page 1'
,
toTitle:
'Page 2'
,
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
flying
(
tester
,
find
.
text
(
'custom'
)),
findsOneWidget
);
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'custom'
)),
0.8393326997756958
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'custom'
))),
const
Offset
(
683.0
,
13.5
),
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
150
));
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'custom'
)),
0.0
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'custom'
))),
const
Offset
(
683.0
,
13.5
),
);
});
testWidgets
(
'Bottom back label fades and slides to the left'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
fromTitle:
'Page 1'
,
toTitle:
'Page 2'
,
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
tester
.
state
<
NavigatorState
>(
find
.
byType
(
Navigator
))
.
push
(
new
CupertinoPageRoute
<
void
>(
title:
'Page 3'
,
builder:
(
BuildContext
context
)
=>
scaffoldForNavBar
(
null
),
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
// 'Page 1' appears once on Page 2 as the back label.
expect
(
flying
(
tester
,
find
.
text
(
'Page 1'
)),
findsOneWidget
);
// Back label fades out faster.
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Page 1'
)),
0.5584745407104492
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'Page 1'
))),
const
Offset
(
24.176071166992188
,
13.5
),
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
150
));
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Page 1'
)),
0.0
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'Page 1'
))),
const
Offset
(-
292.97862243652344
,
13.5
),
);
});
testWidgets
(
'Bottom large title moves to top back label'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
from:
const
CupertinoSliverNavigationBar
(),
fromTitle:
'Page 1'
,
toTitle:
'Page 2'
,
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
// There's 2, one from the bottom large title fading out and one from the
// bottom back label fading in.
expect
(
flying
(
tester
,
find
.
text
(
'Page 1'
)),
findsNWidgets
(
2
));
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Page 1'
)).
first
,
0.8393326997756958
);
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Page 1'
)).
last
,
0.0
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'Page 1'
)).
first
),
const
Offset
(
17.905914306640625
,
51.58156871795654
),
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'Page 1'
)).
last
),
const
Offset
(
17.905914306640625
,
51.58156871795654
),
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
150
));
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Page 1'
)).
first
,
0.0
);
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Page 1'
)).
last
,
0.6276369094848633
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'Page 1'
)).
first
),
const
Offset
(
43.278289794921875
,
19.23011875152588
),
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'Page 1'
)).
last
),
const
Offset
(
43.278289794921875
,
19.23011875152588
),
);
});
testWidgets
(
'Long title turns into the word back mid transition'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
from:
const
CupertinoSliverNavigationBar
(),
fromTitle:
'A title too long to fit'
,
toTitle:
'Page 2'
,
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
flying
(
tester
,
find
.
text
(
'A title too long to fit'
)),
findsOneWidget
);
// Automatically changed to the word 'Back' in the back label.
expect
(
flying
(
tester
,
find
.
text
(
'Back'
)),
findsOneWidget
);
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'A title too long to fit'
)),
0.8393326997756958
);
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Back'
)),
0.0
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'A title too long to fit'
))),
const
Offset
(
17.905914306640625
,
51.58156871795654
),
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'Back'
))),
const
Offset
(
17.905914306640625
,
51.58156871795654
),
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
150
));
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'A title too long to fit'
)),
0.0
);
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Back'
)),
0.6276369094848633
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'A title too long to fit'
))),
const
Offset
(
43.278289794921875
,
19.23011875152588
),
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'Back'
))),
const
Offset
(
43.278289794921875
,
19.23011875152588
),
);
});
testWidgets
(
'Bottom large title and top back label transitions their font'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
from:
const
CupertinoSliverNavigationBar
(),
fromTitle:
'Page 1'
,
);
// Be mid-transition.
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
// The transition's stack is ordered. The bottom large title is inserted first.
final
RenderParagraph
bottomLargeTitle
=
tester
.
renderObject
(
flying
(
tester
,
find
.
text
(
'Page 1'
)).
first
);
expect
(
bottomLargeTitle
.
text
.
style
.
color
,
const
Color
(
0xFF00070F
));
expect
(
bottomLargeTitle
.
text
.
style
.
fontWeight
,
FontWeight
.
w700
);
expect
(
bottomLargeTitle
.
text
.
style
.
fontFamily
,
'.SF Pro Display'
);
expect
(
bottomLargeTitle
.
text
.
style
.
letterSpacing
,
0.21141128540039061
);
// The top back label is styled exactly the same way.
final
RenderParagraph
topBackLabel
=
tester
.
renderObject
(
flying
(
tester
,
find
.
text
(
'Page 1'
)).
last
);
expect
(
topBackLabel
.
text
.
style
.
color
,
const
Color
(
0xFF00070F
));
expect
(
topBackLabel
.
text
.
style
.
fontWeight
,
FontWeight
.
w700
);
expect
(
topBackLabel
.
text
.
style
.
fontFamily
,
'.SF Pro Display'
);
expect
(
topBackLabel
.
text
.
style
.
letterSpacing
,
0.21141128540039061
);
// Move animation further a bit.
await
tester
.
pump
(
const
Duration
(
milliseconds:
200
));
expect
(
bottomLargeTitle
.
text
.
style
.
color
,
const
Color
(
0xFF0073F0
));
expect
(
bottomLargeTitle
.
text
.
style
.
fontWeight
,
FontWeight
.
w400
);
expect
(
bottomLargeTitle
.
text
.
style
.
fontFamily
,
'.SF UI Text'
);
expect
(
bottomLargeTitle
.
text
.
style
.
letterSpacing
,
-
0.2135093951225281
);
expect
(
topBackLabel
.
text
.
style
.
color
,
const
Color
(
0xFF0073F0
));
expect
(
topBackLabel
.
text
.
style
.
fontWeight
,
FontWeight
.
w400
);
expect
(
topBackLabel
.
text
.
style
.
fontFamily
,
'.SF UI Text'
);
expect
(
topBackLabel
.
text
.
style
.
letterSpacing
,
-
0.2135093951225281
);
});
testWidgets
(
'Top middle fades in and slides in from the right'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
toTitle:
'Page 2'
,
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
flying
(
tester
,
find
.
text
(
'Page 2'
)),
findsOneWidget
);
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Page 2'
)),
0.0
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'Page 2'
))),
const
Offset
(
725.1760711669922
,
13.5
),
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
150
));
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Page 2'
)),
0.6972532719373703
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'Page 2'
))),
const
Offset
(
408.02137756347656
,
13.5
),
);
});
testWidgets
(
'Top large title fades in and slides in from the right'
,
(
WidgetTester
tester
)
async
{
await
startTransitionBetween
(
tester
,
to:
const
CupertinoSliverNavigationBar
(),
toTitle:
'Page 2'
,
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
flying
(
tester
,
find
.
text
(
'Page 2'
)),
findsOneWidget
);
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Page 2'
)),
0.0
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'Page 2'
))),
const
Offset
(
768.3521423339844
,
54.0
),
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
150
));
checkOpacity
(
tester
,
flying
(
tester
,
find
.
text
(
'Page 2'
)),
0.6753286570310593
);
expect
(
tester
.
getTopLeft
(
flying
(
tester
,
find
.
text
(
'Page 2'
))),
const
Offset
(
134.04275512695312
,
54.0
),
);
});
testWidgets
(
'Components are not unnecessarily rebuilt during transitions'
,
(
WidgetTester
tester
)
async
{
int
bottomBuildTimes
=
0
;
int
topBuildTimes
=
0
;
await
startTransitionBetween
(
tester
,
from:
new
CupertinoNavigationBar
(
middle:
new
Builder
(
builder:
(
BuildContext
context
)
{
bottomBuildTimes
++;
return
const
Text
(
'Page 1'
);
}),
),
to:
new
CupertinoSliverNavigationBar
(
largeTitle:
new
Builder
(
builder:
(
BuildContext
context
)
{
topBuildTimes
++;
return
const
Text
(
'Page 2'
);
}),
),
);
expect
(
bottomBuildTimes
,
1
);
// RenderSliverPersistentHeader.layoutChild causes 2 builds.
expect
(
topBuildTimes
,
2
);
await
tester
.
pump
();
// The shuttle builder builds the component widgets one more time.
expect
(
bottomBuildTimes
,
2
);
expect
(
topBuildTimes
,
3
);
// Subsequent animation needs to use reprojection of children.
await
tester
.
pump
();
expect
(
bottomBuildTimes
,
2
);
expect
(
topBuildTimes
,
3
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
100
));
expect
(
bottomBuildTimes
,
2
);
expect
(
topBuildTimes
,
3
);
// Finish animations.
await
tester
.
pump
(
const
Duration
(
milliseconds:
400
));
expect
(
bottomBuildTimes
,
2
);
expect
(
topBuildTimes
,
3
);
});
}
packages/flutter/test/cupertino/route_test.dart
View file @
f23c9ae5
...
...
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
...
...
@@ -36,6 +37,71 @@ void main() {
expect
(
tester
.
getCenter
(
find
.
text
(
'An iPod'
)).
dx
,
400.0
);
});
testWidgets
(
'Large title auto-populates with title'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
new
CupertinoApp
(
home:
const
Placeholder
(),
),
);
tester
.
state
<
NavigatorState
>(
find
.
byType
(
Navigator
)).
push
(
new
CupertinoPageRoute
<
void
>(
title:
'An iPod'
,
builder:
(
BuildContext
context
)
{
return
new
CupertinoPageScaffold
(
child:
new
CustomScrollView
(
slivers:
const
<
Widget
>[
CupertinoSliverNavigationBar
(),
],
),
);
}
)
);
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
// There should be 2 Text widget with the title in the nav bar. One in the
// large title position and one in the middle position (though the middle
// position Text is initially invisible while the sliver is expanded).
expect
(
find
.
widgetWithText
(
CupertinoSliverNavigationBar
,
'An iPod'
),
findsNWidgets
(
2
),
);
final
List
<
Element
>
titles
=
tester
.
elementList
(
find
.
text
(
'An iPod'
))
.
toList
()
..
sort
((
Element
a
,
Element
b
)
{
final
RenderParagraph
aParagraph
=
a
.
renderObject
;
final
RenderParagraph
bParagraph
=
b
.
renderObject
;
return
aParagraph
.
text
.
style
.
fontSize
.
compareTo
(
bParagraph
.
text
.
style
.
fontSize
);
});
final
Iterable
<
double
>
opacities
=
titles
.
map
((
Element
element
)
{
final
RenderAnimatedOpacity
renderOpacity
=
element
.
ancestorRenderObjectOfType
(
const
TypeMatcher
<
RenderAnimatedOpacity
>());
return
renderOpacity
.
opacity
.
value
;
});
expect
(
opacities
,
<
double
>
[
0.0
,
// Initially the smaller font title is invisible.
1.0
,
// The larger font title is visible.
]);
// Check that the large font title is at the right spot.
expect
(
tester
.
getTopLeft
(
find
.
byWidget
(
titles
[
1
].
widget
)),
const
Offset
(
16.0
,
54.0
),
);
// The smaller, initially invisible title, should still be positioned in the
// center.
expect
(
tester
.
getCenter
(
find
.
byWidget
(
titles
[
0
].
widget
)).
dx
,
400.0
);
});
testWidgets
(
'Leading auto-populates with back button with previous title'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
new
CupertinoApp
(
...
...
packages/flutter/test/cupertino/scaffold_test.dart
View file @
f23c9ae5
...
...
@@ -239,7 +239,7 @@ void main() {
// Navigate in tab 2.
await
tester
.
tap
(
find
.
text
(
'Next'
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
3
00
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
5
00
));
expect
(
find
.
text
(
'Page 2 of tab 2'
),
isOnstage
);
expect
(
find
.
text
(
'Page 1 of tab 1'
,
skipOffstage:
false
),
isOffstage
);
...
...
@@ -254,7 +254,7 @@ void main() {
// Navigate in tab 1.
await
tester
.
tap
(
find
.
text
(
'Next'
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
3
00
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
5
00
));
expect
(
find
.
text
(
'Page 2 of tab 1'
),
isOnstage
);
expect
(
find
.
text
(
'Page 2 of tab 2'
,
skipOffstage:
false
),
isOffstage
);
...
...
@@ -268,7 +268,7 @@ void main() {
// Pop in tab 2
await
tester
.
tap
(
find
.
text
(
'Back'
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
3
00
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
5
00
));
expect
(
find
.
text
(
'Page 1 of tab 2'
),
isOnstage
);
expect
(
find
.
text
(
'Page 2 of tab 1'
,
skipOffstage:
false
),
isOffstage
);
...
...
packages/flutter/test/widgets/heroes_test.dart
View file @
f23c9ae5
...
...
@@ -1244,4 +1244,95 @@ void main() {
await
tester
.
pump
(
duration
*
0.1
);
expect
(
tester
.
getTopLeft
(
find
.
byKey
(
firstKey
)).
dx
,
x0
);
});
testWidgets
(
'Can override flight shuttle'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
new
MaterialApp
(
home:
new
Material
(
child:
new
ListView
(
children:
<
Widget
>[
const
Hero
(
tag:
'a'
,
child:
Text
(
'foo'
)),
new
Builder
(
builder:
(
BuildContext
context
)
{
return
new
FlatButton
(
child:
const
Text
(
'two'
),
onPressed:
()
=>
Navigator
.
push
<
void
>(
context
,
new
MaterialPageRoute
<
void
>(
builder:
(
BuildContext
context
)
{
return
new
Material
(
child:
new
Hero
(
tag:
'a'
,
child:
const
Text
(
'bar'
),
flightShuttleBuilder:
(
BuildContext
flightContext
,
Animation
<
double
>
animation
,
HeroFlightDirection
flightDirection
,
BuildContext
fromHeroContext
,
BuildContext
toHeroContext
,
)
{
return
const
Text
(
'baz'
);
},
),
);
},
)),
);
}),
],
),
),
));
await
tester
.
tap
(
find
.
text
(
'two'
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
10
));
expect
(
find
.
text
(
'foo'
),
findsNothing
);
expect
(
find
.
text
(
'bar'
),
findsNothing
);
expect
(
find
.
text
(
'baz'
),
findsOneWidget
);
});
testWidgets
(
'Can override flight launch pads'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
new
MaterialApp
(
home:
new
Material
(
child:
new
ListView
(
children:
<
Widget
>[
new
Hero
(
tag:
'a'
,
child:
const
Text
(
'Batman'
),
placeholderBuilder:
(
BuildContext
context
,
Widget
child
)
{
return
const
Text
(
'Venom'
);
},
),
new
Builder
(
builder:
(
BuildContext
context
)
{
return
new
FlatButton
(
child:
const
Text
(
'two'
),
onPressed:
()
=>
Navigator
.
push
<
void
>(
context
,
new
MaterialPageRoute
<
void
>(
builder:
(
BuildContext
context
)
{
return
new
Material
(
child:
new
Hero
(
tag:
'a'
,
child:
const
Text
(
'Wolverine'
),
placeholderBuilder:
(
BuildContext
context
,
Widget
child
)
{
return
const
Text
(
'Joker'
);
},
),
);
},
)),
);
}),
],
),
),
));
await
tester
.
tap
(
find
.
text
(
'two'
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
10
));
expect
(
find
.
text
(
'Batman'
),
findsNothing
);
// This shows up once but in the Hero because by default, the destination
// Hero child is the widget in flight.
expect
(
find
.
text
(
'Wolverine'
),
findsOneWidget
);
expect
(
find
.
text
(
'Venom'
),
findsOneWidget
);
expect
(
find
.
text
(
'Joker'
),
findsOneWidget
);
});
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment