2.4 绘制与动画

Flutter 提供了从基础 Canvas 绘制到复杂物理动画的完整动画系统。掌握动画层次结构,按需选择合适的方案,是流畅 UI 的关键。


一、Canvas 绘制与 CustomPainter

当标准 Widget 无法满足需求时,可以通过 CustomPainter 直接操作 Canvas:

dart 复制代码
class WaveformPainter extends CustomPainter {
  final List<double> amplitudes;
  final Color waveColor;
  final double progress; // 0.0 - 1.0

  WaveformPainter({
    required this.amplitudes,
    required this.waveColor,
    required this.progress,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = waveColor
      ..strokeWidth = 3
      ..strokeCap = StrokeCap.round
      ..style = PaintingStyle.stroke;

    final barWidth = size.width / amplitudes.length;
    final playedPaint = Paint()..color = waveColor;
    final unplayedPaint = Paint()..color = waveColor.withOpacity(0.3);

    for (int i = 0; i < amplitudes.length; i++) {
      final x = i * barWidth + barWidth / 2;
      final barHeight = amplitudes[i] * size.height;
      final y1 = (size.height - barHeight) / 2;
      final y2 = y1 + barHeight;
      final isPlayed = i / amplitudes.length < progress;

      canvas.drawLine(
        Offset(x, y1),
        Offset(x, y2),
        isPlayed ? playedPaint : unplayedPaint,
      );
    }
  }

  @override
  bool shouldRepaint(WaveformPainter oldDelegate) =>
      oldDelegate.progress != progress || oldDelegate.amplitudes != amplitudes;
}

// 使用
CustomPaint(
  size: const Size(double.infinity, 80),
  painter: WaveformPainter(
    amplitudes: waveData,
    waveColor: Colors.blue,
    progress: playProgress,
  ),
)

常用 Canvas API:

方法 功能
canvas.drawLine() 画线
canvas.drawRect() 画矩形
canvas.drawCircle() 画圆
canvas.drawPath() 绘制自定义路径
canvas.drawImage() 绘制图片
canvas.drawText() / TextPainter 绘制文字
canvas.clipPath() 裁剪画布
canvas.save() / restore() 保存/恢复画布状态

二、隐式动画(Implicit Animations)

隐式动画是最简单的动画方式,只需设置目标值,Flutter 自动插值过渡。

2.1 AnimatedContainer

dart 复制代码
class AnimatedBox extends StatefulWidget {
  @override
  State<AnimatedBox> createState() => _AnimatedBoxState();
}

class _AnimatedBoxState extends State<AnimatedBox> {
  bool _expanded = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _expanded = !_expanded),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeInOut,
        width: _expanded ? 200 : 100,
        height: _expanded ? 200 : 100,
        decoration: BoxDecoration(
          color: _expanded ? Colors.purple : Colors.blue,
          borderRadius: BorderRadius.circular(_expanded ? 40 : 8),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(_expanded ? 0.3 : 0.1),
              blurRadius: _expanded ? 20 : 5,
            ),
          ],
        ),
      ),
    );
  }
}

2.2 常用隐式动画 Widget

dart 复制代码
// 透明度渐变
AnimatedOpacity(
  opacity: _visible ? 1.0 : 0.0,
  duration: const Duration(milliseconds: 200),
  child: content,
)

// 位置偏移
AnimatedSlide(
  offset: _show ? Offset.zero : const Offset(0, 1),
  duration: const Duration(milliseconds: 300),
  curve: Curves.easeOut,
  child: bottomPanel,
)

// 尺寸自适应
AnimatedSize(
  duration: const Duration(milliseconds: 200),
  child: showDetail ? DetailWidget() : SizedBox.shrink(),
)

// 交叉淡入淡出
AnimatedSwitcher(
  duration: const Duration(milliseconds: 300),
  transitionBuilder: (child, animation) => FadeTransition(
    opacity: animation,
    child: child,
  ),
  child: Text('$_count', key: ValueKey(_count)), // Key 变化触发动画
)

// 对齐方式变化
AnimatedAlign(
  alignment: _isTop ? Alignment.topCenter : Alignment.bottomCenter,
  duration: const Duration(milliseconds: 300),
  child: const FloatingActionButton(onPressed: null, child: Icon(Icons.add)),
)

三、显式动画(Explicit Animations)

显式动画提供完整的动画控制,适合复杂、精确控制的场景。

3.1 AnimationController

