Open markphillips100 opened 2 years ago
I've found a workaround and updated the herotest app to demonstrate it.
Basically there seems to be some conflict between SDK HeroController and Navigator. The former makes the animation status assertion during it's void start(_HeroFlightManifest initialManifest)
method without first filtering out offending toRoute parameters in its _maybeStartHeroTransition
method - seems a likely spot to put it IMO.
And from the Navigator perspective, even though the route that causes the issues is an "add" rather than a "push" (because it's not a top-level page) the HeroController's didPush
is still called which triggers the _maybeStartHeroTransition
flow.
So, the workaround is to have a custom derived HeroController
and override didPush
and filter the toRoute animation status and don't call the super if status is already dismissed:
class CustomHeroController extends HeroController {
CustomHeroController({ createRectTween })
: super(createRectTween: createRectTween);
static HeroController createMaterialHeroController() {
return CustomHeroController(createRectTween: (Rect? begin, Rect? end) {
return MaterialRectArcTween(begin: begin, end: end);
},
);
}
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route is PageRoute<dynamic> && route.animation?.status == AnimationStatus.dismissed) {
return;
}
super.didPush(route, previousRoute);
}
}
Then use the builder variant of RoutemasterDelegate.builder
to provide a new hero controller scope and specify an instance of the new controller (instantiated outside of the build method):
class MyApp extends StatelessWidget {
// Use this controller to support bypass possible hero animation if the toRoute animation is already dismissed.
final mainHeroController = CustomHeroController.createMaterialHeroController();
// Use this controller to show assertion.
// final mainHeroController = MaterialApp.createMaterialHeroController();
MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Hero Rotation Demo',
routeInformationParser: const RoutemasterParser(),
routeInformationProvider: PlatformRouteInformationProvider(initialRouteInformation: const RouteInformation(location: "/layout")),
routerDelegate: RoutemasterDelegate.builder(
navigatorBuilder: (context, stack) {
return HeroControllerScope(
controller: mainHeroController,
child: PageStackNavigator(
stack: stack,
),
);
},
routesBuilder: (context) {
....
},
),
);
}
}
I've also submitted the issue with Flutter repo too. It's been triaged so will see if they come up with something.
It's difficult to know if this is an issue inherent in what I am doing or if this is actually a flutter bug itself. I'll let others decide I guess.
I've created a public app to demonstrate the issue and it's entirely reproducable. Repo is at https://github.com/markphillips100/herotest. Let me know if you prefer code snippets posted in this issue itself.
The strategy of the app is to show how to switch between responsive layouts; one for mobiles with a single page navigator, and one for wider viewports with a splitview where the left side is the root page and the right side uses it's own page navigator stack (using
StackPage
). Rotating the device would switch between the layouts and retain the existing route path.Hero transitions should work within the mobile layout for all pages. For the wider layout, the left side heros should be disabled, and the right side hero transitions should work inside of their own navigator stack.
This all seems to works fine. Navigation okay, and heros isolated to their navigator stack okay. Problem occurs only when rotating from wide layout to mobile layout, and only when the right side has pushed another page above the stack's default path.
The exception I get is an assertion in flutter's heroes.dart file:
I've stepped through the assertion logic using Android Studio and it fails because the last line's
initial.status
isAnimationStatus.declined
.To reproduce just follow these steps:
I've edited the SDK code as a test to include the "declined" status as being valid in the last line, so that the assert returns true. I get no exception when I do this and no visual indication that something is broken.
Having said that, I'd like to understand if there's something fundamentally wrong with what I am doing that would cause this, and if there's something I can do to mitigate the assertion in the first place.
What I know so far: