引擎层:Impeller 全平台铺开,这才叫底层变化
动画卡不卡,九成问题在渲染引擎。Impeller 这两年的推进路线是这样的:
| 时间点 | 事件 |
|---|---|
| 3.10(2023.5) | iOS 默认启用 |
| 3.27(2024.12) | Android API 29+ 默认启用(此时低版本或无 Vulkan 设备仍回退到 Skia 旧版 OpenGL 渲染器) |
| 3.29(2025.2) | iOS 移除 Skia,Impeller 成为唯一引擎;Android 无 Vulkan 设备改走 Impeller 的 OpenGL ES 后端,设备支持率 100% |
| 3.44(2026.5) | 增强 Vulkan 支持,Hybrid Composition++ 落地 |
| 3.47(2026.8) | macOS / Linux / Windows 默认启用 Impeller |
Android 侧的 Skia 移除按 2026 年路线图收尾,Web 端还是 Skia/CanvasKit + Skwasm,跟 Impeller 无关。
对动画的实际意义在于:Impeller 在构建期预编译 shader,杜绝运行时编译卡顿,帧节奏比 Skia 时代稳定得多。高刷屏上跑满 120fps 理论上没问题------强调"理论上",是因为这中间还有一层系统调度。
避坑:高刷不是"锁"出来的
现实里 Flutter 应用在部分高刷手机上会被系统压回 60Hz,这是官方 issue flutter/flutter#160952 记录的老问题。真遇到的话,社区方案是 refresh_rate 这类插件,往系统提交高刷新率偏好:
csharp
import 'package:refresh_rate/refresh_rate.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
WidgetsBinding.instance.addPostFrameCallback((_) async {
// 请求系统放开刷新率,OS 仍会根据功耗/温度自行决定
await RefreshRate.enable();
});
}
注意它只是"偏好",不是保证。iOS 这边还有个更容易被忽略的坑:如果 Info.plist 里没加 CADisableMinimumFrameDurationOnPhone 这个 key,ProMotion 机型直接会被摁在 60Hz------这跟 Android 那边的调度问题是两码事,得分开排查,别一股脑都甩给"引擎不行"。动画调优之前先用 DevTools 的 Performance 页确认帧率瓶颈在哪,别一上来怀疑渲染引擎。
避坑:3.47 的 GLES render-to-texture 翻转改动
如果你在用 Flutter GPU(flutter_gpu)写自定义 FragmentShader,3.47 有个 breaking change 必须知道:Impeller 的 OpenGL ES 后端把 render-to-texture 内容从 bottom-up 改成了 top-down,和 Metal/Vulkan 对齐。以前为了补偿方向手动翻 Y 的 shader,现在会上下颠倒:
ini
// 3.47 之前:GLES 后端要手动翻 Y
void main() {
vec2 uv = FlutterFragCoord().xy / u_size;
#ifdef IMPELLER_TARGET_OPENGLES
uv.y = 1.0 - uv.y;
#endif
fragColor = texture(u_texture, uv);
}
ini
// 3.47 起:直接删掉翻转,所有后端统一 top-down
void main() {
vec2 uv = FlutterFragCoord().xy / u_size;
fragColor = texture(u_texture, uv);
}
只影响采样 render target 的自定义 shader,常规 widget 和 2D 渲染完全无感,基本可以忽略这条。
框架层:最近几个版本真正跟动画有关的更新
1. Hero 终于能直接传 curve 了(3.44)
以前想让 Hero 飞行动画换个缓动,要么在 PageRoute 上动手脚,要么自造 RectTween 重写 lerp,非常别扭。3.44 起 Hero 直接挂 curve / reverseCurve:
less
// 列表页:商品图飞详情页
Hero(
tag: 'product-$id',
curve: Curves.easeOutCubic, // 进场缓动,3.44 新增
reverseCurve: Curves.easeInCubic, // 返回时缓动
child: Image.network(product.cover, width: 120),
)
返回路径的缓动跟进场分开控制,这个在交互稿里其实很常见------比如"弹出去、缓收回来"。之前做不到干净,现在两行参数搞定。
2. AnimationStyle 补了 merge,lerp 也终于像话了(3.47)
AnimationStyle 是 3.22 引入的统一动效配置,把 duration / curve / reverseDuration / reverseCurve 打包成一个对象,挂到 Material 组件的动效上。copyWith 其实从那时候就在了,不是新东西;3.47 真正加的是 merge()------社区一直吵着要个类似 TextStyle.merge 的方法,这次总算给了。顺带把 lerp() 也修了:以前的 lerp 说白了就是个"非 0 即 1"的硬切换,现在才是真正按比例插值。这下可以在主题层级做样式组合了:
less
// MaterialApp 全局动效样式,弹层、菜单等内置动效统一生效
MaterialApp(
themeAnimationStyle: const AnimationStyle(
duration: Duration(milliseconds: 240),
reverseDuration: Duration(milliseconds: 180),
curve: Curves.easeOutCubic,
reverseCurve: Curves.easeInCubic,
),
home: const HomePage(),
)
单点覆盖用 copyWith,不用重写整份样式:
less
const base = AnimationStyle(
duration: Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
);
// 某个场景单独调慢,copyWith 一直都能这么用
final slow = base.copyWith(duration: const Duration(milliseconds: 420));
showModalBottomSheet<void>(
context: context,
animationStyle: slow,
builder: (context) => const FilterPanel(),
);
showBottomSheet、showModalBottomSheet、PopupMenuButton、ExpansionTile 这些都吃 AnimationStyle。全局一份默认 + 场景级 copyWith 覆盖,是我目前最顺手的动效配置方式。
3. 页面转场:M3 默认早就不是 Zoom 了(3.29 起)
3.29 引入的 FadeForwardsPageTransitionsBuilder 现在是 Material 3 在 Android 上的主要页面转场------进入页右滑淡入、退出页右移淡出,顺带解决了当年 ZoomPageTransitionsBuilder 掉帧的老毛病。严格说,后来的版本把 Android 的默认转场注册成了 PredictiveBackPageTransitionsBuilder(为了支持预测性返回手势),普通 push 没有预测手势时才回退到 FadeForwards 的效果,所以"默认就是 FadeForwards"这个说法已经不准了。有个容易被忽略的副作用:换成新默认之后,转场时长也从 300ms 涨到了 450ms,如果产品经理盯着转场速度较真,这个改动最好提前打个招呼。用默认 Theme 就行,不用手动配;想明确指定的话:
less
ThemeData(
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android: FadeForwardsPageTransitionsBuilder(),
},
),
)
4. 动效无障碍:跟着系统"减弱动态效果"走(3.44 增强)
3.44 给 iOS 补了 motion accessibility 特性。这条跟动画开发者关系最直接,也最容易被忽略:用户在系统里开了"减弱动态效果",你的动画就不该再跑。
javascript
void _trigger() {
if (MediaQuery.disableAnimationsOf(context)) {
// 系统要求减弱动效:直接跳到终态,不做过渡
_controller.value = 1.0;
} else {
_controller.forward();
}
}
上架过 iOS 的团队应该都懂,这条过不了审查基本就是打回。转场类的动效同理可以收敛成淡入淡出甚至直接切页。
物理动画与隐式动画:正本清源
用阻尼比替代裸阻尼值:
less
final spring = SpringSimulation(
const SpringDescription.withDampingRatio(
mass: 1,
stiffness: 180,
ratio: 0.6, // 阻尼比 0~1:越接近 1 越"肉",越小回弹越欢
),
0, // 起点
1, // 终点
0, // 初速度
);
_controller.animateWith(spring);
为什么推荐 withDampingRatio:裸 damping 值没有直观手感,调参全靠猜,阻尼比则直接对应"够不够弹"。同样逻辑,滚动效果调弹簧手感时用 ScrollSpringSimulation 或者干脆 BouncingScrollPhysics,别自己造轮子。
另外强调一句老生常谈但很多人还没做到的事:能上隐式动画就别上 AnimationController 。slide、缩放、透明度这类"从 A 到 B"的诉求,AnimatedSlide、AnimatedScale、AnimatedOpacity、AnimatedContainer 已经是正确解:
less
AnimatedSlide(
// offset 是比例位移:-0.5 表示上移自身高度的一半
offset: _expanded ? Offset.zero : const Offset(0, -0.5),
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
child: const DetailCard(),
)
只有动画时长/曲线需要被外部打断、手动反向、或者手速跟随时,才轮到 AnimationController。
性能:别信"自动优化",这几招才真实有效
实际要做的还是那几条:
RepaintBoundary只认手动 。列表动画的itemBuilder里给每个 item 包一层,效果立竿见影:
css
itemBuilder: (context, index) => RepaintBoundary(
child: AnimatedListItem(item: items[index]),
),
AnimatedBuilder的child参数要会用 。builder 每次 tick 都会跑,但传进来的child子树不会跟着重建------把不动的部分拎出来:
less
// 记得在文件头部加:import 'dart:math' as math;
AnimatedBuilder(
animation: _controller,
child: const Padding(
padding: EdgeInsets.all(48),
child: FlutterLogo(size: 120),
),
builder: (context, child) => Transform.rotate(
angle: _controller.value * math.pi,
child: child,
),
)
- 长循环动画注意
AnimationController的销毁 。放在State里的 controller,dispose()里不调_controller.dispose()就是内存泄漏和幽灵 tick 的来源。CurvedAnimation自己不建 Ticker、不持有 controller 的所有权,跟着用的 controller 走就行,不用单独 dispose。
升级到 3.47 的注意点
跟动画 API 本身相关的 breaking change 基本没有,风险主要在工程链上:
- AGP 9 这条线,版本号得捋清楚:3.41(2026.2)那会儿官方博客原话是"先别把 Flutter 应用升到 AGP 9",插件生态兼容性审计还没做完,那个版本只是把新建插件项目的 Gradle 默认换成了 Kotlin DSL,算打个前站;3.44 才是真正支持 AGP 9 的起点,靠
android.builtInKotlin=false兼容旧版 Kotlin Gradle 插件(KGP)过渡;到了 3.47,android.builtInKotlin=true才能打开,真正甩掉 KGP 用内置 Kotlin 编译。老工程升级按 官方迁移指南 一步步走,别想着一口气跳到位。 - 3.47 开始 Material / Cupertino 设计库拆成独立包(
material_ui/cupertino_ui,已 1.0),目前仍是 opt-in ,11 月版本才正式弃用核心 SDK 里的设计库。动画基建(Curves、Easing、AnimationController)都在animation库里,不受拆包影响。想尝鲜可以跑:
css
dart fix --apply --code=migrate_design_widgets
- iOS 上没有 Skia 了,3.29 之后
FLTEnableImpeller(Info.plist 键)这个开关已失效,别在 Info.plist 里留垃圾配置。Android 侧io.flutter.embedding.android.EnableImpellermeta-data 还是能用的调试开关,但没事别关。
结尾
动画引擎到 3.47 的真实变化,一句话总结:底层靠 Impeller 把全平台铺完,框架层零零散散补了 Hero 曲线、AnimationStyle 的 merge、无障动效这些实用件 。对还在 3.24 及更早版本上扒拉 reverseCurve、手写 RectTween 的开发者来说,升到 3.47 的收益是实打实的------引擎不卡了,样板代码少了。