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

Flutter使用ExpansionWidget实现展开新项时自动收起已展开折叠项

Flutter 手风琴折叠效果实现方案

核心需求

现有基于expansion_widget库封装的CustomExpansionTile组件,多组件放在Column中使用时,需要实现手风琴交互:每次仅允许一个折叠项处于展开状态,展开新项时自动收起之前展开的项。

改造步骤

1. 改造CustomExpansionTile组件

增加状态管控相关参数,将展开状态的控制权交给父组件,改造后代码如下:

import 'package:expansion_widget/expansion_widget.dart';
import 'package:flutter/material.dart';

class CustomExpansionTile extends StatefulWidget {
  final Widget headerBody;
  final Widget expandedBody;
  final Color headerColor;
  final Color expandedBodyColor;
  final double padding;
  // 新增参数:当前组件的唯一标识
  final dynamic id;
  // 新增参数:当前处于展开状态的组件标识
  final dynamic currentActiveId;
  // 新增参数:状态变更回调
  final Function(dynamic?) onToggle;

  const CustomExpansionTile({
    Key? key,
    required this.headerBody,
    required this.expandedBody,
    required this.headerColor,
    required this.expandedBodyColor,
    required this.padding,
    required this.id,
    required this.currentActiveId,
    required this.onToggle,
  }) : super(key: key);

  @override
  _CustomExpansionTileState createState() => _CustomExpansionTileState();
}

class _CustomExpansionTileState extends State<CustomExpansionTile> {
  // 保存ExpansionWidget的toggle方法用于手动控制展开收起
  Function? _toggleFunction;
  bool _isExpanded = false;

  @override
  void didUpdateWidget(covariant CustomExpansionTile oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 父组件传来的激活标识变化时,同步更新当前组件的展开状态
    final shouldExpand = widget.id == widget.currentActiveId;
    if (shouldExpand != _isExpanded && _toggleFunction != null) {
      _toggleFunction!(animated: true);
      _isExpanded = shouldExpand;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Card(
          elevation: 0,
          child: ExpansionWidget(
            initiallyExpanded: widget.id == widget.currentActiveId,
            titleBuilder: (double animationValue, _, bool isExpaned, toogleFunction) {
              _toggleFunction = toogleFunction;
              _isExpanded = isExpaned;
              return Container(
                decoration: BoxDecoration(
                  color: widget.headerColor,
                  borderRadius: BorderRadius.circular(6)),
                height: 59,
                child: InkWell(
                  onTap: () {
                    // 点击时通知父组件更新激活标识
                    if (_isExpanded) {
                      widget.onToggle(null);
                    } else {
                      widget.onToggle(widget.id);
                    }
                  },
                  child: Padding(
                    padding: EdgeInsets.symmetric(horizontal: widget.padding),
                    child: Row(
                      children: [
                        widget.headerBody,
                        const Spacer(),
                        Transform.rotate(
                          angle: 0,
                          child: Icon(
                            isExpaned
                                ? Icons.keyboard_arrow_down_rounded
                                : Icons.keyboard_arrow_right,
                            size: 40),
                          alignment: Alignment.centerRight,
                        )
                      ],
                    ),
                  ),
                ),
              );
            },
            content: Container(
              color: widget.expandedBodyColor,
              width: double.infinity,
              padding: const EdgeInsets.all(20),
              child: Column(
                children: [widget.expandedBody],
              ),
            ),
          ),
        ),
      ],
    );
  }
}

2. 父组件调用示例

父组件维护全局激活标识,管控所有折叠项的状态:

class AccordionPage extends StatefulWidget {
  const AccordionPage({super.key});

  @override
  State<AccordionPage> createState() => _AccordionPageState();
}

class _AccordionPageState extends State<AccordionPage> {
  // 存储当前处于展开状态的折叠项id,null代表所有项都收起
  dynamic? _activeTileId;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          CustomExpansionTile(
            id: 1,
            currentActiveId: _activeTileId,
            onToggle: (id) => setState(() => _activeTileId = id),
            headerBody: Row(
              children: const [
                Text('第一项标题'),
                Text('附加文本'),
              ],
            ),
            expandedBody: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: const [
                Text('第一项内容1'),
                Text('第一项内容2'),
              ],
            ),
            headerColor: Colors.white,
            expandedBodyColor: Colors.white,
            padding: 16,
          ),
          CustomExpansionTile(
            id: 2,
            currentActiveId: _activeTileId,
            onToggle: (id) => setState(() => _activeTileId = id),
            headerBody: Row(
              children: const [
                Text('第二项标题'),
                Text('附加文本'),
              ],
            ),
            expandedBody: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: const [
                Text('第二项内容1'),
                Text('第二项内容2'),
              ],
            ),
            headerColor: Colors.white,
            expandedBodyColor: Colors.white,
            padding: 16,
          ),
          CustomExpansionTile(
            id: 3,
            currentActiveId: _activeTileId,
            onToggle: (id) => setState(() => _activeTileId = id),
            headerBody: Row(
              children: const [
                Text('第三项标题'),
                Text('附加文本'),
              ],
            ),
            expandedBody: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: const [
                Text('第三项内容1'),
                Text('第三项内容2'),
              ],
            ),
            headerColor: Colors.white,
            expandedBodyColor: Colors.white,
            padding: 16,
          ),
        ],
      ),
    );
  }
}

实现说明

  • 所有折叠项的展开状态由父组件统一维护,避免每个组件单独管理状态导致的状态不同步问题
  • 点击任意折叠项时,仅需要更新父组件的_activeTileId变量,所有折叠项会自动对比自身id与激活id,同步更新展开/收起状态
  • 点击已经展开的折叠项时,会自动收起该折叠项,所有项回到全部收起状态

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 21:06:10