🚀 什么是 HeroControllerScope?

Flutter 的 HeroControllerScope 是一个用于 管理 Hero 动画控制器(HeroController)范围的 InheritedWidget

简单说:

它定义了当前 Widget 子树应该使用哪一个 HeroController,从而控制 Hero 动画在页面切换时如何执行。

你可以把它理解为:

  • Flutter 导航页面切换时,会做 Hero 动画(两个页面共享的 hero tag)。
  • Hero 动画需要一个 HeroController 来调度。
  • HeroControllerScope 就是把 HeroController 绑定到当前 子树

🧠 默认情况下 Flutter 是怎么用的?

在 Flutter 的 MaterialApp 里:

  • MaterialApp 会自动创建一个 HeroController
  • 并通过 HeroControllerScope 绑定给整个 app
  • Navigator 在切换页面时就能找到这个 HeroController → 自动做 Hero 动画

结构类似:

MaterialApp
 └── HeroControllerScope
       └── WidgetsApp
            └── Navigator
                 └── pages...

🔍 为什么需要范围(Scope)?

因为某些高级场景中,你不想多个 Navigator 共用一个 HeroController,例如:

✔ 场景 A —— 多 Navigator 且都需要 Hero 动画

例如一个 App 有:

  • 主导航(底部 TabBar)
  • 某个 Tab 内有独立的 Navigator(比如淘宝首页 Tab 有独立层级)

如果所有 Navigator 共用一个 HeroController:

❌ Hero 动画会乱跳(跨 Tab 动画)

所以可以对某个子树使用单独的 HeroController:

HeroControllerScope(
  controller: HeroController(),
  child: Navigator(
    key: _tabNavigatorKey,
    ...
  )
)

这样:

📌 每个 Navigator 内的 Hero 动画互不影响


🔧 HeroControllerScope 的核心 API

HeroControllerScope(
  controller: HeroController? controller,
  child: Widget
)

最关键的是:

  • controller: 指定当前子树使用的 HeroController
  • 如果为 null → 使用父级 scope 的 controller(继承机制)

还可以通过静态方法访问:

final controller = HeroControllerScope.of(context);

🧩 小总结(最容易记住的版本)

名称 作用
HeroController 真正执行 Hero 动画的对象
HeroControllerScope 把 HeroController 挂载到某个 Widget 子树
Navigator 在 push/pop 时触发 Hero 动画,需要从 scope 里读取 controller

📘 一个直观示例:让某个 Navigator 禁用 Hero 动画

把 controller 设置为 null controller

HeroControllerScope(
  controller: HeroController(createRectTween: (_, __) => RectTween(begin: null, end: null)),
  child: Navigator(...),
)

或者完全禁用:

HeroControllerScope(
  controller: null, // 不提供 HeroController
  child: Navigator(...),
)

但 Flutter 默认应该会 fallback,但是动画不会执行。


🧨 什么时候你会用到 HeroControllerScope?

✔ 你有多个 Navigator
✔ 你需要让某些 Navigator 共享动画
✔ 或者隔离动画
✔ 或者想自定义 Hero 动画(RectTween、曲线、自定义动画)
✔ 或者你的项目组件化,想让模块内的动画不影响全局


🎯 总结一句话

HeroControllerScope 用来为特定子树指定 HeroController,使得不同 Navigator 的 Hero 动画隔离或自定义。


一套完整且超清晰的 HeroControllerScope 多 Navigator 动画隔离案例 + 结构图


🧩 一、架构图(最重要)

下面是 MaterialApp 默认结构

MaterialApp
 └── HeroControllerScope   ← 全局默认 HeroController
       └── WidgetsApp
            └── Navigator (root)

当你使用多个 Navigator(常见于 TabBar、BottomNavigationBar)时,你可以给每个 Navigator 分配自己的 HeroController:

