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

Flutter同一属性实现交错动画问题:自定义缩放动效需求

我明白你想要实现的是按下按钮时按自定义曲线缩小、抬起时按自定义曲线放大的交互,而且不想用reverse()方法,希望每个补间都能独立配置起止值和曲线。你之前用Interval拆分动画的方式确实会带来监听冲突的问题,还得维护额外的状态变量,这里有个更简洁的原生解决方案,完全不需要临时的_animationFuture判断:

核心思路

不再用同一个AnimationController的Interval拆分两个动画,而是每次触发交互时(按下/抬起),先停止当前运行的动画避免冲突,再创建对应需求的独立动画(自定义起止值+曲线),直接绑定到控制器上更新缩放比例。同时新增点击取消的处理逻辑,提升交互完整性。

修改后的完整代码

class AnimatedIconButton extends StatefulWidget {
  final Widget child;
  final Function onPress;
  
  const AnimatedIconButton({
    super.key,
    required this.child,
    this.onPress,
  }) : assert(child != null);

  @override
  _AnimatedIconButtonState createState() => _AnimatedIconButtonState();
}

class _AnimatedIconButtonState extends State<AnimatedIconButton> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  double _scale = 1.0;

  Function get onPress => widget.onPress ?? () {};

  void _tapDown(TapDownDetails details) {
    // 先停止当前动画,防止新旧动画冲突
    _animationController.stop();
    
    // 创建缩小动画:从当前缩放值到0.9,使用easeOut曲线
    final shrinkAnimation = Tween<double>(
      begin: _scale,
      end: 0.9,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeOut,
    ));
    
    shrinkAnimation.addListener(() {
      setState(() {
        _scale = shrinkAnimation.value;
      });
    });
    
    // 启动动画,从0开始对应Tween的begin到end的完整过渡
    _animationController.forward(from: 0);
  }

  void _tapUp(TapUpDetails details) {
    _animationController.stop();
    
    // 创建放大动画:从当前缩放值到1.0,使用elasticOut曲线
    final expandAnimation = Tween<double>(
      begin: _scale,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.elasticOut,
    ));
    
    expandAnimation.addListener(() {
      setState(() {
        _scale = expandAnimation.value;
      });
    });
    
    _animationController.forward(from: 0);
    // 触发点击回调
    onPress();
  }

  void _onTapCancel() {
    // 处理用户按下后滑动离开按钮的情况,恢复原始缩放比例
    _animationController.stop();
    final resetAnimation = Tween<double>(
      begin: _scale,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeOut,
    ));
    resetAnimation.addListener(() {
      setState(() {
        _scale = resetAnimation.value;
      });
    });
    _animationController.forward(from: 0);
  }

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300), // 可根据需求调整动画时长
      animationBehavior: AnimationBehavior.preserve,
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: _tapDown,
      onTapUp: _tapUp,
      onTapCancel: _onTapCancel,
      // 用AnimatedBuilder优化性能,只有Transform.scale会随动画重建
      child: AnimatedBuilder(
        animation: _animationController,
        builder: (context, child) {
          return Transform.scale(
            scale: _scale,
            transformHitTests: false,
            child: child,
          );
        },
        child: widget.child,
      ),
    );
  }
}

方案优势

  1. 无额外状态维护:不需要_animationFuture这类临时变量,每次触发动画前先停止旧动画,从根源避免了多动画监听冲突的问题。
  2. 完全自定义动画:每个交互对应的动画都可以独立设置起止值、曲线和时长,完全满足你的需求。
  3. 更完整的交互逻辑:新增onTapCancel处理,当用户按下按钮后滑动离开时,按钮会自动恢复原始大小,不会停留在缩小状态。
  4. 性能优化:通过AnimatedBuilder包裹缩放组件,只有Transform.scale会随动画重建,避免了整个GestureDetector的不必要重建。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 21:27:28