Commit 6f9bfbda authored by Hans Muller's avatar Hans Muller Committed by GitHub

Support aborted hero flights (#8805)

parent 15330ffb
...@@ -222,6 +222,7 @@ class _HeroFlight { ...@@ -222,6 +222,7 @@ class _HeroFlight {
ProxyAnimation _proxyAnimation; ProxyAnimation _proxyAnimation;
_HeroFlightManifest manifest; _HeroFlightManifest manifest;
OverlayEntry overlayEntry; OverlayEntry overlayEntry;
bool _aborted = false;
RectTween _doCreateRectTween(Rect begin, Rect end) { RectTween _doCreateRectTween(Rect begin, Rect end) {
if (manifest.createRectTween != null) if (manifest.createRectTween != null)
...@@ -237,9 +238,10 @@ class _HeroFlight { ...@@ -237,9 +238,10 @@ class _HeroFlight {
child: manifest.toHero.config, child: manifest.toHero.config,
builder: (BuildContext context, Widget child) { builder: (BuildContext context, Widget child) {
final RenderBox toHeroBox = manifest.toHero.context?.findRenderObject(); final RenderBox toHeroBox = manifest.toHero.context?.findRenderObject();
if (toHeroBox == null || !toHeroBox.attached) { if (_aborted || toHeroBox == null || !toHeroBox.attached) {
// The toHero no longer exists. Continue flying while fading out. // The toHero no longer exists or it's no longer the flight's destination.
if (_heroOpacity == kAlwaysCompleteAnimation) { // Continue flying while fading out.
if (_heroOpacity.isCompleted) {
_heroOpacity = new Tween<double>(begin: 1.0, end: 0.0) _heroOpacity = new Tween<double>(begin: 1.0, end: 0.0)
.chain(new CurveTween(curve: new Interval(_proxyAnimation.value, 1.0))) .chain(new CurveTween(curve: new Interval(_proxyAnimation.value, 1.0)))
.animate(_proxyAnimation); .animate(_proxyAnimation);
...@@ -294,6 +296,7 @@ class _HeroFlight { ...@@ -294,6 +296,7 @@ class _HeroFlight {
// The simple case: we're either starting a push or a pop animation. // The simple case: we're either starting a push or a pop animation.
void start(_HeroFlightManifest initialManifest) { void start(_HeroFlightManifest initialManifest) {
assert(!_aborted);
assert(() { assert(() {
final Animation<double> initial = initialManifest.animation; final Animation<double> initial = initialManifest.animation;
switch (initialManifest.type) { switch (initialManifest.type) {
...@@ -305,6 +308,7 @@ class _HeroFlight { ...@@ -305,6 +308,7 @@ class _HeroFlight {
}); });
manifest = initialManifest; manifest = initialManifest;
if (manifest.type == _HeroFlightType.pop) if (manifest.type == _HeroFlightType.pop)
_proxyAnimation.parent = new ReverseAnimation(manifest.animation); _proxyAnimation.parent = new ReverseAnimation(manifest.animation);
else else
...@@ -388,9 +392,14 @@ class _HeroFlight { ...@@ -388,9 +392,14 @@ class _HeroFlight {
newManifest.toHero.startFlight(); newManifest.toHero.startFlight();
} }
_aborted = false;
manifest = newManifest; manifest = newManifest;
} }
void abort() {
_aborted = true;
}
@override @override
String toString() { String toString() {
final RouteSettings from = manifest.fromRoute.settings; final RouteSettings from = manifest.fromRoute.settings;
...@@ -418,8 +427,7 @@ class HeroController extends NavigatorObserver { ...@@ -418,8 +427,7 @@ class HeroController extends NavigatorObserver {
// All of the heroes that are currently in the overlay and in motion. // All of the heroes that are currently in the overlay and in motion.
// Indexed by the hero tag. // Indexed by the hero tag.
// TBD: final? final Map<Object, _HeroFlight> _flights = <Object, _HeroFlight>{};
Map<Object, _HeroFlight> _flights = <Object, _HeroFlight>{};
@override @override
void didPush(Route<dynamic> to, Route<dynamic> from) { void didPush(Route<dynamic> to, Route<dynamic> from) {
...@@ -506,6 +514,8 @@ class HeroController extends NavigatorObserver { ...@@ -506,6 +514,8 @@ class HeroController extends NavigatorObserver {
_flights[tag].restart(manifest); _flights[tag].restart(manifest);
else else
_flights[tag] = new _HeroFlight(_handleFlightEnded)..start(manifest); _flights[tag] = new _HeroFlight(_handleFlightEnded)..start(manifest);
} else if (_flights[tag] != null) {
_flights[tag].abort();
} }
} }
} }
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
Key firstKey = const Key('first'); Key firstKey = const Key('first');
Key secondKey = const Key('second'); Key secondKey = const Key('second');
...@@ -804,4 +805,110 @@ void main() { ...@@ -804,4 +805,110 @@ void main() {
expect(find.byKey(routeHeroKey), findsNothing); expect(find.byKey(routeHeroKey), findsNothing);
}); });
testWidgets('Aborted flight', (WidgetTester tester) async {
// See https://github.com/flutter/flutter/issues/5798
final Key heroABKey = const Key('AB hero');
final Key heroBCKey = const Key('BC hero');
// Show a 150x150 Hero tagged 'BC'
final MaterialPageRoute<Null> routeC = new MaterialPageRoute<Null>(
builder: (BuildContext context) {
return new Material(
child: new ListView(
children: <Widget>[
// This container will appear at Y=0
new Container(
child: new Hero(tag: 'BC', child: new Container(key: heroBCKey, height: 150.0))
),
new SizedBox(height: 800.0),
],
)
);
},
);
// Show a height=200 Hero tagged 'AB' and a height=50 Hero tagged 'BC'
final MaterialPageRoute<Null> routeB = new MaterialPageRoute<Null>(
builder: (BuildContext context) {
return new Material(
child: new ListView(
children: <Widget>[
new SizedBox(height: 100.0),
// This container will appear at Y=100
new Container(
child: new Hero(tag: 'AB', child: new Container(key: heroABKey, height: 200.0))
),
new FlatButton(
child: new Text('PUSH C'),
onPressed: () { Navigator.push(context, routeC); }
),
new Container(
child: new Hero(tag: 'BC', child: new Container(height: 150.0))
),
new SizedBox(height: 800.0),
],
)
);
},
);
// Show a 100x100 Hero tagged 'AB' with key heroABKey
await tester.pumpWidget(
new MaterialApp(
home: new Scaffold(
body: new Builder(
builder: (BuildContext context) { // Navigator.push() needs context
return new ListView(
children: <Widget> [
new SizedBox(height: 200.0),
// This container will appear at Y=200
new Container(
child: new Hero(tag: 'AB', child: new Container(height: 100.0, width: 100.0)),
),
new FlatButton(
child: new Text('PUSH B'),
onPressed: () { Navigator.push(context, routeB); }
),
],
);
},
),
),
)
);
// Pushes routeB
await tester.tap(find.text('PUSH B'));
await tester.pump();
await tester.pump();
final double initialY = tester.getTopLeft(find.byKey(heroABKey)).y;
expect(initialY, 200.0);
await tester.pump(const Duration(milliseconds: 200));
final double yAt200ms = tester.getTopLeft(find.byKey(heroABKey)).y;
// Hero AB is mid flight.
expect(yAt200ms, lessThan(200.0));
expect(yAt200ms, greaterThan(100.0));
// Pushes route C, causes hero AB's flight to abort, hero BC's flight to start
await tester.tap(find.text('PUSH C'));
await tester.pump();
await tester.pump();
// Hero AB's aborted flight finishes where it was expected although
// it's been faded out.
await tester.pump(const Duration(milliseconds: 100));
expect(tester.getTopLeft(find.byKey(heroABKey)).y, 100.0);
// One Opacity widget per Hero, only one now has opacity 0.0
final Iterable<RenderOpacity> renderers = tester.renderObjectList(find.byType(Opacity));
final Iterable<double> opacities = renderers.map((RenderOpacity r) => r.opacity);
expect(opacities.singleWhere((double opacity) => opacity == 0.0), 0.0);
// Hero BC's flight finishes normally.
await tester.pump(const Duration(milliseconds: 300));
expect(tester.getTopLeft(find.byKey(heroBCKey)).y, 0.0);
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment