Flutter矩形路径动画:实现容器等距分布与匀速运动
Flutter矩形路径小球动画优化:等距分布+匀速运动
问题:制作矩形路径的小球动画时,出现两个异常:
- 垂直方向小球间距小于水平方向
- 小球在垂直方向的运动速度慢于水平方向
需求:实现小球沿矩形总周长等距分布,且沿路径运动速度恒定。
现有代码
class AnimatedContainer extends StatefulWidget { const AnimatedContainer({super.key}); @override State<AnimatedContainer> createState() => _AnimatedContainerState(); } class _AnimatedContainerState extends State<AnimatedContainer> with TickerProviderStateMixin { late AnimationController _controller; double dotSize = 10.0; double x = 286; int numBalls = 40; // Number of balls @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(seconds: 7), //Speed of balls )..addListener(() { setState(() {}); }); _controller.repeat(); } double _getPositionX(double value) { value = (value % 1); // Use % to keep value within [0, 1] if (value < 0.25) { //@ value (x) = 0 ,returned value (y) = 0 //@ value (x) = 0.25 , returned value (y) = 286 //slope = (286-0)/(0.25-0) = 4*286 //path equation : y = slope*x + constant //path equation : y = (4*286)*value + c , by sub c= 0 // y = 4*286*value //use the previous steps in the next conditions return 4 * 286 * value; } else if (value < 0.5) { return 286; } else if (value < 0.75) { return (-4 * value + 3) * 286; } else { return 0; } } double _getPositionY(double value) { value = (value % 1); if (value < 0.25) { return 0; } else if (value < 0.5) { //@ value (x) = 0.25 ,returned value (y) = 0 //@ value (x) = 0.5 , returned value (y) = 137 //slope = (137-0)/(0.5-0.25) = 4*137 //path equation : y = slope*x + constant //path equation : y = (4*137)*value + c , by sub c = -137 // y = 4*137*value - 137 = (4*value - 1)*137 //use the previous steps in the next conditions return (4 * value - 1) * 137; } else if (value < 0.75) { return 137; } else { return (4 - 4 * value) * 137; } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { double screenwidth = MediaQuery.of(context).size.width; double screenheight = MediaQuery.of(context).size.height; double paddleft = screenwidth / 2 - 143; double paddtop = screenheight / 2 - 68.5; return Scaffold( body: Center( child: Stack( children: [ for (int i = 0; i < numBalls; i++) Positioned( left: (paddleft) + _getPositionX(_controller.value - i / numBalls), // Adjust position top: (paddtop) + _getPositionY( _controller.value - i / numBalls), // Adjust position child: Container( width: dotSize, height: dotSize, decoration: const BoxDecoration( shape: BoxShape.circle, color: Colors.red, ), ), ), ], ), ), ); } }
解决方案
核心问题分析
原有代码将动画时间均分为4段(每段占0.25),但矩形水平边长(286)与垂直边长(137)长度差异大,导致相同时间内小球移动的实际距离不一致,直接造成速度和间距异常。正确逻辑应为按边长占周长的比例分配动画时间,让进度变化与实际路径长度线性绑定。
优化后完整代码
class AnimatedContainer extends StatefulWidget { const AnimatedContainer({super.key}); @override State<AnimatedContainer> createState() => _AnimatedContainerState(); } class _AnimatedContainerState extends State<AnimatedContainer> with TickerProviderStateMixin { late AnimationController _controller; double dotSize = 10.0; int numBalls = 40; // Number of balls final double rectWidth = 286; final double rectHeight = 137; late final double perimeter; late final double ratioTop; late final double ratioRight; late final double ratioBottom; late final double ratioLeft; @override void initState() { super.initState(); // 计算矩形周长及各边占总路径的比例 perimeter = 2 * (rectWidth + rectHeight); ratioTop = rectWidth / perimeter; ratioRight = rectHeight / perimeter; ratioBottom = rectWidth / perimeter; ratioLeft = rectHeight / perimeter; _controller = AnimationController( vsync: this, duration: const Duration(seconds: 7), // 整体运动速度 )..addListener(() { setState(() {}); }); _controller.repeat(); } double _getPositionX(double progress) { progress = progress % 1; if (progress < ratioTop) { // 上边:从左到右,进度与移动距离线性关联 return rectWidth * (progress / ratioTop); } else if (progress < ratioTop + ratioRight) { // 右边:保持x坐标最大 return rectWidth; } else if (progress < ratioTop + ratioRight + ratioBottom) { // 下边:从右到左 final double segmentProgress = (progress - ratioTop - ratioRight) / ratioBottom; return rectWidth * (1 - segmentProgress); } else { // 左边:保持x坐标最小 return 0; } } double _getPositionY(double progress) { progress = progress % 1; if (progress < ratioTop) { // 上边:保持y坐标最小 return 0; } else if (progress < ratioTop + ratioRight) { // 右边:从上到下,进度与移动距离线性关联 final double segmentProgress = (progress - ratioTop) / ratioRight; return rectHeight * segmentProgress; } else if (progress < ratioTop + ratioRight + ratioBottom) { // 下边:保持y坐标最大 return rectHeight; } else { // 左边:从下到上 final double segmentProgress = (progress - ratioTop - ratioRight - ratioBottom) / ratioLeft; return rectHeight * (1 - segmentProgress); } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { double screenwidth = MediaQuery.of(context).size.width; double screenheight = MediaQuery.of(context).size.height; double paddleft = screenwidth / 2 - rectWidth / 2; double paddtop = screenheight / 2 - rectHeight / 2; return Scaffold( body: Center( child: Stack( children: [ for (int i = 0; i < numBalls; i++) Positioned( left: paddleft + _getPositionX(_controller.value - i / numBalls), top: paddtop + _getPositionY(_controller.value - i / numBalls), child: Container( width: dotSize, height: dotSize, decoration: const BoxDecoration( shape: BoxShape.circle, color: Colors.red, ), ), ), ], ), ), ); } }
优化说明
- 匀速实现:通过计算各边占周长的比例,将动画进度与实际路径长度绑定,确保相同进度变化对应相同的移动距离,实现匀速运动。
- 等距分布:每个小球的进度偏移量为
i / numBalls,对应路径上的间隔为perimeter / numBalls,保证所有小球沿矩形周长均匀分布。
内容的提问来源于stack exchange,提问作者Ahm2025
相关产品推荐
相关产品推荐

