跳转到正文
YGJoon 技术博客

Flutter 组合动画详解

发布于:
·1.5k 字 · 约 4 分钟·作者:
缩放、旋转、位移拼成一套连贯动作:顺序播放用 Interval 排时间轴、并行叠加直接一起跑,复杂动效拆解开来就这么简单
Flutter 组合动画详解文章封面
目录

简介

组合动画是指将多个简单的动画组合在一起,实现更加复杂和丰富的动画效果。本文介绍如何在 Flutter 中实现动画组合。

基本组合

顺序动画

class SequentialAnimationDemo extends StatefulWidget {
  @override
  _SequentialAnimationDemoState createState() => _SequentialAnimationDemoState();
}

class _SequentialAnimationDemoState extends State<SequentialAnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;
  late Animation<double> _rotateAnimation;
  late Animation<Offset> _slideAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 3),
      vsync: this,
    );

    // 缩放动画 (0-1秒)
    _scaleAnimation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.0, 0.33, curve: Curves.easeOut),
    ));

    // 旋转动画 (1-2秒)
    _rotateAnimation = Tween<double>(
      begin: 0.0,
      end: 2 * pi,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.33, 0.66, curve: Curves.easeInOut),
    ));

    // 滑动动画 (2-3秒)
    _slideAnimation = Tween<Offset>(
      begin: Offset.zero,
      end: Offset(1.0, 0.0),
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.66, 1.0, curve: Curves.easeIn),
    ));
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Transform.translate(
          offset: _slideAnimation.value * 100,
          child: Transform.rotate(
            angle: _rotateAnimation.value,
            child: Transform.scale(
              scale: _scaleAnimation.value,
              child: Container(
                width: 100,
                height: 100,
                decoration: BoxDecoration(
                  color: Colors.blue,
                  borderRadius: BorderRadius.circular(10),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

并行动画

class ParallelAnimationDemo extends StatefulWidget {
  @override
  _ParallelAnimationDemoState createState() => _ParallelAnimationDemoState();
}

class _ParallelAnimationDemoState extends State<ParallelAnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _sizeAnimation;
  late Animation<Color?> _colorAnimation;
  late Animation<double> _opacityAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );

    // 所有动画同时进行
    _sizeAnimation = Tween<double>(
      begin: 50.0,
      end: 200.0,
    ).animate(_controller);

    _colorAnimation = ColorTween(
      begin: Colors.blue,
      end: Colors.red,
    ).animate(_controller);

    _opacityAnimation = Tween<double>(
      begin: 0.5,
      end: 1.0,
    ).animate(_controller);
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Opacity(
          opacity: _opacityAnimation.value,
          child: Container(
            width: _sizeAnimation.value,
            height: _sizeAnimation.value,
            decoration: BoxDecoration(
              color: _colorAnimation.value,
              borderRadius: BorderRadius.circular(10),
            ),
          ),
        );
      },
    );
  }
}

复杂组合

交叉动画

class CrossFadeAnimationDemo extends StatefulWidget {
  @override
  _CrossFadeAnimationDemoState createState() => _CrossFadeAnimationDemoState();
}

class _CrossFadeAnimationDemoState extends State<CrossFadeAnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _fadeOutAnimation;
  late Animation<double> _fadeInAnimation;
  late Animation<double> _slideAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );

    _fadeOutAnimation = Tween<double>(
      begin: 1.0,
      end: 0.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.0, 0.5, curve: Curves.easeOut),
    ));

    _fadeInAnimation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.5, 1.0, curve: Curves.easeIn),
    ));

    _slideAnimation = Tween<double>(
      begin: 0.0,
      end: 200.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    ));
  }

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

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        AnimatedBuilder(
          animation: _fadeOutAnimation,
          builder: (context, child) {
            return Opacity(
              opacity: _fadeOutAnimation.value,
              child: Container(
                width: 100,
                height: 100,
                color: Colors.blue,
              ),
            );
          },
        ),
        AnimatedBuilder(
          animation: _fadeInAnimation,
          builder: (context, child) {
            return Transform.translate(
              offset: Offset(_slideAnimation.value, 0),
              child: Opacity(
                opacity: _fadeInAnimation.value,
                child: Container(
                  width: 100,
                  height: 100,
                  color: Colors.red,
                ),
              ),
            );
          },
        ),
      ],
    );
  }
}

