631 字
3 分钟
flutter-fix-hero-desync
使用 HeroControllerScope
在整个界面中共享同一个 HeroController
。这样就能避免在不同叠层之间切换时出现闪烁、跳跃或“丢失”过渡的情况。
为什么会出现这个问题
每个 Navigator
都拥有自己的 HeroController
。当存在多个导航器时(例如 TabBarView
加上用于底部弹窗的模态 Navigator
),某一个导航树里的 Hero 无法与另一棵树里的 Hero 协调,因此无法计算正确的飞行动画。
实现方式
创建一个共享的 HeroController
,并把它注入到需要共享过渡效果的所有导航器之上。
class App extends StatefulWidget { const App({super.key});
@override State<App> createState() => _AppState();}
class _AppState extends State<App> { late final HeroController _sharedHeroController;
@override void initState() { super.initState(); _sharedHeroController = HeroController(); // 或者使用 MaterialApp.createMaterialHeroController() }
@override Widget build(BuildContext context) { return HeroControllerScope( controller: _sharedHeroController, child: MaterialApp( // 如果共享这个 controller,就不要在 MaterialApp 上再设置另一个 heroController。 home: const RootShell(), ), ); }}
现在,在应用内部,任意嵌套的 Navigator
(标签页、模态流程、带自己导航器的 showModalBottomSheet
等)都会协调 Hero 飞行动画:
class RootShell extends StatelessWidget { const RootShell({super.key});
@override Widget build(BuildContext context) { return Scaffold( body: TabBarView( children: const [ _TabWithOwnNavigator(), // 嵌套 Navigator _AnotherTab(), ], ), ); }}
class _TabWithOwnNavigator extends StatelessWidget { const _TabWithOwnNavigator();
@override Widget build(BuildContext context) { return Navigator( onGenerateRoute: (settings) => MaterialPageRoute( builder: (_) => const MasterListPage(), ), ); }}
注意事项(速查)
- 每个“视觉簇”只用一个 controller。需要共享 Hero 动画的导航器必须位于同一个
HeroControllerScope
下。 - 确保
heroTag
唯一。重复的标签会破坏飞行动画,优先使用稳定的 ID(如数据项的 ID),不要使用索引。 - 保持组件形状一致。源和目标 Hero 应该有相近的尺寸与裁剪;可以用同一个
Material
(如Material(type: MaterialType.transparency)
)包裹,避免层级闪烁。 - 避免双重包裹。如果你已经自己添加了
HeroControllerScope
,不要再在MaterialApp
上提供另一个HeroController
。 - 模态底部弹窗和对话框如果使用独立导航器,也要放在同一个
HeroControllerScope
内(通常需要把 scope 放得足够高,比如在MaterialApp
或 Shell 组件之上)。
进阶:自定义飞行动画
如果需要自定义动画,可以统一设置一个 flightShuttleBuilder
,全局都会生效:
late final _sharedHeroController = HeroController(createRectTween: (begin, end) { return MaterialRectArcTween(begin: begin, end: end); // 也可以换成自定义 RectTween});
总结
- 多个导航器意味着多个
HeroController
,从而导致过渡缺失或异常。 - 使用
HeroControllerScope(controller: sharedController, child: ...)
把应用包裹起来。 - 保持标签唯一、组件形状一致。
flutter-fix-hero-desync
https://blog.lpkt.cn/posts/flutter-fix-hero-desync/