首页 关于 文章 作品集 工具 联系
返回文章列表

Flutter 动画实战指南:从隐式动画到 Lottie/Rive 高级特效

动画是让 APP "活"起来的灵魂。Flutter 提供了从简单到复杂的完整动画体系:隐式动画(Implicit)一行代码出效果,显式动画(Explicit)精细控制每一帧,再加上第三方库 Lottie / Rive 的加持,几乎可以实现任何你能想到的动效。这篇文章覆盖 Flutter 动画的方方面面。

一、动画体系全景图

Flutter 的动画体系可以分为以下几个层次:

  • 隐式动画(Implicit Animation):AnimatedContainer、AnimatedOpacity 等,值变自动触发动画
  • 显式动画(Explicit Animation):AnimationController + Tween 手动驱动
  • Hero 动画:页面间元素共享转场
  • 物理动画:SpringSimulation 模拟真实物理效果
  • 第三方动效:Lottie(JSON 动画)、Rive(状态机动画)

二、隐式动画(Implicit Animation)

最简单也最常用的方式——你只需要改变属性值,Flutter 自动在旧值和新值之间做过渡动画。

2.1 AnimatedContainer — 万能过渡容器

// 基础用法:点击切换颜色、大小、圆角
class ImplicitAnimDemo extends StatefulWidget {
  @override
  _ImplicitAnimDemoState createState() => _ImplicitAnimDemoState();
}

class _ImplicitAnimDemoState extends State<ImplicitAnimDemo> {
  bool _isExpanded = false;
  Color _color = Colors.blue;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          _isExpanded = !_isExpanded;
          _color = _isExpanded ? Colors.purple : Colors.blue;
        });
      },
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 600),
        curve: Curves.elasticOut,
        width: _isExpanded ? 300 : 150,
        height: _isExpanded ? 200 : 100,
        decoration: BoxDecoration(
          color: _color,
          borderRadius: BorderRadius.circular(_isExpanded ? 30 : 12),
        ),
        child: const Center(
          child: Text('点我变化', style: TextStyle(color: Colors.white, fontSize: 18)),
        ),
      ),
    );
  }
}

2.2 隐式动画组件全家桶

Flutter 内置了丰富的隐式动画组件:

// AnimatedOpacity — 渐显渐隐
AnimatedOpacity(
  opacity: _visible ? 1.0 : 0.0,
  duration: Duration(milliseconds: 400),
  child: Container(width: 100, height: 100, color: Colors.blue),
)

// AnimatedPadding — 内边距动画
AnimatedPadding(
  padding: EdgeInsets.all(_selected ? 24 : 8),
  duration: Duration(milliseconds: 300),
  child: Card(child: Text('可点击卡片')),
)

// AnimatedAlign — 对齐位置动画
AnimatedAlign(
  alignment: _top ? Alignment.topCenter : Alignment.bottomCenter,
  duration: Duration(milliseconds: 500),
  curve: Curves.easeInOutCubic,
  child: Icon(Icons.arrow_upward, size: 40),
)

// AnimatedDefaultTextStyle — 文字样式动画
AnimatedDefaultTextStyle(
  style: TextStyle(fontSize: _large ? 32 : 18, color: _large ? Colors.red : Colors.grey),
  duration: Duration(milliseconds: 300),
  child: Text('动态文字'),
)

// AnimatedCrossFade — 两 Widget 交叉淡入淡出
AnimatedCrossfade(
  firstChild: Icon(Icons.pause, size: 60),
  secondChild: Icon(Icons.play_arrow, size: 60),
  crossFadeState: _playing ? CrossFadeState.showSecond : CrossFadeState.showFirst,
  duration: Duration(milliseconds: 200),
)

// AnimatedSwitcher — 子 Widget 切换时的过渡效果
AnimatedSwitcher(
  duration: Duration(milliseconds: 500),
  transitionBuilder: (child, animation) {
    return FadeTransition(opacity: animation, child: child);
  },
  child: Text('第 $_count 次', key: ValueKey($_count)),
)

2.3 TweenAnimationBuilder — 自定义属性的隐式动画

当内置组件不够用时,用 TweenAnimationBuilder 可以对任意属性做动画:

