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

如何用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 00:42:03