Fix calculation of hero rectTween when Navigator isn't fullscreen (#29677)
diff --git a/packages/flutter/lib/src/widgets/heroes.dart b/packages/flutter/lib/src/widgets/heroes.dart index e9c50c1..9966ab9 100644 --- a/packages/flutter/lib/src/widgets/heroes.dart +++ b/packages/flutter/lib/src/widgets/heroes.dart
@@ -53,11 +53,15 @@ pop, } -// The bounding box for context in global coordinates. -Rect _globalBoundingBoxFor(BuildContext context) { +// The bounding box for context in ancestorContext coordinate system, or in the global +// coordinate system when null. +Rect _boundingBoxFor(BuildContext context, [BuildContext ancestorContext]) { final RenderBox box = context.findRenderObject(); assert(box != null && box.hasSize); - return MatrixUtils.transformRect(box.getTransformTo(null), Offset.zero & box.size); + return MatrixUtils.transformRect( + box.getTransformTo(ancestorContext?.findRenderObject()), + Offset.zero & box.size, + ); } /// A widget that marks its child as being a candidate for @@ -496,8 +500,8 @@ manifest.toHero.startFlight(); heroRectTween = _doCreateRectTween( - _globalBoundingBoxFor(manifest.fromHero.context), - _globalBoundingBoxFor(manifest.toHero.context), + _boundingBoxFor(manifest.fromHero.context, manifest.fromRoute.subtreeContext), + _boundingBoxFor(manifest.toHero.context, manifest.toRoute.subtreeContext), ); overlayEntry = OverlayEntry(builder: _buildOverlay); @@ -540,7 +544,10 @@ if (manifest.fromHero != newManifest.toHero) { manifest.fromHero.endFlight(); newManifest.toHero.startFlight(); - heroRectTween = _doCreateRectTween(heroRectTween.end, _globalBoundingBoxFor(newManifest.toHero.context)); + heroRectTween = _doCreateRectTween( + heroRectTween.end, + _boundingBoxFor(newManifest.toHero.context, newManifest.toRoute.subtreeContext), + ); } else { // TODO(hansmuller): Use ReverseTween here per github.com/flutter/flutter/pull/12203. heroRectTween = _doCreateRectTween(heroRectTween.end, heroRectTween.begin); @@ -552,7 +559,10 @@ assert(manifest.fromHero != newManifest.fromHero); assert(manifest.toHero != newManifest.toHero); - heroRectTween = _doCreateRectTween(heroRectTween.evaluate(_proxyAnimation), _globalBoundingBoxFor(newManifest.toHero.context)); + heroRectTween = _doCreateRectTween( + heroRectTween.evaluate(_proxyAnimation), + _boundingBoxFor(newManifest.toHero.context, newManifest.toRoute.subtreeContext), + ); shuttle = null; if (newManifest.type == HeroFlightDirection.pop) @@ -706,7 +716,7 @@ return; } - final Rect navigatorRect = _globalBoundingBoxFor(navigator.context); + final Rect navigatorRect = _boundingBoxFor(navigator.context); // At this point the toHeroes may have been built and laid out for the first time. final Map<Object, _HeroState> fromHeroes = Hero._allHeroesFor(from.subtreeContext, isUserGestureTransition, navigator);
diff --git a/packages/flutter/test/widgets/heroes_test.dart b/packages/flutter/test/widgets/heroes_test.dart index 3cf6679..83fa423 100644 --- a/packages/flutter/test/widgets/heroes_test.dart +++ b/packages/flutter/test/widgets/heroes_test.dart
@@ -1172,6 +1172,128 @@ expect(tester.getCenter(find.byKey(firstKey)), const Offset(50.0, 50.0)); }); + testWidgets('Hero createRectTween for Navigator that is not full screen', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/25272 + + RectTween createRectTween(Rect begin, Rect end) { + return RectTween(begin: begin, end: end); + } + + final Map<String, WidgetBuilder> createRectTweenHeroRoutes = <String, WidgetBuilder>{ + '/': (BuildContext context) => Material( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: <Widget>[ + Hero( + tag: 'a', + createRectTween: createRectTween, + child: Container(height: 100.0, width: 100.0, key: firstKey), + ), + FlatButton( + child: const Text('two'), + onPressed: () { Navigator.pushNamed(context, '/two'); }, + ), + ], + ), + ), + '/two': (BuildContext context) => Material( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: <Widget>[ + SizedBox( + height: 200.0, + child: FlatButton( + child: const Text('pop'), + onPressed: () { Navigator.pop(context); }, + ), + ), + Hero( + tag: 'a', + createRectTween: createRectTween, + child: Container(height: 200.0, width: 100.0, key: secondKey), + ), + ], + ), + ), + }; + + const double leftPadding = 10.0; + + // MaterialApp and its Navigator are offset from the left + await tester.pumpWidget(Padding( + padding: const EdgeInsets.only(left: leftPadding), + child: MaterialApp(routes: createRectTweenHeroRoutes), + )); + expect(tester.getCenter(find.byKey(firstKey)), const Offset(leftPadding + 50.0, 50.0)); + + const double epsilon = 0.001; + const Duration duration = Duration(milliseconds: 300); + const Curve curve = Curves.fastOutSlowIn; + final RectTween pushRectTween = RectTween( + begin: Rect.fromLTWH(leftPadding, 0.0, 100.0, 100.0), + end: Rect.fromLTWH(350.0 + leftPadding / 2, 200.0, 100.0, 200.0), + ); + + await tester.tap(find.text('two')); + await tester.pump(); // begin navigation + + // Verify that the rect of the secondKey Hero transforms as the + // pushRectTween rect for the push /two flight. + + await tester.pump(); + expect(tester.getCenter(find.byKey(secondKey)), const Offset(50.0 + leftPadding, 50.0)); + + await tester.pump(duration * 0.25); + Rect actualHeroRect = tester.getRect(find.byKey(secondKey)); + Rect predictedHeroRect = pushRectTween.lerp(curve.transform(0.25)); + expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect)); + + await tester.pump(duration * 0.25); + actualHeroRect = tester.getRect(find.byKey(secondKey)); + predictedHeroRect = pushRectTween.lerp(curve.transform(0.5)); + expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect)); + + await tester.pump(duration * 0.25); + actualHeroRect = tester.getRect(find.byKey(secondKey)); + predictedHeroRect = pushRectTween.lerp(curve.transform(0.75)); + expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect)); + + await tester.pumpAndSettle(); + expect(tester.getCenter(find.byKey(secondKey)), const Offset(400.0 + leftPadding / 2, 300.0)); + + // Verify that the rect of the firstKey Hero transforms as the + // pushRectTween rect for the pop /two flight. + + await tester.tap(find.text('pop')); + await tester.pump(); // begin navigation + + final RectTween popRectTween = RectTween( + begin: Rect.fromLTWH(350.0 + leftPadding / 2, 200.0, 100.0, 200.0), + end: Rect.fromLTWH(leftPadding, 0.0, 100.0, 100.0), + ); + await tester.pump(); + expect(tester.getCenter(find.byKey(firstKey)), const Offset(400.0 + leftPadding / 2, 300.0)); + + await tester.pump(duration * 0.25); + actualHeroRect = tester.getRect(find.byKey(firstKey)); + predictedHeroRect = popRectTween.lerp(curve.flipped.transform(0.25)); + expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect)); + + await tester.pump(duration * 0.25); + actualHeroRect = tester.getRect(find.byKey(firstKey)); + predictedHeroRect = popRectTween.lerp(curve.flipped.transform(0.5)); + expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect)); + + await tester.pump(duration * 0.25); + actualHeroRect = tester.getRect(find.byKey(firstKey)); + predictedHeroRect = popRectTween.lerp(curve.flipped.transform(0.75)); + expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect)); + + await tester.pumpAndSettle(); + expect(tester.getCenter(find.byKey(firstKey)), const Offset(50.0 + leftPadding, 50.0)); + }); + + testWidgets('Pop interrupts push, reverses flight', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp(routes: routes)); await tester.tap(find.text('twoInset'));