跳转到正文
YGJoon 技术博客

Flutter 基础动画详解

发布于:
·1.4k 字 · 约 3 分钟·作者:
隐式动画与显式动画的分水岭:AnimatedContainer 改属性就自动过渡,AnimationController 给你逐帧控制权
Flutter 基础动画详解文章封面
目录

简介

Flutter 提供了强大的动画系统,包括隐式动画和显式动画。通过动画可以让应用界面更加生动,提升用户体验。

隐式动画

AnimatedContainer

class AnimatedContainerDemo extends StatefulWidget {
  @override
  _AnimatedContainerDemoState createState() => _AnimatedContainerDemoState();
}

class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> {
  bool _expanded = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          _expanded = !_expanded;
        });
      },
      child: AnimatedContainer(
        duration: Duration(milliseconds: 300),
        width: _expanded ? 200.0 : 100.0,
        height: _expanded ? 200.0 : 100.0,
        color: _expanded ? Colors.blue : Colors.red,
        curve: Curves.fastOutSlowIn,
      ),
    );
  }
}

其他隐式动画组件

// 透明度动画
AnimatedOpacity(
  opacity: _visible ? 1.0 : 0.0,
  duration: Duration(milliseconds: 500),
  child: Container(
    width: 200,
    height: 200,
    color: Colors.blue,
  ),
)

// 位置动画
AnimatedPositioned(
  duration: Duration(milliseconds: 500),
  left: _left,
  top: _top,
  child: Container(
    width: 50,
    height: 50,
    color: Colors.red,
  ),
)

// 大小动画
AnimatedSize(
  duration: Duration(milliseconds: 300),
  child: Container(
    width: _size,
    height: _size,
    color: Colors.green,
  ),
)

显式动画

基本用法

class AnimationDemo extends StatefulWidget {
  @override
  _AnimationDemoState createState() => _AnimationDemoState();
}

class _AnimationDemoState extends State<AnimationDemo> 
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );
    
    _animation = Tween<double>(
      begin: 0,
      end: 1,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    ));
    
    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return Transform.scale(
          scale: _animation.value,
          child: Container(
            width: 200,
            height: 200,
            color: Colors.blue,
          ),
        );
      },
    );
  }
}

动画控制器

// 启动动画
_controller.forward();

// 反向运行
_controller.reverse();

// 重置动画
_controller.reset();

// 重复动画
_controller.repeat();

// 停止动画
_controller.stop();

完整示例

class AnimatedCard extends StatefulWidget {
  @override
  _AnimatedCardState createState() => _AnimatedCardState();
}

