公司动态
Flutter动画插值全解析:从Tween到Curve的十五个常用缓动参数详解
Flutter动画插值全解析从Tween到Curve的十五个常用缓动参数详解在 Flutter 里写动画大多数人卡在两个字上感觉。Curve就是这两个字的数学表达——它把弹一下滑进去果断干脆这些感觉翻译成了 0 到 1 之间的数值曲线。本文将从 Tween 到 Curve把 Flutter 动画插值的所有细节像菜谱一样摊开。一、从0到1的旅程Flutter动画系统的底层逻辑Flutter 的动画系统本质上是一个从动画进度0.0→1.0到属性值如200px、Colors.blue的映射系统。理解了这个映射关系就理解了 Flutter 动画的底层逻辑。动画值计算的三个参与者这三个参与者的职责参与者职责类比AnimationController驱动动画进度0→1电影放映机Tween定义数值范围begin→end比例尺Curve定义变化节奏线性/非线性变速齿轮最基础的动画代码理解底层逻辑// 最基础的 Flutter 动画从 0 到 200 的宽度动画 class BasicAnimation extends StatefulWidget { const BasicAnimation({super.key}); override StateBasicAnimation createState() _BasicAnimationState(); } class _BasicAnimationState extends StateBasicAnimation with SingleTickerProviderStateMixin { late AnimationController _controller; late Animationdouble _widthAnimation; override void initState() { super.initState(); // 1. Controller驱动 0→1 的进度 _controller AnimationController( duration: const Duration(milliseconds: 300), // 持续时间300ms vsync: this, // 垂直同步保证60fps ); // 2. Tween映射 0→1 到 0→200 _widthAnimation Tweendouble( begin: 0.0, end: 200.0, ).animate(_controller); // 启动动画 _controller.forward(); } override Widget build(BuildContext context) { return AnimatedBuilder( animation: _widthAnimation, builder: (context, child) { return Container( width: _widthAnimation.value, // 当前宽度值 height: 100, color: Colors.blue, ); }, ); } override void dispose() { _controller.dispose(); // 必须释放否则内存泄漏 super.dispose(); } }关键参数说明像菜谱一样精确duration: 300ms这是即时反馈类动画的最佳持续时间Material Design 规范vsync: thisSingleTickerProviderStateMixin提供保证动画与屏幕刷新率同步Tweendouble(begin: 0, end: 200)double类型因为宽度是浮点数加入Curve让动画有感觉上面的代码动画是线性的——匀速从0到200看起来很机械。加入Curve后动画就有了节奏感。// 加入 Curve 的动画 _widthAnimation Tweendouble( begin: 0.0, end: 200.0, ).animate( CurvedAnimation( parent: _controller, curve: Curves.easeOut, // 缓出快速开始慢速结束 ), );Curves.easeOut的数学表达f(t) 1 - (1 - t)² // 其中 t 是进度0→1 // 效果开始时变化快结束时变化慢二、Tween家族全解析不只适用于数值Tween是 Flutter 动画中最灵活的部分——任何可以被插值的值都可以用 Tween。以下是我整理的 Tween 家族完整图谱。基础Tween数值与颜色// 1. 数值 Tween最常用 final widthAnim Tweendouble(begin: 0, end: 200).animate(controller); // 2. 整数 Tween用于计数动画 final countAnim IntTween(begin: 0, end: 100).animate(controller); // 3. 颜色 TweenColor.lerp 实现插值 final colorAnim ColorTween( begin: Colors.blue, end: Colors.red, ).animate(controller); // 4. 尺寸 TweenSize.lerp final sizeAnim TweenSize( begin: const Size(0, 0), end: const Size(200, 100), ).animate(controller);高级Tween边距、装饰、边框// 5. EdgeInsets Tween边距动画 final paddingAnim EdgeInsetsTween( begin: const EdgeInsets.all(0), end: const EdgeInsets.all(20), ).animate(controller); // 6. BoxDecoration Tween装饰动画如圆角/阴影/渐变 final decorationAnim DecorationTween( begin: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(0), ), end: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(12), ), ).animate(controller); // 7. BorderRadius Tween圆角动画 final radiusAnim BorderRadiusTween( begin: BorderRadius.circular(0), end: BorderRadius.circular(12), ).animate(controller); // 8. TextStyle Tween文字样式动画 final textStyleAnim TextStyleTween( begin: const TextStyle(fontSize: 14, color: Colors.black), end: const TextStyle(fontSize: 20, color: Colors.blue), ).animate(controller);自定义Tween当内置Tween不够用时Flutter 内置的 Tween 覆盖了大多数场景但有时候你需要插值自定义对象。这时可以继承TweenT。// 自定义 Tween插值一个 进度环 的起止角度 class ArcTween extends Tweendouble { ArcTween({double? begin, double? end}) : super(begin: begin, end: end); override double lerp(double t) { // t: 0→1 的动画进度 // 返回值对应的角度值 return super.lerp(t); } } // 更高级的自定义插值一个完整的自定义对象 class _MyCustomTween extends TweenMyCustomObject { override MyCustomObject lerp(double t) { return MyCustomObject( value1: begin!.value1 (end!.value1 - begin!.value1) * t, value2: Color.lerp(begin!.value2, end!.value2, t)!, ); } }Tween的链式调用animate driveFlutter 的动画 API 支持链式调用这让代码更简洁// 方式一先创建 Tween再调用 .animate() final widthAnim Tweendouble(begin: 0, end: 200).animate( CurvedAnimation(parent: controller, curve: Curves.easeOut), ); // 方式二用 controller.drive()更推荐语义更清晰 final widthAnim controller.drive( Tweendouble(begin: 0, end: 200).chain( CurveTween(curve: Curves.easeOut), ), ); // 两种方式完全等价但方式二的链式语义更强 // controller 驱动一个 TweenTween 经过一个 Curve 变换三、Curve详解十五个常用缓动参数的完整参考手册Curve是 Flutter 动画系统的灵魂。选对 Curve动画就有感觉选错 Curve动画就假。以下是我整理的十五个最常用 Curve 的完整参考手册每个都给出了使用场景和视觉效果。基础曲线族最常用先记住这五个具体参数与视觉效果Curve数学表达简化持续时间建议使用场景Curves.linearf(t) t不推荐用于UI动效环形进度条、旋转动画Curves.easeInf(t) t³200-400ms元素消失、退出场景Curves.easeOutf(t) 1-(1-t)³200-400ms元素出现、入场场景Curves.easeInOutf(t) 3t²-2t³300-600ms元素形态变化、页面转场Curves.fastOutSlowIn三次贝塞尔(0.4,0,0.2,1)300msMaterial Design 标准fastOutSlowIn的参数说明Material Design 官方推荐贝塞尔控制点(0.4, 0, 0.2, 1)效果开始快速吸引注意力结束缓慢到位感这是 Flutter 的默认 CurveThemeData.curve弹性曲线族有回弹感的动画弹性曲线让动画有物理感——就像真实世界中的弹簧。// Flutter 内置的弹性曲线 final elasticOut Curves.elasticOut; // 结束时回弹 final elasticIn Curves.elasticIn; // 开始时回弹 // 注意elasticInOut 不存在Flutter 没有内置 // 自定义弹性曲线更可控 final customElastic CurveTween( curve: ElasticCurve( amplitude: 0.5, // 回弹幅度0.5 50%过冲 period: 0.4, // 振荡周期秒 ), );弹性参数说明直接可用的菜谱参数推荐值效果amplitude0.3轻微回弹适合按钮amplitude0.5明显回弹适合强调amplitude0.8夸张回弹适合品牌动效period0.3快速振荡弹得急period0.6慢速振荡弹得缓回弹曲线族BounceBounce曲线模拟了自由落体多次弹跳的物理效果常用于掉落类动画。// Bounce 曲线 final bounceOut Curves.bounceOut; // 结束时弹跳多次 final bounceIn Curves.bounceIn; // 开始时弹跳多次 // 效果示意用文字描述 // bounceOut: |¯¯¯\___/¯¯\___/¯\___/\_ (落地后弹跳三次) // bounceIn: _/¯¯\___/¯¯\___/¯¯\___| (弹跳三次后落地)使用场景卡片掉落到容器中bounceOut元素从弹跳中稳定下来bounceIn不要用在频繁触发的动画中弹跳会让人烦躁贝塞尔曲线自定义你的品牌缓动当内置 Curve 不能满足需求时比如要做品牌化的缓动曲线可以用Cubic定义自定义贝塞尔曲线。// 自定义贝塞尔曲线 final brandCurve Cubic(0.25, 0.1, 0.25, 1); // 用法 final animation CurvedAnimation( parent: controller, curve: brandCurve, );如何设计品牌化缓动曲线打开 Cubic Bezier 可视化工具拖动控制点实时预览曲线效果找到满意的曲线后记录四个控制点的值用Cubic(x1, y1, x2, y2)定义在代码中三个经典自定义曲线可直接使用// 1. 优雅曲线适合高端品牌 final elegant Cubic(0.25, 0.1, 0.25, 1); // 控制点说明y10.1开始慢y21结束快产生加速感 // 2. 果断曲线适合工具类产品 final decisive Cubic(0.4, 0, 0.2, 1); // 这是 Material Design 的 fastOutSlowIn果断、专业 // 3. 柔和曲线适合生活方式类产品 final gentle Cubic(0.42, 0, 0.58, 1); // 对称曲线变化均匀无明显的加速/减速感十五个Curve速查表完整参考以下是我整理的十五个最常用 Curve 的完整速查表建议收藏#Curve类型视觉效果推荐持续时间典型场景1linear线性匀速-环形进度2easeIn缓入慢→快200-400ms元素消失3easeOut缓出快→慢200-400ms元素出现4easeInOut缓入缓出慢→快→慢300-600ms形态变化5fastOutSlowIn标准快出慢入300msMaterial标准6slowMiddle特殊两端快中间慢500-800ms强调动效7decelerate减速快→停300-500ms自然停止8elasticOut弹性结束时回弹800-1200ms品牌动效9elasticIn弹性开始时回弹800-1200ms入场强调10bounceOut弹跳落地弹跳1000-1500ms掉落动效11bounceIn弹跳弹跳后稳定1000-1500ms稳定动效12easeInCubic三次缓入加速更明显200-400ms快速退出13easeOutCubic三次缓出减速更明显200-400ms优雅入场14easeInOutCubic三次缓入缓出对称加速减速300-600ms标准转场15linearToEaseOut组合先匀速后减速400-600ms加载完成四、实战用TweenCurve构建高级动效的完整流程理论说了这么多来一个完整的实战案例一个可交互的卡片支持点击展开/收起带有精心设计的缓动曲线。需求描述卡片默认状态收起高度100px点击后展开到300px同时圆角从12px变为0px全宽展示收起时反向动画动效要求优雅不急躁有分寸感完整实现代码// expandable_card.dart import package:flutter/material.dart; class ExpandableCard extends StatefulWidget { const ExpandableCard({super.key}); override StateExpandableCard createState() _ExpandableCardState(); } class _ExpandableCardState extends StateExpandableCard with SingleTickerProviderStateMixin { late final AnimationController _controller; // 用 CurvedAnimation 包装 controller应用缓动曲线 late final Animationdouble _heightAnimation; late final Animationdouble _radiusAnimation; late final Animationdouble _opacityAnimation; bool _isExpanded false; override void initState() { super.initState(); // Controller持续时间600ms优雅的节奏 _controller AnimationController( duration: const Duration(milliseconds: 600), vsync: this, ); // 高度动画100 → 300 // Curve: easeInOutCubic对称加速减速优雅 _heightAnimation Tweendouble( begin: 100, end: 300, ).animate( CurvedAnimation( parent: _controller, curve: Curves.easeInOutCubic, ), ); // 圆角动画12 → 0 // Curve: easeOut快速开始优雅结束 _radiusAnimation Tweendouble( begin: 12, end: 0, ).animate( CurvedAnimation( parent: _controller, curve: Curves.easeOut, ), ); // 透明度动画1 → 0.7收起时内容略微透明 // Curve: linear透明度变化用线性避免闪烁感 _opacityAnimation Tweendouble( begin: 1.0, end: 0.7, ).animate( CurvedAnimation( parent: _controller, curve: Curves.linear, ), ); } void _toggle() { setState(() { _isExpanded !_isExpanded; if (_isExpanded) { _controller.forward(); } else { _controller.reverse(); } }); } override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { return GestureDetector( onTap: _toggle, child: Container( width: double.infinity, height: _heightAnimation.value, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(_radiusAnimation.value), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.08), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Opacity( opacity: _opacityAnimation.value, child: const Padding( padding: EdgeInsets.all(20), child: Text(点击展开/收起, style: TextStyle(fontSize: 16)), ), ), ), ); }, ); } override void dispose() { _controller.dispose(); super.dispose(); } }动效参数的感觉说明为什么这样选参数持续时间 600ms大于 300ms标准反馈小于 1000ms用户不会不耐烦是优雅区间的中点高度用easeInOutCubic对称加速减速符合展开/收起的物理直觉圆角用easeOut展开时圆角快速消失强调变全屏的感觉收起时圆角慢慢回来优雅回归透明度用linear透明度变化用非线性会产生闪烁感线性最自然优化用AnimatedContainer简化代码上面的代码是手动动画的方式适合需要精细控制的场景。如果只是简单的属性动画可以用AnimatedContainer简化// 简化版用 AnimatedContainer class ExpandableCardSimple extends StatefulWidget { const ExpandableCardSimple({super.key}); override StateExpandableCardSimple createState() _ExpandableCardSimpleState(); } class _ExpandableCardSimpleState extends StateExpandableCardSimple { bool _isExpanded false; override Widget build(BuildContext context) { return GestureDetector( onTap: () setState(() _isExpanded !_isExpanded), child: AnimatedContainer( duration: const Duration(milliseconds: 600), curve: Curves.easeInOutCubic, // 直接指定 curve width: double.infinity, height: _isExpanded ? 300 : 100, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(_isExpanded ? 0 : 12), ), child: const Padding( padding: EdgeInsets.all(20), child: Text(点击展开/收起), ), ), ); } }AnimatedContainer的局限性只能动画可动画的属性不是所有属性都支持不能精细控制每个属性的 Curve所有属性共用一个 Curve不适合复杂动效如链式动画、Stagger 动画选择建议简单动效用AnimatedContainer复杂动效用AnimationControllerTween。五、总结Flutter 的动画系统从Tween到Curve是一套精心设计的感觉翻译系统——它把人类对运动的感知翻译成了 0 到 1 之间的数学曲线。理解了这套系统你就能用代码画出任何你想要的运动感觉。从美院到 Flutter我始终觉得动效设计和色彩一样是需要感觉的但这种感觉可以被分解、被量化、被传授。Curve就是这种分解的结果——它让感觉变成了可以讨论、可以迭代、可以系统化的设计元素。关键要点底层逻辑Controller驱动→Tween映射→Curve节奏→ 属性值Tween家族从基础数值到自定义对象Flutter 的 Tween 系统极为灵活十五个Curve记住easeOut入场、easeInOutCubic标准、fastOutSlowInMaterial标准这三个就能应对80%的场景参数要有感觉持续时间、Curve 选择、属性映射每个决定背后都有感觉的支撑简化 vs 精细简单动效用AnimatedContainer复杂动效用AnimationController导师曾说动画是界面的呼吸。如今在 Flutter 里我可以精确地控制每一次呼吸的节奏——用Curve定义吸气和呼气的快慢用Tween定义胸廓的起伏幅度。代码成了另一种形式的身体记忆。下一篇预告设计Token运行时动态切换——让多主题系统真正活起来的前端工程化方案。