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

如何在Flutter中实现指定的容器切换动画效果?

实现容器展开/收起的流畅动画效果

我需要实现点击“Add Price”按钮后,容器平滑切换并展示价格输入界面,动画效果需与参考动图(容器从60px高度平滑展开至335px,收起时反向过渡)一致。目前通过布尔值clickedOnAddPrice切换两个容器,但尝试Tween动画后效果不符合预期,同时需要点击“May be later”按钮也能触发容器收起。

以下是修改后的实现代码:

Widget build(BuildContext context) {
  var mQSize = MediaQuery.of(context).size;
  var mQWidth = mQSize.width;
  var mQHeight = mQSize.height;

  return AnimatedContainer(
    duration: const Duration(milliseconds: 300),
    curve: Curves.easeInOut,
    height: clickedOnAddPrice ? 335 : 60,
    decoration: BoxDecoration(
      borderRadius: BorderRadius.circular(10),
      color: const Color.fromRGBO(247, 247, 247, 1),
      border: Border.all(color: const Color.fromRGBO(238, 240, 243, 1)),
    ),
    child: SingleChildScrollView(
      physics: const NeverScrollableScrollPhysics(),
      child: Column(
        children: [
          Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Container(
                margin: const EdgeInsets.only(left: 12, top: 8),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Graphic Designing',
                      style: GoogleFonts.poppins(
                        fontSize: 15,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 3),
                    Text(
                      'Logo Designing',
                      style: GoogleFonts.poppins(fontSize: 12),
                    ),
                  ],
                ),
              ),
              IconButton(
                onPressed: () {},
                icon: const Icon(Icons.delete_outline_rounded),
              ),
            ],
          ),
          if (clickedOnAddPrice) ...[
            const SizedBox(height: 10),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  'Price Details',
                  style: GoogleFonts.poppins(
                    fontSize: 15,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(width: 10),
                Tooltip(
                  message: 'This is informational data',
                  child: const Icon(Icons.info_outline, size: 18),
                ),
              ],
            ),
            // slider
            Column(
              children: [
                const SizedBox(height: 10),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    Text(
                      '0-1 Yrs',
                      style: GoogleFonts.poppins(
                        fontWeight: workExp == 0 ? FontWeight.bold : FontWeight.normal,
                      ),
                    ),
                    Text(
                      '2-4 Yrs',
                      style: GoogleFonts.poppins(
                        fontWeight: workExp == 0.5 ? FontWeight.bold : FontWeight.normal,
                      ),
                    ),
                    Text(
                      '5+ Yrs',
                      style: GoogleFonts.poppins(
                        fontWeight: workExp == 1 ? FontWeight.bold : FontWeight.normal,
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 10),
                Container(
                  margin: const EdgeInsets.symmetric(horizontal: 20),
                  width: double.infinity,
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(30),
                  ),
                  child: CupertinoSlider(
                    activeColor: Colors.blue,
                    divisions: 2,
                    value: workExp,
                    onChanged: (value) {
                      setState(() {
                        workExp = value;
                      });
                    },
                  ),
                ),
                const SizedBox(height: 20),
              ],
            ),
            Container(
              margin: const EdgeInsets.symmetric(horizontal: 12),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  SizedBox(
                    width: mQWidth / 2.5,
                    child: TextField(
                      decoration: InputDecoration(
                        fillColor: Colors.white,
                        filled: true,
                        enabledBorder: OutlineInputBorder(
                          borderSide: const BorderSide(color: Color.fromRGBO(238, 240, 243, 1)),
                          borderRadius: BorderRadius.circular(10),
                        ),
                        focusedBorder: OutlineInputBorder(
                          borderSide: const BorderSide(color: Color.fromRGBO(238, 240, 243, 1)),
                          borderRadius: BorderRadius.circular(10),
                        ),
                        border: OutlineInputBorder(
                          borderSide: const BorderSide(color: Color.fromRGBO(238, 240, 243, 1)),
                          borderRadius: BorderRadius.circular(10),
                        ),
                        labelText: 'Minimum',
                        labelStyle: GoogleFonts.poppins(color: Colors.black),
                        hintStyle: GoogleFonts.poppins(color: Colors.black),
                      ),
                    ),
                  ),
                  SizedBox(
                    width: mQWidth / 2.5,
                    child: TextField(
                      decoration: InputDecoration(
                        fillColor: Colors.white,
                        filled: true,
                        enabledBorder: OutlineInputBorder(
                          borderSide: const BorderSide(color: Color.fromRGBO(238, 240, 243, 1)),
                          borderRadius: BorderRadius.circular(10),
                        ),
                        focusedBorder: OutlineInputBorder(
                          borderSide: const BorderSide(color: Color.fromRGBO(238, 240, 243, 1)),
                          borderRadius: BorderRadius.circular(10),
                        ),
                        border: OutlineInputBorder(
                          borderSide: const BorderSide(color: Color.fromRGBO(238, 240, 243, 1)),
                          borderRadius: BorderRadius.circular(10),
                        ),
                        labelText: 'Maximum',
                        labelStyle: GoogleFonts.poppins(color: Colors.black),
                        hintStyle: GoogleFonts.poppins(color: Colors.black),
                      ),
                    ),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 20),
            Container(
              margin: const EdgeInsets.symmetric(horizontal: 15),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  TextButton(
                    onPressed: () {
                      setState(() {
                        clickedOnAddPrice = false;
                      });
                    },
                    child: Text(
                      'May be later',
                      style: GoogleFonts.poppins(
                        color: Colors.red,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                  ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(5),
                      ),
                      backgroundColor: Colors.red,
                    ),
                    onPressed: () {
                      // 提交逻辑
                    },
                    child: SizedBox(
                      height: 50,
                      width: mQWidth / 3.65,
                      child: Center(
                        child: Text(
                          'Submit',
                          style: GoogleFonts.poppins(
                            color: Colors.white,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            )
          ] else ...[
            const SizedBox(height: 8),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                TextButton(
                  onPressed: () {
                    setState(() {
                      clickedOnAddPrice = true;
                    });
                  },
                  child: Container(
                    decoration: BoxDecoration(
                      borderRadius: BorderRadiusDirectional.circular(4),
                      color: Colors.red,
                    ),
                    child: const Padding(
                      padding: EdgeInsets.symmetric(vertical: 5, horizontal: 12.0),
                      child: Text(
                        'Add Price',
                        style: TextStyle(color: Colors.white),
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ]
        ],
      ),
    ),
  );
}

关键修改说明

  • 使用AnimatedContainer替代普通Container,通过duration和curve控制动画时长与缓动效果,自动处理高度变化的平滑过渡
  • 将两个容器的公共部分(顶部标题和删除按钮)提取出来,只根据clickedOnAddPrice切换显示额外内容,避免重复代码
  • 给“May be later”按钮添加setState逻辑,切换clickedOnAddPrice为false触发收起动画
  • 用SingleChildScrollView包裹内容,避免容器高度变化时内容溢出,同时设置NeverScrollableScrollPhysics禁用滚动,保持容器高度完全由动画控制

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 02:45:56