dart 复制代码
class PulseAnimation extends StatefulWidget {
  final Widget child;
  const PulseAnimation({super.key, required this.child});

  @override
  State<PulseAnimation> createState() => _PulseAnimationState();
}

class _PulseAnimationState extends State<PulseAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;
  late Animation<double> _opacityAnimation;

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

    _controller = AnimationController(
      duration: const Duration(milliseconds: 800),
      vsync: this, // TickerProvider,防止后台操作
    );

    _scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );

    _opacityAnimation = Tween<double>(begin: 1.0, end: 0.6).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );

    _controller.repeat(reverse: true); // 往复循环
  }

  @override
  void dispose() {
    _controller.dispose(); // ⚠️ 必须释放
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Transform.scale(
          scale: _scaleAnimation.value,
          child: Opacity(
            opacity: _opacityAnimation.value,
            child: child,
          ),
        );
      },
      child: widget.child, // 不随动画变化的子 Widget,避免重复构建
    );
  }
}

3.2 Tween 与 Curve

dart 复制代码
// Tween 定义起止值
Tween<double>(begin: 0, end: 1)
Tween<Color?>(begin: Colors.red, end: Colors.blue)
Tween<Offset>(begin: Offset.zero, end: const Offset(1, 0))

// ColorTween
ColorTween(begin: Colors.white, end: Colors.black)

// 自定义 Tween
class SizeTween extends Tween<Size> {
  SizeTween({required super.begin, required super.end});

  @override
  Size lerp(double t) => Size.lerp(begin, end, t)!;
}

// 常用 Curve(缓动曲线)
Curves.linear         // 匀速
Curves.easeIn         // 加速
Curves.easeOut        // 减速
Curves.easeInOut      // 先加速后减速(最常用)
Curves.bounceOut      // 弹跳效果
Curves.elasticOut     // 弹性效果
Curves.decelerate     // 急减速

3.3 AnimationController 常用方法

dart 复制代码
_controller.forward();           // 正向播放
_controller.reverse();           // 反向播放
_controller.repeat();            // 循环播放
_controller.repeat(reverse: true); // 往复循环
_controller.reset();             // 重置到起始状态
_controller.stop();              // 停止
_controller.animateTo(0.5);      // 动画到指定进度

四、过渡动画

4.1 Hero 动画

Hero 动画实现页面间的共享元素过渡:

dart 复制代码
// 列表页
Hero(
  tag: 'product_image_${product.id}', // 唯一标识符
  child: ClipRRect(
    borderRadius: BorderRadius.circular(8),
    child: Image.network(product.imageUrl, fit: BoxFit.cover),
  ),
)

// 详情页
Hero(
  tag: 'product_image_${product.id}', // 相同 tag
  child: Image.network(product.imageUrl, fit: BoxFit.cover),
)

// 自定义 Hero 动画效果
Hero(
  tag: heroTag,
  flightShuttleBuilder: (
    flightContext, animation, direction, fromContext, toContext,
  ) {
    return ScaleTransition(
      scale: animation,
      child: toContext.widget,
    );
  },
  child: image,
)

4.2 PageRouteBuilder(自定义页面过渡)

dart 复制代码
// 滑入过渡
class SlidePageRoute<T> extends PageRouteBuilder<T> {
  final Widget page;
  final Offset beginOffset;

  SlidePageRoute({
    required this.page,
    this.beginOffset = const Offset(1, 0),
  }) : super(
    pageBuilder: (context, animation, secondaryAnimation) => page,
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return SlideTransition(
        position: Tween<Offset>(
          begin: beginOffset,
          end: Offset.zero,
        ).animate(CurvedAnimation(parent: animation, curve: Curves.easeOut)),
        child: child,
      );
    },
    transitionDuration: const Duration(milliseconds: 300),
  );
}

// 使用
Navigator.push(
  context,
  SlidePageRoute(page: const DetailPage()),
)

// 淡入淡出
Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (_, __, ___) => const NextPage(),
    transitionsBuilder: (context, animation, _, child) {
      return FadeTransition(opacity: animation, child: child);
    },
  ),
)

4.3 AnimatedBuilder 与 AnimatedWidget

dart 复制代码
// AnimatedBuilder:适合在 build 方法中使用动画值
AnimatedBuilder(
  animation: _controller,
  builder: (context, child) {
    return Transform.rotate(
      angle: _controller.value * 2 * pi,
      child: child,
    );
  },
  child: const Icon(Icons.refresh, size: 32), // 静态子 Widget
)