TweenAnimationBuilder<double>(
  tween: Tween(begin: 0.0, end: 1.0),
  duration: Duration(seconds: 2),
  builder: (context, value, child) {
    return Transform.rotate(
      angle: value * 6.28,  // 一圈 = 2π ≈ 6.28
      child: Container(
        width: 80,
        height: 80,
        color: Color.lerp(Colors.blue, Colors.red, value)!,
        child: const Icon(Icons.star, color: Colors.white),
      ),
    );
  },
)

三、显式动画(Explicit Animation)

当你需要精确控制动画的每一帧时,使用 AnimationController + Tween 组合。这是最强大的动画方式。

3.1 AnimationController 基础

class ExplicitAnimDemo extends StatefulWidget {
  @override
  _ExplicitAnimDemoState createState() => _ExplicitAnimDemoState();
}

class _ExplicitAnimDemoState extends State<ExplicitAnimDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    // 创建控制器,vsync 绑定 this 用于提供 Ticker
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 1500),
    );

    // 定义 Tween:从 0 到 1 的线性插值
    _animation = CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOutCubic,
    );

    // 监听动画状态
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward();
      }
    });

    // 开始动画
    _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: 0.5 + (_animation.value * 0.5),  // 0.5x → 1.0x
          child: Opacity(
            opacity: _animation.value,
            child: Container(
              width: 120, height: 120,
              decoration: BoxDecoration(
                color: Colors.deepPurple,
                borderRadius: BorderRadius.circular(20 * _animation.value),
                boxShadow: [
                  BoxShadow(
                    color: Colors.deepPurple.withOpacity(0.4 * _animation.value),
                    blurRadius: 20 * _animation.value,
                    spreadRadius: 5 * _animation.value,
                  )
                ],
              ),
              ),
            ),
          ),
        );
      },
    );
  }
}

3.2 常用 Curve 曲线速查表

Curve 决定了动画的节奏感,选对曲线能让动效提升一个档次:

// 线性(无加速)
Curves.linear

// 标准曲线(推荐默认使用)
Curves.easeIn       // 先慢后快
Curves.easeOut      // 先快后慢(最常用)
Curves.easeInOut     // 头尾慢中间快

// 弹性效果
Curves.bounceOut    // 弹跳结束
Curves.elasticOut   // 弹簧过冲

// 特殊曲线
Curves.fastOutSlowIn  // 快出慢入(Material Design 标准)
Curves.slowMiddle     // 中间减速
Curve(debounce: 0.3)   // 反弹衰减(自定义)

// 贝塞尔自定义(最灵活)
CubicBezier(0.68, -0.55, 0.27, 1.55)  // 过弹效果
CubicBezier(0.25, 0.1, 0.25, 1)       // 标准 ease

四、Hero 动画 — 页面间丝滑转场

Hero 动画是 Flutter 最令人印象深刻的特性之一:两个页面的同名 Hero Widget 会自动产生飞入/飞出的过渡效果。

// ===== 页面 A:列表项 =====
GestureDetector(
  onTap: () => Navigator.push(
    context,
    MaterialPageRoute(builder: (_) => DetailPage(heroTag: item.id)),
  ),
  child: Hero(
    tag: 'hero-${item.id}',  // 必须唯一
    child: Image.network(item.imageUrl, width: 100, height: 100, fit: BoxFit.cover),
  ),
)

// ===== 页面 B:详情页 =====
Scaffold(
  body: CustomScrollView(
    slivers: [
      SliverAppBar(
        expandedHeight: 280,
        pinned: true,
        flexibleSpace: FlexibleSpaceBar(
          title: Text(item.title),
          background: Hero(
            tag: 'hero-${item.id}',
            child: Image.network(item.imageUrl, fit: BoxFit.cover),
          ),
        ),
      ),
      SliverList(delegate: SliverChildBuilderDelegate(
        (context, index) => ListTile(title: Text('详情内容 $index')),
        childCount: 10,
      )),
    ],
  ),
)

