Make the Hero transition animation configurable (#12215)


diff --git a/packages/flutter/lib/src/material/arc.dart b/packages/flutter/lib/src/material/arc.dart
index 005f300..49ed980 100644
--- a/packages/flutter/lib/src/material/arc.dart
+++ b/packages/flutter/lib/src/material/arc.dart
@@ -221,9 +221,13 @@
 ///
 /// 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 @@
     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)';
+  }
+}
diff --git a/packages/flutter/lib/src/widgets/heroes.dart b/packages/flutter/lib/src/widgets/heroes.dart
index e2bbe63..51c2a0a 100644
--- a/packages/flutter/lib/src/widgets/heroes.dart
+++ b/packages/flutter/lib/src/widgets/heroes.dart
@@ -80,6 +80,7 @@
   const Hero({
     Key key,
     @required this.tag,
+    this.createRectTween,
     @required this.child,
   }) : assert(tag != null),
        assert(child != null),
@@ -90,6 +91,19 @@
   /// 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 @@
   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);
   }
 
diff --git a/packages/flutter/test/widgets/heroes_test.dart b/packages/flutter/test/widgets/heroes_test.dart
index 7aa5410..97a5d44 100644
--- a/packages/flutter/test/widgets/heroes_test.dart
+++ b/packages/flutter/test/widgets/heroes_test.dart
@@ -1005,4 +1005,119 @@
     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));
+  });
+
+
 }