简介
动画性能对用户体验有着重要影响。本文介绍如何优化 Flutter 动画性能,包括减少重建、使用 RepaintBoundary、优化动画曲线等方面。
基本原则
避免不必要的重建
// 不推荐
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
width: 100,
height: 100,
color: Colors.blue,
child: ComplexWidget(), // 每次动画都会重建
);
},
);
// 推荐
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
width: 100,
height: 100,
color: Colors.blue,
child: child,
);
},
child: ComplexWidget(), // 只构建一次
);
使用 RepaintBoundary
class OptimizedAnimation extends StatelessWidget {
const OptimizedAnimation({
super.key,
required this.animation,
required this.child,
});
final Animation<double> animation;
final Widget child;
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: FadeTransition(
opacity: animation,
child: child,
),
);
}
}
AnimatedWidget 是供子类继承的基类,不能直接实例化。上面的组件接收一个真实的 Animation<double> 和子组件;是否需要 RepaintBoundary 仍应根据 DevTools 的重绘情况测量。
性能监控
查看引擎上报的帧耗时
import 'package:flutter/scheduler.dart';
import 'package:flutter/foundation.dart';
class FrameTimingListener {
bool _started = false;
void start() {
if (_started) return;
SchedulerBinding.instance.addTimingsCallback(_onTimings);
_started = true;
}
void stop() {
if (!_started) return;
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
_started = false;
}
void _onTimings(List<FrameTiming> timings) {
for (final timing in timings) {
debugPrint(
'build=${timing.buildDuration.inMilliseconds}ms, '
'raster=${timing.rasterDuration.inMilliseconds}ms, '
'total=${timing.totalSpan.inMilliseconds}ms',
);
}
}
}
把监听器的 start() 放在页面或监控服务启动时,在对应的 dispose / stop 中调用 stop()。FrameTiming 提供引擎上报的 build、raster 和 total span 时长;它不是通过递归 post-frame 回调估算 FPS。也可以临时在 MaterialApp 上启用 showPerformanceOverlay: true 查看图形化帧耗时。
优化技巧
1. 使用 AnimatedWidget
class OptimizedAnimatedWidget extends AnimatedWidget {
OptimizedAnimatedWidget({
Key? key,
required Animation<double> animation,
}) : super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Transform.scale(
scale: animation.value,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
);
}
}
2. 分离动画逻辑
class AnimationManager {
final AnimationController controller;
final Animation<double> scaleAnimation;
final Animation<Color?> colorAnimation;
AnimationManager({required TickerProvider vsync})
: controller = AnimationController(
duration: Duration(seconds: 1),
vsync: vsync,
),
scaleAnimation = Tween<double>(
begin: 1.0,
end: 2.0,
).animate(CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
)),
colorAnimation = ColorTween(
begin: Colors.blue,
end: Colors.red,
).animate(CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
));
void dispose() {
controller.dispose();
}
}
3. 优化动画曲线
// 避免复杂的自定义曲线
class SimpleCustomCurve extends Curve {
@override
double transform(double t) {
return t * t; // 简单的平方曲线
}
}
完整示例
class OptimizedAnimationDemo extends StatefulWidget {
@override
_OptimizedAnimationDemoState createState() => _OptimizedAnimationDemoState();
}
class _OptimizedAnimationDemoState extends State<OptimizedAnimationDemo>
with SingleTickerProviderStateMixin {
late AnimationManager _animationManager;
final int itemCount = 100;
late List<CurvedAnimation> _itemAnimations;
@override
void initState() {
super.initState();
_animationManager = AnimationManager(vsync: this);
_itemAnimations = List.generate(
itemCount,
(index) {
final begin = index / itemCount * 0.5;
return CurvedAnimation(
parent: _animationManager.controller,
curve: Interval(
begin,
begin + 0.5,
curve: Curves.easeInOut,
),
);
},
);
}
@override
void dispose() {
for (final animation in _itemAnimations) {
animation.dispose();
}
_animationManager.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Optimized Animation'),
),
body: ListView.builder(
itemCount: itemCount,
itemBuilder: (context, index) {
return OptimizedAnimatedWidget(
animation: _itemAnimations[index],
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (_animationManager.controller.status ==
AnimationStatus.completed) {
_animationManager.controller.reverse();
} else {
_animationManager.controller.forward();
}
},
child: Icon(Icons.play_arrow),
),
);
}
}
最佳实践
- AnimatedBuilder 的静态子树走 child 参数,builder 只处理随动画变化的部分;不要把一次重建直接等同于掉帧,先在 DevTools 中定位 build/raster 热点。
- RepaintBoundary 只包”确实每帧都在变”的区域(轮播图、进度条),把它隔离成独立图层避免整页重绘;但每个 RepaintBoundary 都是一个图层,有内存和合成成本——给小动画或静态区包它反而亏。
- 列表滚动掉帧先查图片再查动画:商品图走 cached_network_image 磁盘+内存两级缓存,并用 cacheWidth 压到实际显示尺寸;一张 4000px 大图直接塞进 100px 的 Image,解码开销顶得上几十个动画。
- 在 profile 模式和目标设备上复测交互;性能浮层提供帧耗时线索,具体瓶颈再用 DevTools Performance 视图定位。
注意事项
- 不要为了动画而给整页或每个列表项机械地添加 RepaintBoundary;列表 delegate 默认会为子项添加重绘边界,额外边界应以 profile 结果为依据。
- 动画播之前准备必要资源;网络请求应在动画开始前完成。图片是否需要预解码,按实际设备、显示尺寸和内存占用决定。
- controller 不 dispose 等于 Ticker 泄漏:页面 pop 后回调还在每帧空转;长列表里”每个 item 一个 controller”的写法泄漏起来非常可观——能用一个 controller 驱动全场就别用 N 个。
- 低端机要有降级预案:同时跑 3~4 个以上的复杂动效、或检测到连续掉帧时,降级比硬播强——去掉弹性曲线、缩短时长、极端情况直接静态兜底,保证功能先可用。
实战:电商列表卡顿,真凶往往不是动画
在电商类项目(美妆电商项目 这类 GetX 栈)里,首页商品流滚动掉帧,我的第一反应不是优化动画,而是先查图片:商品图全走 cached_network_image(这些项目的统一依赖),磁盘加内存两级缓存——列表滚动时图片解码才是 jank 的大头,动画本身反而排后面。
动画方面的 checklist 我其实很短:第一,AnimatedBuilder 的 child 参数把静态子树提出来,别让整张商品卡片每帧重建;第二,轮播图区域包一层 RepaintBoundary,避免轮播每帧重绘把整个首页拖下水——但别滥用,RepaintBoundary 本身有图层成本,给小动画包它反而亏。顺序别反了:先保证图片和图片缓存,再谈动画优化。
总结
通过合理的性能优化,可以显著提升动画的流畅度和用户体验。掌握这些优化技巧对于开发高质量的 Flutter 应用至关重要。
