Flutter 3.47 动画引擎盘点:Impeller 全平台,和那些"新特性"

引擎层: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。

性能:别信"自动优化",这几招才真实有效

实际要做的还是那几条:

  1. RepaintBoundary 只认手动 。列表动画的 itemBuilder 里给每个 item 包一层,效果立竿见影:
css 复制代码
itemBuilder: (context, index) => RepaintBoundary(
  child: AnimatedListItem(item: items[index]),
),
  1. 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,
  ),
)
  1. 长循环动画注意 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.EnableImpeller meta-data 还是能用的调试开关,但没事别关。

结尾

动画引擎到 3.47 的真实变化,一句话总结:底层靠 Impeller 把全平台铺完,框架层零零散散补了 Hero 曲线、AnimationStyle 的 merge、无障动效这些实用件 。对还在 3.24 及更早版本上扒拉 reverseCurve、手写 RectTween 的开发者来说,升到 3.47 的收益是实打实的------引擎不卡了,样板代码少了。

相关推荐
程序员老刘3 小时前
Flet 1.0 思路很好,可惜来晚了
flutter·ai编程·客户端
m0_738185829 小时前
Flutter 鸿蒙化实战:qr_code_scanner_plus 适配 OpenHarmony,二维码扫描
flutter·华为·harmonyos·鸿蒙
恋猫de小郭9 小时前
Flutter + EmbeddingGemma 2,谷歌发布完全端侧的 AI Edge Foresight
android·前端·flutter
君赏1 天前
基于 FlutterPatch 快速接入 Flutter 热更新
flutter
律宏阔1 天前
Dart FFI 内存管理:用 using + Arena 替代嵌套 try-finally
前端·flutter
律宏阔1 天前
Flutter 调用 Go:从 c-shared + ffigen 到 @Native + Native Assets 踩坑记录
前端·flutter
律宏阔1 天前
Dart FFI 回调无法使用局部变量?使用 NativeCallable.isolateLocal
前端·flutter
天空之城--1 天前
Android一周动态:AI编程、Compose与车载生态新信号
android·flutter
码林鼠1 天前
Flutter之Widget/State 入门文档
flutter