[Flutter 进阶] 动画实践 - 精通动画的起点,

193 阅读8分钟

动画在我们日常开发中其实使用的不是特别频繁,大部分应用主要还是以功能为主,不会做特别复杂的动画效果,即使复杂的动画效果一般也是通过使用第三方库或者lottie动画。但是,作为一名开发者,不可能完全不了解动画。因此这里就通过一些demo对Flutter基础动画做个了解,以便在需要的时候,我们有能力能够完成一些基础动画的开发。

一、动画核心要素

Flutter 动画系统基于四个核心组件:

  1. Animation:动画的当前值(如 0.0~1.0)
  2. AnimationController:控制动画播放(时长、方向等)
  3. Tween:定义动画的值域范围
  4. CurvedAnimation:控制动画的速度曲线

image.png

作为初学者,这个UML图扫一眼就行了,不用太纠结,知道一下他们的继承关系就行。

二、基础动画实现

通过一个简单的缩放动画来了解一下动画的实现步骤。

缩放动画.gif

GIF的效果显得有点卡顿,但实际上在手机上运行效果还是比较流畅的。

代码示例:

import 'package:flutter/material.dart';
​
void main() => runApp(const AnimatedApp());
​
class AnimatedApp extends StatelessWidget {
  const AnimatedApp({super.key});
​
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Flutter 基础动画')),
        body: const Center(child: ScalingBox()),
      ),
    );
  }
}
​
class ScalingBox extends StatefulWidget {
  const ScalingBox({super.key});
​
  @override
  State<ScalingBox> createState() => _ScalingBoxState();
}
​
class _ScalingBoxState extends State<ScalingBox> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;
​
  @override
  void initState() {
    super.initState();
    
    // 1. 创建动画控制器(1秒周期)
    _controller = AnimationController(
      duration: const Duration(seconds: 1),
      vsync: this,
    )..repeat(reverse: true); // 循环播放
    
    // 2. 配置动画值域和曲线
    _animation = Tween(begin: 100.0, end: 200.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Curves.easeInOut, // 使用缓动曲线
      ),
    );
  }
​
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return Container(
          width: _animation.value,
          height: _animation.value,
          decoration: BoxDecoration(
            color: Colors.blue,
            borderRadius: BorderRadius.circular(12),
        );
      },
    );
  }
​
  @override
  void dispose() {
    _controller.dispose(); // 释放资源
    super.dispose();
  }
}

代码解析

1. 在state里创建一个controller

 late AnimationController _controller;

2. 在initState里初始化Controller

   // 1. 创建动画控制器(1秒周期)
    _controller = AnimationController(
      duration: const Duration(seconds: 1),// 设置动画时间为1秒
      vsync: this,
    );
    
    _controller.repeat(reverse: true); // 设置循环。动画效果为:小->大->小->大这样一直反反复复循环
  •  duration:控制动画时长和播放状态
  • vsync: 防止后台动画消耗资源
  • reverse: true: 这个属性可以让动画反向执行,比如你设置的是放大的动画,则在动画执行完之后会继续缩小回去。

3. 配置动画值域和曲线

_animation = Tween(begin: 100.0, end: 200.0).animate(
  CurvedAnimation(
    parent: _controller,
    curve: Curves.easeInOut, // 使用缓动曲线【开始和结束的时候比较慢,中间会快一点;场景的自然运动效果】
  ),
);
  • 将 0.0 ~ 1.0 的进度映射到 100 ~ 200 的实际值
  • Curves的动画曲线效果:开始和结束的时候比较慢,中间会快一点;场景的自然运动效果。Curves支持的运动曲线可以查看这篇介绍[Flutter 进阶] 动画曲线(Curve)深度解析

4. 提高渲染效率

AnimatedBuilder(
  animation: _animation,
  builder: (context, child) {
    // 仅重建动画部分
  }
)

三、常见动画实现

通过前面的示例,我们可以了解到动画的基本实现方式,接下来我们再看看我们常见的一些动画是怎么实现的。

  1. 淡入淡出动画(Fade)