// AnimatedWidget:自定义动画 Widget(继承方式)
class RotatingIcon extends AnimatedWidget {
  const RotatingIcon({super.key, required Animation<double> animation})
      : super(listenable: animation);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Transform.rotate(
      angle: animation.value * 2 * pi,
      child: const Icon(Icons.refresh),
    );
  }
}

五、高级动画

5.1 Rive 动画集成

yaml 复制代码
# pubspec.yaml
dependencies:
  rive: ^0.12.0
dart 复制代码
import 'package:rive/rive.dart';

class RiveAnimationWidget extends StatefulWidget {
  @override
  State<RiveAnimationWidget> createState() => _RiveAnimationWidgetState();
}

class _RiveAnimationWidgetState extends State<RiveAnimationWidget> {
  SMIBool? _isHovered;

  void _onRiveInit(Artboard artboard) {
    final controller = StateMachineController.fromArtboard(
      artboard, 'State Machine',
    );
    if (controller != null) {
      artboard.addController(controller);
      _isHovered = controller.findInput<bool>('isHovered') as SMIBool;
    }
  }

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onEnter: (_) => _isHovered?.change(true),
      onExit: (_) => _isHovered?.change(false),
      child: RiveAnimation.asset(
        'assets/animations/button.riv',
        onInit: _onRiveInit,
      ),
    );
  }
}

5.2 Lottie 动画集成

yaml 复制代码
dependencies:
  lottie: ^3.0.0
dart 复制代码
import 'package:lottie/lottie.dart';

// 简单播放
Lottie.asset(
  'assets/animations/loading.json',
  width: 200,
  height: 200,
  fit: BoxFit.contain,
  repeat: true,
)

// 控制播放
class SuccessAnimation extends StatefulWidget {
  @override
  State<SuccessAnimation> createState() => _SuccessAnimationState();
}

class _SuccessAnimationState extends State<SuccessAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

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

  @override
  Widget build(BuildContext context) {
    return Lottie.asset(
      'assets/animations/success.json',
      controller: _controller,
      onLoaded: (composition) {
        _controller
          ..duration = composition.duration
          ..forward().then((_) => _controller.stop()); // 播放一次停止
      },
    );
  }

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

小结

类型 Widget/API 适用场景
CustomPainter CustomPaint 自定义图形绘制
隐式动画 AnimatedContainer, AnimatedOpacity 简单属性过渡
AnimatedSwitcher AnimatedSwitcher 切换不同子组件
显式动画 AnimationController + Tween 精确控制动画
Hero Hero 页面间共享元素
路由过渡 PageRouteBuilder 自定义页面切换
AnimatedBuilder AnimatedBuilder 在 build 中使用动画值
Rive RiveAnimation 交互式矢量动画
Lottie Lottie.asset After Effects 导出动画

👉 下一章:三、状态管理(State Management)

相关推荐
空中海2 小时前
2.6 表单与输入处理
flutter·dart
AI_零食2 小时前
开源鸿蒙跨平台Flutter开发:脑筋急转弯应用开发文档
flutter·华为·开源·harmonyos·鸿蒙
2301_822703204 小时前
Flutter 框架跨平台鸿蒙开发 - 家庭时间胶囊应用
算法·flutter·华为·图形渲染·harmonyos·鸿蒙
提子拌饭1334 小时前
Flutter 框架跨平台鸿蒙开发 - 声音风景分享应用
flutter·华为·harmonyos·鸿蒙·风景
独特的螺狮粉4 小时前
开源鸿蒙跨平台Flutter开发:超市购物清单应用
flutter·华为·开源·harmonyos·鸿蒙
2301_822703204 小时前
成语小词典:鸿蒙Flutter实现的成语查询与管理应用
算法·flutter·华为·开源·图形渲染·harmonyos
2301_822703205 小时前
Flutter 框架跨平台鸿蒙开发 - 智能植物生长记录应用
算法·flutter·华为·harmonyos·鸿蒙
世人万千丶5 小时前
开源鸿蒙跨平台Flutter开发:成语接龙游戏应用
学习·flutter·游戏·华为·开源·harmonyos·鸿蒙
浮芷.5 小时前
开源鸿蒙跨平台Flutter开发:校园闲置物品交换应用
科技·flutter·华为·开源·ar·harmonyos·鸿蒙