如何在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
相关产品推荐
相关产品推荐