class _AnimatedCardState extends State<AnimatedCard> 
    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(milliseconds: 800),
      vsync: this,
    );
    
    _scaleAnimation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.0, 0.5, curve: Curves.easeOut),
    ));
    
    _rotateAnimation = Tween<double>(
      begin: 0.0,
      end: 2 * pi,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.5, 1.0, curve: Curves.easeOut),
    ));
    
    _slideAnimation = Tween<Offset>(
      begin: Offset(-1.0, 0.0),
      end: Offset.zero,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeOut,
    ));
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Animated Card')),
      body: Center(
        child: AnimatedBuilder(
          animation: _controller,
          builder: (context, child) {
            return Transform.scale(
              scale: _scaleAnimation.value,
              child: Transform.rotate(
                angle: _rotateAnimation.value,
                child: SlideTransition(
                  position: _slideAnimation,
                  child: Container(
                    width: 200,
                    height: 300,
                    decoration: BoxDecoration(
                      color: Colors.blue,
                      borderRadius: BorderRadius.circular(16),
                      boxShadow: [
                        BoxShadow(
                          color: Colors.black26,
                          offset: Offset(0, 4),
                          blurRadius: 10,
                        ),
                      ],
                    ),
                    child: Center(
                      child: Text(
                        'Animated Card',
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: 24,
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          if (_controller.status == AnimationStatus.completed) {
            _controller.reverse();
          } else {
            _controller.forward();
          }
        },
        child: Icon(Icons.play_arrow),
      ),
    );
  }
}

自定义动画

Tween

// 自定义 Tween
class ColorTween extends Tween<Color?> {
  ColorTween({Color? begin, Color? end}) : super(begin: begin, end: end);

  @override
  Color? lerp(double t) {
    return Color.lerp(begin, end, t);
  }
}

// 使用自定义 Tween
final colorAnimation = ColorTween(
  begin: Colors.blue,
  end: Colors.red,
).animate(_controller);

AnimatedWidget

class AnimatedLogo extends AnimatedWidget {
  AnimatedLogo({Key? key, required Animation<double> animation})
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Transform.rotate(
      angle: animation.value,
      child: FlutterLogo(size: 100),
    );
  }
}

最佳实践

  1. 选型顺序固定:200~400ms 的一次性小交互(显隐、展开收起、缩放反馈)优先隐式动画(AnimatedContainer/AnimatedOpacity/AnimatedScale),setState 改值就行;多阶段编排(先放大、再旋转、最后滑走)才上 AnimationController + Interval——隐式覆盖约 80% 场景,别为简单的显隐付显式动画的管理成本。
  2. 时长经验值:按钮按反馈 100200ms,页面元素入场 200400ms,页面转场 300~500ms;超过 1s 的常驻动画用户会觉得拖沓(刻意的加载/氛围动画除外)。
  3. 显式动画优先 AnimatedBuilder 只重建动画子树,别在 controller 的 listener 里调整页 setState——复杂页面每帧全量重建,直接掉帧。
  4. AnimationController 建完就写 dispose:State 的 dispose 里必须 dispose controller;常驻 repeat 的动画在页面不可见时 stop,否则后台每帧空转烧 CPU——这是最常见的 Ticker 泄漏。

注意事项

  1. 别在 listener 里做重型计算:60fps 下 listener 每 16.6ms 触发一次,里面做 JSON 解析、排序、大 list 遍历必然掉帧;listener 里只做赋值,计算放外面或提前缓存。
  2. controller.forward() 在已 completed 的动画上调用会直接跳过不播——“再播一次”没反应时,先 reset() 再 forward;反向同理,检查 dismissed 状态。
  3. 隐式动画不耐高频改值:快速连续 setState 改 AnimatedContainer 的属性,动画会不断打断重起、看起来发抖;跟手拖拽这种高频场景用显式动画直接设 controller.value。
  4. 一个 State 里要跑多个 controller 时,用 TickerProviderStateMixin 而不是 SingleTickerProviderStateMixin——Single 下创建第二个 controller 直接抛异常。

实战:隐式动画先行,是我的默认策略

做电商 App 时,商品卡片的收藏按钮点一下缩放、筛选面板的展开收起、提示条的显隐——这类 200~300ms 的小交互,我一律先用隐式动画(AnimatedContainer、AnimatedOpacity、AnimatedScale)解决:setState 改个值就行,不用管 controller 的创建和 dispose,心智负担小得多。

只有当一个动画需要精确编排(比如”先放大、再旋转、最后滑走”这种多阶段),我才上 AnimationController + Interval。取舍逻辑很简单:隐式动画解决 80% 的场景,剩下的 20% 才值得为显式动画付管理成本。反例就是别把 AnimationController 用在简单的显隐上——代码量翻倍,还容易漏写 dispose,白白埋个内存泄漏的坑。

参考:Flutter 官方文档《Animations》中 implicit vs explicit 的划分 (docs.flutter.dev/ui/animations)

总结

Flutter 的动画系统提供了丰富的功能,通过合理使用可以创建出流畅、生动的用户界面。理解并掌握动画的使用对于开发高质量的 Flutter 应用至关重要。


分享这篇文章:


相关文章

上一篇
Flutter路由管理进阶
下一篇
Flutter 基础导航详解