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

Flutter 如何实现点击标题触发列表动画折叠收起的功能?

实现步骤

你只需要给组件增加折叠状态变量+动画控制,修改现有代码的4处即可实现效果,具体操作如下:

1. 转换为Stateful组件

当前承载这个Column的组件需要改成StatefulWidget,因为需要维护折叠状态和动画控制器的生命周期。

2. 定义状态与动画参数

在State类中增加如下代码:

// 折叠状态标识,默认展开
bool _isCollapsed = false;
// 动画控制器
late AnimationController _collapseController;
// 高度系数动画
late Animation<double> _heightFactor;

@override
void initState() {
  super.initState();
  // 初始化动画控制器,时长可自定义
  _collapseController = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 300),
  );
  // 定义高度从1(完全展开)到0(完全收起)的动画曲线
  _heightFactor = Tween<double>(begin: 1.0, end: 0.0).animate(
    CurvedAnimation(parent: _collapseController, curve: Curves.easeInOut),
  );
}

@override
void dispose() {
  // 销毁控制器避免内存泄漏
  _collapseController.dispose();
  super.dispose();
}

3. 给标题绑定点击事件

修改标题区域GestureDetector的onTap回调:

onTap: () {
  setState(() {
    _isCollapsed = !_isCollapsed;
    _isCollapsed ? _collapseController.forward() : _collapseController.reverse();
  });
}

4. 给列表区域加动画包裹

将原来生成列表的Column用SizeTransition组件包裹,实现高度渐变的折叠效果:

SizeTransition(
  axisAlignment: 1.0, // 从顶部开始收起动画
  sizeFactor: _heightFactor,
  child: Column(
    // 你原来的List.generate生成列表的代码直接放在这里即可,无需修改
  ),
)
特殊场景适配

如果你是多分类列表(有多个这种可折叠的Column),需要将_isCollapsed状态定义在你的Category模型类中,不要存在当前组件的State里,避免点击一个标题所有分类都同时折叠。

修改后完整代码参考
Column(
  children: [
    GestureDetector(
      onTap: () {
        setState(() {
          _isCollapsed = !_isCollapsed;
          _isCollapsed ? _collapseController.forward() : _collapseController.reverse();
        });
      },
      child: Container(
        color: Theme.of(context).sliderTheme.inactiveTrackColor,
        width: MediaQuery.of(context).size.width,
        child: Padding(
          padding: const EdgeInsets.symmetric(vertical: 3),
          child: Center(
            child: Text('${category.name}',
                style: Theme.of(context).textTheme.headline6.copyWith(
                    fontSize: 16,
                    color: Theme.of(context).selectedRowColor)),
          ),
        ),
      ),
    ),
    SizeTransition(
      axisAlignment: 1.0,
      sizeFactor: _heightFactor,
      child: Column(
        children: List.generate(category.items.length, (index) {
          return Container(
            decoration: BoxDecoration(
                border: (index + 1) != category.items.length
                    ? Border(
                        bottom: BorderSide(
                            color: Style.inactiveColorDark.withOpacity(1.0),
                            width: 0.5))
                    : null),
            child: Padding(
              padding:
                  const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Row(
                    children: [
                      Container(
                        width: 12,
                        height: 12,
                        decoration: BoxDecoration(
                            color: Color(int.parse(category.color)),
                            shape: BoxShape.circle),
                      ),
                      SizedBox(
                        width: 12,
                      ),
                      Text('${category.items[index].description}',
                          style: Theme.of(context)
                              .primaryTextTheme
                              .headline1
                              .copyWith(fontSize: 16)),
                    ],
                  ),
                  SwitchButton(
                    isActive: category.items[index].isAdded,
                    activeColor:
                        Theme.of(context).sliderTheme.activeTrackColor,
                    inactiveColor:
                        Theme.of(context).sliderTheme.inactiveTrackColor,
                    activeCircleColor:
                        Theme.of(context).sliderTheme.activeTickMarkColor,
                    inactiveCircleColor:
                        Theme.of(context).sliderTheme.inactiveTickMarkColor,
                    turnOn: () {
                      ChosenFeeling removingElement = ChosenFeeling(
                        id: 000,
                        isAdded: false,
                      );

                      //  If chosen list is empty
                      if (chosenfeelings.isEmpty) {
                        chosenfeelings.add(ChosenFeeling(
                            isAdded: true, id: category.items[index].id));
                      } else {
                        //  If user tap on switchButton 2 times
                        chosenfeelings.removeWhere((element) {
                          if (element.id != null &&
                              element.id == category.items[index].id) {
                            removingElement = element;
                          }
                          return _isNeedToRemoveWhenOn(
                            currentItem: category.items[index],
                            listItem: element,
                          );
                        });

                        // If list isn`t empty and chosen item isn`t in list
                        if (category.items[index].id != removingElement.id) {
                          chosenfeelings.add(ChosenFeeling(
                            id: category.items[index].id,
                            isAdded: true,
                          ));
                        }
                      }
                    },
                    turnOff: () {
                      ChosenFeeling removingElement = ChosenFeeling(
                        id: 000,
                        isAdded: false,
                      );

                      if (chosenfeelings.isEmpty) {
                        chosenfeelings.add(ChosenFeeling(
                          id: category.items[index].id,
                          isAdded: false,
                        ));
                      } else {
                        //  If user tap on switchButton 2 times
                        chosenfeelings.removeWhere((element) {
                          if (element.id != null &&
                              element.id == category.items[index].id) {
                            removingElement = element;
                          }
                          return _isNeedToRemoveWhenOff(
                            currentItem: category.items[index],
                            listItem: element,
                          );
                        });
                        // If list isn`t empty and chosen item isn`t in list
                        if (category.items[index].id != removingElement.id) {
                          chosenfeelings.add(ChosenFeeling(
                            id: category.items[index].id,
                            isAdded: false,
                          ));
                        }
                      }
                    },
                  )
                ],
              ),
            ),
          );
        }),
      ),
    )
  ],
);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.04 09:51:00