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

