From d3d6198852e2afdbf05ac992b98f89240d043b86 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Fri, 22 Sep 2017 11:34:44 -0700 Subject: [PATCH] Make the Hero transition animation configurable (#12215) --- packages/flutter/lib/src/material/arc.dart | 90 ++++++++++++++ packages/flutter/lib/src/widgets/heroes.dart | 19 ++- .../flutter/test/widgets/heroes_test.dart | 115 ++++++++++++++++++ 3 files changed, 222 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/material/arc.dart b/packages/flutter/lib/src/material/arc.dart index 005f30048be..49ed98002a4 100644 --- a/packages/flutter/lib/src/material/arc.dart +++ b/packages/flutter/lib/src/material/arc.dart @@ -221,9 +221,13 @@ T _maxBy(Iterable input, _KeyFunc 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)'; + } +} diff --git a/packages/flutter/lib/src/widgets/heroes.dart b/packages/flutter/lib/src/widgets/heroes.dart index e2bbe637e9b..51c2a0a2940 100644 --- a/packages/flutter/lib/src/widgets/heroes.dart +++ b/packages/flutter/lib/src/widgets/heroes.dart @@ -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); } diff --git a/packages/flutter/test/widgets/heroes_test.dart b/packages/flutter/test/widgets/heroes_test.dart index 7aa54107e72..97a5d44b04c 100644 --- a/packages/flutter/test/widgets/heroes_test.dart +++ b/packages/flutter/test/widgets/heroes_test.dart @@ -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 createRectTweenHeroRoutes = { + '/': (BuildContext context) => new Material( + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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: [ + 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)); + }); + + }