Make the Hero transition animation configurable (#12215)

This commit is contained in:
Hans Muller 2017-09-22 11:34:44 -07:00 committed by GitHub
parent fde26cd14c
commit d3d6198852
3 changed files with 222 additions and 2 deletions

View file

@ -221,9 +221,13 @@ T _maxBy<T>(Iterable<T> input, _KeyFunc<T> keyFunc) {
///
/// See also:
///
/// * [MaterialRectCenterArcTween], which interpolates a rect along a circular
/// arc between the begin and end [Rect]'s centers.
/// * [Tween], for a discussion on how to use interpolation objects.
/// * [MaterialPointArcTween], the analogue for [Offset] interporation.
/// * [RectTween], which does a linear rectangle interpolation.
/// * [Hero.createRectTween], which can be used to specify the tween that defines
/// a hero's path.
class MaterialRectArcTween extends RectTween {
/// Creates a [Tween] for animating [Rect]s along a circular arc.
///
@ -323,3 +327,89 @@ class MaterialRectArcTween extends RectTween {
return '$runtimeType($begin \u2192 $end; beginArc=$beginArc, endArc=$endArc)';
}
}
/// A [Tween] that interpolates a [Rect] by moving it along a circular
/// arc from [begin.center] to [end.center] while interpoloting the rectangle's
/// width and height.
///
/// The arc that defines that center of the interpolated rectangle as it morphs
/// from [begin] to [end] is a [MaterialPointArcTween].
///
/// See also:
///
/// * [MaterialRectArcTween], A [Tween] that interpolates a [Rect] by having
/// its opposite corners follow circular arcs.
/// * [Tween], for a discussion on how to use interpolation objects.
/// * [MaterialPointArcTween], the analogue for [Offset] interporation.
/// * [RectTween], which does a linear rectangle interpolation.
/// * [Hero.createRectTween], which can be used to specify the tween that defines
/// a hero's path.
class MaterialRectCenterArcTween extends RectTween {
/// Creates a [Tween] for animating [Rect]s along a circular arc.
///
/// The [begin] and [end] properties must be non-null before the tween is
/// first used, but the arguments can be null if the values are going to be
/// filled in later.
MaterialRectCenterArcTween({
Rect begin,
Rect end,
}) : super(begin: begin, end: end);
bool _dirty = true;
void _initialize() {
assert(begin != null);
assert(end != null);
_centerArc = new MaterialPointArcTween(
begin: begin.center,
end: end.center,
);
_dirty = false;
}
/// If [begin] and [end] are non-null, returns a tween that interpolates
/// along a circular arc between [begin.center] and [end.center].
MaterialPointArcTween get centerArc {
if (begin == null || end == null)
return null;
if (_dirty)
_initialize();
return _centerArc;
}
MaterialPointArcTween _centerArc;
@override
set begin(Rect value) {
if (value != begin) {
super.begin = value;
_dirty = true;
}
}
@override
set end(Rect value) {
if (value != end) {
super.end = value;
_dirty = true;
}
}
@override
Rect lerp(double t) {
if (_dirty)
_initialize();
if (t == 0.0)
return begin;
if (t == 1.0)
return end;
final Offset center = _centerArc.lerp(t);
final double width = lerpDouble(begin.width, end.width, t);
final double height = lerpDouble(begin.height, end.height, t);
return new Rect.fromLTWH(center.dx - width / 2.0, center.dy - height / 2.0, width, height);
}
@override
String toString() {
return '$runtimeType($begin \u2192 $end; centerArc=$centerArc)';
}
}

View file

@ -80,6 +80,7 @@ class Hero extends StatefulWidget {
const Hero({
Key key,
@required this.tag,
this.createRectTween,
@required this.child,
}) : assert(tag != null),
assert(child != null),
@ -90,6 +91,19 @@ class Hero extends StatefulWidget {
/// a hero animation will be triggered.
final Object tag;
/// Defines how the destination hero's bounds change as it flies from the starting
/// 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
/// to compute the hero's bounds as the flight animation's value goes from 0.0
/// to 1.0.
///
/// If this property is null, the default, then the value of
/// [HeroController.createRectTween] is used. The [HeroController] created by
/// [MaterialApp] creates a [MaterialArcRectTween].
final CreateRectTween createRectTween;
/// The widget subtree that will "fly" from one route to another during a
/// [Navigator] push or pop transition.
///
@ -230,8 +244,9 @@ class _HeroFlight {
bool _aborted = false;
RectTween _doCreateRectTween(Rect begin, Rect end) {
if (manifest.createRectTween != null)
return manifest.createRectTween(begin, end);
final CreateRectTween createRectTween = manifest.toHero.widget.createRectTween ?? manifest.createRectTween;
if (createRectTween != null)
return createRectTween(begin, end);
return new RectTween(begin: begin, end: end);
}

View file

@ -1005,4 +1005,119 @@ void main() {
expect(find.text('456'), findsOneWidget);
});
testWidgets('Hero createRectTween', (WidgetTester tester) async {
RectTween createRectTween(Rect begin, Rect end) {
return new MaterialRectCenterArcTween(begin: begin, end: end);
}
final Map<String, WidgetBuilder> createRectTweenHeroRoutes = <String, WidgetBuilder>{
'/': (BuildContext context) => new Material(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Hero(
tag: 'a',
createRectTween: createRectTween,
child: new Container(height: 100.0, width: 100.0, key: firstKey),
),
new FlatButton(
child: const Text('two'),
onPressed: () { Navigator.pushNamed(context, '/two'); }
),
]
)
),
'/two': (BuildContext context) => new Material(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new SizedBox(
height: 200.0,
child: new FlatButton(
child: const Text('pop'),
onPressed: () { Navigator.pop(context); }
),
),
new Hero(
tag: 'a',
createRectTween: createRectTween,
child: new Container(height: 200.0, width: 100.0, key: secondKey),
),
],
),
),
};
await tester.pumpWidget(new MaterialApp(routes: createRectTweenHeroRoutes));
expect(tester.getCenter(find.byKey(firstKey)), const Offset(50.0, 50.0));
final double epsilon = 0.001;
final Duration duration = const Duration(milliseconds: 300);
final Curve curve = Curves.fastOutSlowIn;
final MaterialPointArcTween pushCenterTween = new MaterialPointArcTween(
begin: const Offset(50.0, 50.0),
end: const Offset(400.0, 300.0),
);
await tester.tap(find.text('two'));
await tester.pump(); // begin navigation
// Verify that the center of the secondKey Hero flies along the
// pushCenterTween arc for the push /two flight.
await tester.pump();
expect(tester.getCenter(find.byKey(secondKey)), const Offset(50.0, 50.0));
await tester.pump(duration * 0.25);
Offset actualHeroCenter = tester.getCenter(find.byKey(secondKey));
Offset predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.25));
expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon));
await tester.pump(duration * 0.25);
actualHeroCenter = tester.getCenter(find.byKey(secondKey));
predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.5));
expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon));
await tester.pump(duration * 0.25);
actualHeroCenter = tester.getCenter(find.byKey(secondKey));
predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.75));
expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon));
await tester.pumpAndSettle();
expect(tester.getCenter(find.byKey(secondKey)), const Offset(400.0, 300.0));
// Verify that the center of the firstKey Hero flies along the
// pushCenterTween arc for the pop /two flight.
await tester.tap(find.text('pop'));
await tester.pump(); // begin navigation
final MaterialPointArcTween popCenterTween = new MaterialPointArcTween(
begin: const Offset(400.0, 300.0),
end: const Offset(50.0, 50.0),
);
await tester.pump();
expect(tester.getCenter(find.byKey(firstKey)), const Offset(400.0, 300.0));
await tester.pump(duration * 0.25);
actualHeroCenter = tester.getCenter(find.byKey(firstKey));
predictedHeroCenter = popCenterTween.lerp(curve.flipped.transform(0.25));
expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon));
await tester.pump(duration * 0.25);
actualHeroCenter = tester.getCenter(find.byKey(firstKey));
predictedHeroCenter = popCenterTween.lerp(curve.flipped.transform(0.5));
expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon));
await tester.pump(duration * 0.25);
actualHeroCenter = tester.getCenter(find.byKey(firstKey));
predictedHeroCenter = popCenterTween.lerp(curve.flipped.transform(0.75));
expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon));
await tester.pumpAndSettle();
expect(tester.getCenter(find.byKey(firstKey)), const Offset(50.0, 50.0));
});
}