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 {
ProxyAnimation _proxyAnimation;
_HeroFlightManifest manifest;
OverlayEntry overlayEntry;
bool _aborted = false;
RectTween _doCreateRectTween(Rect begin, Rect end) {
if (manifest.createRectTween != null)
......@@ -237,9 +238,10 @@ class _HeroFlight {
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) {
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 = new Tween<double>(begin: 1.0, end: 0.0)
.chain(new CurveTween(curve: new Interval(_proxyAnimation.value, 1.0)))
.animate(_proxyAnimation);
......@@ -294,6 +296,7 @@ class _HeroFlight {
// The simple case: we're either starting a push or a pop animation.
void start(_HeroFlightManifest initialManifest) {
assert(!_aborted);
assert(() {
final Animation<double> initial = initialManifest.animation;
switch (initialManifest.type) {
......@@ -305,6 +308,7 @@ class _HeroFlight {
});
manifest = initialManifest;
if (manifest.type == _HeroFlightType.pop)
_proxyAnimation.parent = new ReverseAnimation(manifest.animation);
else
......@@ -388,9 +392,14 @@ class _HeroFlight {
newManifest.toHero.startFlight();
}
_aborted = false;
manifest = newManifest;
}
void abort() {
_aborted = true;
}
@override
String toString() {
final RouteSettings from = manifest.fromRoute.settings;
......@@ -418,8 +427,7 @@ class HeroController extends NavigatorObserver {
// 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>{};
final Map<Object, _HeroFlight> _flights = <Object, _HeroFlight>{};
@override
void didPush(Route<dynamic> to, Route<dynamic> from) {
......@@ -506,6 +514,8 @@ class HeroController extends NavigatorObserver {
_flights[tag].restart(manifest);
else
_flights[tag] = new _HeroFlight(_handleFlightEnded)..start(manifest);
} else if (_flights[tag] != null) {
_flights[tag].abort();
}
}
}
......
......@@ -4,6 +4,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
Key firstKey = const Key('first');
Key secondKey = const Key('second');
......@@ -804,4 +805,110 @@ void main() {
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