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
相关产品推荐
相关产品推荐

