You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Flutter中实现指定效果的页面跳转过渡动画?

问题

我尝试使用Hero widget和页面路由实现类似这张gif的页面切换效果。
我也尝试过自定义页面路由,使用Stack widget包裹两个页面,并在buildTransitions中实现动画,但缩放对齐参数计算难度较高,且Hero widget不会从上一个页面中移除,实现代码如下:

class CustomPageRoute extends MaterialPageRoute {
  final Widget parentWidget;
  final Alignment parentAlignment;
  final Tween<double> childScaleTween;
  final double childWidth;

  CustomPageRoute(
      {required this.parentWidget,
      required this.parentAlignment,
      required this.childScaleTween,
      required this.childWidth,
      required WidgetBuilder builder,
      RouteSettings? settings})
      : super(builder: builder, settings: settings);

  @override
  Widget buildPage(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation) {
    return builder(context);
  }

  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    Curve animatedCurves = Curves.linear;
    var scale = MediaQuery.of(context).size.width / childWidth;
    var scaleAnim = Tween(begin: 1.0, end: scale)
        .animate(CurvedAnimation(parent: animation, curve: animatedCurves));
    var fadeAnim = Tween(begin: 1.0, end: 0.0)
        .animate(CurvedAnimation(parent: animation, curve: animatedCurves));
    var childScaleAnim = childScaleTween
        .animate(CurvedAnimation(parent: animation, curve: animatedCurves));
    var childFadeAnim = Tween(begin: 0.0, end: 1.0)
        .animate(CurvedAnimation(parent: animation, curve: animatedCurves));

    return Stack(children: [
      Container(
        color: Colors.white,
      ),
      ScaleTransition(
          scale: scaleAnim,
          alignment: parentAlignment,
          child: FadeTransition(opacity: fadeAnim, child: parentWidget)),
      ScaleTransition(
          scale: childScaleAnim,
          alignment: parentAlignment,
          child: FadeTransition(opacity: childFadeAnim, child: child)),
    ]);
  }
}

请问还有其他可参考的实现思路吗?

解答

思路1:系统原生Hero配合PageRouteBuilder实现

你遇到的Hero不消失的问题,本质是自定义路由时没有把Hero的过渡逻辑纳入路由动画的生命周期。系统Hero本身已经处理了共享元素的挂载/卸载逻辑,不需要手动维护两个页面的Stack嵌套。只需要给两个页面的共享元素设置同一个tag,再用PageRouteBuilder自定义页面整体的过渡动画,Hero会自动完成共享元素的位移、缩放过渡,不需要手动计算对齐参数。
参考路由配置:

Navigator.of(context).push(
  PageRouteBuilder(
    transitionDuration: const Duration(milliseconds: 300),
    pageBuilder: (context, animation, secondaryAnimation) => const TargetPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      // 仅需处理页面背景、非共享元素的位移/透明度动画,Hero动画由系统自动处理
      var fade = Tween<double>(begin: 0, end: 1).animate(animation);
      return FadeTransition(opacity: fade, child: child);
    },
  ),
);

思路2:使用官方animations库的OpenContainer组件

这是官方维护的动画组件,专门封装了容器展开、收起的过渡效果,内部已经处理了共享元素的对齐、缩放、页面生命周期逻辑,不需要手动计算参数。只需要把触发跳转的组件作为closedBuilder的返回值,目标页面作为openBuilder的返回值即可,直接实现你需要的过渡效果。
基础使用示例:

OpenContainer(
  transitionDuration: const Duration(milliseconds: 300),
  closedBuilder: (context, openContainer) => GestureDetector(
    onTap: openContainer,
    child: // 触发跳转的列表卡片组件
  ),
  openBuilder: (context, closeContainer) => // 目标详情页面
)

思路3:自定义路由前先获取共享元素的位置参数

如果坚持要完全自定义路由动画,可以在点击跳转前,通过RenderBox拿到点击元素在屏幕中的全局坐标和尺寸,将这两个参数传给自定义路由,代替手动传入的parentAlignment和childWidth,避免手动对齐计算出错。
参考获取坐标代码:

// 点击元素时获取位置与尺寸
final renderBox = context.findRenderObject() as RenderBox;
final offset = renderBox.localToGlobal(Offset.zero);
final size = renderBox.size;
// 把offset和size传给自定义路由,后续换算对齐参数直接使用这两个值即可

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.05 23:24:04