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
ef004ae9
Commit
ef004ae9
authored
Feb 14, 2017
by
Hans Muller
Committed by
GitHub
Feb 14, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
New Heroes (#8112)
parent
cd3fd475
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
371 additions
and
439 deletions
+371
-439
stock_symbol_viewer.dart
examples/stocks/lib/stock_symbol_viewer.dart
+0
-1
heroes.dart
packages/flutter/lib/src/widgets/heroes.dart
+335
-438
heroes_test.dart
packages/flutter/test/widgets/heroes_test.dart
+32
-0
navigator_test.dart
packages/flutter/test/widgets/navigator_test.dart
+1
-0
page_forward_transitions_test.dart
...s/flutter/test/widgets/page_forward_transitions_test.dart
+2
-0
page_transitions_test.dart
packages/flutter/test/widgets/page_transitions_test.dart
+1
-0
No files found.
examples/stocks/lib/stock_symbol_viewer.dart
View file @
ef004ae9
...
...
@@ -81,7 +81,6 @@ class StockSymbolPage extends StatelessWidget {
stock:
stock
,
arrow:
new
Hero
(
tag:
stock
,
turns:
2
,
child:
new
StockArrow
(
percentChange:
stock
.
percentChange
)
)
)
...
...
packages/flutter/lib/src/widgets/heroes.dart
View file @
ef004ae9
...
...
@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:collection'
;
import
'package:flutter/foundation.dart'
;
import
'basic.dart'
;
...
...
@@ -14,71 +12,54 @@ import 'overlay.dart';
import
'pages.dart'
;
import
'transitions.dart'
;
// TODO(ianh): Make the appear/disappear animations pretty. Right now they're
// pretty crude (just rotate and shrink the constraints). They should probably
// involve actually scaling and fading, at a minimum.
// TODO(ianh): If the widgets use Inherited properties, they are taken from the
// Navigator's position in the widget hierarchy, not the source or target. We
// should interpolate the inherited properties from their value at the source to
// their value at the target. See: https://github.com/flutter/flutter/issues/213
class
_HeroManifest
{
const
_HeroManifest
({
this
.
key
,
this
.
config
,
this
.
sourceStates
,
this
.
currentRect
,
this
.
currentTurns
});
final
GlobalKey
key
;
final
Widget
config
;
final
Set
<
_HeroState
>
sourceStates
;
final
Rect
currentRect
;
final
double
currentTurns
;
/// Signature for a function that takes two [Rect] instances and returns a
/// [RectTween] that transitions between them.
///
/// This is typically used with a [HeroController] to provide an animation for
/// [Hero] positions that looks nicer than a linear movement. For example, see
/// [MaterialRectArcTween].
typedef
RectTween
CreateRectTween
(
Rect
begin
,
Rect
end
);
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.
}
abstract
class
_HeroHandle
{
bool
get
alwaysAnimate
;
_HeroManifest
_takeChild
(
Animation
<
double
>
currentAnimation
);
// The bounding box for context in global coordinates.
Rect
_globalRect
(
BuildContext
context
)
{
final
RenderBox
box
=
context
.
findRenderObject
();
assert
(
box
!=
null
&&
box
.
hasSize
);
return
MatrixUtils
.
transformRect
(
box
.
getTransformTo
(
null
),
Point
.
origin
&
box
.
size
);
}
/// A widget that marks its child as being a candidate for hero animations.
///
/// During a page transition (see [Navigator]), if a particular feature (e.g. a
/// picture or heading) appears on both pages, it can be helpful for orienting
/// the user if the feature appears to physically move from one page to the
/// other. Such an animation is called a *hero animation*.
///
/// To label a widget as such a feature, wrap it in a [Hero] widget. When a
/// navigation happens, the [Hero] widgets on each page are collected up. For
/// each pair of [Hero] widgets that have the same tag, a hero animation is
/// triggered.
/// When a [PageRoute] is pushed or popped with the [Navigator], the entire
/// screen's content is replaced. An old route disappears and a new route
/// appears. If there's a common visual feature on both routes then it can
/// 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.
///
/// Hero animations are managed by a [HeroController].
/// 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
/// by the [HeroController]. For each pair of [Hero] widgets that have the
/// same tag, a hero animation is triggered.
///
/// If a [Hero] is already in flight when
another navigation occurs, then it
///
will continue to the next page
.
/// If a [Hero] is already in flight when
navigation occurs, its
///
flight animation will be redirected to its new destination
.
///
///
A particular page must not have
more than one [Hero] for each [tag].
///
Routes must not contain
more than one [Hero] for each [tag].
///
/// ## Discussion
///
/// Heroes are the parts of an application's screen-to-screen transitions where
/// a widget from one screen shifts to a position on the other. For example,
/// album art from a list of albums growing to become the centerpiece of the
/// album's details view. In this context, a screen is a [ModalRoute] in a
/// [Navigator].
///
/// To get this effect, all you have to do is wrap each hero on each route with
/// a [Hero] widget, and give each hero a [tag]. The tag must be unique within
/// the current route's widget subtree, and must match the tag of a [Hero] in
/// the target route. When the app transitions from one route to another, each
/// hero is animated to its new location.
///
/// Heroes and the [Navigator]'s [Overlay]'s [Stack] must be axis-aligned for
/// Heroes and the [Navigator]'s [Overlay] [Stack] must be axis-aligned for
/// all this to work. The top left and bottom right coordinates of each animated
///
[Hero]
will be converted to global coordinates and then from there converted
///
Hero
will be converted to global coordinates and then from there converted
/// to that [Stack]'s coordinate space, and the entire Hero subtree will, for
/// the duration of the animation, be lifted out of its original place, and
/// positioned on that stack. If the [Hero] isn't axis aligned, this is going to
...
...
@@ -95,56 +76,43 @@ abstract class _HeroHandle {
class
Hero
extends
StatefulWidget
{
/// Create a hero.
///
/// The [tag] and [child]
are required
.
/// The [tag] and [child]
parameters must not be null
.
Hero
({
Key
key
,
@required
this
.
tag
,
this
.
turns
:
1
,
this
.
alwaysAnimate
:
false
,
@required
this
.
child
,
})
:
super
(
key:
key
)
{
assert
(
tag
!=
null
);
assert
(
turns
!=
null
);
assert
(
alwaysAnimate
!=
null
);
assert
(
child
!=
null
);
}
/// The identifier for this particular hero. If the tag of this hero matches
/// the tag of a hero on
the other page during a page transition, then a hero
/// animation will be triggered.
/// the tag of a hero on
a [PageRoute] that we're navigating to or from, then
/// a
hero a
nimation will be triggered.
final
Object
tag
;
/// The relative number of full rotations that the hero is conceptually at.
///
/// If a hero is animated from a [Hero] with [turns] set to 1 to a [Hero] with
/// [turns] set to 2, then it will turn by one full rotation during its
/// animation. Normally, all heroes have a [turns] value of 1.
final
int
turns
;
/// If true, the hero will always animate, even if it has no matching hero to
/// animate to or from. If it has no target, it will imply a target at the
/// same position with zero width and height and with [turns] set to zero.
/// This will typically cause it to shrink and spin.
final
bool
alwaysAnimate
;
/// The widget below this widget in the tree.
/// The widget subtree that will "fly" from one route to another during a
/// [Naviator] push or pop transition.
///
/// This subtree should match the appearance of the subtrees of any other
/// heroes in the application with the same [tag].
/// The appearance of this subtree should be similar to the appearance of
/// the subtrees of any other heroes in the application with the same [tag].
/// Changes in scale and aspect ratio work well in hero animations, changes
/// in layout or composition do not.
final
Widget
child
;
/// Return a hero tag to _HeroState map of all of the heroes within the given subtree.
static
Map
<
Object
,
_HeroHandle
>
_of
(
BuildContext
context
)
{
final
Map
<
Object
,
_HeroHandle
>
result
=
<
Object
,
_HeroHandle
>{};
// Returns a map of all of the heroes in context, indexed by hero tag.
static
Map
<
Object
,
_HeroState
>
_allHeroesFor
(
BuildContext
context
)
{
assert
(
context
!=
null
);
final
Map
<
Object
,
_HeroState
>
result
=
<
Object
,
_HeroState
>{};
void
visitor
(
Element
element
)
{
if
(
element
.
widget
is
Hero
)
{
StatefulElement
hero
=
element
;
Hero
heroWidget
=
element
.
widget
;
Object
tag
=
heroWidget
.
tag
;
final
StatefulElement
hero
=
element
;
final
Hero
heroWidget
=
element
.
widget
;
final
Object
tag
=
heroWidget
.
tag
;
assert
(
tag
!=
null
);
assert
(()
{
if
(
result
.
containsKey
(
tag
))
{
new
FlutterError
(
throw
new
FlutterError
(
'There are multiple heroes that share the same tag within a subtree.
\n
'
'Within each subtree for which heroes are to be animated (typically a PageRoute subtree), '
'each Hero must have a unique non-null tag.
\n
'
...
...
@@ -166,299 +134,270 @@ class Hero extends StatefulWidget {
_HeroState
createState
()
=>
new
_HeroState
();
}
class
_HeroState
extends
State
<
Hero
>
implements
_HeroHandle
{
class
_HeroState
extends
State
<
Hero
>
{
GlobalKey
_key
=
new
GlobalKey
();
Size
_placeholderSize
;
VoidCallback
_disposeCallback
;
@override
bool
get
alwaysAnimate
=>
config
.
alwaysAnimate
;
@override
_HeroManifest
_takeChild
(
Animation
<
double
>
currentAnimation
)
{
assert
(
mounted
);
final
RenderBox
renderObject
=
context
.
findRenderObject
();
assert
(
renderObject
!=
null
);
assert
(!
renderObject
.
debugNeedsLayout
);
assert
(
renderObject
.
hasSize
);
if
(
_placeholderSize
==
null
)
{
// We are a "from" hero, about to depart on a quest.
// Remember our size so that we can leave a placeholder.
_placeholderSize
=
renderObject
.
size
;
}
final
Point
heroTopLeft
=
renderObject
.
localToGlobal
(
Point
.
origin
);
final
Point
heroBottomRight
=
renderObject
.
localToGlobal
(
renderObject
.
size
.
bottomRight
(
Point
.
origin
));
final
Rect
heroArea
=
new
Rect
.
fromLTRB
(
heroTopLeft
.
x
,
heroTopLeft
.
y
,
heroBottomRight
.
x
,
heroBottomRight
.
y
);
_HeroManifest
result
=
new
_HeroManifest
(
key:
_key
,
// might be null, e.g. if the hero is returning to us
config:
config
,
sourceStates:
new
HashSet
<
_HeroState
>.
from
(<
_HeroState
>[
this
]),
currentRect:
heroArea
,
currentTurns:
config
.
turns
.
toDouble
()
);
if
(
_key
!=
null
)
setState
(()
{
_key
=
null
;
});
return
result
;
}
void
_setChild
(
GlobalKey
value
)
{
assert
(
_key
==
null
);
assert
(
_placeholderSize
!=
null
);
assert
(
value
!=
null
);
void
startFlight
()
{
assert
(
mounted
);
final
RenderBox
box
=
context
.
findRenderObject
();
assert
(
box
!=
null
&&
box
.
hasSize
);
setState
(()
{
_key
=
value
;
_placeholderSize
=
null
;
_placeholderSize
=
box
.
size
;
});
}
void
_resetChild
()
{
if
(
mounted
)
_setChild
(
new
GlobalKey
());
}
@override
void
dispose
()
{
if
(
_disposeCallback
!=
null
)
_disposeCallback
();
super
.
dispose
();
void
endFlight
()
{
if
(
mounted
)
{
setState
(()
{
_placeholderSize
=
null
;
});
}
}
@override
Widget
build
(
BuildContext
context
)
{
if
(
_placeholderSize
!=
null
)
{
assert
(
_key
==
null
);
return
new
SizedBox
(
width:
_placeholderSize
.
width
,
height:
_placeholderSize
.
height
);
return
new
SizedBox
(
width:
_placeholderSize
.
width
,
height:
_placeholderSize
.
height
);
}
return
new
KeyedSubtree
(
key:
_key
,
child:
config
.
child
child:
config
.
child
,
);
}
}
class
_HeroQuestState
implements
_HeroHandle
{
_HeroQuestState
({
this
.
tag
,
this
.
key
,
this
.
child
,
this
.
sourceStates
,
this
.
animationArea
,
this
.
targetRect
,
this
.
targetTurns
,
this
.
targetState
,
this
.
currentRect
,
this
.
currentTurns
// Everything known about a hero flight that's to be started or restarted.
class
_HeroFlightManifest
{
_HeroFlightManifest
({
@required
this
.
type
,
@required
this
.
overlay
,
@required
this
.
navigatorRect
,
@required
this
.
fromRoute
,
@required
this
.
toRoute
,
@required
this
.
fromHero
,
@required
this
.
toHero
,
@required
this
.
createRectTween
,
})
{
assert
(
tag
!=
null
);
for
(
_HeroState
state
in
sourceStates
)
state
.
_disposeCallback
=
()
=>
sourceStates
.
remove
(
state
);
if
(
targetState
!=
null
)
targetState
.
_disposeCallback
=
_handleTargetStateDispose
;
assert
(
fromHero
.
config
.
tag
==
toHero
.
config
.
tag
);
}
final
Object
tag
;
final
GlobalKey
key
;
final
Widget
child
;
final
Set
<
_HeroState
>
sourceStates
;
final
Rect
animationArea
;
Rect
targetRect
;
int
targetTurns
;
_HeroState
targetState
;
final
RectTween
currentRect
;
final
Tween
<
double
>
currentTurns
;
OverlayEntry
overlayEntry
;
@override
bool
get
alwaysAnimate
=>
true
;
final
_HeroFlightType
type
;
final
OverlayState
overlay
;
final
Rect
navigatorRect
;
final
PageRoute
<
dynamic
>
fromRoute
;
final
PageRoute
<
dynamic
>
toRoute
;
final
_HeroState
fromHero
;
final
_HeroState
toHero
;
final
CreateRectTween
createRectTween
;
bool
get
taken
=>
_taken
;
bool
_taken
=
false
;
Object
get
tag
=>
fromHero
.
config
.
tag
;
@override
_HeroManifest
_takeChild
(
Animation
<
double
>
currentAnimation
)
{
assert
(!
taken
);
_taken
=
true
;
Set
<
_HeroState
>
states
=
sourceStates
;
if
(
targetState
!=
null
)
states
=
states
.
union
(
new
HashSet
<
_HeroState
>.
from
(<
_HeroState
>[
targetState
]));
for
(
_HeroState
state
in
states
)
state
.
_disposeCallback
=
null
;
return
new
_HeroManifest
(
key:
key
,
config:
child
,
sourceStates:
states
,
currentRect:
currentRect
.
evaluate
(
currentAnimation
),
currentTurns:
currentTurns
.
evaluate
(
currentAnimation
)
Animation
<
double
>
get
animation
{
return
new
CurvedAnimation
(
parent:
(
type
==
_HeroFlightType
.
push
)
?
toRoute
.
animation
:
fromRoute
.
animation
,
curve:
Curves
.
fastOutSlowIn
,
);
}
void
_handleTargetStateDispose
()
{
targetState
=
null
;
targetTurns
=
0
;
targetRect
=
targetRect
.
center
&
Size
.
zero
;
WidgetsBinding
.
instance
.
addPostFrameCallback
((
Duration
d
)
=>
overlayEntry
.
markNeedsBuild
());
@override
String
toString
()
{
return
'_HeroFlightManifest(
$type
hero:
$tag
from:
${fromRoute.settings}
to:
${toRoute.settings}
)'
;
}
}
Widget
build
(
BuildContext
context
,
Animation
<
double
>
animation
)
{
return
new
RelativePositionedTransition
(
rect:
currentRect
.
animate
(
animation
),
size:
animationArea
.
size
,
child:
new
RotationTransition
(
turns:
currentTurns
.
animate
(
animation
),
child:
new
IgnorePointer
(
child:
new
RepaintBoundary
(
key:
key
,
child:
child
)
)
)
);
// Builds the in-flight hero widget.
class
_HeroFlight
{
_HeroFlight
(
this
.
onFlightEnded
)
{
_proxyAnimation
=
new
ProxyAnimation
()..
addStatusListener
(
_handleAnimationUpdate
);
}
@mustCallSuper
void
dispose
()
{
overlayEntry
=
null
;
for
(
_HeroState
state
in
sourceStates
)
state
.
_disposeCallback
=
null
;
if
(
targetState
!=
null
)
targetState
.
_disposeCallback
=
null
;
}
}
final
_OnFlightEnded
onFlightEnded
;
class
_HeroMatch
{
const
_HeroMatch
(
this
.
from
,
this
.
to
,
this
.
tag
);
final
_HeroHandle
from
;
final
_HeroHandle
to
;
final
Object
tag
;
}
RectTween
heroRect
;
Animation
<
double
>
_heroOpacity
=
kAlwaysCompleteAnimation
;
ProxyAnimation
_proxyAnimation
;
_HeroFlightManifest
manifest
;
OverlayEntry
overlayEntry
;
/// Signature for a function that takes two [Rect] instances and returns a
/// [RectTween] that transitions between them.
///
/// This is typically used with a [HeroController] to provide an animation for
/// [Hero] positions that looks nicer than a linear movement. For example, see
/// [MaterialRectArcTween].
typedef
RectTween
CreateRectTween
(
Rect
begin
,
Rect
end
);
RectTween
_doCreateRectTween
(
Rect
begin
,
Rect
end
)
{
if
(
manifest
.
createRectTween
!=
null
)
return
manifest
.
createRectTween
(
begin
,
end
);
return
new
RectTween
(
begin:
begin
,
end:
end
);
}
class
_HeroParty
{
_HeroParty
({
this
.
onQuestFinished
,
this
.
createRectTween
});
// The OverlayEntry WidgetBuilder callback for the hero's overlay.
Widget
_buildOverlay
(
BuildContext
context
)
{
assert
(
manifest
!=
null
);
return
new
AnimatedBuilder
(
animation:
_proxyAnimation
,
child:
manifest
.
toHero
.
config
,
builder:
(
BuildContext
context
,
Widget
child
)
{
final
RenderBox
toHeroBox
=
manifest
.
toHero
.
context
?.
findRenderObject
();
if
(
toHeroBox
==
null
||
!
toHeroBox
.
attached
)
{
// The toHero no longer exists. Continue flying while fading out.
if
(
_heroOpacity
==
kAlwaysCompleteAnimation
)
{
_heroOpacity
=
new
Tween
<
double
>(
begin:
1.0
,
end:
0.0
)
.
chain
(
new
CurveTween
(
curve:
new
Interval
(
_proxyAnimation
.
value
,
1.0
)))
.
animate
(
_proxyAnimation
);
}
}
else
if
(
toHeroBox
.
hasSize
)
{
// The toHero has been laid out. If it's no longer where the hero animation is
// supposed to end up (heroRect.end) then recreate the heroRect tween.
final
RenderBox
routeBox
=
manifest
.
toRoute
.
subtreeContext
?.
findRenderObject
();
final
Point
heroOriginEnd
=
toHeroBox
.
localToGlobal
(
Point
.
origin
,
ancestor:
routeBox
);
if
(
heroOriginEnd
!=
heroRect
.
end
.
topLeft
)
{
final
Rect
heroRectEnd
=
heroOriginEnd
&
heroRect
.
end
.
size
;
heroRect
=
_doCreateRectTween
(
heroRect
.
begin
,
heroRectEnd
);
}
}
final
Rect
rect
=
heroRect
.
evaluate
(
_proxyAnimation
);
final
Size
size
=
manifest
.
navigatorRect
.
size
;
final
RelativeRect
offsets
=
new
RelativeRect
.
fromSize
(
rect
,
size
);
return
new
Positioned
(
top:
offsets
.
top
,
right:
offsets
.
right
,
bottom:
offsets
.
bottom
,
left:
offsets
.
left
,
child:
new
IgnorePointer
(
child:
new
RepaintBoundary
(
child:
new
Opacity
(
key:
manifest
.
toHero
.
_key
,
opacity:
_heroOpacity
.
value
,
child:
child
,
),
),
),
);
},
);
}
final
VoidCallback
onQuestFinished
;
final
CreateRectTween
createRectTween
;
void
_handleAnimationUpdate
(
AnimationStatus
status
)
{
if
(
status
==
AnimationStatus
.
completed
||
status
==
AnimationStatus
.
dismissed
)
{
_proxyAnimation
.
parent
=
null
;
List
<
_HeroQuestState
>
_heroes
=
<
_HeroQuestState
>[];
bool
get
isEmpty
=>
_heroes
.
isEmpty
;
assert
(
overlayEntry
!=
null
);
overlayEntry
.
remove
();
overlayEntry
=
null
;
Map
<
Object
,
_HeroHandle
>
getHeroesToAnimate
()
{
Map
<
Object
,
_HeroHandle
>
result
=
new
Map
<
Object
,
_HeroHandle
>();
for
(
_HeroQuestState
hero
in
_heroes
)
result
[
hero
.
tag
]
=
hero
;
assert
(!
result
.
containsKey
(
null
));
return
result
;
manifest
.
fromHero
.
endFlight
();
manifest
.
toHero
.
endFlight
();
onFlightEnded
(
this
);
}
}
RectTween
_doCreateRectTween
(
Rect
begin
,
Rect
end
)
{
if
(
createRectTween
!=
null
)
return
createRectTween
(
begin
,
end
);
return
new
RectTween
(
begin:
begin
,
end:
end
);
}
// The simple case: we're either starting a push or a pop animation.
void
start
(
_HeroFlightManifest
initialManifest
)
{
assert
(()
{
final
Animation
<
double
>
initial
=
initialManifest
.
animation
;
switch
(
initialManifest
.
type
)
{
case
_HeroFlightType
.
pop
:
return
initial
.
value
==
1.0
&&
initial
.
status
==
AnimationStatus
.
reverse
;
case
_HeroFlightType
.
push
:
return
initial
.
value
==
0.0
&&
initial
.
status
==
AnimationStatus
.
forward
;
}
});
Tween
<
double
>
createTurnsTween
(
double
begin
,
double
end
)
{
assert
(
end
.
floor
()
==
end
);
return
new
Tween
<
double
>(
begin:
begin
,
end:
end
);
}
manifest
=
initialManifest
;
if
(
manifest
.
type
==
_HeroFlightType
.
pop
)
_proxyAnimation
.
parent
=
new
ReverseAnimation
(
manifest
.
animation
);
else
_proxyAnimation
.
parent
=
manifest
.
animation
;
void
animate
(
Map
<
Object
,
_HeroHandle
>
heroesFrom
,
Map
<
Object
,
_HeroHandle
>
heroesTo
,
Rect
animationArea
)
{
assert
(!
heroesFrom
.
containsKey
(
null
));
assert
(!
heroesTo
.
containsKey
(
null
));
// make a list of pairs of heroes, based on the from and to lists
Map
<
Object
,
_HeroMatch
>
heroes
=
<
Object
,
_HeroMatch
>{};
for
(
Object
tag
in
heroesFrom
.
keys
)
heroes
[
tag
]
=
new
_HeroMatch
(
heroesFrom
[
tag
],
heroesTo
[
tag
],
tag
);
for
(
Object
tag
in
heroesTo
.
keys
)
{
if
(!
heroes
.
containsKey
(
tag
))
heroes
[
tag
]
=
new
_HeroMatch
(
heroesFrom
[
tag
],
heroesTo
[
tag
],
tag
);
}
manifest
.
fromHero
.
startFlight
();
manifest
.
toHero
.
startFlight
();
// create a heroating hero out of each pair
final
List
<
_HeroQuestState
>
_newHeroes
=
<
_HeroQuestState
>[];
for
(
_HeroMatch
heroPair
in
heroes
.
values
)
{
assert
(
heroPair
.
from
!=
null
||
heroPair
.
to
!=
null
);
if
((
heroPair
.
from
==
null
&&
!
heroPair
.
to
.
alwaysAnimate
)
||
(
heroPair
.
to
==
null
&&
!
heroPair
.
from
.
alwaysAnimate
))
continue
;
_HeroManifest
from
=
heroPair
.
from
?.
_takeChild
(
_currentAnimation
);
assert
(
heroPair
.
to
==
null
||
heroPair
.
to
is
_HeroState
);
_HeroManifest
to
=
heroPair
.
to
?.
_takeChild
(
_currentAnimation
);
assert
(
from
!=
null
||
to
!=
null
);
assert
(
to
==
null
||
to
.
sourceStates
.
length
==
1
);
assert
(
to
==
null
||
to
.
currentTurns
.
floor
()
==
to
.
currentTurns
);
_HeroState
targetState
=
to
!=
null
?
to
.
sourceStates
.
elementAt
(
0
)
:
null
;
Set
<
_HeroState
>
sourceStates
=
from
?.
sourceStates
??
new
HashSet
<
_HeroState
>();
sourceStates
.
remove
(
targetState
);
Rect
sourceRect
=
from
?.
currentRect
??
to
.
currentRect
.
center
&
Size
.
zero
;
Rect
targetRect
=
to
?.
currentRect
??
from
.
currentRect
.
center
&
Size
.
zero
;
double
sourceTurns
=
from
?.
currentTurns
??
0.0
;
double
targetTurns
=
to
?.
currentTurns
??
0.0
;
_newHeroes
.
add
(
new
_HeroQuestState
(
tag:
heroPair
.
tag
,
key:
from
?.
key
??
to
.
key
,
child:
to
?.
config
??
from
.
config
,
sourceStates:
sourceStates
,
animationArea:
animationArea
,
targetRect:
targetRect
,
targetTurns:
targetTurns
.
floor
(),
targetState:
targetState
,
currentRect:
_doCreateRectTween
(
sourceRect
,
targetRect
),
currentTurns:
createTurnsTween
(
sourceTurns
,
targetTurns
)
));
}
heroRect
=
new
RectTween
(
begin:
_globalRect
(
manifest
.
fromHero
.
context
),
end:
_globalRect
(
manifest
.
toHero
.
context
),
);
assert
(!
_heroes
.
any
((
_HeroQuestState
hero
)
=>
!
hero
.
taken
));
for
(
_HeroQuestState
hero
in
_heroes
)
hero
.
dispose
();
_heroes
=
_newHeroes
;
overlayEntry
=
new
OverlayEntry
(
builder:
_buildOverlay
);
manifest
.
overlay
.
insert
(
overlayEntry
);
}
Animation
<
double
>
_currentAnimation
;
// While this flight's hero was in transition a push or a pop occurred for
// routes with the same hero. Redirect the in-flight hero to the new toRoute.
void
restart
(
_HeroFlightManifest
newManifest
)
{
assert
(
manifest
.
tag
==
newManifest
.
tag
);
void
_clearCurrentAnimation
()
{
_currentAnimation
?.
removeStatusListener
(
_handleUpdate
);
_currentAnimation
=
null
;
}
if
(
manifest
.
type
==
_HeroFlightType
.
push
&&
newManifest
.
type
==
_HeroFlightType
.
pop
)
{
// A push flight was interrupted by a pop.
assert
(
newManifest
.
animation
.
status
==
AnimationStatus
.
reverse
);
assert
(
manifest
.
fromHero
==
newManifest
.
toHero
);
assert
(
manifest
.
toHero
==
newManifest
.
fromHero
);
assert
(
manifest
.
fromRoute
==
newManifest
.
toRoute
);
assert
(
manifest
.
toRoute
==
newManifest
.
fromRoute
);
void
setAnimation
(
Animation
<
double
>
animation
)
{
assert
(
animation
!=
null
||
_heroes
.
isEmpty
);
if
(
animation
!=
_currentAnimation
)
{
_clearCurrentAnimation
();
_currentAnimation
=
animation
;
_currentAnimation
?.
addStatusListener
(
_handleUpdate
);
}
}
_proxyAnimation
.
parent
=
new
ReverseAnimation
(
newManifest
.
animation
);
void
_handleUpdate
(
AnimationStatus
status
)
{
if
(
status
==
AnimationStatus
.
completed
||
status
==
AnimationStatus
.
dismissed
)
{
for
(
_HeroQuestState
hero
in
_heroes
)
{
if
(
hero
.
targetState
!=
null
)
hero
.
targetState
.
_setChild
(
hero
.
key
);
for
(
_HeroState
source
in
hero
.
sourceStates
)
source
.
_resetChild
();
hero
.
dispose
();
heroRect
=
new
RectTween
(
begin:
heroRect
.
end
,
end:
heroRect
.
begin
,
);
}
else
if
(
manifest
.
type
==
_HeroFlightType
.
pop
&&
newManifest
.
type
==
_HeroFlightType
.
push
)
{
// A pop flight was interrupted by a push.
assert
(
newManifest
.
animation
.
status
==
AnimationStatus
.
forward
);
assert
(
manifest
.
toHero
==
newManifest
.
fromHero
);
assert
(
manifest
.
toRoute
==
newManifest
.
fromRoute
);
_proxyAnimation
.
parent
=
new
Tween
<
double
>(
begin:
manifest
.
animation
.
value
,
end:
1.0
,
).
animate
(
newManifest
.
animation
);
if
(
manifest
.
fromHero
!=
newManifest
.
toHero
)
{
manifest
.
fromHero
.
endFlight
();
newManifest
.
toHero
.
startFlight
();
heroRect
=
new
RectTween
(
begin:
heroRect
.
end
,
end:
_globalRect
(
newManifest
.
toHero
.
context
),
);
}
else
{
heroRect
=
new
RectTween
(
begin:
heroRect
.
end
,
end:
heroRect
.
begin
,
);
}
_heroes
.
clear
();
_clearCurrentAnimation
();
if
(
onQuestFinished
!=
null
)
onQuestFinished
();
}
else
{
// A push or a pop flight is heading to a new route, i.e.
// manifest.type == _HeroFlightType.push && newManifest.type == _HeroFlightType.push ||
// manifest.type == _HeroFlightType.pop && newManifest.type == _HeroFlightType.pop
assert
(
manifest
.
fromHero
!=
newManifest
.
fromHero
);
assert
(
manifest
.
toHero
!=
newManifest
.
toHero
);
heroRect
=
new
RectTween
(
begin:
heroRect
.
evaluate
(
_proxyAnimation
),
end:
_globalRect
(
newManifest
.
toHero
.
context
),
);
if
(
newManifest
.
type
==
_HeroFlightType
.
pop
)
_proxyAnimation
.
parent
=
new
ReverseAnimation
(
newManifest
.
animation
);
else
_proxyAnimation
.
parent
=
newManifest
.
animation
;
manifest
.
fromHero
.
endFlight
();
manifest
.
toHero
.
endFlight
();
newManifest
.
fromHero
.
startFlight
();
newManifest
.
toHero
.
startFlight
();
}
manifest
=
newManifest
;
}
@override
String
toString
()
=>
'
$_heroes
'
;
String
toString
()
{
final
RouteSettings
from
=
manifest
.
fromRoute
.
settings
;
final
RouteSettings
to
=
manifest
.
toRoute
.
settings
;
final
Object
tag
=
manifest
.
tag
;
return
'HeroFlight(for:
$tag
, from:
$from
, to:
$to
${_proxyAnimation.parent}
)'
;
}
}
/// A [Navigator] observer that manages [Hero] transitions.
...
...
@@ -468,59 +407,34 @@ class _HeroParty {
class
HeroController
extends
NavigatorObserver
{
/// Creates a hero controller with the given [RectTween] constructor if any.
///
/// The [createRectTween] argument is optional.
By default
, a linear
/// The [createRectTween] argument is optional.
If null
, a linear
/// [RectTween] is used.
HeroController
({
CreateRectTween
createRectTween
})
{
_party
=
new
_HeroParty
(
onQuestFinished:
_handleQuestFinished
,
createRectTween:
createRectTween
);
}
HeroController
({
this
.
createRectTween
});
// The current party, if they're on a quest.
_HeroParty
_party
;
final
CreateRectTween
createRectTween
;
// The settings used to prepare the next quest.
// These members are only non-null between the didPush/didPop call and the
// corresponding _updateQuest call.
PageRoute
<
dynamic
>
_from
;
PageRoute
<
dynamic
>
_to
;
Animation
<
double
>
_animation
;
// Disable Hero animations while a user gesture is controlling the navigation.
bool
_questsEnabled
=
true
;
final
List
<
OverlayEntry
>
_overlayEntries
=
new
List
<
OverlayEntry
>();
// All of the heroes that are currently in the overlay and in motion.
// Indexed by the hero tag.
// TBD: final?
Map
<
Object
,
_HeroFlight
>
_flights
=
<
Object
,
_HeroFlight
>{};
@override
void
didPush
(
Route
<
dynamic
>
route
,
Route
<
dynamic
>
previousRoute
)
{
void
didPush
(
Route
<
dynamic
>
to
,
Route
<
dynamic
>
from
)
{
assert
(
navigator
!=
null
);
assert
(
route
!=
null
);
if
(
_questsEnabled
&&
route
is
PageRoute
<
dynamic
>)
{
assert
(
route
.
animation
!=
null
);
if
(
previousRoute
is
PageRoute
<
dynamic
>)
// could be null
_from
=
previousRoute
;
_to
=
route
;
_animation
=
route
.
animation
;
_checkForHeroQuest
();
}
assert
(
to
!=
null
);
_maybeStartHeroTransition
(
from
,
to
,
_HeroFlightType
.
push
);
}
@override
void
didPop
(
Route
<
dynamic
>
route
,
Route
<
dynamic
>
previousRoute
)
{
void
didPop
(
Route
<
dynamic
>
from
,
Route
<
dynamic
>
to
)
{
assert
(
navigator
!=
null
);
assert
(
route
!=
null
);
if
(
_questsEnabled
&&
route
is
PageRoute
<
dynamic
>)
{
assert
(
route
.
animation
!=
null
);
if
(
route
.
animation
.
status
!=
AnimationStatus
.
dismissed
&&
previousRoute
is
PageRoute
<
dynamic
>)
{
_from
=
route
;
_to
=
previousRoute
;
_animation
=
route
.
animation
;
_checkForHeroQuest
();
}
}
assert
(
from
!=
null
);
_maybeStartHeroTransition
(
from
,
to
,
_HeroFlightType
.
pop
);
}
// Disable Hero animations while a user gesture is controlling the navigation.
bool
_questsEnabled
=
true
;
@override
void
didStartUserGesture
()
{
_questsEnabled
=
false
;
...
...
@@ -531,89 +445,72 @@ class HeroController extends NavigatorObserver {
_questsEnabled
=
true
;
}
void
_checkForHeroQuest
()
{
assert
(
_questsEnabled
);
if
(
_from
!=
null
&&
_to
!=
null
&&
_from
!=
_to
)
{
assert
(
_animation
!=
null
);
_to
.
offstage
=
_to
.
animation
.
status
!=
AnimationStatus
.
completed
;
_questsEnabled
=
false
;
WidgetsBinding
.
instance
.
addPostFrameCallback
(
_updateQuest
);
}
else
{
// this isn't a valid quest
_clearPendingHeroQuest
();
// 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
)
{
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
;
// A "user" gesture may have already completed the pop.
if
(
flightType
==
_HeroFlightType
.
pop
&&
animation
.
status
==
AnimationStatus
.
dismissed
)
return
;
// Putting a route offstage changes its animation value to 1.0.
// Once this frame completes, we'll know where the heroes in the toRoute
// are going to end up, and the toRoute will go back on stage.
to
.
offstage
=
animation
.
value
==
0.0
||
animation
.
value
==
1.0
;
WidgetsBinding
.
instance
.
addPostFrameCallback
((
Duration
_
)
{
_startHeroTransition
(
from
,
to
,
flightType
);
});
}
}
void
_handleQuestFinished
()
{
_removeHeroesFromOverlay
();
}
Rect
_getAnimationArea
(
BuildContext
context
)
{
RenderBox
box
=
context
.
findRenderObject
();
Point
topLeft
=
box
.
localToGlobal
(
Point
.
origin
);
Point
bottomRight
=
box
.
localToGlobal
(
box
.
size
.
bottomRight
(
Point
.
origin
));
return
new
Rect
.
fromLTRB
(
topLeft
.
x
,
topLeft
.
y
,
bottomRight
.
x
,
bottomRight
.
y
);
}
void
_removeHeroesFromOverlay
()
{
for
(
OverlayEntry
entry
in
_overlayEntries
)
entry
.
remove
();
_overlayEntries
.
clear
();
}
OverlayEntry
_addHeroToOverlay
(
WidgetBuilder
hero
,
Object
tag
,
OverlayState
overlay
)
{
OverlayEntry
entry
=
new
OverlayEntry
(
builder:
hero
);
assert
(
_animation
.
status
!=
AnimationStatus
.
dismissed
&&
_animation
.
status
!=
AnimationStatus
.
completed
);
if
(
_animation
.
status
==
AnimationStatus
.
forward
)
_to
.
insertHeroOverlayEntry
(
entry
,
tag
,
overlay
);
else
_from
.
insertHeroOverlayEntry
(
entry
,
tag
,
overlay
);
_overlayEntries
.
add
(
entry
);
return
entry
;
}
void
_updateQuest
(
Duration
timeStamp
)
{
assert
(!
_questsEnabled
);
if
(
navigator
==
null
)
{
// The navigator was removed before this end-of-frame callback was called.
_clearPendingHeroQuest
();
// Find the matching pairs of heros in from and to and either start or a new
// hero flight, or restart an existing one.
void
_startHeroTransition
(
PageRoute
<
dynamic
>
from
,
PageRoute
<
dynamic
>
to
,
_HeroFlightType
flightType
)
{
// 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.
// TBD: need to generate tests for these cases
if
(
navigator
==
null
||
from
.
subtreeContext
==
null
||
to
.
subtreeContext
==
null
)
{
to
.
offstage
=
false
;
// TBD: only do this if to.subtreeContext != null?
return
;
}
assert
(
_from
.
subtreeContext
!=
null
);
assert
(
_to
.
subtreeContext
!=
null
);
Map
<
Object
,
_HeroHandle
>
heroesFrom
=
_party
.
isEmpty
?
Hero
.
_of
(
_from
.
subtreeContext
)
:
_party
.
getHeroesToAnimate
();
Map
<
Object
,
_HeroHandle
>
heroesTo
=
Hero
.
_of
(
_to
.
subtreeContext
);
_to
.
offstage
=
false
;
Animation
<
double
>
animation
=
_animation
;
// The route's animation.
Curve
curve
=
Curves
.
fastOutSlowIn
;
if
(
animation
.
status
==
AnimationStatus
.
reverse
)
{
animation
=
new
ReverseAnimation
(
animation
);
curve
=
new
Interval
(
animation
.
value
,
1.0
,
curve:
curve
);
}
animation
=
new
CurvedAnimation
(
parent:
animation
,
curve:
curve
);
_party
.
animate
(
heroesFrom
,
heroesTo
,
_getAnimationArea
(
navigator
.
context
));
_removeHeroesFromOverlay
();
_party
.
setAnimation
(
animation
);
for
(
_HeroQuestState
hero
in
_party
.
_heroes
)
{
hero
.
overlayEntry
=
_addHeroToOverlay
(
(
BuildContext
context
)
=>
hero
.
build
(
navigator
.
context
,
animation
),
hero
.
tag
,
navigator
.
overlay
);
final
Rect
navigatorRect
=
_globalRect
(
navigator
.
context
);
// At this point the toHeroes may have been built and laid out for the first time.
final
Map
<
Object
,
_HeroState
>
fromHeroes
=
Hero
.
_allHeroesFor
(
from
.
subtreeContext
);
final
Map
<
Object
,
_HeroState
>
toHeroes
=
Hero
.
_allHeroesFor
(
to
.
subtreeContext
);
// If the to route was offstage, then we're implicitly restoring its
// animation value back to what it was before it was "moved" offstage.
to
.
offstage
=
false
;
for
(
Object
tag
in
fromHeroes
.
keys
)
{
if
(
toHeroes
[
tag
]
!=
null
)
{
final
_HeroFlightManifest
manifest
=
new
_HeroFlightManifest
(
type:
flightType
,
overlay:
navigator
.
overlay
,
navigatorRect:
navigatorRect
,
fromRoute:
from
,
toRoute:
to
,
fromHero:
fromHeroes
[
tag
],
toHero:
toHeroes
[
tag
],
createRectTween:
createRectTween
,
);
if
(
_flights
[
tag
]
!=
null
)
_flights
[
tag
].
restart
(
manifest
);
else
_flights
[
tag
]
=
new
_HeroFlight
(
_handleFlightEnded
)..
start
(
manifest
);
}
}
_clearPendingHeroQuest
();
}
void
_clearPendingHeroQuest
()
{
_from
=
null
;
_to
=
null
;
_animation
=
null
;
_questsEnabled
=
true
;
void
_handleFlightEnded
(
_HeroFlight
flight
)
{
_flights
.
remove
(
flight
.
manifest
.
tag
);
}
}
packages/flutter/test/widgets/heroes_test.dart
View file @
ef004ae9
...
...
@@ -357,4 +357,36 @@ void main() {
// TODO(ianh): once https://github.com/flutter/flutter/issues/5631 is fixed, remove this line:
await
tester
.
pump
(
const
Duration
(
hours:
1
));
});
testWidgets
(
'One route, two heroes, same tag, throws'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
new
MaterialApp
(
home:
new
Material
(
child:
new
ListView
(
children:
<
Widget
>[
new
Hero
(
tag:
'a'
,
child:
new
Text
(
'a'
)),
new
Hero
(
tag:
'a'
,
child:
new
Text
(
'a too'
)),
new
Builder
(
builder:
(
BuildContext
context
)
{
return
new
FlatButton
(
child:
new
Text
(
'push'
),
onPressed:
()
{
Navigator
.
push
(
context
,
new
PageRouteBuilder
<
Null
>(
pageBuilder:
(
BuildContext
context
,
Animation
<
double
>
_
,
Animation
<
double
>
__
)
{
return
new
Text
(
'fail'
);
},
));
},
);
},
),
],
),
),
));
await
tester
.
tap
(
find
.
text
(
'push'
));
await
tester
.
pump
();
expect
(
tester
.
takeException
(),
isFlutterError
);
});
}
packages/flutter/test/widgets/navigator_test.dart
View file @
ef004ae9
...
...
@@ -148,6 +148,7 @@ void main() {
expect
(
find
.
text
(
'X'
),
findsNothing
);
expect
(
find
.
text
(
'Y'
),
findsOneWidget
);
await
tester
.
pump
();
await
tester
.
pump
();
expect
(
find
.
text
(
'X'
),
findsOneWidget
);
expect
(
find
.
text
(
'Y'
),
findsOneWidget
);
...
...
packages/flutter/test/widgets/page_forward_transitions_test.dart
View file @
ef004ae9
...
...
@@ -132,6 +132,8 @@ void main() {
navigator
.
pop
();
expect
(
state
(),
equals
(
'E'
));
// transition 1<-2 is at 1.0, just reversed
await
tester
.
pump
();
await
tester
.
pump
();
expect
(
state
(),
equals
(
'BDE'
));
// transition 1<-2 is at 1.0
await
tester
.
pump
(
kFourTenthsOfTheTransitionDuration
);
...
...
packages/flutter/test/widgets/page_transitions_test.dart
View file @
ef004ae9
...
...
@@ -113,6 +113,7 @@ void main() {
expect
(
Navigator
.
canPop
(
containerKey2
.
currentContext
),
isTrue
);
Navigator
.
pop
(
containerKey2
.
currentContext
);
await
tester
.
pump
();
await
tester
.
pump
();
expect
(
find
.
text
(
'Home'
),
isOnstage
);
expect
(
find
.
text
(
'Settings'
),
isOnstage
);
...
...
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