You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }
}

优化说明

  1. 匀速实现:通过计算各边占周长的比例,将动画进度与实际路径长度绑定,确保相同进度变化对应相同的移动距离,实现匀速运动。
  2. 等距分布:每个小球的进度偏移量为i / numBalls,对应路径上的间隔为perimeter / numBalls,保证所有小球沿矩形周长均匀分布。

内容的提问来源于stack exchange,提问作者Ahm2025

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 01:12:18