MaterialApp
 └── HeroControllerScope (全局)
       └── Scaffold
            └── IndexedStack
                 ├── Tab1
                 │    └── HeroControllerScope  ← Navigator A 独立
                 │          └── Navigator A
                 └── Tab2
                      └── HeroControllerScope  ← Navigator B 独立
                            └── Navigator B

💡 这样 Hero 动画不会跨 Tab 混乱。


🧪 二、完整可运行 Demo —— 多 Navigator + Hero 隔离

你可以直接复制到 main.dart 并运行。


✔ 1. 主入口

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: const HomeTabs(),
    );
  }
}

✔ 2. 底部导航 + 两个独立 Navigator(重点)

class HomeTabs extends StatefulWidget {
  const HomeTabs({Key? key}) : super(key: key);

  
  State<HomeTabs> createState() => _HomeTabsState();
}

class _HomeTabsState extends State<HomeTabs> {
  int _index = 0;

  final _tab1NavKey = GlobalKey<NavigatorState>();
  final _tab2NavKey = GlobalKey<NavigatorState>();

  final _tab1HeroController = HeroController();
  final _tab2HeroController = HeroController();

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _index,
        children: [
          /// Tab 1 使用独立 HeroController
          HeroControllerScope(
            controller: _tab1HeroController,
            child: Navigator(
              key: _tab1NavKey,
              onGenerateRoute: (_) =>
                  MaterialPageRoute(builder: (_) => const Tab1Page()),
            ),
          ),

          /// Tab 2 使用独立 HeroController
          HeroControllerScope(
            controller: _tab2HeroController,
            child: Navigator(
              key: _tab2NavKey,
              onGenerateRoute: (_) =>
                  MaterialPageRoute(builder: (_) => const Tab2Page()),
            ),
          ),
        ],
      ),

      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _index,
        onTap: (i) => setState(() => _index = i),
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: "Tab1"),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: "Tab2"),
        ],
      ),
    );
  }
}

✔ 3. Tab1 与 Tab2 页面(包含 Hero)

Tab1 页面

class Tab1Page extends StatelessWidget {
  const Tab1Page({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Tab1")),
      body: Center(
        child: GestureDetector(
          onTap: () {
            Navigator.of(context).push(
              MaterialPageRoute(builder: (_) => const Tab1Detail()),
            );
          },
          child: Hero(
            tag: "hero-tag",
            child: Container(
              width: 100,
              height: 100,
              color: Colors.blue,
            ),
          ),
        ),
      ),
    );
  }
}

class Tab1Detail extends StatelessWidget {
  const Tab1Detail({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Tab1 Detail")),
      body: Center(
        child: Hero(
          tag: "hero-tag",
          child: Container(
            width: 200,
            height: 200,
            color: Colors.blue,
          ),
        ),
      ),
    );
  }
}

Tab2 页面(同样用相同的 hero tag,但不会互相干扰)

class Tab2Page extends StatelessWidget {
  const Tab2Page({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Tab2")),
      body: Center(
        child: GestureDetector(
          onTap: () {
            Navigator.of(context).push(
              MaterialPageRoute(builder: (_) => const Tab2Detail()),
            );
          },
          child: Hero(
            tag: "hero-tag", // 🔥 与 Tab1 相同 tag,但不会冲突
            child: Container(
              width: 100,
              height: 100,
              color: Colors.red,
            ),
          ),
        ),
      ),
    );
  }
}

class Tab2Detail extends StatelessWidget {
  const Tab2Detail({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Tab2 Detail")),
      body: Center(
        child: Hero(
          tag: "hero-tag",
          child: Container(
            width: 200,
            height: 200,
            color: Colors.red,
          ),
        ),
      ),
    );
  }
}

🎉 三、测试效果

  • 点击 Tab1 蓝色方块 → 有 Hero 动画
  • 切到 Tab2 → 点击红色方块 → 有 Hero 动画
  • 两个 Tab 使用相同 tag 但互不影响

没有跨 Tab 混乱!

这就是 HeroControllerScope 的核心能力


Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