diff --git a/packages/flutter/lib/src/widgets/heroes.dart b/packages/flutter/lib/src/widgets/heroes.dart index a12befa5e19..667d62dc050 100644 --- a/packages/flutter/lib/src/widgets/heroes.dart +++ b/packages/flutter/lib/src/widgets/heroes.dart @@ -677,12 +677,11 @@ class _HeroFlight { final HeroFlightDirection type = initialManifest.type; switch (type) { case HeroFlightDirection.pop: - return initial.value == 1.0 && initialManifest.isUserGestureTransition + return initialManifest.isUserGestureTransition // During user gesture transitions, the animation controller isn't - // driving the reverse transition, but should still be in a previously - // completed stage with the initial value at 1.0. - ? initial.status == AnimationStatus.completed - : initial.status == AnimationStatus.reverse; + // driving the reverse transition, so the status is not important. + || + initial.status == AnimationStatus.reverse; case HeroFlightDirection.push: return initial.value == 0.0 && initial.status == AnimationStatus.forward; } @@ -923,8 +922,15 @@ class HeroController extends NavigatorObserver { // For pop transitions driven by a user gesture: if the "to" page has // maintainState = true, then the hero's final dimensions can be measured - // immediately because their page's layout is still valid. - if (isUserGestureTransition && flightType == HeroFlightDirection.pop && toRoute.maintainState) { + // immediately because their page's layout is still valid. Unless due to directly + // adding routes to the pages stack causing the route to never get laid out. + final RenderBox? fromRouteRenderBox = toRoute.subtreeContext?.findRenderObject() as RenderBox?; + final bool hasValidSize = + (fromRouteRenderBox?.hasSize ?? false) && fromRouteRenderBox!.size.isFinite; + if (isUserGestureTransition && + flightType == HeroFlightDirection.pop && + toRoute.maintainState && + hasValidSize) { _startHeroTransition(fromRoute, toRoute, flightType, isUserGestureTransition); } else { // Otherwise, delay measuring until the end of the next frame to allow @@ -934,7 +940,6 @@ class HeroController extends NavigatorObserver { // frame completes, we'll know where the heroes in the `to` route are // going to end up, and the `to` route will go back onstage. toRoute.offstage = toRoute.animation!.value == 0.0; - WidgetsBinding.instance.addPostFrameCallback((Duration value) { if (fromRoute.navigator == null || toRoute.navigator == null) { return; diff --git a/packages/flutter/test/widgets/heroes_test.dart b/packages/flutter/test/widgets/heroes_test.dart index 83902f38fb8..26ab66d4744 100644 --- a/packages/flutter/test/widgets/heroes_test.dart +++ b/packages/flutter/test/widgets/heroes_test.dart @@ -216,6 +216,73 @@ class MyStatefulWidgetState extends State { Widget build(BuildContext context) => Text(widget.value); } +class DeepLinkApp extends StatefulWidget { + const DeepLinkApp({super.key}); + + static const CupertinoPage _homeScreen = CupertinoPage( + name: '/', + child: CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar(middle: Text('First')), + child: Center(child: Text('Home Screen')), + ), + ); + static const CupertinoPage _middleScreen = CupertinoPage( + name: '/middle', + child: CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar(middle: Text('Second')), + child: Center(child: Text('Middle Screen')), + ), + ); + static const CupertinoPage _lastScreen = CupertinoPage( + name: '/middle/last', + child: CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar(middle: Text('Third')), + child: Center(child: Text('Last Screen')), + ), + ); + + @override + DeepLinkAppState createState() => DeepLinkAppState(); +} + +class DeepLinkAppState extends State { + late List> _pages; + + @override + void initState() { + super.initState(); + _pages = >[DeepLinkApp._homeScreen]; + } + + void goToDeepScreen() { + setState(() { + _pages = >[ + DeepLinkApp._homeScreen, + DeepLinkApp._middleScreen, + DeepLinkApp._lastScreen, + ]; + }); + } + + @override + Widget build(BuildContext context) { + return CupertinoApp( + builder: (BuildContext context, Widget? child) { + return Navigator( + pages: _pages, + onDidRemovePage: (Page page) { + setState(() { + if (_pages.length > 1) { + _pages = List>.from(_pages)..removeLast(); + } + }); + }, + ); + }, + ); + } +} + Future main() async { final ui.Image testImage = await createTestImage(); @@ -2984,6 +3051,41 @@ Future main() async { }), ); + // Regression test for https://github.com/flutter/flutter/issues/168267. + testWidgets('Check if previous page is laid out on backswipe gesture before flight', ( + WidgetTester tester, + ) async { + final GlobalKey appKey = GlobalKey(); + await tester.pumpWidget(DeepLinkApp(key: appKey)); + + await tester.pumpAndSettle(); + + expect(find.text('Home Screen'), findsOneWidget); + expect(find.text('Last Screen'), findsNothing); + + appKey.currentState?.goToDeepScreen(); + + await tester.pumpAndSettle(); + + expect(find.text('Home Screen'), findsNothing); + expect(find.text('Last Screen'), findsOneWidget); + + final TestGesture gesture = await tester.startGesture(const Offset(0.01, 300)); + + await gesture.moveTo(const Offset(10, 300)); + await tester.pump(); + // Should not throw an assert here for size and finite space. + await gesture.moveTo(const Offset(500, 300)); + await tester.pump(); + + await gesture.up(); + await tester.pumpAndSettle(); + + expect(find.text('Home Screen'), findsNothing); + expect(find.text('Middle Screen'), findsOneWidget); + expect(find.text('Last Screen'), findsNothing); + }); + // Regression test for https://github.com/flutter/flutter/issues/40239. testWidgets( 'In a pop transition, when fromHero is null, the to hero should eventually become visible',