631 字
3 分钟
flutter-fix-hero-desync
2025-10-12
无标签

使用 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/
作者
lollipopkit
发布于
2025-10-12
许可协议
CC BY-NC-SA 4.0