Aborted Hero push transitions should retrace their flight path (#12203)
This commit is contained in:
parent
f2d096010d
commit
c44aa2665d
@ -178,6 +178,22 @@ class Tween<T extends dynamic> extends Animatable<T> {
|
||||
String toString() => '$runtimeType($begin \u2192 $end)';
|
||||
}
|
||||
|
||||
/// A [Tween] that evaluates its [parent] in reverse.
|
||||
class ReverseTween<T> extends Tween<T> {
|
||||
/// Construct a [Tween] that evaluates its [parent] in reverse.
|
||||
ReverseTween(this.parent) : assert(parent != null), super(begin: parent.end, end: parent.begin);
|
||||
|
||||
/// This tween's value is the same as the parent's value evaluated in reverse.
|
||||
///
|
||||
/// This tween's [begin] is the parent's [end] and its [end] is the parent's
|
||||
/// [begin]. The [lerp] method returns `parent.lerp(1.0 - t)` and its
|
||||
/// [evaluate] method is similar.
|
||||
final Tween<T> parent;
|
||||
|
||||
@override
|
||||
T lerp(double t) => parent.lerp(1.0 - t);
|
||||
}
|
||||
|
||||
/// An interpolation between two colors.
|
||||
///
|
||||
/// This class specializes the interpolation of [Tween<Color>] to use
|
||||
|
@ -18,7 +18,7 @@ import 'transitions.dart';
|
||||
/// 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 Tween<Rect> CreateRectTween(Rect begin, Rect end);
|
||||
|
||||
typedef void _OnFlightEnded(_HeroFlight flight);
|
||||
|
||||
@ -95,7 +95,7 @@ class Hero extends StatefulWidget {
|
||||
/// route to the destination route.
|
||||
///
|
||||
/// A hero flight begins with the destination hero's [child] aligned with the
|
||||
/// starting hero's child. The [RectTween] returned by this callback is used
|
||||
/// starting hero's child. The [Tween<Rect>] returned by this callback is used
|
||||
/// to compute the hero's bounds as the flight animation's value goes from 0.0
|
||||
/// to 1.0.
|
||||
///
|
||||
@ -236,14 +236,14 @@ class _HeroFlight {
|
||||
|
||||
final _OnFlightEnded onFlightEnded;
|
||||
|
||||
RectTween heroRect;
|
||||
Tween<Rect> heroRect;
|
||||
Animation<double> _heroOpacity = kAlwaysCompleteAnimation;
|
||||
ProxyAnimation _proxyAnimation;
|
||||
_HeroFlightManifest manifest;
|
||||
OverlayEntry overlayEntry;
|
||||
bool _aborted = false;
|
||||
|
||||
RectTween _doCreateRectTween(Rect begin, Rect end) {
|
||||
Tween<Rect> _doCreateRectTween(Rect begin, Rect end) {
|
||||
final CreateRectTween createRectTween = manifest.toHero.widget.createRectTween ?? manifest.createRectTween;
|
||||
if (createRectTween != null)
|
||||
return createRectTween(begin, end);
|
||||
@ -268,11 +268,11 @@ class _HeroFlight {
|
||||
}
|
||||
} 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 Offset heroOriginEnd = toHeroBox.localToGlobal(Offset.zero, ancestor: routeBox);
|
||||
if (heroOriginEnd != heroRect.end.topLeft) {
|
||||
final Rect heroRectEnd = heroOriginEnd & heroRect.end.size;
|
||||
// supposed to end up then recreate the heroRect tween.
|
||||
final RenderBox finalRouteBox = manifest.toRoute.subtreeContext?.findRenderObject();
|
||||
final Offset toHeroOrigin = toHeroBox.localToGlobal(Offset.zero, ancestor: finalRouteBox);
|
||||
if (toHeroOrigin != heroRect.end.topLeft) {
|
||||
final Rect heroRectEnd = toHeroOrigin & heroRect.end.size;
|
||||
heroRect = _doCreateRectTween(heroRect.begin, heroRectEnd);
|
||||
}
|
||||
}
|
||||
@ -359,9 +359,13 @@ class _HeroFlight {
|
||||
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).
|
||||
// That's because tweens like MaterialRectArcTween may create a different
|
||||
// path for swapped begin and end parameters. We want the pop flight
|
||||
// path to be the same (in reverse) as the push flight path.
|
||||
_proxyAnimation.parent = new ReverseAnimation(newManifest.animation);
|
||||
|
||||
heroRect = _doCreateRectTween(heroRect.end, heroRect.begin);
|
||||
heroRect = new ReverseTween<Rect>(heroRect);
|
||||
} else if (manifest.type == _HeroFlightType.pop && newManifest.type == _HeroFlightType.push) {
|
||||
// A pop flight was interrupted by a push.
|
||||
assert(newManifest.animation.status == AnimationStatus.forward);
|
||||
@ -378,6 +382,7 @@ class _HeroFlight {
|
||||
newManifest.toHero.startFlight();
|
||||
heroRect = _doCreateRectTween(heroRect.end, _globalBoundingBoxFor(newManifest.toHero.context));
|
||||
} else {
|
||||
// TODO(hansmuller): Use ReverseTween here per github.com/flutter/flutter/pull/12203.
|
||||
heroRect = _doCreateRectTween(heroRect.end, heroRect.begin);
|
||||
}
|
||||
} else {
|
||||
@ -425,7 +430,7 @@ class HeroController extends NavigatorObserver {
|
||||
/// Creates a hero controller with the given [RectTween] constructor if any.
|
||||
///
|
||||
/// The [createRectTween] argument is optional. If null, the controller uses a
|
||||
/// linear [RectTween].
|
||||
/// linear [Tween<Rect>].
|
||||
HeroController({ this.createRectTween });
|
||||
|
||||
/// Used to create [RectTween]s that interpolate the position of heros in flight.
|
||||
|
@ -26,6 +26,10 @@ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
|
||||
child: const Text('two'),
|
||||
onPressed: () { Navigator.pushNamed(context, '/two'); }
|
||||
),
|
||||
new FlatButton(
|
||||
child: const Text('twoInset'),
|
||||
onPressed: () { Navigator.pushNamed(context, '/twoInset'); }
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
@ -47,6 +51,34 @@ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
|
||||
]
|
||||
)
|
||||
),
|
||||
// This route is the same as /two except that Hero 'a' is shifted to the right by
|
||||
// 50 pixels. When the hero's in-flight bounds between / and /twoInset are animated
|
||||
// using MaterialRectArcTween (the default) they'll follow a different path
|
||||
// then when the flight starts at /twoInset and returns to /.
|
||||
'/twoInset': (BuildContext context) => new Material(
|
||||
child: new ListView(
|
||||
key: routeTwoKey,
|
||||
children: <Widget>[
|
||||
new FlatButton(
|
||||
child: const Text('pop'),
|
||||
onPressed: () { Navigator.pop(context); }
|
||||
),
|
||||
new Container(height: 150.0, width: 150.0),
|
||||
new Card(
|
||||
child: new Padding(
|
||||
padding: const EdgeInsets.only(left: 50.0),
|
||||
child: new Hero(tag: 'a', child: new Container(height: 150.0, width: 150.0, key: secondKey))
|
||||
),
|
||||
),
|
||||
new Container(height: 150.0, width: 150.0),
|
||||
new FlatButton(
|
||||
child: const Text('three'),
|
||||
onPressed: () { Navigator.push(context, new ThreeRoute()); },
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
|
||||
};
|
||||
|
||||
class ThreeRoute extends MaterialPageRoute<Null> {
|
||||
@ -1119,5 +1151,96 @@ void main() {
|
||||
expect(tester.getCenter(find.byKey(firstKey)), const Offset(50.0, 50.0));
|
||||
});
|
||||
|
||||
testWidgets('Pop interrupts push, reverses flight', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(new MaterialApp(routes: routes));
|
||||
await tester.tap(find.text('twoInset'));
|
||||
await tester.pump(); // begin navigation from / to /twoInset.
|
||||
|
||||
final double epsilon = 0.001;
|
||||
final Duration duration = const Duration(milliseconds: 300);
|
||||
|
||||
await tester.pump();
|
||||
final double x0 = tester.getTopLeft(find.byKey(secondKey)).dx;
|
||||
|
||||
// Flight begins with the secondKey Hero widget lined up with the firstKey widget.
|
||||
expect(x0, 4.0);
|
||||
|
||||
await tester.pump(duration * 0.1);
|
||||
final double x1 = tester.getTopLeft(find.byKey(secondKey)).dx;
|
||||
|
||||
await tester.pump(duration * 0.1);
|
||||
final double x2 = tester.getTopLeft(find.byKey(secondKey)).dx;
|
||||
|
||||
await tester.pump(duration * 0.1);
|
||||
final double x3 = tester.getTopLeft(find.byKey(secondKey)).dx;
|
||||
|
||||
await tester.pump(duration * 0.1);
|
||||
final double x4 = tester.getTopLeft(find.byKey(secondKey)).dx;
|
||||
|
||||
// Pop route /twoInset before the push transition from / to /twoInset has finished.
|
||||
await tester.tap(find.text('pop'));
|
||||
|
||||
|
||||
// We expect the hero to take the same path as it did flying from /
|
||||
// to /twoInset as it does now, flying from '/twoInset' back to /. The most
|
||||
// important checks below are the first (x4) and last (x0): the hero should
|
||||
// not jump from where it was when the push transition was interrupted by a
|
||||
// pop, and it should end up where the push started.
|
||||
|
||||
await tester.pump();
|
||||
expect(tester.getTopLeft(find.byKey(secondKey)).dx, closeTo(x4, epsilon));
|
||||
|
||||
await tester.pump(duration * 0.1);
|
||||
expect(tester.getTopLeft(find.byKey(secondKey)).dx, closeTo(x3, epsilon));
|
||||
|
||||
await tester.pump(duration * 0.1);
|
||||
expect(tester.getTopLeft(find.byKey(secondKey)).dx, closeTo(x2, epsilon));
|
||||
|
||||
await tester.pump(duration * 0.1);
|
||||
expect(tester.getTopLeft(find.byKey(secondKey)).dx, closeTo(x1, epsilon));
|
||||
|
||||
await tester.pump(duration * 0.1);
|
||||
expect(tester.getTopLeft(find.byKey(secondKey)).dx, closeTo(x0, epsilon));
|
||||
|
||||
// Below: show that a different pop Hero path is in fact taken after
|
||||
// a completed push transition.
|
||||
|
||||
// Complete the pop transition and we're back to showing /.
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getTopLeft(find.byKey(firstKey)).dx, 4.0); // Card contents are inset by 4.0.
|
||||
|
||||
// Push /twoInset and wait for the transition to finish.
|
||||
await tester.tap(find.text('twoInset'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getTopLeft(find.byKey(secondKey)).dx, 54.0);
|
||||
|
||||
// Start the pop transition from /twoInset to /.
|
||||
await tester.tap(find.text('pop'));
|
||||
await tester.pump();
|
||||
|
||||
// Now the firstKey widget is the flying hero widget and it starts
|
||||
// out lined up with the secondKey widget.
|
||||
await tester.pump();
|
||||
expect(tester.getTopLeft(find.byKey(firstKey)).dx, 54.0);
|
||||
|
||||
// x0-x4 are the top left x coordinates for the beginning 40% of
|
||||
// the incoming flight. Advance the outgoing flight to the same
|
||||
// place.
|
||||
await tester.pump(duration * 0.6);
|
||||
|
||||
await tester.pump(duration * 0.1);
|
||||
expect(tester.getTopLeft(find.byKey(firstKey)).dx, isNot(closeTo(x4, epsilon)));
|
||||
|
||||
await tester.pump(duration * 0.1);
|
||||
expect(tester.getTopLeft(find.byKey(firstKey)).dx, isNot(closeTo(x3, epsilon)));
|
||||
|
||||
// At this point the flight path arcs do start to get pretty close so
|
||||
// there's no point in comparing them.
|
||||
await tester.pump(duration * 0.1);
|
||||
|
||||
// After the remaining 40% of the incoming flight is complete, we
|
||||
// expect to end up where the outgoing flight started.
|
||||
await tester.pump(duration * 0.1);
|
||||
expect(tester.getTopLeft(find.byKey(firstKey)).dx, x0);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user