Commit b87cc8b1 authored by Jason Simmons's avatar Jason Simmons Committed by GitHub

Handle disposal of a HeroState while a hero is animating (#5189)

Fixes https://github.com/flutter/flutter/issues/5178
parent 22210c8b
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
import 'dart:collection'; import 'dart:collection';
import 'package:meta/meta.dart';
import 'basic.dart'; import 'basic.dart';
import 'binding.dart'; import 'binding.dart';
import 'framework.dart'; import 'framework.dart';
...@@ -156,6 +158,7 @@ class HeroState extends State<Hero> implements HeroHandle { ...@@ -156,6 +158,7 @@ class HeroState extends State<Hero> implements HeroHandle {
GlobalKey _key = new GlobalKey(); GlobalKey _key = new GlobalKey();
Size _placeholderSize; Size _placeholderSize;
VoidCallback _disposeCallback;
@override @override
bool get alwaysAnimate => config.alwaysAnimate; bool get alwaysAnimate => config.alwaysAnimate;
...@@ -202,6 +205,13 @@ class HeroState extends State<Hero> implements HeroHandle { ...@@ -202,6 +205,13 @@ class HeroState extends State<Hero> implements HeroHandle {
_setChild(null); _setChild(null);
} }
@override
void dispose() {
if (_disposeCallback != null)
_disposeCallback();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_placeholderSize != null) { if (_placeholderSize != null) {
...@@ -231,6 +241,12 @@ class _HeroQuestState implements HeroHandle { ...@@ -231,6 +241,12 @@ class _HeroQuestState implements HeroHandle {
this.currentTurns this.currentTurns
}) { }) {
assert(tag != null); assert(tag != null);
for (HeroState state in sourceStates)
state._disposeCallback = () => sourceStates.remove(state);
if (targetState != null)
targetState._disposeCallback = _handleTargetStateDispose;
} }
final Object tag; final Object tag;
...@@ -238,12 +254,14 @@ class _HeroQuestState implements HeroHandle { ...@@ -238,12 +254,14 @@ class _HeroQuestState implements HeroHandle {
final Widget child; final Widget child;
final Set<HeroState> sourceStates; final Set<HeroState> sourceStates;
final Rect animationArea; final Rect animationArea;
final Rect targetRect; Rect targetRect;
final int targetTurns; int targetTurns;
final HeroState targetState; HeroState targetState;
final RectTween currentRect; final RectTween currentRect;
final Tween<double> currentTurns; final Tween<double> currentTurns;
OverlayEntry overlayEntry;
@override @override
bool get alwaysAnimate => true; bool get alwaysAnimate => true;
...@@ -257,6 +275,10 @@ class _HeroQuestState implements HeroHandle { ...@@ -257,6 +275,10 @@ class _HeroQuestState implements HeroHandle {
Set<HeroState> states = sourceStates; Set<HeroState> states = sourceStates;
if (targetState != null) if (targetState != null)
states = states.union(new HashSet<HeroState>.from(<HeroState>[targetState])); states = states.union(new HashSet<HeroState>.from(<HeroState>[targetState]));
for (HeroState state in states)
state._disposeCallback = null;
return new _HeroManifest( return new _HeroManifest(
key: key, key: key,
config: child, config: child,
...@@ -266,6 +288,13 @@ class _HeroQuestState implements HeroHandle { ...@@ -266,6 +288,13 @@ class _HeroQuestState implements HeroHandle {
); );
} }
void _handleTargetStateDispose() {
targetState = null;
targetTurns = 0;
targetRect = targetRect.center & Size.zero;
WidgetsBinding.instance.addPostFrameCallback((Duration d) => overlayEntry.markNeedsBuild());
}
Widget build(BuildContext context, Animation<double> animation) { Widget build(BuildContext context, Animation<double> animation) {
return new RelativePositionedTransition( return new RelativePositionedTransition(
rect: currentRect.animate(animation), rect: currentRect.animate(animation),
...@@ -279,6 +308,17 @@ class _HeroQuestState implements HeroHandle { ...@@ -279,6 +308,17 @@ class _HeroQuestState implements HeroHandle {
) )
); );
} }
@mustCallSuper
void dispose() {
overlayEntry = null;
for (HeroState state in sourceStates)
state._disposeCallback = null;
if (targetState != null)
targetState._disposeCallback = null;
}
} }
class _HeroMatch { class _HeroMatch {
...@@ -366,6 +406,8 @@ class HeroParty { ...@@ -366,6 +406,8 @@ class HeroParty {
} }
assert(!_heroes.any((_HeroQuestState hero) => !hero.taken)); assert(!_heroes.any((_HeroQuestState hero) => !hero.taken));
for (_HeroQuestState hero in _heroes)
hero.dispose();
_heroes = _newHeroes; _heroes = _newHeroes;
} }
...@@ -393,6 +435,7 @@ class HeroParty { ...@@ -393,6 +435,7 @@ class HeroParty {
hero.targetState._setChild(hero.key); hero.targetState._setChild(hero.key);
for (HeroState source in hero.sourceStates) for (HeroState source in hero.sourceStates)
source._resetChild(); source._resetChild();
hero.dispose();
} }
_heroes.clear(); _heroes.clear();
_clearCurrentAnimation(); _clearCurrentAnimation();
...@@ -476,14 +519,15 @@ class HeroController extends NavigatorObserver { ...@@ -476,14 +519,15 @@ class HeroController extends NavigatorObserver {
_overlayEntries.clear(); _overlayEntries.clear();
} }
void _addHeroToOverlay(Widget hero, Object tag, OverlayState overlay) { OverlayEntry _addHeroToOverlay(WidgetBuilder hero, Object tag, OverlayState overlay) {
OverlayEntry entry = new OverlayEntry(builder: (_) => hero); OverlayEntry entry = new OverlayEntry(builder: hero);
assert(_animation.status != AnimationStatus.dismissed && _animation.status != AnimationStatus.completed); assert(_animation.status != AnimationStatus.dismissed && _animation.status != AnimationStatus.completed);
if (_animation.status == AnimationStatus.forward) if (_animation.status == AnimationStatus.forward)
_to.insertHeroOverlayEntry(entry, tag, overlay); _to.insertHeroOverlayEntry(entry, tag, overlay);
else else
_from.insertHeroOverlayEntry(entry, tag, overlay); _from.insertHeroOverlayEntry(entry, tag, overlay);
_overlayEntries.add(entry); _overlayEntries.add(entry);
return entry;
} }
Set<Key> _getMostValuableKeys() { Set<Key> _getMostValuableKeys() {
...@@ -523,8 +567,11 @@ class HeroController extends NavigatorObserver { ...@@ -523,8 +567,11 @@ class HeroController extends NavigatorObserver {
curve: curve curve: curve
)); ));
for (_HeroQuestState hero in _party._heroes) { for (_HeroQuestState hero in _party._heroes) {
Widget widget = hero.build(navigator.context, animation); hero.overlayEntry = _addHeroToOverlay(
_addHeroToOverlay(widget, hero.tag, navigator.overlay); (BuildContext context) => hero.build(navigator.context, animation),
hero.tag,
navigator.overlay
);
} }
} }
} }
...@@ -40,6 +40,18 @@ class ThreeRoute extends MaterialPageRoute<Null> { ...@@ -40,6 +40,18 @@ class ThreeRoute extends MaterialPageRoute<Null> {
}); });
} }
class MutatingRoute extends MaterialPageRoute<Null> {
MutatingRoute() : super(builder: (BuildContext context) {
return new Hero(tag: 'a', child: new Text('MutatingRoute'), key: new UniqueKey());
});
void markNeedsBuild() {
setState(() {
// Trigger a rebuild
});
}
}
void main() { void main() {
testWidgets('Heroes animate', (WidgetTester tester) async { testWidgets('Heroes animate', (WidgetTester tester) async {
...@@ -143,4 +155,25 @@ void main() { ...@@ -143,4 +155,25 @@ void main() {
expect(find.byKey(thirdKey), isOnStage); expect(find.byKey(thirdKey), isOnStage);
expect(find.byKey(thirdKey), isInCard); expect(find.byKey(thirdKey), isInCard);
}); });
testWidgets('Heroes animate', (WidgetTester tester) async {
MutatingRoute route = new MutatingRoute();
await tester.pumpWidget(new MaterialApp(
home: new Material(child: new Block(children: <Widget>[
new Hero(tag: 'a', child: new Text('foo')),
new Builder(builder: (BuildContext context) {
return new FlatButton(child: new Text('two'), onPressed: () => Navigator.push(context, route));
})
]))
));
await tester.tap(find.text('two'));
await tester.pump(new Duration(milliseconds: 10));
route.markNeedsBuild();
await tester.pump(new Duration(milliseconds: 10));
await tester.pump(new Duration(seconds: 1));
});
} }
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