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

Hero组件转场动画与自定义抖动动画冲突问题求助

Hero转场与竖直抖动动画冲突的解决方案

问题核心

Hero转场动画与自定义竖直抖动动画存在帧时序冲突,导致编号100的卡片转场时抖动效果异常,而200、300卡片表现正常。已尝试WidgetsBinding.instance.addPostFrameCallback和SchedulerBinding.instance.addPersistentFrameCallback,未解决问题。

解决方案

方案1:延迟抖动动画至Hero转场完成后启动

Hero默认转场时长约300ms,将抖动动画的启动时机延迟至Hero转场结束,避免两者动画帧相互干扰。

修改ShakeTransitionWidget的initState方法:

@override
void initState() {
  super.initState();
  _controller = AnimationController(
    vsync: this,
    duration: widget.duration,
  );
  _animation = Tween(
    begin: 1.0,
    end: 0.0,
  ).chain(CurveTween(curve: Curves.elasticOut)).animate(_controller);
  
  // 延迟300ms启动,匹配Hero默认转场时长
  Future.delayed(const Duration(milliseconds: 300), () {
    if (mounted) _controller.forward();
  });
}

方案2:监听路由动画状态精准触发抖动

通过监听页面路由的动画状态,在路由完全进入后再启动抖动动画,适配不同路由过渡时长。

步骤1:修改PlayGroundDetail的状态逻辑

class _PlayGroundDetailState extends State<PlayGroundDetail>
    with TickerProviderStateMixin {
  late final PageController _pageController;
  late AnimationController _textShakeController;
  late AnimationController _iconShakeController;

  @override
  void initState() {
    super.initState();
    _pageController = PageController();
    
    // 初始化抖动控制器
    _textShakeController = AnimationController(
      vsync: this,
      duration: _shakeDuration,
    );
    _iconShakeController = AnimationController(
      vsync: this,
      duration: _shakeDuration,
    );

    // 监听路由动画完成状态
    final route = ModalRoute.of(context);
    route?.animation?.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _textShakeController.forward();
        _iconShakeController.forward();
      }
    });
  }

  @override
  void dispose() {
    _pageController.dispose();
    _textShakeController.dispose();
    _iconShakeController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          CustomScrollView(
            slivers: [
              const SliverToBoxAdapter(child: SizedBox(height: 50)),
              SliverToBoxAdapter(
                child: SizedBox(
                  height: 350,
                  child: Stack(
                    children: [
                      Positioned.fill(
                        child: Hero(
                          tag: widget.dummy.colors.value,
                          child: Material(color: widget.dummy.colors),
                        ),
                      ),
                      Align(
                        alignment: Alignment.topCenter,
                        child: Hero(
                          tag: widget.dummy.backText,
                          child: Material(
                            color: Colors.transparent,
                            child: ShakeTransitionWidget(
                              axis: Axis.vertical,
                              duration: _shakeDuration,
                              offset: 30,
                              controller: _textShakeController,
                              child: Text(
                                widget.dummy.backText,
                                textAlign: TextAlign.center,
                                style: const TextStyle(
                                  fontWeight: FontWeight.bold,
                                  color: Colors.black,
                                  fontSize: 140.0,
                                ),
                              ),
                            ),
                          ),
                        ),
                      ),
                      Center(
                        child: Hero(
                          tag: widget.dummy.icons,
                          child: ShakeTransitionWidget(
                            axis: Axis.vertical,
                            offset: 5,
                            duration: _shakeDuration,
                            controller: _iconShakeController,
                            child: Icon(widget.dummy.icons, size: 60.0),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

步骤2:更新ShakeTransitionWidget支持外部控制器

class ShakeTransitionWidget extends StatefulWidget {
  final Widget child;
  final Duration duration;
  final double offset;
  final Axis axis;
  final AnimationController? controller;

  const ShakeTransitionWidget({
    super.key,
    required this.child,
    required this.duration,
    required this.offset,
    required this.axis,
    this.controller,
  });

  @override
  State<ShakeTransitionWidget> createState() => _ShakeTransitionWidgetState();
}

class _ShakeTransitionWidgetState extends State<ShakeTransitionWidget>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation _animation;

  @override
  void initState() {
    super.initState();
    // 使用外部控制器或创建内部控制器
    _controller = widget.controller ?? AnimationController(
      vsync: this,
      duration: widget.duration,
    );
    _animation = Tween(
      begin: 1.0,
      end: 0.0,
    ).chain(CurveTween(curve: Curves.elasticOut)).animate(_controller);
    
    // 无外部控制器时自动延迟启动
    if (widget.controller == null) {
      Future.delayed(const Duration(milliseconds: 300), () {
        if (mounted) _controller.forward();
      });
    }
  }

  @override
  void dispose() {
    // 仅释放内部创建的控制器
    if (widget.controller == null) _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (ctx, _) => Transform.translate(
        offset: widget.axis == Axis.horizontal
            ? Offset(_animation.value * widget.offset, 0.0)
            : Offset(0.0, _animation.value * widget.offset),
        child: widget.child,
      ),
    );
  }
}

方案3:将抖动动画移至Hero组件外部

把抖动动画组件从Hero子节点移到父级,让Hero完成转场后再对整个区域应用抖动,避免层级冲突。

修改PlayGroundDetail中的布局:

SliverToBoxAdapter(
  child: SizedBox(
    height: 350,
    child: ShakeTransitionWidget(
      axis: Axis.vertical,
      duration: _shakeDuration,
      offset: 30,
      child: Stack(
        children: [
          Positioned.fill(
            child: Hero(
              tag: widget.dummy.colors.value,
              child: Material(color: widget.dummy.colors),
            ),
          ),
          Align(
            alignment: Alignment.topCenter,
            child: Hero(
              tag: widget.dummy.backText,
              child: Material(
                color: Colors.transparent,
                child: Text(
                  widget.dummy.backText,
                  textAlign: TextAlign.center,
                  style: const TextStyle(
                    fontWeight: FontWeight.bold,
                    color: Colors.black,
                    fontSize: 140.0,
                  ),
                ),
              ),
            ),
          ),
          Center(
            child: Hero(
              tag: widget.dummy.icons,
              child: Icon(widget.dummy.icons, size: 60.0),
            ),
          ),
        ],
      ),
    ),
  ),
),

问题原因

Hero转场动画是框架级的跨页面组件复用过渡,帧更新优先级高。抖动动画在Hero内部过早启动时,会干扰Hero的位置计算逻辑,而列表第一个卡片因渲染顺序问题,冲突表现更明显。通过延迟启动或层级解耦,可避免两者的帧时序冲突。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 04:45:29