如何用Transform.translate实现Flutter列表的完美曲线移动动画?
实现Flutter列表项的曲线轨迹移动优化
我正在开发一个Flutter动画,需要让列表项沿曲线轨迹移动。目前已实现平移动画,但效果不理想——列表项从A点到B点是直线移动,而非沿曲线移动。需要优化以实现完美的曲线移动效果,当前代码如下:
class CurvedAn extends StatefulWidget { const CurvedAn({super.key}); @override State<CurvedAn> createState() => _CurvedAnState(); } class _CurvedAnState extends State<CurvedAn> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _animation; List<Widget> _items = [ Icon(Icons.account_circle), Icon(Icons.account_box_sharp), Icon(Icons.access_time), Icon(Icons.ac_unit_sharp), Icon(Icons.account_balance_wallet_sharp), ]; bool _animating = false; @override void initState() { _controller = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, ); _animation = Tween<double>(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _controller, curve: Curves.easeInOut), ); _controller.addStatusListener(_listener); super.initState(); } @override void dispose() { _controller.dispose(); super.dispose(); } void _listener(AnimationStatus status) { _animating = status.isAnimating; setState(() {}); } void _moveRight() { _controller.reset(); _controller.forward(); } @override Widget build(BuildContext context) { return Column( children: [ FloatingActionButton( onPressed: () { _moveRight(); }, child: Icon(Icons.navigate_next), ), SizedBox(height: 8.0), ClipPath( clipper: CurveClipper(), child: Container( width: 360.0, height: 80.0, color: Colors.red, child: Stack( children: [ for (int i = 0; i < _items.length; i++) AnimatedBuilder( animation: _animation, builder: (context, child) { double x = 0; double y = 0; if (i == 0) { if (_animating) { x = 144 + (72 * _animation.value); y = -20 + (12 * _animation.value); } else { x = 72 * 2; y = -20; } } else if (i == 1) { if (_animating) { x = 216 + (72 * _animation.value); y = -8 + (18 * _animation.value); } else { x = 72 * 3; y = -8; } } else if (i == 2) { if (_animating) { x = 288 + (72 * _animation.value); y = 10 + (20 * _animation.value); } else { x = 72 * 4; y = 10; } } else if (i == 4) { if (_animating) { x = 72 + (72 * _animation.value); y = -8 - (12 * _animation.value); } else { x = 72 * 1; y = -8; } } else if (i == 3) { if (_animating) { x = 72 * _animation.value; y = 10 - (20 * _animation.value); } else { x = 0; y = 10; } } return Transform.translate( offset: Offset(x, y), child: Container( height: 72, width: 72, decoration: BoxDecoration( border: Border.all(color: Colors.transparent), ), child: _items[i], ), ); }, ), if (_animating) AnimatedBuilder( animation: _animation, builder: (context, child) { double x = 0; double y = 0; x = -60 + (60 * _animation.value); y = 30 + (-20 * _animation.value); return Transform.translate( offset: Offset(x, y), child: Container( height: 72, width: 72, decoration: BoxDecoration( border: Border.all(color: Colors.transparent), ), child: _items[0], ), ); }, ), ], ), ), ), ], ); } }
优化方案:替换线性插值为曲线轨迹计算
当前代码中x、y坐标的计算都是线性插值(值 = 初始值 + 增量 * animation.value),所以移动轨迹必然是直线。要实现曲线轨迹,需要让至少一个轴的变化遵循非线性规律,以下是两种可行方案:
方案1:为Y轴单独应用曲线动画
给不同列表项的Y轴变化绑定不同的曲线,让Y轴移动速度随动画进度非线性变化,示例代码如下:
class _CurvedAnState extends State<CurvedAn> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _xAnimation; // 为每个列表项定义独立的Y轴曲线动画 late Animation<double> _item0YAnim; late Animation<double> _item1YAnim; late Animation<double> _item2YAnim; late Animation<double> _item3YAnim; late Animation<double> _item4YAnim; List<Widget> _items = [ Icon(Icons.account_circle), Icon(Icons.account_box_sharp), Icon(Icons.access_time), Icon(Icons.ac_unit_sharp), Icon(Icons.account_balance_wallet_sharp), ]; bool _animating = false; @override void initState() { _controller = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, ); // X轴保持线性移动 _xAnimation = Tween<double>(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _controller, curve: Curves.linear), ); // 为每个项绑定不同的曲线,实现差异化的曲线轨迹 _item0YAnim = Tween<double>(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _controller, curve: Curves.easeInOutCubic), ); _item1YAnim = Tween<double>(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _controller, curve: Curves.easeOutQuad), ); _item2YAnim = Tween<double>(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _controller, curve: Curves.easeInQuad), ); _item3YAnim = Tween<double>(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _controller, curve: Curves.easeInOutSine), ); _item4YAnim = Tween<double>(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _controller, curve: Curves.easeOutSine), ); _controller.addStatusListener(_listener); super.initState(); } @override void dispose() { _controller.dispose(); super.dispose(); } void _listener(AnimationStatus status) { _animating = status.isAnimating; setState(() {}); } void _moveRight() { _controller.reset(); _controller.forward(); } @override Widget build(BuildContext context) { return Column( children: [ FloatingActionButton( onPressed: _moveRight, child: Icon(Icons.navigate_next), ), SizedBox(height: 8.0), ClipPath( clipper: CurveClipper(), child: Container( width: 360.0, height: 80.0, color: Colors.red, child: Stack( children: [ for (int i = 0; i < _items.length; i++) AnimatedBuilder( animation: Listenable.merge([_xAnimation, _item0YAnim, _item1YAnim, _item2YAnim, _item3YAnim, _item4YAnim]), builder: (context, child) { double x = 0; double y = 0; switch(i) { case 0: x = 144 + (72 * _xAnimation.value); y = -20 + (12 * _item0YAnim.value); break; case 1: x = 216 + (72 * _xAnimation.value); y = -8 + (18 * _item1YAnim.value); break; case 2: x = 288 + (72 * _xAnimation.value); y = 10 + (20 * _item2YAnim.value); break; case 4: x = 72 + (72 * _xAnimation.value); y = -8 - (12 * _item4YAnim.value); break; case 3: x = 72 * _xAnimation.value; y = 10 - (20 * _item3YAnim.value); break; } return Transform.translate( offset: Offset(x, y), child: Container( height: 72, width: 72, child: _items[i], ), ); }, ), if (_animating) AnimatedBuilder( animation: _controller, builder: (context, child) { // 示例:用二阶贝塞尔曲线定义轨迹 Path path = Path() ..moveTo(-60, 30) ..quadraticBezierTo(-30, 10, 0, 10); PathMetric metric = path.computeMetrics().first; Offset pos = metric.getTangentForOffset(metric.length * _xAnimation.value)?.position ?? Offset(-60,30); return Transform.translate( offset: pos, child: Container( height: 72, width: 72, child: _items[0], ), ); }, ), ], ), ), ), ], ); } }
方案2:用Path定义自定义曲线轨迹
如果需要更复杂的轨迹(如圆弧、多段贝塞尔曲线),可以先绘制Path,再通过PathMetric获取动画过程中的实时坐标,完全自定义移动路径。
关键原理
线性移动的本质是x、y的变化率恒定,轨迹为直线;曲线轨迹需要至少一个轴的变化率随动画进度非线性变化,通过CurvedAnimation或Path可以轻松实现这一点。
内容的提问来源于stack exchange,提问作者Roy Krueger
相关产品推荐
相关产品推荐

