[Flutter 进阶] 动画曲线(Curve)- 你想要的样子我都有

378 阅读4分钟

在 Flutter 动画系统中,Curve(曲线) 是控制动画变化速率的核心概念。它决定了动画在时间线上的变化节奏,使动画效果更加自然流畅。

曲线的作用原理

在动画系统中,曲线将线性时间进度(0.0 ~ 1.0)转换为非线性的变化过程:

image.png

常用内置曲线类型

Flutter 提供了丰富的内置曲线,以下是主要分类:

1. 基础缓动曲线

曲线名称效果描述适用场景
Curves.linear匀速变化机械运动、进度条
Curves.easeIn开始慢,逐渐加速物体从静止开始运动
Curves.easeOut开始快,逐渐减速物体逐渐停止运动
Curves.easeInOut开始和结束慢,中间快最常见的自然运动效果
Curves.fastOutSlowIn快速开始,慢速结束(Material Design默认)移动设备UI动画

2. 弹性曲线

曲线名称效果描述
Curves.elasticIn开始时有弹性收缩效果
Curves.elasticOut结束时有弹性伸展效果
Curves.elasticInOut开始和结束都有弹性效果

3. 弹跳曲线

曲线名称效果描述
Curves.bounceIn结束时产生弹跳效果
Curves.bounceOut开始时产生弹跳效果
Curves.bounceInOut开始和结束都产生弹跳效果

4. 特殊效果曲线

曲线名称效果描述
Curves.decelerate急剧减速效果
Curves.slowMiddle中间慢,两端快
Curves.easeInBack开始时有轻微后退效果
Curves.easeOutBack结束时有轻微超出效果

曲线可视化示例

curve.gif

可复制代码查看运行效果

import 'package:flutter/material.dart';
​
class CurveVisualizer extends StatefulWidget {
  const CurveVisualizer({super.key});
​
  @override
  State<CurveVisualizer> createState() => _CurveVisualizerState();
}
​
class _CurveVisualizerState extends State<CurveVisualizer> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final Map<String, Curve> curves = {
    'linear': Curves.linear,
    'easeIn': Curves.easeIn,
    'easeOut': Curves.easeOut,
    'easeInOut': Curves.easeInOut,
    'bounceIn': Curves.bounceIn,
    'bounceOut': Curves.bounceOut,
    'bounceInOut': Curves.bounceInOut,
    'elasticIn': Curves.elasticIn,
    'elasticOut': Curves.elasticOut,
    'elasticInOut': Curves.elasticInOut,
    'fastOutSlowIn': Curves.fastOutSlowIn,
    'decelerate': Curves.decelerate,
  };
  
  String selectedCurve = 'linear';
​
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    )..repeat(reverse: true);
  }
​
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 曲线选择器
        Padding(
          padding: const EdgeInsets.all(16.0),
          child: Wrap(
            spacing: 8,
            children: curves.keys.map((name) {
              return ChoiceChip(
                label: Text(name),
                selected: selectedCurve == name,
                onSelected: (selected) {
                  setState(() => selectedCurve = name);
                },
              );
            }).toList(),
          ),
        ),
        
        // 曲线可视化
        Expanded(
          child: CustomPaint(
            painter: CurvePainter(
              curve: curves[selectedCurve]!,
              animation: _controller,
              curveName: selectedCurve,
            ),
          ),
        ),
        
        // 动画预览
        SizedBox(
          height: 100,
          child: Center(
            child: AnimatedBuilder(
              animation: _controller,
              builder: (context, child) {
                final animation = CurvedAnimation(
                  parent: _controller,
                  curve: curves[selectedCurve]!,
                );
                
                return Container(
                  width: 50 + animation.value * 200,
                  height: 50,
                  decoration: BoxDecoration(
                    color: Colors.blue,
                    borderRadius: BorderRadius.circular(25),
                  ),
                );
              },
            ),
          ),
        ),
      ],
    );
  }
​
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}
​
class CurvePainter extends CustomPainter {
  final Curve curve;
  final Animation<double> animation;
  final String curveName;
  
  CurvePainter({
    required this.curve,
    required this.animation,
    required this.curveName,
  }) : super(repaint: animation);
​
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 3
      ..style = PaintingStyle.stroke;
    
    final path = Path();
    const pointCount = 100;
    final points = List.generate(pointCount, (i) {
      final t = i / (pointCount - 1);
      final y = curve.transform(t);
      return Offset(t * size.width, (1 - y) * size.height);
    });
    