// ===== Hero 高级配置 =====
Hero(
  tag: 'avatar-hero',
  // 自定义飞行的形状(圆形 → 圆形)
  createRectTween: (begin, end) => RectTween(begin: begin, end: end),
  // 飞行曲线
  flightShuttleBuilder: (flightContext, animation, flightDirection) {
    return AnimatedBuilder(
      animation: animation,
      builder: (context, child) {
        final double value = animation.value;
        return Transform.rotate(
          angle: value * math.pi * 2,  // 飞行中旋转
          child: Transform.scale(
            scale: 0.8 + (0.2 * value),
            child: Opacity(opacity: value, child: child),
          ),
        );
      },
    );
  },
  placeholderBuilder: (context, size, heroWidget) {
    return Container(color: Colors.grey[300]);
  },
  child: CircleAvatar(radius: 50, backgroundImage: NetworkImage(user.avatar)),
)

五、交错动画(Staggered Animation)

让一组元素依次出现,而不是同时出现,这种"波浪式"入场效果叫交错动画(Staggered Animation),能极大提升视觉质感:

class StaggeredAnimDemo extends StatefulWidget {
  @override
  _StaggeredAnimDemoState createState() => _StaggeredAnimDemoState();
}

class _StaggeredAnimDemoState extends State<StaggeredAnimDemo>
    with TickerProviderStateMixin {

  late final AnimationController _controller =
      AnimationController(vsync: this, duration: Duration(milliseconds: 1800));

  @override
  void initState() {
    super.initState();
    _controller.forward();
  }

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

  // 每个元素的延迟不同
  Interval _interval(int index, int total) {
    final start = index / total;
    const duration = 0.6;  // 每个动画占总时长的比例
    return Interval(start.clamp(0.0, 1.0 - duration), (start + duration).clamp(0.0, 1.0));
  }

  @override
  Widget build(BuildContext context) {
    final items = List.generate(6, (i) => i);
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: items.map((i) {
        return AnimatedBuilder(
          animation: _controller.drive(_interval(i, items.length)),
          builder: (context, child) {
            return Transform.translate(
              offset: Offset((1 - _controller.value) * 200, 0),  // 从右侧滑入
              child: Opacity(
                opacity: _controller.value,
                child: Container(
                  margin: EdgeInsets.symmetric(vertical: 8),
                  width: 280 - i * 8,  // 渐变小
                  height: 64,
                  decoration: BoxDecoration(
                    color: Color.lerp(Colors.blue, Colors.purple, i / items.length)!,
                    borderRadius: BorderRadius.circular(16),
                  ),
                  alignment: Alignment.centerLeft,
                  padding: EdgeInsets.symmetric(horizontal: 24),
                  child: Text('列表项 ${i + 1}',
                    style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600)),
                ),
              ),
            );
          },
        );
      }).toList(),
    );
  }
}

六、物理动画(Physics-based Animation)

使用弹簧模拟真实的物理效果,让交互更自然:

import 'package:flutter/physics.dart';

class SpringAnimationDemo extends StatefulWidget {
  @override
  _SpringAnimationDemoState createState() => _SpringAnimationDemoState();
}

class _SpringAnimationDemoState extends State<SpringAnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController.unbounded(vsync: this);
    
    // 使用 SpringSimulation 物理模拟
    _animation = _controller.drive(CurvedAnimation(
      parent: _controller,
      curve: Curves.elasticOut,
    ));
  }

  void _onPanDown(DragDownDetails details) {
    _controller.stop();  // 用户触摸时停止当前动画
  }

  void _onPanUpdate(DragUpdateDetails details) {
    _controller.value += details.delta.dx / 300;  // 跟随手指拖拽
  }

  void _onPanEnd(DragEndDetails details) {
    // 松手后模拟弹簧回弹
    _controller.animateWith(SpringSimulation(
      spring: SpringDescription(mass: 1, stiffness: 180, ratio: 0.9),
      velocity: details.velocity.pixelsPerSecond.dx / 300,
      // position: 从当前位置开始
      from: _controller.value,
      to: 0,  // 回到中心
    ));
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanDown: _onPanDown,
      onPanUpdate: _onPanUpdate,
      onPanEnd: _onPanEnd,
      child: AnimatedBuilder(
        animation: _animation,
        builder: (context, _) {
          return Align(
            alignment: Alignment(-_animation.value, 0),  // 左右拖动
            child: Container(
              width: 80, height: 80,
              decoration: BoxDecoration(
                color: Colors.orange,
                shape: BoxShape.circle,
                boxShadow: [BoxShadow(
                  color: Colors.orange.withOpacity(0.4),
                  blurRadius: 12 + _animation.value.abs() * 20,
                )],
              ),
              child: const Icon(Icons.pan_tool, color: Colors.white),
            ),
          );
        },
      ),
    );
  }

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