动画管理

动画状态管理

class AnimationStateManager {
  final List<AnimationController> controllers;
  
  AnimationStateManager(this.controllers);
  
  void startAll() {
    for (var controller in controllers) {
      controller.forward();
    }
  }
  
  void stopAll() {
    for (var controller in controllers) {
      controller.stop();
    }
  }
  
  void resetAll() {
    for (var controller in controllers) {
      controller.reset();
    }
  }
  
  void dispose() {
    for (var controller in controllers) {
      controller.dispose();
    }
  }
}

动画队列

class AnimationQueue {
  final List<AnimationController> _queue = [];
  int _currentIndex = 0;
  
  void addAnimation(AnimationController controller) {
    _queue.add(controller);
    
    controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _playNext();
      }
    });
  }
  
  void start() {
    if (_queue.isNotEmpty) {
      _queue[0].forward();
    }
  }
  
  void _playNext() {
    _currentIndex++;
    if (_currentIndex < _queue.length) {
      _queue[_currentIndex].forward();
    }
  }
  
  void dispose() {
    for (var controller in _queue) {
      controller.dispose();
    }
  }
}

最佳实践

  1. 一时间轴一 controller:顺序/交叉动画能用一个 controller + Interval 分段就别开多个——多个 controller 要自己写状态机串行,还多 N 个 dispose 风险;只有各段需要独立触发、独立循环(一个常驻转、一个点击才播)时才拆。
  2. 同一属性的多段变化用 TweenSequence:“先放大到 1.2 再回弹到 1.0”用它比两个 Interval 拼 Tween 更直白,拼接点也不容易跳变。
  3. 阶段时长按”视觉重量”分配:入场(淡入+位移)给足 60% 时间轴,装饰性收尾(颜色微调)20% 就够;平均分配会让重点动作显得仓促、次要动作显得拖沓。
  4. 三段以上的组合先画时间轴:纸上或注释里写清每段的 Interval 区间和时长再写代码——靠脑子记顺序返工率极高,Interval 写错一段整个节奏全乱。

注意事项

  1. Interval 的 begin 必须小于 end,写反了动画直接不播且不报错;多段首尾相接时留 0.01~0.02 的重叠,避免拼接帧肉眼可见的”顿一下”。
  2. 状态转换先查 status:completed 状态下调 forward() 无效果、dismissed 下调 reverse() 无效果——弹窗”再播一次”没反应,十有八九是没 reset 就直接 forward。
  3. 别把”并行”写成”串行”:三个动画都直接 animate(_controller) 不带 Interval 就是并行;想错峰必须显式给 Interval——漏写 Interval 是组合动画里最常见的”为什么它们一起动”。
  4. 动画队列(如文中的 AnimationQueue)里 addStatusListener 要配好移除:页面重建后旧 listener 残留会导致 _playNext 被多次触发,动画乱序——要么 dispose 时 removeListener,要么把 listener 的管理收进统一的 dispose。

实战:能用 Interval,就别开多个 controller

组合动画的需求我遇到过:营销弹窗”先弹出来再轻微回缩”,或者页面转场里标题和内容错峰出现。我的选型很固定:同一时间轴上的多段动画,一个 AnimationController + Interval 分段——比开三四个 controller 再写状态机串起来省事得多,也少了漏写 dispose 的风险。

多个 controller 只在一种情况下值得:各段动画需要独立触发、独立重复(比如一个常驻循环转、一个点击才播)。另外 TweenSequence 也值得知道:“先放大到 1.2 再缩回 1.0”这种同一个属性内的多段变化,用它比 Interval 更直白。这篇的顺序/并行示例思路是对的,落地时先问一句:时间轴能不能统一?能,就别拆。

总结

组合动画可以创建出更加复杂和丰富的动画效果。通过合理组织和管理多个动画,可以实现流畅自然的用户界面交互。理解并掌握动画组合的技巧对于开发高质量的 Flutter 应用很有帮助。


分享这篇文章:


相关文章

上一篇
Flutter 动画曲线详解
下一篇
Flutter Hero 动画详解