    path.moveTo(points.first.dx, points.first.dy);
    for (int i = 1; i < points.length; i++) {
      path.lineTo(points[i].dx, points[i].dy);
    }
    
    canvas.drawPath(path, paint);
    
    // 绘制当前动画点
    final currentT = animation.value;
    final currentY = curve.transform(currentT);
    final currentPoint = Offset(
      currentT * size.width, 
      (1 - currentY) * size.height
    );
    
    canvas.drawCircle(
      currentPoint, 
      8, 
      Paint()..color = Colors.red
    );
    
    // 添加曲线名称
    final textPainter = TextPainter(
      text: TextSpan(
        text: curveName,
        style: const TextStyle(
          color: Colors.black, 
          fontSize: 16,
          fontWeight: FontWeight.bold
        ),
      ),
      textDirection: TextDirection.ltr,
    );
    textPainter.layout();
    textPainter.paint(
      canvas, 
      Offset(size.width/2 - textPainter.width/2, 20)
    );
  }
​
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

自定义曲线实现

Flutter 允许开发者创建自定义动画曲线:

step.gif

class CustomStepCurve extends Curve {
  final int steps;
  
  const CustomStepCurve({this.steps = 5});
  
  @override
  double transform(double t) {
    return (t * steps).floorToDouble() / (steps - 1);
  }
}

使用自定义曲线

// 在动画控制器中使用
_animation = Tween(begin: 0.0, end: 1.0).animate(
  CurvedAnimation(
    parent: _controller,
    curve: const CustomBounceCurve(elasticity: 0.5),
  ),
);

解析:

  • 数学原理:离散化连续输入

    • t * steps:将 [0,1] 映射到 [0, steps]
    • .floorToDouble():取整实现离散化
    • / (steps - 1):归一化到 [0,1] 范围
  • 参数作用

    • steps:控制动画跳跃次数(必须 ≥2)
    • 默认 5 步:值变化序列为 0, 0.25, 0.5, 0.75, 1.0
  • 动画效果:非连续的阶梯状跳跃动画

  • 示例输出

    • t=0.1 → 0.0
    • t=0.3 → 0.25
    • t=0.7 → 0.75
    • t=1.0 → 1.0

曲线选择建议

动画场景推荐曲线说明
页面过渡Curves.fastOutSlowInMaterial Design 标准
按钮点击Curves.easeInOut自然平滑效果
加载指示器Curves.linear匀速运动
弹出对话框Curves.elasticOut弹性效果增强视觉反馈
下拉刷新Curves.bounceOut模拟物理弹跳
滑动消失Curves.decelerate快速开始,急剧减速
分步动画CustomStepCurve(steps:5)离散变化效果
背景波动效果CustomWaveCurve()波浪式变化

性能考虑

动画开发对性能的影响是非常大的,同一个动画的不同实现方式对性能的影响很可能不一样,所以在日常开发中有一些对性能产生明显有影响的地方尽量避免。

  1. 简单曲线性能最佳

    • linear, easeIn, easeOut等基础曲线计算开销最小
    • 弹性、弹跳曲线计算复杂度较高
  2. 避免频繁重建

    // 错误:每次build都重建CurvedAnimation
    Widget build() {
      final animation = CurvedAnimation(parent: controller, curve: curve);
      // ...
    }
    ​
    // 正确:在initState或类成员中创建
    late final Animation<double> animation;
    ​
    @override
    void initState() {
      super.initState();
      animation = CurvedAnimation(parent: controller, curve: curve);
    }
    
  3. 预计算曲线值: 对于特别复杂的自定义曲线,可以预计算值表:

    final valueTable = List.generate(100, (i) {
      return customCurve.transform(i / 100);
    });
    ​
    double getCurveValue(double t) {
      final index = (t * 99).clamp(0, 99).toInt();
      return valueTable[index];
    }
    

总结

Flutter 的曲线系统为动画提供了强大的节奏控制能力:

  1. 内置曲线丰富:覆盖了大多数常见动画场景
  2. 自定义灵活:通过继承 Curve 类实现任意变化规律
  3. 物理集成:可与 SpringSimulation 等物理模拟结合
  4. 性能可控:简单曲线高效,复杂曲线需谨慎使用

掌握曲线原理是创建自然流畅动画的关键,合理选择曲线可以:

  • 增强用户界面的响应感
  • 提供有意义的视觉反馈
  • 引导用户注意力
  • 提升整体用户体验