七、Lottie 动画集成

Lottie 是 Airbnb 开源的高性能动画库,设计师可以用 After Effects 制作动画并导出 JSON 文件,Flutter 端直接渲染。

7.1 配置与基础用法

// pubspec.yaml
dependencies:
  lottie: ^3.1.0

// 基本使用
import 'package:lottie/lottie.dart';

Lottie.asset(
  'assets/animations/loading.json',  // 本地 JSON 文件
  width: 200,
  height: 200,
  fit: BoxFit.contain,
  // 控制播放
  controller: LottieAnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 2000),
    repeat: true,
    autoPlay: true,
  ),
)

// 网络加载
Lottie.network(
  'https://assets2.lottiefiles.com/packages/lottie_animations/...',
  width: 150,
  height: 150,
)

// 实时控制
final lottieKey = GlobalKey<LottieState>();

Column(
  children: [
    Lottie.asset('anim/success.json', key: lottieKey, width: 200, height: 200),
    Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        ElevatedButton(onPressed: () => lottieKey.currentState?.play(), child: Text('播放')),
        ElevatedButton(onPressed: () => lottieKey.currentState?.pause(), child: Text('暂停')),
        ElevatedButton(onPressed: () => lottieKey.currentState?.stop(), child: Text('停止')),
      ],
    )
  ],
)

7.2 典型应用场景

  • 加载动画:骨架屏、进度指示器、品牌化 loading
  • 空状态插画:无数据、搜索无结果、网络错误
  • 成功/失败反馈:打勾动画、报错抖动
  • 引导页动画:首次使用的步骤演示
  • Splash 页面:品牌 Logo 动态展示

八、Rive 动画集成

Rive(原名 Flare)比 Lottie 更进一步——支持状态机(State Machine)运行时交互。动画可以在运行时通过代码切换状态、混合动画。

// pubspec.yaml
dependencies:
  rive: ^0.13.0

import 'package:rive/rive.dart';

RiveAnimation.asset(
  'assets/rive/character.riv',  // Rive 文件
  fit: BoxFit.contain,
  stateMachines: ['State Machine 1'],
  // 运行时控制状态机输入
  onInit: (artboard) {
    final smc = artboard.stateMachineByName('State Machine 1');
    // 切换到 idle 状态
    smc?.inputs.first.value = true;
  },
)

// 更常见的使用方式:通过 RiveController
final riveController = RiveAnimationController('idle', 'assets/rive/bot.riv');

@override
void initState() {
  super.initState();
  riveController.addStatusListener((status) {
    if (status == RiveStatus.complete) {
      riveController.reset();  // 循环播放
    }
  });
  riveController.active = true;  // 开始播放
}

@override
Widget build(BuildContext context) {
  return GestureDetector(
    onTap: () {
      // 点击切换到另一个状态
      riveController.inputChange({'trigger': true});
    },
    child: RiveAnimation.simple(
      'assets/rive/bot.riv',
      controller: riveController,
      fit: BoxFit.contain,
    ),
  );
}

九、动画性能优化最佳实践

  • 优先使用 const 构造函数:减少不必要的对象重建
  • RepaintBoundary 包裹动画区域:隔离重绘范围,避免整树重建
  • 避免在动画中使用复杂 Widget:动画中的子树越轻量越好
  • 合理设置 duration:太短用户感知不到,太长感觉卡顿(推荐 200ms~600ms)
  • saveLayer 与 clip:圆角裁剪会触发 saveLayer,影响性能
  • shouldComponentate:显式动画中善用此方法避免不必要的重建
  • GPU 加速:transform 和 opacity 触发合成层,不触发布局和绘制,性能最好
  • will-change 属性:提前告知浏览器该元素会动画,但不要滥用
  • prefers-reduced-motion:尊重用户的系统级动画偏好设置

总结

Flutter 的动画体系层次分明:日常 UI 交互用 Implicit Animation 一行搞定;需要精细控制的场景上 Explicit Animation;页面切换用 Hero;复杂序列用 Staggered Animation;自然交互用 Physics Simulation;设计师给的复杂动效用 Lottie/Rive。根据场景选择合适的方案,你的 APP 动效一定能脱颖而出。