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
05a80eb8
Unverified
Commit
05a80eb8
authored
Dec 30, 2020
by
LongCatIsLooong
Committed by
GitHub
Dec 30, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Handle infinite/NaN rects in Hero flights. Less exclamation marks. (#72946)
parent
373ec58d
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
428 additions
and
161 deletions
+428
-161
heroes.dart
packages/flutter/lib/src/widgets/heroes.dart
+220
-161
heroes_test.dart
packages/flutter/test/widgets/heroes_test.dart
+208
-0
No files found.
packages/flutter/lib/src/widgets/heroes.dart
View file @
05a80eb8
...
...
@@ -71,16 +71,6 @@ enum HeroFlightDirection {
pop
,
}
// The bounding box for context in ancestorContext coordinate system, or in the global
// coordinate system when null.
Rect
_boundingBoxFor
(
BuildContext
context
,
[
BuildContext
?
ancestorContext
])
{
final
RenderBox
box
=
context
.
findRenderObject
()!
as
RenderBox
;
assert
(
box
!=
null
&&
box
.
hasSize
);
return
MatrixUtils
.
transformRect
(
box
.
getTransformTo
(
ancestorContext
?.
findRenderObject
()),
Offset
.
zero
&
box
.
size
,
);
}
/// A widget that marks its child as being a candidate for
/// [hero animations](https://flutter.dev/docs/development/ui/animations/hero-animations).
...
...
@@ -282,7 +272,7 @@ class Hero extends StatefulWidget {
}
else
{
// If transition is not allowed, we need to make sure hero is not hidden.
// A hero can be hidden previously due to hero transition.
heroState
.
en
surePlaceholderIsHidden
();
heroState
.
en
dFlight
();
}
}
...
...
@@ -325,6 +315,13 @@ class Hero extends StatefulWidget {
}
}
/// The [Hero] widget displays different content based on whether it is in an
/// animated transition ("flight"), from/to another [Hero] with the same tag:
/// * When [startFlight] is called, the real content of this [Hero] will be
/// replaced by a "placeholder" widget.
/// * When the flight ends, the "toHero"'s [endFlight] method must be called
/// by the hero controller, so the real content of that [Hero] becomes
/// visible again when the animation completes.
class
_HeroState
extends
State
<
Hero
>
{
final
GlobalKey
_key
=
GlobalKey
();
Size
?
_placeholderSize
;
...
...
@@ -354,20 +351,21 @@ class _HeroState extends State<Hero> {
});
}
void
ensurePlaceholderIsHidden
()
{
if
(
mounted
)
{
setState
(()
{
_placeholderSize
=
null
;
});
}
}
// When `keepPlaceholder` is true, the placeholder will continue to be shown
// after the flight ends. Otherwise the child of the Hero will become visible
// and its TickerMode will be re-enabled.
//
// This method can be safely called even when this [Hero] is currently not in
// a flight.
void
endFlight
({
bool
keepPlaceholder
=
false
})
{
if
(!
keepPlaceholder
)
{
ensurePlaceholderIsHidden
();
if
(
keepPlaceholder
||
_placeholderSize
==
null
)
return
;
_placeholderSize
=
null
;
if
(
mounted
)
{
// Tell the widget to rebuild if it's mounted. _paceholderSize has already
// been updated.
setState
(()
{});
}
}
...
...
@@ -406,11 +404,12 @@ class _HeroState extends State<Hero> {
}
// Everything known about a hero flight that's to be started or diverted.
@immutable
class
_HeroFlightManifest
{
_HeroFlightManifest
({
required
this
.
type
,
required
this
.
overlay
,
required
this
.
navigator
Rect
,
required
this
.
navigator
Size
,
required
this
.
fromRoute
,
required
this
.
toRoute
,
required
this
.
fromHero
,
...
...
@@ -422,8 +421,8 @@ class _HeroFlightManifest {
})
:
assert
(
fromHero
.
widget
.
tag
==
toHero
.
widget
.
tag
);
final
HeroFlightDirection
type
;
final
OverlayState
?
overlay
;
final
Rect
navigatorRect
;
final
OverlayState
overlay
;
final
Size
navigatorSize
;
final
PageRoute
<
dynamic
>
fromRoute
;
final
PageRoute
<
dynamic
>
toRoute
;
final
_HeroState
fromHero
;
...
...
@@ -443,10 +442,47 @@ class _HeroFlightManifest {
);
}
Tween
<
Rect
?>
createHeroRectTween
({
required
Rect
?
begin
,
required
Rect
?
end
})
{
final
CreateRectTween
?
createRectTween
=
toHero
.
widget
.
createRectTween
??
this
.
createRectTween
;
return
createRectTween
?.
call
(
begin
,
end
)
??
RectTween
(
begin:
begin
,
end:
end
);
}
// The bounding box for `context`'s render object, in `ancestorContext`'s
// render object's coordinate space.
static
Rect
_boundingBoxFor
(
BuildContext
context
,
BuildContext
?
ancestorContext
)
{
assert
(
ancestorContext
!=
null
);
final
RenderBox
box
=
context
.
findRenderObject
()!
as
RenderBox
;
assert
(
box
!=
null
&&
box
.
hasSize
&&
box
.
size
.
isFinite
);
return
MatrixUtils
.
transformRect
(
box
.
getTransformTo
(
ancestorContext
?.
findRenderObject
()),
Offset
.
zero
&
box
.
size
,
);
}
/// The bounding box of [fromHero], in [fromRoute]'s coordinate space.
///
/// This property should only be accessed in [_HeroFlight.start].
late
final
Rect
fromHeroLocation
=
_boundingBoxFor
(
fromHero
.
context
,
fromRoute
.
subtreeContext
);
/// The bounding box of [toHero], in [toRoute]'s coordinate space.
///
/// This property should only be accessed in [_HeroFlight.start] or
/// [_HeroFlight.divert].
late
final
Rect
toHeroLocation
=
_boundingBoxFor
(
toHero
.
context
,
toRoute
.
subtreeContext
);
/// Whether this [_HeroFlightManifest] is valid and can be used to start or
/// divert a [_HeroFlight].
///
/// When starting or diverting a [_HeroFlight] with a brand new
/// [_HeroFlightManifest], this flag must be checked to ensure the [RectTween]
/// the [_HeroFlightManifest] produces does not contain coordinates that have
/// [double.infinity] or [double.nan].
late
final
bool
isValid
=
toHeroLocation
.
isFinite
&&
(
isDiverted
||
fromHeroLocation
.
isFinite
);
@override
String
toString
()
{
return
'_HeroFlightManifest(
$type
tag:
$tag
from route:
${fromRoute.settings}
'
'to route:
${toRoute.settings}
with hero:
$fromHero
to
$toHero
)'
;
'to route:
${toRoute.settings}
with hero:
$fromHero
to
$toHero
)
${isValid ? '' : ', INVALID'}
'
;
}
}
...
...
@@ -463,28 +499,23 @@ class _HeroFlight {
Animation
<
double
>
_heroOpacity
=
kAlwaysCompleteAnimation
;
late
ProxyAnimation
_proxyAnimation
;
_HeroFlightManifest
?
manifest
;
// The manifest will be available once `start` is called, throughout the
// flight's lifecycle.
late
_HeroFlightManifest
manifest
;
OverlayEntry
?
overlayEntry
;
bool
_aborted
=
false
;
Tween
<
Rect
?>
_doCreateRectTween
(
Rect
?
begin
,
Rect
?
end
)
{
final
CreateRectTween
?
createRectTween
=
manifest
!.
toHero
.
widget
.
createRectTween
??
manifest
!.
createRectTween
;
if
(
createRectTween
!=
null
)
return
createRectTween
(
begin
,
end
);
return
RectTween
(
begin:
begin
,
end:
end
);
}
static
final
Animatable
<
double
>
_reverseTween
=
Tween
<
double
>(
begin:
1.0
,
end:
0.0
);
// The OverlayEntry WidgetBuilder callback for the hero's overlay.
Widget
_buildOverlay
(
BuildContext
context
)
{
assert
(
manifest
!=
null
);
shuttle
??=
manifest
!
.
shuttleBuilder
(
shuttle
??=
manifest
.
shuttleBuilder
(
context
,
manifest
!
.
animation
,
manifest
!
.
type
,
manifest
!
.
fromHero
.
context
,
manifest
!
.
toHero
.
context
,
manifest
.
animation
,
manifest
.
type
,
manifest
.
fromHero
.
context
,
manifest
.
toHero
.
context
,
);
assert
(
shuttle
!=
null
);
...
...
@@ -492,32 +523,8 @@ class _HeroFlight {
animation:
_proxyAnimation
,
child:
shuttle
,
builder:
(
BuildContext
context
,
Widget
?
child
)
{
final
RenderBox
?
toHeroBox
=
manifest
!.
toHero
.
mounted
?
manifest
!.
toHero
.
context
.
findRenderObject
()
as
RenderBox
?
:
null
;
if
(
_aborted
||
toHeroBox
==
null
||
!
toHeroBox
.
attached
)
{
// The toHero no longer exists or it's no longer the flight's destination.
// Continue flying while fading out.
if
(
_heroOpacity
.
isCompleted
)
{
_heroOpacity
=
_proxyAnimation
.
drive
(
_reverseTween
.
chain
(
CurveTween
(
curve:
Interval
(
_proxyAnimation
.
value
,
1.0
))),
);
}
}
else
if
(
toHeroBox
.
hasSize
)
{
// The toHero has been laid out. If it's no longer where the hero animation is
// supposed to end up then recreate the heroRect tween.
final
RenderBox
?
finalRouteBox
=
manifest
!.
toRoute
.
subtreeContext
?.
findRenderObject
()
as
RenderBox
?;
final
Offset
toHeroOrigin
=
toHeroBox
.
localToGlobal
(
Offset
.
zero
,
ancestor:
finalRouteBox
);
if
(
toHeroOrigin
!=
heroRectTween
.
end
!.
topLeft
)
{
final
Rect
heroRectEnd
=
toHeroOrigin
&
heroRectTween
.
end
!.
size
;
heroRectTween
=
_doCreateRectTween
(
heroRectTween
.
begin
,
heroRectEnd
);
}
}
final
Rect
rect
=
heroRectTween
.
evaluate
(
_proxyAnimation
)!;
final
Size
size
=
manifest
!.
navigatorRect
.
size
;
final
RelativeRect
offsets
=
RelativeRect
.
fromSize
(
rect
,
size
);
final
RelativeRect
offsets
=
RelativeRect
.
fromSize
(
rect
,
manifest
.
navigatorSize
);
return
Positioned
(
top:
offsets
.
top
,
right:
offsets
.
right
,
...
...
@@ -548,9 +555,10 @@ class _HeroFlight {
// fromHero hidden. If [AnimationStatus.dismissed], the animation is
// triggered but canceled before it finishes. In this case, we keep toHero
// hidden instead.
manifest
!
.
fromHero
.
endFlight
(
keepPlaceholder:
status
==
AnimationStatus
.
completed
);
manifest
!
.
toHero
.
endFlight
(
keepPlaceholder:
status
==
AnimationStatus
.
dismissed
);
manifest
.
fromHero
.
endFlight
(
keepPlaceholder:
status
==
AnimationStatus
.
completed
);
manifest
.
toHero
.
endFlight
(
keepPlaceholder:
status
==
AnimationStatus
.
dismissed
);
onFlightEnded
(
this
);
_proxyAnimation
.
removeListener
(
onTick
);
}
}
...
...
@@ -559,7 +567,7 @@ class _HeroFlight {
// The animation will not finish until the user lifts their finger, so we
// should suppress the status update if the gesture is in progress, and
// delay it until the finger is lifted.
if
(
manifest
!
.
fromRoute
.
navigator
?.
userGestureInProgress
!=
true
)
{
if
(
manifest
.
fromRoute
.
navigator
?.
userGestureInProgress
!=
true
)
{
_performAnimationUpdate
(
status
);
return
;
}
...
...
@@ -569,7 +577,7 @@ class _HeroFlight {
// The `navigator` must be non-null here, or the first if clause above would
// have returned from this method.
final
NavigatorState
navigator
=
manifest
!
.
fromRoute
.
navigator
!;
final
NavigatorState
navigator
=
manifest
.
fromRoute
.
navigator
!;
void
delayedPerformAnimtationUpdate
()
{
assert
(!
navigator
.
userGestureInProgress
);
...
...
@@ -583,6 +591,33 @@ class _HeroFlight {
navigator
.
userGestureInProgressNotifier
.
addListener
(
delayedPerformAnimtationUpdate
);
}
void
onTick
()
{
final
RenderBox
?
toHeroBox
=
(!
_aborted
&&
manifest
.
toHero
.
mounted
)
?
manifest
.
toHero
.
context
.
findRenderObject
()
as
RenderBox
?
:
null
;
// Try to find the new origin of the toHero, if the flight isn't aborted.
final
Offset
?
toHeroOrigin
=
toHeroBox
!=
null
&&
toHeroBox
.
attached
&&
toHeroBox
.
hasSize
?
toHeroBox
.
localToGlobal
(
Offset
.
zero
,
ancestor:
manifest
.
toRoute
.
subtreeContext
?.
findRenderObject
()
as
RenderBox
?)
:
null
;
if
(
toHeroOrigin
!=
null
&&
toHeroOrigin
.
isFinite
)
{
// If the new origin of toHero is available and also paintable, try to
// update heroRectTween with it.
if
(
toHeroOrigin
!=
heroRectTween
.
end
!.
topLeft
)
{
final
Rect
heroRectEnd
=
toHeroOrigin
&
heroRectTween
.
end
!.
size
;
heroRectTween
=
manifest
.
createHeroRectTween
(
begin:
heroRectTween
.
begin
,
end:
heroRectEnd
);
}
}
else
if
(
_heroOpacity
.
isCompleted
)
{
// The toHero no longer exists or it's no longer the flight's destination.
// Continue flying while fading out.
_heroOpacity
=
_proxyAnimation
.
drive
(
_reverseTween
.
chain
(
CurveTween
(
curve:
Interval
(
_proxyAnimation
.
value
,
1.0
))),
);
}
// Update _aborted for the next animation tick.
_aborted
=
toHeroOrigin
==
null
||
!
toHeroOrigin
.
isFinite
;
}
// The simple case: we're either starting a push or a pop animation.
void
start
(
_HeroFlightManifest
initialManifest
)
{
assert
(!
_aborted
);
...
...
@@ -606,34 +641,36 @@ class _HeroFlight {
manifest
=
initialManifest
;
if
(
manifest
!.
type
==
HeroFlightDirection
.
pop
)
_proxyAnimation
.
parent
=
ReverseAnimation
(
manifest
!.
animation
);
else
_proxyAnimation
.
parent
=
manifest
!.
animation
;
manifest
!.
fromHero
.
startFlight
(
shouldIncludedChildInPlaceholder:
manifest
!.
type
==
HeroFlightDirection
.
push
);
manifest
!.
toHero
.
startFlight
();
heroRectTween
=
_doCreateRectTween
(
_boundingBoxFor
(
manifest
!.
fromHero
.
context
,
manifest
!.
fromRoute
.
subtreeContext
),
_boundingBoxFor
(
manifest
!.
toHero
.
context
,
manifest
!.
toRoute
.
subtreeContext
),
);
final
bool
shouldIncludeChildInPlacehold
;
switch
(
manifest
.
type
)
{
case
HeroFlightDirection
.
pop
:
_proxyAnimation
.
parent
=
ReverseAnimation
(
manifest
.
animation
);
shouldIncludeChildInPlacehold
=
false
;
break
;
case
HeroFlightDirection
.
push
:
_proxyAnimation
.
parent
=
manifest
.
animation
;
shouldIncludeChildInPlacehold
=
true
;
break
;
}
overlayEntry
=
OverlayEntry
(
builder:
_buildOverlay
);
manifest
!.
overlay
!.
insert
(
overlayEntry
!);
heroRectTween
=
manifest
.
createHeroRectTween
(
begin:
manifest
.
fromHeroLocation
,
end:
manifest
.
toHeroLocation
);
manifest
.
fromHero
.
startFlight
(
shouldIncludedChildInPlaceholder:
shouldIncludeChildInPlacehold
);
manifest
.
toHero
.
startFlight
();
manifest
.
overlay
.
insert
(
overlayEntry
=
OverlayEntry
(
builder:
_buildOverlay
));
_proxyAnimation
.
addListener
(
onTick
);
}
// 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
divert
(
_HeroFlightManifest
newManifest
)
{
assert
(
manifest
!
.
tag
==
newManifest
.
tag
);
if
(
manifest
!
.
type
==
HeroFlightDirection
.
push
&&
newManifest
.
type
==
HeroFlightDirection
.
pop
)
{
assert
(
manifest
.
tag
==
newManifest
.
tag
);
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
);
assert
(
manifest
!
.
toHero
==
newManifest
.
fromHero
);
assert
(
manifest
!
.
fromRoute
==
newManifest
.
toRoute
);
assert
(
manifest
!
.
toRoute
==
newManifest
.
fromRoute
);
assert
(
manifest
.
fromHero
==
newManifest
.
toHero
);
assert
(
manifest
.
toHero
==
newManifest
.
fromHero
);
assert
(
manifest
.
fromRoute
==
newManifest
.
toRoute
);
assert
(
manifest
.
toRoute
==
newManifest
.
fromRoute
);
// The same heroRect tween is used in reverse, rather than creating
// a new heroRect with _doCreateRectTween(heroRect.end, heroRect.begin).
...
...
@@ -642,39 +679,36 @@ class _HeroFlight {
// path to be the same (in reverse) as the push flight path.
_proxyAnimation
.
parent
=
ReverseAnimation
(
newManifest
.
animation
);
heroRectTween
=
ReverseTween
<
Rect
?>(
heroRectTween
);
}
else
if
(
manifest
!
.
type
==
HeroFlightDirection
.
pop
&&
newManifest
.
type
==
HeroFlightDirection
.
push
)
{
}
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
);
assert
(
manifest
!
.
toRoute
==
newManifest
.
fromRoute
);
assert
(
manifest
.
toHero
==
newManifest
.
fromHero
);
assert
(
manifest
.
toRoute
==
newManifest
.
fromRoute
);
_proxyAnimation
.
parent
=
newManifest
.
animation
.
drive
(
Tween
<
double
>(
begin:
manifest
!
.
animation
.
value
,
begin:
manifest
.
animation
.
value
,
end:
1.0
,
),
);
if
(
manifest
!
.
fromHero
!=
newManifest
.
toHero
)
{
manifest
!
.
fromHero
.
endFlight
(
keepPlaceholder:
true
);
if
(
manifest
.
fromHero
!=
newManifest
.
toHero
)
{
manifest
.
fromHero
.
endFlight
(
keepPlaceholder:
true
);
newManifest
.
toHero
.
startFlight
();
heroRectTween
=
_doCreateRectTween
(
heroRectTween
.
end
,
_boundingBoxFor
(
newManifest
.
toHero
.
context
,
newManifest
.
toRoute
.
subtreeContext
),
);
heroRectTween
=
manifest
.
createHeroRectTween
(
begin:
heroRectTween
.
end
,
end:
newManifest
.
toHeroLocation
);
}
else
{
// TODO(hansmuller): Use ReverseTween here per github.com/flutter/flutter/pull/12203.
heroRectTween
=
_doCreateRectTween
(
heroRectTween
.
end
,
heroRectTween
.
begin
);
heroRectTween
=
manifest
.
createHeroRectTween
(
begin:
heroRectTween
.
end
,
end:
heroRectTween
.
begin
);
}
}
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
);
assert
(
manifest
.
fromHero
!=
newManifest
.
fromHero
);
assert
(
manifest
.
toHero
!=
newManifest
.
toHero
);
heroRectTween
=
_doCreate
RectTween
(
heroRectTween
.
evaluate
(
_proxyAnimation
),
_boundingBoxFor
(
newManifest
.
toHero
.
context
,
newManifest
.
toRoute
.
subtreeContext
)
,
heroRectTween
=
manifest
.
createHero
RectTween
(
begin:
heroRectTween
.
evaluate
(
_proxyAnimation
),
end:
newManifest
.
toHeroLocation
,
);
shuttle
=
null
;
...
...
@@ -683,8 +717,8 @@ class _HeroFlight {
else
_proxyAnimation
.
parent
=
newManifest
.
animation
;
manifest
!
.
fromHero
.
endFlight
(
keepPlaceholder:
true
);
manifest
!
.
toHero
.
endFlight
(
keepPlaceholder:
true
);
manifest
.
fromHero
.
endFlight
(
keepPlaceholder:
true
);
manifest
.
toHero
.
endFlight
(
keepPlaceholder:
true
);
// Let the heroes in each of the routes rebuild with their placeholders.
newManifest
.
fromHero
.
startFlight
(
shouldIncludedChildInPlaceholder:
newManifest
.
type
==
HeroFlightDirection
.
push
);
...
...
@@ -695,7 +729,6 @@ class _HeroFlight {
overlayEntry
!.
markNeedsBuild
();
}
_aborted
=
false
;
manifest
=
newManifest
;
}
...
...
@@ -705,9 +738,9 @@ class _HeroFlight {
@override
String
toString
()
{
final
RouteSettings
from
=
manifest
!
.
fromRoute
.
settings
;
final
RouteSettings
to
=
manifest
!
.
toRoute
.
settings
;
final
Object
tag
=
manifest
!
.
tag
;
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}
)'
;
}
}
...
...
@@ -770,14 +803,14 @@ class HeroController extends NavigatorObserver {
if
(
navigator
!.
userGestureInProgress
)
return
;
//
If the user horizontal drag gesture initiated the flight (i.e. the back swipe)
//
didn't move towards the pop direction at all, the animation will not play
// a
nd thus the status update callback _handleAnimationUpdate will never be
//
called when the gesture finishes. In this case the initiated flight needs
// to be manually invalidated.
//
When the user gesture ends, if the user horizontal drag gesture initiated
//
the flight (i.e. the back swipe) didn't move towards the pop direction at
// a
ll, the animation will not play and thus the status update callback
//
_handleAnimationUpdate will never be called when the gesture finishes. In
// t
his case the initiated flight needs t
o be manually invalidated.
bool
isInvalidFlight
(
_HeroFlight
flight
)
{
return
flight
.
manifest
!
.
isUserGestureTransition
&&
flight
.
manifest
!
.
type
==
HeroFlightDirection
.
pop
return
flight
.
manifest
.
isUserGestureTransition
&&
flight
.
manifest
.
type
==
HeroFlightDirection
.
pop
&&
flight
.
_proxyAnimation
.
isDismissed
;
}
...
...
@@ -849,63 +882,89 @@ class HeroController extends NavigatorObserver {
HeroFlightDirection
flightType
,
bool
isUserGestureTransition
,
)
{
// 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
)
{
to
.
offstage
=
false
;
// in case we set this in _maybeStartHeroTransition
return
;
}
final
Rect
navigatorRect
=
_boundingBoxFor
(
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
!,
isUserGestureTransition
,
navigator
!);
final
Map
<
Object
,
_HeroState
>
toHeroes
=
Hero
.
_allHeroesFor
(
to
.
subtreeContext
!,
isUserGestureTransition
,
navigator
!);
// 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
(
final
Object
tag
in
fromHeroes
.
keys
)
{
if
(
toHeroes
[
tag
]
!=
null
)
{
final
HeroFlightShuttleBuilder
?
fromShuttleBuilder
=
fromHeroes
[
tag
]!.
widget
.
flightShuttleBuilder
;
final
HeroFlightShuttleBuilder
?
toShuttleBuilder
=
toHeroes
[
tag
]!.
widget
.
flightShuttleBuilder
;
final
bool
isDiverted
=
_flights
[
tag
]
!=
null
;
final
_HeroFlightManifest
manifest
=
_HeroFlightManifest
(
type:
flightType
,
overlay:
navigator
!.
overlay
,
navigatorRect:
navigatorRect
,
fromRoute:
from
,
toRoute:
to
,
fromHero:
fromHeroes
[
tag
]!,
toHero:
toHeroes
[
tag
]!,
createRectTween:
createRectTween
,
shuttleBuilder:
toShuttleBuilder
??
fromShuttleBuilder
??
_defaultHeroFlightShuttleBuilder
,
isUserGestureTransition:
isUserGestureTransition
,
isDiverted:
isDiverted
,
);
final
NavigatorState
?
navigator
=
this
.
navigator
;
final
OverlayState
?
overlay
=
navigator
?.
overlay
;
// If the navigator or the overlay was removed before this end-of-frame
// callback was called, then don't actually start a transition, and we don'
// t have to worry about any Hero widget we might have hidden in a previous
// flight, or onging flights.
if
(
navigator
==
null
||
overlay
==
null
)
return
;
if
(
isDiverted
)
_flights
[
tag
]!.
divert
(
manifest
);
else
final
RenderObject
?
navigatorRenderObject
=
navigator
.
context
.
findRenderObject
();
if
(
navigatorRenderObject
is
!
RenderBox
)
{
assert
(
false
,
'Navigator
$navigator
has an invalid RenderObject type
${navigatorRenderObject.runtimeType}
.'
);
return
;
}
assert
(
navigatorRenderObject
.
hasSize
);
// At this point, the toHeroes may have been built and laid out for the first time.
//
// If `fromSubtreeContext` is null, call endFlight on all toHeroes, for good measure.
// If `toSubtreeContext` is null abort existingFlights.
final
BuildContext
?
fromSubtreeContext
=
from
.
subtreeContext
;
final
Map
<
Object
,
_HeroState
>
fromHeroes
=
fromSubtreeContext
!=
null
?
Hero
.
_allHeroesFor
(
fromSubtreeContext
,
isUserGestureTransition
,
navigator
)
:
const
<
Object
,
_HeroState
>{};
final
BuildContext
?
toSubtreeContext
=
to
.
subtreeContext
;
final
Map
<
Object
,
_HeroState
>
toHeroes
=
toSubtreeContext
!=
null
?
Hero
.
_allHeroesFor
(
toSubtreeContext
,
isUserGestureTransition
,
navigator
)
:
const
<
Object
,
_HeroState
>{};
for
(
final
MapEntry
<
Object
,
_HeroState
>
fromHeroEntry
in
fromHeroes
.
entries
)
{
final
Object
tag
=
fromHeroEntry
.
key
;
final
_HeroState
fromHero
=
fromHeroEntry
.
value
;
final
_HeroState
?
toHero
=
toHeroes
[
tag
];
final
_HeroFlight
?
existingFlight
=
_flights
[
tag
];
final
_HeroFlightManifest
?
manifest
=
toHero
==
null
?
null
:
_HeroFlightManifest
(
type:
flightType
,
overlay:
overlay
,
navigatorSize:
navigatorRenderObject
.
size
,
fromRoute:
from
,
toRoute:
to
,
fromHero:
fromHero
,
toHero:
toHero
,
createRectTween:
createRectTween
,
shuttleBuilder:
fromHero
.
widget
.
flightShuttleBuilder
??
toHero
.
widget
.
flightShuttleBuilder
??
_defaultHeroFlightShuttleBuilder
,
isUserGestureTransition:
isUserGestureTransition
,
isDiverted:
existingFlight
!=
null
,
);
// Only proceed with a valid manifest. Otherwise abort the existing
// flight, and call endFlight when this for loop finishes.
if
(
manifest
!=
null
&&
manifest
.
isValid
)
{
toHeroes
.
remove
(
tag
);
if
(
existingFlight
!=
null
)
{
existingFlight
.
divert
(
manifest
);
}
else
{
_flights
[
tag
]
=
_HeroFlight
(
_handleFlightEnded
)..
start
(
manifest
);
}
else
if
(
_flights
[
tag
]
!=
null
)
{
_flights
[
tag
]!.
abort
();
}
}
else
{
existingFlight
?.
abort
();
}
}
// If the from hero is gone, the flight won't start and the to hero needs to
// be put on stage again.
for
(
final
Object
tag
in
toHeroes
.
keys
)
{
if
(
fromHeroes
[
tag
]
==
null
)
toHeroes
[
tag
]!.
ensurePlaceholderIsHidden
();
}
// The remaining entries in toHeroes are those failed to participate in a
// new flight (for not having a valid manifest).
//
// This can happen in a route pop transition when a fromHero is no longer
// mounted, or kept alive by the [KeepAlive] mechanism but no longer visible.
// TODO(LongCatIsLooong): resume aborted flights: https://github.com/flutter/flutter/issues/72947
for
(
final
_HeroState
toHero
in
toHeroes
.
values
)
toHero
.
endFlight
();
}
void
_handleFlightEnded
(
_HeroFlight
flight
)
{
_flights
.
remove
(
flight
.
manifest
!
.
tag
);
_flights
.
remove
(
flight
.
manifest
.
tag
);
}
static
final
HeroFlightShuttleBuilder
_defaultHeroFlightShuttleBuilder
=
(
...
...
packages/flutter/test/widgets/heroes_test.dart
View file @
05a80eb8
...
...
@@ -2780,4 +2780,212 @@ Future<void> main() async {
expect
(
find
.
byKey
(
secondKey
),
isInCard
);
expect
(
find
.
byKey
(
secondKey
),
isOnstage
);
});
testWidgets
(
'kept alive Hero does not throw when the transition begins'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
NavigatorState
>
navigatorKey
=
GlobalKey
<
NavigatorState
>();
await
tester
.
pumpWidget
(
MaterialApp
(
navigatorKey:
navigatorKey
,
home:
Scaffold
(
body:
ListView
(
addAutomaticKeepAlives:
false
,
addRepaintBoundaries:
false
,
addSemanticIndexes:
false
,
children:
<
Widget
>[
const
KeepAlive
(
keepAlive:
true
,
child:
Hero
(
tag:
'a'
,
child:
Placeholder
(),
),
),
Container
(
height:
1000.0
),
],
),
),
),
);
// Scroll to make the Hero invisible.
await
tester
.
drag
(
find
.
byType
(
ListView
),
const
Offset
(
0.0
,
-
1000.0
));
await
tester
.
pump
();
expect
(
find
.
byType
(
TextField
),
findsNothing
);
navigatorKey
.
currentState
?.
push
(
MaterialPageRoute
<
void
>(
builder:
(
BuildContext
context
)
{
return
const
Scaffold
(
body:
Center
(
child:
Hero
(
tag:
'a'
,
child:
Placeholder
(),
),
),
);
},
),
);
await
tester
.
pumpAndSettle
();
expect
(
tester
.
takeException
(),
isNull
);
// The Hero on the new route should be visible .
expect
(
find
.
byType
(
Placeholder
),
findsOneWidget
);
});
testWidgets
(
'toHero becomes unpaintable after the transition begins'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
NavigatorState
>
navigatorKey
=
GlobalKey
<
NavigatorState
>();
final
ScrollController
controller
=
ScrollController
();
RenderOpacity
?
findRenderOpacity
()
{
AbstractNode
?
parent
=
tester
.
renderObject
(
find
.
byType
(
Placeholder
));
while
(
parent
is
RenderObject
&&
parent
is
!
RenderOpacity
)
{
parent
=
parent
.
parent
;
}
return
parent
is
RenderOpacity
?
parent
:
null
;
}
await
tester
.
pumpWidget
(
MaterialApp
(
navigatorKey:
navigatorKey
,
home:
Scaffold
(
body:
ListView
(
controller:
controller
,
addAutomaticKeepAlives:
false
,
addRepaintBoundaries:
false
,
addSemanticIndexes:
false
,
children:
<
Widget
>[
const
KeepAlive
(
keepAlive:
true
,
child:
Hero
(
tag:
'a'
,
child:
Placeholder
(),
),
),
Container
(
height:
1000.0
),
],
),
),
),
);
navigatorKey
.
currentState
?.
push
(
MaterialPageRoute
<
void
>(
builder:
(
BuildContext
context
)
{
return
const
Scaffold
(
body:
Center
(
child:
Hero
(
tag:
'a'
,
child:
Placeholder
(),
),
),
);
},
),
);
await
tester
.
pump
();
await
tester
.
pumpAndSettle
();
// Pop the new route, and before the animation finishes we scroll the toHero
// to make it unpaintable.
navigatorKey
.
currentState
?.
pop
();
await
tester
.
pump
();
controller
.
jumpTo
(
1000
);
// Starts Hero animation and scroll animation almost simutaneously.
// Scroll to make the Hero invisible.
await
tester
.
pump
();
expect
(
findRenderOpacity
()?.
opacity
,
anyOf
(
isNull
,
1.0
));
// In this frame the Hero animation finds out the toHero is not paintable,
// and starts fading.
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
100
));
expect
(
findRenderOpacity
()?.
opacity
,
lessThan
(
1.0
));
await
tester
.
pumpAndSettle
();
// The Hero on the new route should be invisible.
expect
(
find
.
byType
(
Placeholder
),
findsNothing
);
});
testWidgets
(
'diverting to a keepalive but unpaintable hero'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
NavigatorState
>
navigatorKey
=
GlobalKey
<
NavigatorState
>();
await
tester
.
pumpWidget
(
CupertinoApp
(
navigatorKey:
navigatorKey
,
home:
CupertinoPageScaffold
(
child:
ListView
(
addAutomaticKeepAlives:
false
,
addRepaintBoundaries:
false
,
addSemanticIndexes:
false
,
children:
<
Widget
>[
const
KeepAlive
(
keepAlive:
true
,
child:
Hero
(
tag:
'a'
,
child:
Placeholder
(),
),
),
Container
(
height:
1000.0
),
],
),
),
),
);
// Scroll to make the Hero invisible.
await
tester
.
drag
(
find
.
byType
(
ListView
),
const
Offset
(
0.0
,
-
1000.0
));
await
tester
.
pump
();
expect
(
find
.
byType
(
Placeholder
),
findsNothing
);
expect
(
find
.
byType
(
Placeholder
,
skipOffstage:
false
),
findsOneWidget
);
navigatorKey
.
currentState
?.
push
(
MaterialPageRoute
<
void
>(
builder:
(
BuildContext
context
)
{
return
const
Scaffold
(
body:
Center
(
child:
Hero
(
tag:
'a'
,
child:
Placeholder
(),
),
),
);
},
),
);
await
tester
.
pumpAndSettle
();
// Yet another route that contains Hero 'a'.
navigatorKey
.
currentState
?.
push
(
MaterialPageRoute
<
void
>(
builder:
(
BuildContext
context
)
{
return
const
Scaffold
(
body:
Center
(
child:
Hero
(
tag:
'a'
,
child:
Placeholder
(),
),
),
);
},
),
);
await
tester
.
pumpAndSettle
();
// Pop both routes.
navigatorKey
.
currentState
?.
pop
();
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
10
));
navigatorKey
.
currentState
?.
pop
();
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
10
));
expect
(
find
.
byType
(
Placeholder
),
findsOneWidget
);
await
tester
.
pumpAndSettle
();
expect
(
tester
.
takeException
(),
isNull
);
});
}
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