Fade.gif

关键点:

  1. 通过更改透明度的方式实现显示和隐藏的效果。
  2. 通过设置Curves.easeInOut使淡入淡出变得更加自然一点。

关键代码:

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

    _opacityAnimation = Tween(begin: 0.1, end: 1.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Curves.easeInOut,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: FadeTransition(
          opacity: _opacityAnimation,
          child: Container(
            width: 200,
            height: 200,
            decoration: BoxDecoration(
              color: Colors.deepPurple,
              borderRadius: BorderRadius.circular(20),
            ),
          ),
        ),
      )
    );
  }

2. 旋转动画

refresh.gif

关键点:

  1. 通过Transform.rotate随着时间的变化不断设置更改旋转角度。
  2. 设置变化的值范围为0 ~ 2Π(约等于2 x 3.14159)之间,换个角度理解就是从0 ~ 360度的变化。

*关键代码:

 @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 700),
      vsync: this,
    )..repeat();

    _rotationAnimation = Tween(
      begin: 0.0,
      end: 2 * 3.14159,
    ).animate(_controller);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: AnimatedBuilder(
          animation: _rotationAnimation,
          builder: (context, child) {
            return Transform.rotate(
              angle: _rotationAnimation.value,
              child: const Icon(Icons.refresh, size: 50),
            );
          },
        ),
      ),
    );
  }

3. 平移动画

平移.gif

关键点:

  1. 设置平移的起始位置和结束位置
  2. 设置elasticOut,让小球在开始和结束时保持一点弹性
  3. 通过动画改变SlideTransition的position的值达到小球平移的效果。

关键代码:

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

  _positionAnimation = Tween<Offset>(
    begin: const Offset(-1.0, 0.0),
    end: const Offset(1.0, 0.0),
  ).animate(CurvedAnimation(
    parent: _controller,
    curve: Curves.elasticOut,
  ));
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: SlideTransition(
        position: _positionAnimation,
        child: Container(
          width: 100,
          height: 100,
          decoration: BoxDecoration(
            color: Colors.green,
            borderRadius: BorderRadius.circular(50),
          ),
        ),
      ),
    ),
  );
}

4. 物理动画

物理动画与常规动画的核心区别在于:物理动画基于物理定律模拟真实世界的运动规律,而常规动画则基于预设的数学曲线。如果我们要做一些类似小球下落之后自然弹起的效果,可以通过物理动画做的更真实一点。在游戏开发里使用的应该会比较多,比如碰撞效果等。

掉落.gif

关键点:
  1. 设置模拟物体的三个重要参数
  • 质量mass: 1 // 质量 (kg) - 影响惯性
  • stiffness: 200 // 刚度 (N/m) - 影响弹性
  • damping: 10 // 阻尼 (N·s/m) - 影响阻力
  1. 设置初始值:
final spring = SpringSimulation(
  SpringDescription(
    mass: 1,
    stiffness: 200,
    damping: 10,
  ),
  0, // 起始位置
  300, // 目标位置
  1000, // 初始速度
);

这个初始值主要是用来确定小球的初始速度,有了初始速度之后就可以通过物理方法计算整个下落过程。

核心代码:

final spring = SpringSimulation(
  SpringDescription(
    mass: 1,
    stiffness: 200,
    damping: 10,
  ),
  0, // 起始位置
  300, // 目标位置
  1000, // 初始速度
);

@override
void initState() {
  super.initState();
  _controller = AnimationController.unbounded(vsync: this);

  _springAnimation = _controller.drive(
    Tween(begin: 0.0, end: 1.0),
  );

//  _controller.animateWith(spring);
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Column(
      children: [
        SizedBox(height: 200,),
        ElevatedButton(onPressed: (){
          _controller.animateWith(spring);
        }, child: Text('开始')),
        SizedBox(height: 100,),
        Center(
          child: AnimatedBuilder(
            animation: _springAnimation,
            builder: (context, child) {
              return Transform.translate(
                offset: Offset(0, _controller.value),
                child: Container(
                  width: 80,
                  height: 80,
                  decoration: BoxDecoration(
                    color: Colors.red,
                    borderRadius: BorderRadius.circular(10),
                  ),
                ),
              );
            },
          ),
        ),
      ],
    )
  );
}

