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

Flutter中如何为两个不同尺寸的Widget交换添加动画效果

Flutter实现不同尺寸Text组件的平滑交换动画

需求说明

需要实现两个不同尺寸的Text组件交换功能:点击下方的文本区域触发交换操作,交换时文本的位置和尺寸要同步平滑过渡。之前尝试用AnimatedPositioned,但因为两个文本尺寸差异大,动画效果很怪异。参考动画效果为:上方大尺寸数字与下方小尺寸数字交换位置,同时各自切换到对方的字体大小,整个过程自然流畅。

原示例代码

class SwapWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            SizedBox(
              height: 80,
              child: FittedBox(
                fit: BoxFit.scaleDown,
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: const [
                    Text(
                      "\$",
                      style: TextStyle(
                        fontSize: 40,
                        fontWeight: FontWeight.w400,
                      ),
                    ),
                    Text(
                      "400",
                      style: TextStyle(
                        fontSize: 80,
                        fontWeight: FontWeight.w500,
                        height: 1,
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
        const SizedBox(height: 20),
        GestureDetector(
          onTap: () {},
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: const [
              Text(
                '2345',
                style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.normal,
                ),
              ),
              Icon(Icons.swap_vert),
            ],
          ),
        ),
      ],
    );
  }
}

解决方案

改用StatefulWidget管理状态,结合AnimationController和Transform组件同时控制位置偏移与缩放动画,解决尺寸差异导致的动画异常问题。

修改后的完整代码

class SwapWidget extends StatefulWidget {
  @override
  State<SwapWidget> createState() => _SwapWidgetState();
}

class _SwapWidgetState extends State<SwapWidget> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _positionAnim;
  late Animation<double> _topScaleAnim;
  late Animation<double> _bottomScaleAnim;

  String _topNum = "400";
  String _bottomNum = "2345";
  bool _swapped = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    // 控制上下偏移的动画
    _positionAnim = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
    // 上方文本从大到小的缩放动画(80→20,比例0.25)
    _topScaleAnim = Tween<double>(begin: 1, end: 0.25).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
    // 下方文本从小到大的缩放动画(20→80,比例4)
    _bottomScaleAnim = Tween<double>(begin: 0.25, end: 1).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
  }

  void _triggerSwap() {
    if (_controller.isAnimating) return;
    setState(() {
      _swapped = !_swapped;
      // 交换文本内容
      final temp = _topNum;
      _topNum = _bottomNum;
      _bottomNum = temp;
    });
    // 根据状态正向/反向执行动画
    _swapped ? _controller.forward() : _controller.reverse();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    const double topTextSize = 80;
    const double bottomTextSize = 20;
    const double swapOffset = 100; // 上下交换的距离

    return Column(
      children: [
        SizedBox(
          height: 80,
          child: AnimatedBuilder(
            animation: _controller,
            builder: (context, child) {
              return Stack(
                alignment: Alignment.centerLeft,
                children: [
                  // $符号固定位置,不参与动画
                  const Text(
                    "\$",
                    style: TextStyle(
                      fontSize: 40,
                      fontWeight: FontWeight.w400,
                    ),
                  ),
                  // 原上方数字的动画层
                  Transform.translate(
                    offset: Offset(40, _positionAnim.value * swapOffset),
                    child: Transform.scale(
                      scale: _topScaleAnim.value,
                      child: Text(
                        _swapped ? _bottomNum : _topNum,
                        style: const TextStyle(
                          fontSize: topTextSize,
                          fontWeight: FontWeight.w500,
                          height: 1,
                        ),
                      ),
                    ),
                  ),
                  // 原下方数字的动画层
                  Transform.translate(
                    offset: Offset(40, (1 - _positionAnim.value) * -swapOffset),
                    child: Transform.scale(
                      scale: _bottomScaleAnim.value,
                      child: Text(
                        _swapped ? _topNum : _bottomNum,
                        style: const TextStyle(
                          fontSize: bottomTextSize,
                          fontWeight: FontWeight.normal,
                        ),
                      ),
                    ),
                  ),
                ],
              );
            },
          ),
        ),
        const SizedBox(height: 20),
        GestureDetector(
          onTap: _triggerSwap,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              // 下方文本随动画隐藏/显示
              Opacity(
                opacity: _swapped ? 0 : 1,
                child: Text(
                  _bottomNum,
                  style: const TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.normal,
                  ),
                ),
              ),
              const Icon(Icons.swap_vert),
            ],
          ),
        ),
      ],
    );
  }
}

关键说明

  • 用AnimationController统一控制动画时长,搭配CurvedAnimation实现自然的缓动效果
  • 通过Transform.translate控制两个文本的上下位置偏移,Transform.scale同步匹配字体大小的缩放比例
  • 利用Stack将固定的$符号与两个动画文本层叠加,保证布局结构稳定
  • 点击触发时先交换文本内容,再执行动画,同时判断动画状态避免重复触发

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 20:45:15