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

Flutter如何实现GridView点击网格项展开显示列表的交互效果

Flutter GridView点击行下展开关联列表实现方案

原生GridView默认基于固定尺寸的子项布局,无法直接在两行网格之间插入跨整行的展开内容,最易维护的实现方式是按行拆分渲染结构,不需要修改复杂的Sliver网格布局逻辑。

实现逻辑

  • 放弃纯GridView.builder渲染,改用ListView.builder按行构建内容,每一行作为一个独立渲染单元
  • 每行列数固定为3,计算总共有多少行:(总网格数 / 3).ceil()
  • 单行结构分为两部分:
    • 顶部为横向排列的3个网格项,保留你已经实现的选中高亮、边距变化样式
    • 底部判断:如果当前选中的网格项属于本行,就渲染跨整行宽度的关联列表内容,搭配尺寸动画实现顺滑展开/收起效果
  • 点击逻辑优化:点击已选中的网格项时,将选中索引重置为-1,实现再次点击收起的交互
  • 网格项尺寸手动适配:按照你原本设置的3:4宽高比,给每个网格项设置对应高度,和原有视觉效果保持一致

修改后可直接运行的完整代码

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  int checkedIndex = -1;
  // 每行列数
  final int crossAxisCount = 3;
  // 总网格数
  final int totalItemCount = 9;
  // 网格项宽高比 和原有配置一致
  final double childAspectRatio = 3 / 4;
  // 网格间距 和原有配置一致
  final double crossAxisSpacing = 2;
  final double mainAxisSpacing = 5;

  // 构建单个网格项 复用原有样式
  Widget _buildGridItem(int index) {
    bool checked = index == checkedIndex;
    return Expanded(
      child: InkWell(
        onTap: (() {
          setState(() {
            // 点击已选中项则收起
            checkedIndex = checked ? -1 : index;
          });
        }),
        child: Container(
          margin: checked ? EdgeInsets.zero : const EdgeInsets.all(10),
          decoration: BoxDecoration(
              color: Colors.white,
              border: checked
                  ? Border.all(color: Colors.cyan, width: 2)
                  : Border.all(color: Colors.white),
              borderRadius: const BorderRadius.all(Radius.circular(10))),
          child: Stack(
            children: [
              const Align(
                alignment: Alignment.topCenter,
                child: Text(
                  "Gift",
                  style: TextStyle(height: 2),
                ),
              ),
              Align(
                alignment: Alignment.bottomCenter,
                child: Container(
                  decoration: const BoxDecoration(
                    borderRadius: BorderRadius.only(
                        topRight: Radius.circular(50),
                        bottomLeft: Radius.circular(10),
                        bottomRight: Radius.circular(10),
                        topLeft: Radius.circular(50)),
                    color: Colors.red,
                    shape: BoxShape.rectangle,
                  ),
                  height: 75,
                  width: double.infinity,
                  // 替换为你的asset图片即可
                  child: const Icon(Icons.card_giftcard, color: Colors.white, size: 40),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  // 构建选中后展示的关联列表内容
  Widget _buildExpandedContent(int selectedIndex) {
    return AnimatedSize(
      duration: const Duration(milliseconds: 250),
      curve: Curves.easeInOut,
      child: Container(
        width: double.infinity,
        margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
        padding: const EdgeInsets.all(12),
        decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(10),
            border: Border.all(color: Colors.cyan)
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            Text("选中项$selectedIndex 关联内容", style: const TextStyle(fontWeight: FontWeight.bold),),
            const SizedBox(height: 8,),
            // 替换为你实际的关联列表内容
            for(int i=0;i<3;i++)
              ListTile(
                dense: true,
                leading: const Icon(Icons.list_alt),
                title: Text("关联条目${i+1}"),
                onTap: (){},
              )
          ],
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: Colors.amberAccent,
        body: LayoutBuilder(
          builder: (context, constraints) {
            // 计算单个网格项宽度 用于匹配原有宽高比
            double itemWidth = (constraints.maxWidth - (crossAxisCount-1)*crossAxisSpacing) / crossAxisCount;
            double itemHeight = itemWidth / childAspectRatio;
            // 计算总行数
            int totalRow = (totalItemCount / crossAxisCount).ceil();

            return ListView.builder(
              padding: const EdgeInsets.symmetric(vertical: 5),
              itemCount: totalRow,
              itemBuilder: (context, rowIndex) {
                // 计算当前行包含的网格索引
                List<int> rowItems = [];
                for(int i=0;i<crossAxisCount;i++){
                  int itemIndex = rowIndex * crossAxisCount + i;
                  if(itemIndex < totalItemCount){
                    rowItems.add(itemIndex);
                  }
                }
                // 判断当前行是否包含选中项
                bool hasSelectedItem = rowItems.contains(checkedIndex);

                return Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    // 行内网格
                    SizedBox(
                      height: itemHeight,
                      child: Row(
                        crossAxisAlignment: CrossAxisAlignment.stretch,
                        children: [
                          for(int index in rowItems)
                            ...[
                              _buildGridItem(index),
                              // 网格之间加横向间距 最后一个不加
                              if(index != rowItems.last)
                                SizedBox(width: crossAxisSpacing,)
                            ]
                        ],
                      ),
                    ),
                    // 行和行之间的纵向间距
                    SizedBox(height: mainAxisSpacing,),
                    // 展开内容 仅当前行有选中项时展示
                    if(hasSelectedItem)
                      _buildExpandedContent(checkedIndex)
                  ],
                );
              },
            );
          },
        ),
      ),
    );
  }
}

效果参考

点击展开效果示意图

你可以直接替换_buildExpandedContent里的内容为你实际需要的关联列表,调整动画时长、样式参数匹配设计规范即可。如果需要更复杂的自定义滚动效果,也可以把这个按行渲染的逻辑替换为CustomScrollView的Sliver组合同样可以实现。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 08:12:25