5. 组合动画: 这个其实就是针对同一个view设置多个动画,这些动画可能同时执行,按顺序执行,主要取决于设置的时间点是怎么样的。 其实很多看似酷炫的动画都是通过组合动画来实现的,只要把那些复杂的动画拆接下来,也就是一个个单独的简单动画。

交错动画.gif

关键解析:

  1. 组合动画时间片:
    • 0 ~ 0.5 执行淡入淡出动画
    • 0.3 ~ 1.0 执行缩放动画
    • 0.5 ~ 1.0 执行颜色变化动画

也就是在0.3 ~ 0.5这段时间同时执行了淡入淡出和缩放动画;
在0.5 ~ 1.0之间,同时执行了缩放和颜色变化动画。

@override
void initState() {
  super.initState();

  _controller = AnimationController(
    duration: const Duration(seconds: 2),
    vsync: this,
  );

  // 定义组合时间区间
  _opacity = Tween(begin: 0.0, end: 1.0).animate(
    CurvedAnimation(
      parent: _controller,
      curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
    ),
  );

  _width = Tween(begin: 50.0, end: 200.0).animate(
    CurvedAnimation(
      parent: _controller,
      curve: const Interval(0.3, 1.0, curve: Curves.easeOut),
    ),
  );

  _color = ColorTween(begin: Colors.blue, end: Colors.purple).animate(
    CurvedAnimation(
      parent: _controller,
      curve: const Interval(0.5, 1.0),
    ),
  );

  _controller.repeat(reverse: true);
}

@override
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: _controller,
    builder: (context, child) {
      return Container(
        height: 100,
        width: _width.value,
        decoration: BoxDecoration(
          color: _color.value,
          borderRadius: BorderRadius.circular(8),
        ),
        child: Opacity(
          opacity: _opacity.value,
          child: const Center(child: Text('交错动画', style: TextStyle(color: Colors.white))),
        ),
      );
    },
  );
}

四、动画的生命周期

其实基础动画的实现还是非常简单的,而且我们还通过repeat方法可以让动画重复执行,通过reverse参数可以让动画反向执行。那整体这个循环的生命周期大概是怎么样的呢?看图

image.png

解析:

  1. 大黑点是开始,空心小圆点带空心圆的是结束

  2. dismissed (未启动状态)

    • 动画的初始状态
    • 动画值为初始值(通常是0.0)
    • 动画未开始播放
  3. forward (正向播放状态)

    • 动画从开始值向结束值播放
    • 通过调用forward()方法进入此状态
    • 动画值从0.0 → 1.0变化
  4. reverse (反向播放状态)

    • 动画从结束值向开始值播放
    • 通过调用reverse()方法进入此状态
    • 动画值从1.0 → 0.0变化
  5. completed (完成状态)

    • 动画播放完成的状态
    • 正向播放完成后值为1.0
    • 可在此状态重新开始动画

五、动画开发心得

  1. 使用 const 修饰静态组件,避免组件的频繁刷新
  2. 避免在动画builder中构建复杂子树,降低动画执行时的性能消耗
  3. 对位移动画使用 Transform 代替修改布局属性
  4. 使用 Opacity 组件实现透明度渐变的动画

总结

Flutter 的动画系统通过分层架构实现了灵活的组合方式。核心要点:

  • 使用 AnimationController 管理动画生命周期
  • 通过 Tween 实现值域映射
  • 使用 Curves 控制动画节奏
  • 通过 AnimatedBuilder 优化重建范围

这些示例覆盖了Flutter动画开发中的常见需求,可以根据实际场景选择合适的动画实现方式。Flutter的动画系统既强大又灵活,掌握这些基础模式后,可以创造出丰富多样的动效体验。