You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Flutter:如何同时实现Hero转场与相邻组件自定义动画?

解决Flutter路由切换时同步Hero动画与相邻组件自定义动画的问题

嘿,我来帮你搞定这个场景!首先得明确核心问题:你的自定义标签导航和WidgetsApp是兄弟节点,它不在路由栈的页面层级里,默认情况下路由切换的通知不会自动传递给它,所以Hero动画跑起来的时候,标签导航的自定义动画没法同步触发。下面是一步步的解决方案:

核心思路

通过监听路由切换状态,把路由变化的信号传递给标签导航组件,让它和Hero动画同步执行。我们会用到NavigatorObserver监听路由事件,再通过你的自定义InheritedWidget把状态同步给所有需要的组件。

具体实现步骤

1. 扩展你的自定义InheritedWidget,加入路由状态

首先在你的全局状态管理InheritedWidget里添加路由切换相关的状态和回调,这样标签导航能感知到路由变化:

class AppGlobalState extends InheritedWidget {
  final bool isRouteTransitioning; // 标记是否正在路由切换
  final VoidCallback? onTransitionStart;
  final VoidCallback? onTransitionEnd;

  const AppGlobalState({
    super.key,
    required this.isRouteTransitioning,
    this.onTransitionStart,
    this.onTransitionEnd,
    required super.child,
  });

  static AppGlobalState of(BuildContext context) {
    final result = context.dependOnInheritedWidgetOfExactType<AppGlobalState>();
    assert(result != null, "找不到AppGlobalState,请确保组件在其范围内");
    return result!;
  }

  @override
  bool updateShouldNotify(AppGlobalState oldWidget) {
    return isRouteTransitioning != oldWidget.isRouteTransitioning;
  }
}

2. 添加自定义路由观察者,监听路由切换

创建一个NavigatorObserver的子类,用来监听路由的push/pop事件,以及动画的开始和结束:

class RouteTransitionObserver extends NavigatorObserver {
  final VoidCallback? onPushStart;
  final VoidCallback? onPushEnd;
  final VoidCallback? onPopStart;
  final VoidCallback? onPopEnd;

  RouteTransitionObserver({
    this.onPushStart,
    this.onPushEnd,
    this.onPopStart,
    this.onPopEnd,
  });

  @override
  void didPush(Route route, Route? previousRoute) {
    super.didPush(route, previousRoute);
    onPushStart?.call();
    // 监听路由动画完成事件
    if (route is PageRoute) {
      route.animation?.addStatusListener((status) {
        if (status == AnimationStatus.completed) onPushEnd?.call();
      });
    }
  }

  @override
  void didPop(Route route, Route? previousRoute) {
    super.didPop(route, previousRoute);
    onPopStart?.call();
    if (route is PageRoute) {
      route.animation?.addStatusListener((status) {
        if (status == AnimationStatus.dismissed) onPopEnd?.call();
      });
    }
  }
}

3. 在根StatefulWidget里绑定状态与观察者

在你的根组件中,管理路由切换状态,并把观察者添加到WidgetsAppnavigatorObservers里:

class AppRoot extends StatefulWidget {
  const AppRoot({super.key});

  @override
  State<AppRoot> createState() => _AppRootState();
}

class _AppRootState extends State<AppRoot> {
  bool _isTransitioning = false;
  late final RouteTransitionObserver _routeObserver;

  @override
  void initState() {
    super.initState();
    _routeObserver = RouteTransitionObserver(
      onPushStart: () => _updateTransitionState(true),
      onPushEnd: () => _updateTransitionState(false),
      onPopStart: () => _updateTransitionState(true),
      onPopEnd: () => _updateTransitionState(false),
    );
  }

  void _updateTransitionState(bool isTransitioning) {
    if (mounted) {
      setState(() => _isTransitioning = isTransitioning);
    }
  }

  @override
  Widget build(BuildContext context) {
    return AppGlobalState(
      isRouteTransitioning: _isTransitioning,
      child: Stack(
        children: [
          WidgetsApp(
            navigatorObservers: [_routeObserver], // 添加自定义观察者
            home: const HomePage(),
            // 其他WidgetsApp配置(比如color、theme等)
          ),
          const CustomTabNavigation(), // 你的自定义标签导航
        ],
      ),
    );
  }
}

4. 给标签导航添加同步动画

在你的自定义标签导航组件里,通过AppGlobalState的状态触发动画,注意动画时长要和Hero默认的300ms保持一致:

class CustomTabNavigation extends StatefulWidget {
  const CustomTabNavigation({super.key});

  @override
  State<CustomTabNavigation> createState() => _CustomTabNavigationState();
}

class _CustomTabNavigationState extends State<CustomTabNavigation> with SingleTickerProviderStateMixin {
  late AnimationController _animController;
  late Animation<double> _scaleAnim;

  @override
  void initState() {
    super.initState();
    _animController = AnimationController(
      duration: const Duration(milliseconds: 300), // 和Hero动画时长同步
      vsync: this,
    );
    _scaleAnim = Tween<double>(begin: 1.0, end: 0.9).animate(_animController);
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // 监听路由切换状态,触发动画
    final globalState = AppGlobalState.of(context);
    if (globalState.isRouteTransitioning) {
      _animController.forward();
    } else {
      _animController.reverse();
    }
  }

  @override
  void dispose() {
    _animController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Positioned(
      bottom: 20,
      left: 20,
      right: 20,
      child: ScaleTransition(
        scale: _scaleAnim,
        child: Container(
          height: 60,
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(30),
          ),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              IconButton(icon: const Icon(Icons.home), onPressed: () {}),
              IconButton(icon: const Icon(Icons.search), onPressed: () {}),
              IconButton(icon: const Icon(Icons.person), onPressed: () {}),
            ],
          ),
        ),
      ),
    );
  }
}

5. 确保Hero动画正常配置

最后,保证你的页面里Hero标签正确配对,比如首页和详情页的Hero使用同一个tag:

// 首页的Hero组件
Hero(
  tag: 'feature-card',
  child: GestureDetector(
    onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const DetailPage())),
    child: Container(width: 150, height: 150, color: Colors.orange),
  ),
)

// 详情页的Hero组件
Hero(
  tag: 'feature-card',
  child: Container(width: 300, height: 300, color: Colors.orange),
)

关键注意点

  • 动画时长一定要和Hero默认的300ms对齐,这样两个动画才能完全同步;
  • 如果需要更精细的同步(比如Hero动画的进度回调),可以通过HeroController获取Hero的动画对象,但上面的方案已经能覆盖大部分场景;
  • 你的自定义InheritedWidget如果已经有状态管理逻辑,只需要把路由相关的状态和回调加进去即可,不用重构整个结构。

内容的提问来源于stack exchange,提问作者JacobHK

火山引擎 最新活动