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

Flutter卡片高度展开动画实现:点击卡片旋转箭头并展开子卡片

解决Flutter动态高度卡片的展开动画问题

哈哈,这个场景我在做Flutter项目时也遇到过!因为卡片里的内容高度不固定,直接写死动画高度肯定不行,得让Flutter先帮我们算出真实高度再做动画。给你两个靠谱的原生解决方案:

方案一:用GlobalKey获取真实高度(最灵活)

核心思路是先让目标卡片完成一次布局,获取它的真实高度,再把这个高度作为动画的目标值来驱动展开/收起效果。

步骤和代码修改:

  1. 先定义必要的变量和全局Key:
bool _notfall = false;
late AnimationController _rotateArrowController;
late Animation<double> _rotateAnimation;
late AnimationController _cardAnimationController;
late Animation<double> _cardAnimation;
final GlobalKey _expandCardKey = GlobalKey(); // 用来获取卡片尺寸的Key
double _targetCardHeight = 0;
  1. 在initState里初始化动画控制器:
@override
void initState() {
  super.initState();
  // 箭头旋转动画控制器
  _rotateArrowController = AnimationController(
    duration: const Duration(milliseconds: 300),
    vsync: this,
  );
  _rotateAnimation = Tween<double>(begin: 0, end: 1).animate(
    CurvedAnimation(parent: _rotateArrowController, curve: Curves.easeInOut),
  );

  // 卡片高度动画控制器
  _cardAnimationController = AnimationController(
    duration: const Duration(milliseconds: 300),
    vsync: this,
  );
}
  1. 修改点击事件的逻辑,先获取高度再启动动画:
onTap: () {
  setState(() {
    _notfall = !_notfall;
  });

  if (_notfall) {
    // 等待当前帧布局完成,才能获取到卡片的真实尺寸
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final RenderBox renderBox = _expandCardKey.currentContext!.findRenderObject() as RenderBox;
      _targetCardHeight = renderBox.size.height;
      
      // 绑定高度动画的目标值
      _cardAnimation = Tween<double>(begin: 0, end: _targetCardHeight).animate(
        CurvedAnimation(parent: _cardAnimationController, curve: Curves.easeInOut),
      );
      
      // 启动箭头旋转和卡片展开动画
      _rotateArrowController.forward();
      _cardAnimationController.forward();
    });
  } else {
    // 收起动画
    _rotateArrowController.reverse();
    _cardAnimationController.reverse();
  }
},
  1. 最后修改展开卡片的代码,用AnimatedBuilder和ClipRect实现裁剪动画:
if (_notfall) {
  return AnimatedBuilder(
    animation: _cardAnimation,
    builder: (context, child) {
      return ClipRect(
        // 用动画值控制容器高度,裁剪超出部分
        child: SizedBox(
          height: _cardAnimation.value,
          child: child,
        ),
      );
    },
    child: Card(
      key: _expandCardKey, // 绑定全局Key
      color: Colors.white70,
      child: Container(
        // 这里放你的任意Widget内容,高度会自动计算
        child: Column(
          children: const [
            Text("这是第一行内容"),
            Text("这是第二行内容"),
            // ... 更多Widget
          ],
        ),
      ),
    ),
  );
} else {
  return const SizedBox.shrink(); // 收起时隐藏
}

方案二:用SizeTransition简化实现(更简洁)

如果你不想手动处理高度计算,可以用Flutter自带的SizeTransition组件,它会自动根据子Widget的尺寸生成动画,不过需要注意它的动画轴是基于父容器的:

代码示例:

bool _notfall = false;
late AnimationController _rotateArrowController;
late Animation<double> _rotateAnimation;
late Animation<double> _sizeAnimation;
late AnimationController _sizeController;

@override
void initState() {
  super.initState();
  _rotateArrowController = AnimationController(
    duration: const Duration(milliseconds: 300),
    vsync: this,
  );
  _rotateAnimation = Tween<double>(begin: 0, end: 1).animate(
    CurvedAnimation(parent: _rotateArrowController, curve: Curves.easeInOut),
  );

  _sizeController = AnimationController(
    duration: const Duration(milliseconds: 300),
    vsync: this,
  );
  _sizeAnimation = CurvedAnimation(
    parent: _sizeController,
    curve: Curves.easeInOut,
  );
}

// 点击事件
onTap: () {
  setState(() {
    _notfall = !_notfall;
  });
  if (_notfall) {
    _rotateArrowController.forward();
    _sizeController.forward();
  } else {
    _rotateArrowController.reverse();
    _sizeController.reverse();
  }
},

// 展开卡片的代码
if (_notfall) {
  return SizeTransition(
    sizeFactor: _sizeAnimation,
    axisAlignment: -1.0, // 从顶部开始展开
    child: Card(
      color: Colors.white70,
      child: Container(
        // 你的任意Widget内容
        child: Column(
          children: const [
            Text("动态高度的内容"),
            // ... 更多Widget
          ],
        ),
      ),
    ),
  );
} else {
  return const SizedBox.shrink();
}

注意事项:

  • 方案一更灵活,适合需要精确控制高度动画的场景;方案二更简洁,适合快速实现需求。
  • 记得在页面销毁时dispose动画控制器,避免内存泄漏:
@override
void dispose() {
  _rotateArrowController.dispose();
  _cardAnimationController.dispose(); // 或者_sizeController.dispose();
  super.dispose();
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 10:08:52