Flutter 框架跨平台鸿蒙开发——Card网格布局
overridetitle: '卡片${index1',content: '这是卡片${index1的详细内容',));
·

网格布局是组织多个Card的常见方式,能够有效利用屏幕空间,展示大量卡片内容。本文将详细介绍Card在网格布局中的应用,包括基础网格布局、响应式设计、卡片排序、瀑布流布局等。
一、网格布局基础
GridView是Flutter中实现网格布局的核心组件,可以方便地排列多个Card。
1.1 GridView类型对比
1.2 不同GridView对比
| 类型 | 列数控制 | 性能 | 使用场景 | 难度 |
|---|---|---|---|---|
| GridView.count | 固定列数 | 一般 | 布局固定 | 简单 |
| GridView.extent | 自适应 | 一般 | 卡片宽度固定 | 简单 |
| GridView.builder | 自适应 | 好 | 大量数据 | 中等 |
| GridView.custom | 完全自定义 | 最好 | 复杂布局 | 较难 |
二、基础网格布局
2.1 GridView.count实现
class BasicCardGrid extends StatelessWidget {
final int itemCount;
const BasicCardGrid({super.key, required this.itemCount});
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 2, // 两列
crossAxisSpacing: 16, // 水平间距
mainAxisSpacing: 16, // 垂直间距
padding: const EdgeInsets.all(16),
children: List.generate(
itemCount,
(index) => Card(
child: Column(
children: [
Container(
height: 100,
color: Colors.primaries[index % Colors.primaries.length],
child: Center(
child: Text(
'${index + 1}',
style: const TextStyle(
fontSize: 32,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'卡片 ${index + 1}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
'这是第${index + 1}个卡片的描述',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
),
],
),
),
),
);
}
}
2.2 GridView.extent实现
class FixedWidthCardGrid extends StatelessWidget {
const FixedWidthCardGrid({super.key});
Widget build(BuildContext context) {
return GridView.extent(
maxCrossAxisExtent: 200, // 卡片最大宽度
crossAxisSpacing: 16,
mainAxisSpacing: 16,
padding: const EdgeInsets.all(16),
childAspectRatio: 0.75, // 宽高比
children: List.generate(
20,
(index) => Card(
child: Column(
children: [
Container(
height: 120,
color: Colors.teal.shade100,
child: const Center(
child: Icon(
Icons.image,
size: 48,
color: Colors.teal,
),
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'图片卡片 ${index + 1}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
'固定宽度网格',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600,
),
),
],
),
),
],
),
),
),
);
}
}
2.3 GridView.builder实现
class EfficientCardGrid extends StatelessWidget {
const EfficientCardGrid({super.key});
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 0.8,
),
padding: const EdgeInsets.all(16),
itemCount: 100, // 大量数据
itemBuilder: (context, index) {
return Card(
child: Column(
children: [
Container(
height: 100,
color: Colors.blue.shade100,
child: Center(
child: Text(
'${index + 1}',
style: const TextStyle(
fontSize: 28,
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'卡片 ${index + 1}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 4),
Text(
'性能优化网格',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600,
),
),
],
),
),
],
),
);
},
);
}
}
三、响应式网格布局
3.1 根据屏幕宽度调整列数
class ResponsiveCardGrid extends StatelessWidget {
const ResponsiveCardGrid({super.key});
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
// 根据屏幕宽度决定列数
int crossAxisCount;
if (constraints.maxWidth < 600) {
crossAxisCount = 2; // 手机
} else if (constraints.maxWidth < 900) {
crossAxisCount = 3; // 小平板
} else if (constraints.maxWidth < 1200) {
crossAxisCount = 4; // 大平板
} else {
crossAxisCount = 5; // 桌面
}
return GridView.count(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
padding: const EdgeInsets.all(16),
children: List.generate(
20,
(index) => Card(
child: Column(
children: [
Container(
height: 80,
color: Colors.primaries[index % Colors.primaries.length],
child: Center(
child: Text(
'${index + 1}',
style: const TextStyle(
fontSize: 24,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'响应式卡片 ${index + 1}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
Text(
'当前列数: $crossAxisCount',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600,
),
),
],
),
),
],
),
),
),
);
},
);
}
}
3.2 设备类型检测
class AdaptiveCardGrid extends StatelessWidget {
const AdaptiveCardGrid({super.key});
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: _getMaxCrossAxisExtent(context),
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: _getChildAspectRatio(context),
),
padding: const EdgeInsets.all(16),
itemCount: 30,
itemBuilder: (context, index) => _buildCard(index, context),
);
}
double _getMaxCrossAxisExtent(BuildContext context) {
final width = MediaQuery.of(context).size.width;
if (width < 600) return 180;
if (width < 900) return 220;
if (width < 1200) return 260;
return 300;
}
double _getChildAspectRatio(BuildContext context) {
final width = MediaQuery.of(context).size.width;
if (width < 600) return 0.8;
if (width < 900) return 0.85;
if (width < 1200) return 0.9;
return 0.95;
}
Widget _buildCard(int index, BuildContext context) {
return Card(
child: Column(
children: [
Container(
height: MediaQuery.of(context).size.width < 600 ? 80 : 100,
color: Colors.orange.shade100,
child: const Center(
child: Icon(
Icons.card_giftcard,
size: 40,
color: Colors.orange,
),
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'自适应卡片 ${index + 1}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
Text(
'根据设备自动调整',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600,
),
),
],
),
),
],
),
);
}
}
四、瀑布流布局
4.1 使用Flutter Masonry包
dependencies:
flutter_staggered_grid_view: ^0.6.2
class MasonryCardGrid extends StatelessWidget {
const MasonryCardGrid({super.key});
Widget build(BuildContext context) {
return MasonryGridView.count(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
padding: const EdgeInsets.all(16),
itemCount: 15,
itemBuilder: (context, index) {
final height = 100.0 + (index * 20) % 150; // 随机高度
return Card(
child: Container(
height: height,
color: Colors.primaries[index % Colors.primaries.length].shade100,
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'卡片 ${index + 1}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 8),
Text(
'高度: ${height.toInt()}',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade700,
),
),
const Spacer(),
Text(
'瀑布流布局\n不同高度的卡片',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600,
),
),
],
),
),
);
},
);
}
}
4.2 自定义瀑布流
class CustomMasonryLayout extends StatelessWidget {
const CustomMasonryLayout({super.key});
Widget build(BuildContext context) {
final cards = List.generate(20, (index) => _CardData(
title: '卡片 ${index + 1}',
content: '这是卡片 ${index + 1} 的详细内容',
height: 100 + (index % 5) * 30,
color: Colors.primaries[index % Colors.primaries.length],
));
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
children: cards
.where((card) => cards.indexOf(card) % 2 == 0)
.map((card) => _buildMasonryCard(card))
.toList(),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
children: cards
.where((card) => cards.indexOf(card) % 2 == 1)
.map((card) => _buildMasonryCard(card))
.toList(),
),
),
],
),
);
}
Widget _buildMasonryCard(_CardData card) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: Container(
height: card.height.toDouble(),
decoration: BoxDecoration(
color: card.color.shade100,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
card.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 8),
Text(
card.content,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade700,
),
),
],
),
),
);
}
}
class _CardData {
final String title;
final String content;
final int height;
final Color color;
_CardData({
required this.title,
required this.content,
required this.height,
required this.color,
});
}
五、卡片排序与过滤
5.1 排序功能
class SortableCardGrid extends StatefulWidget {
const SortableCardGrid({super.key});
State<SortableCardGrid> createState() => _SortableCardGridState();
}
class _SortableCardGridState extends State<SortableCardGrid> {
int _sortType = 0;
final List<_SortCardData> _cards = List.generate(
20,
(index) => _SortCardData(
id: index + 1,
title: '卡片 ${index + 1}',
value: (index + 1) * 10,
date: DateTime.now().subtract(Duration(days: index)),
),
);
List<_SortCardData> get _sortedCards {
switch (_sortType) {
case 0:
return _cards; // 默认
case 1:
return List.from(_cards)..sort((a, b) => a.title.compareTo(b.title));
case 2:
return List.from(_cards)..sort((a, b) => a.value.compareTo(b.value));
case 3:
return List.from(_cards)..sort((a, b) => b.value.compareTo(a.value));
default:
return _cards;
}
}
Widget build(BuildContext context) {
return Column(
children: [
_buildSortOptions(),
Expanded(
child: GridView.count(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
padding: const EdgeInsets.all(16),
children: _sortedCards
.map((card) => Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
card.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
Text('值: ${card.value}'),
Text('日期: ${card.date.toString().substring(0, 10)}'),
],
),
),
))
.toList(),
),
),
],
);
}
Widget _buildSortOptions() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Wrap(
spacing: 8,
children: [
ChoiceChip(
label: const Text('默认'),
selected: _sortType == 0,
onSelected: (selected) {
setState(() {
_sortType = 0;
});
},
),
ChoiceChip(
label: const Text('按名称'),
selected: _sortType == 1,
onSelected: (selected) {
setState(() {
_sortType = 1;
});
},
),
ChoiceChip(
label: const Text('值升序'),
selected: _sortType == 2,
onSelected: (selected) {
setState(() {
_sortType = 2;
});
},
),
ChoiceChip(
label: const Text('值降序'),
selected: _sortType == 3,
onSelected: (selected) {
setState(() {
_sortType = 3;
});
},
),
],
),
);
}
}
class _SortCardData {
final int id;
final String title;
final int value;
final DateTime date;
_SortCardData({
required this.id,
required this.title,
required this.value,
required this.date,
});
}
5.2 过滤功能
class FilterableCardGrid extends StatefulWidget {
const FilterableCardGrid({super.key});
State<FilterableCardGrid> createState() => _FilterableCardGridState();
}
class _FilterableCardGridState extends State<FilterableCardGrid> {
final List<String> _categories = ['全部', '红色', '蓝色', '绿色', '黄色'];
String _selectedCategory = '全部';
final List<_FilterCardData> _cards = [
_FilterCardData(title: '红色卡片1', category: '红色', value: 10),
_FilterCardData(title: '蓝色卡片1', category: '蓝色', value: 20),
_FilterCardData(title: '绿色卡片1', category: '绿色', value: 30),
_FilterCardData(title: '黄色卡片1', category: '黄色', value: 40),
_FilterCardData(title: '红色卡片2', category: '红色', value: 15),
_FilterCardData(title: '蓝色卡片2', category: '蓝色', value: 25),
_FilterCardData(title: '绿色卡片2', category: '绿色', value: 35),
_FilterCardData(title: '黄色卡片2', category: '黄色', value: 45),
];
List<_FilterCardData> get _filteredCards {
if (_selectedCategory == '全部') {
return _cards;
}
return _cards.where((card) => card.category == _selectedCategory).toList();
}
Widget build(BuildContext context) {
return Column(
children: [
_buildFilterOptions(),
Expanded(
child: GridView.count(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
padding: const EdgeInsets.all(16),
children: _filteredCards
.map((card) => Card(
color: _getCategoryColor(card.category).shade100,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
card.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
Text('分类: ${card.category}'),
Text('值: ${card.value}'),
],
),
),
))
.toList(),
),
),
],
);
}
Widget _buildFilterOptions() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Wrap(
spacing: 8,
children: _categories.map((category) {
return FilterChip(
label: Text(category),
selected: _selectedCategory == category,
onSelected: (selected) {
setState(() {
_selectedCategory = category;
});
},
selectedColor: _getCategoryColor(category).shade200,
);
}).toList(),
),
),
);
}
Color _getCategoryColor(String category) {
switch (category) {
case '红色':
return Colors.red;
case '蓝色':
return Colors.blue;
case '绿色':
return Colors.green;
case '黄色':
return Colors.yellow;
default:
return Colors.grey;
}
}
}
class _FilterCardData {
final String title;
final String category;
final int value;
_FilterCardData({
required this.title,
required this.category,
required this.value,
});
}
六、卡片拖拽排序
6.1 Reorderable卡片
dependencies:
reorderable_grid_view: ^2.2.7
class ReorderableCardGrid extends StatefulWidget {
const ReorderableCardGrid({super.key});
State<ReorderableCardGrid> createState() => _ReorderableCardGridState();
}
class _ReorderableCardGridState extends State<ReorderableCardGrid> {
final List<_ReorderCardData> _cards = List.generate(
12,
(index) => _ReorderCardData(
id: index,
title: '卡片 ${index + 1}',
color: Colors.primaries[index % Colors.primaries.length],
),
);
Widget build(BuildContext context) {
return ReorderableGridView.count(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
padding: const EdgeInsets.all(16),
children: _cards
.map((card) => Card(
key: ValueKey(card.id),
color: card.color.shade100,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Icon(
Icons.drag_handle,
color: Colors.grey.shade600,
),
const SizedBox(height: 12),
Text(
card.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
Text(
'长按拖拽',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
),
))
.toList(),
onReorder: (oldIndex, newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final item = _cards.removeAt(oldIndex);
_cards.insert(newIndex, item);
});
},
);
}
}
class _ReorderCardData {
final int id;
final String title;
final Color color;
_ReorderCardData({
required this.id,
required this.title,
required this.color,
});
}
七、完整网格布局示例
class CardGridLayoutExample extends StatelessWidget {
const CardGridLayoutExample({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Card网格布局'),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
body: DefaultTabController(
length: 5,
child: Column(
children: [
TabBar(
tabs: const [
Tab(text: '基础网格'),
Tab(text: '响应式'),
Tab(text: '瀑布流'),
Tab(text: '排序'),
Tab(text: '过滤'),
],
),
Expanded(
child: TabBarView(
children: [
const BasicCardGrid(itemCount: 20),
const ResponsiveCardGrid(),
const MasonryCardGrid(),
const SortableCardGrid(),
const FilterableCardGrid(),
],
),
),
],
),
),
);
}
}
八、网格布局最佳实践
性能优化建议
| 优化项 | 说明 | 效果 |
|---|---|---|
| 使用builder | 避免一次性创建所有Widget | 内存优化 |
| 添加缓存 | 保持列表位置状态 | 用户体验 |
| 懒加载图片 | 按需加载图片资源 | 性能提升 |
| 使用const | 减少重建开销 | 渲染优化 |
| 虚拟滚动 | 只渲染可见项 | 性能大幅提升 |
布局设计原则
// ✅ 好的做法
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: 1000,
itemBuilder: (context, index) {
return const Card(
child: Text('卡片'),
);
},
)
// ❌ 避免的做法
GridView.count(
crossAxisCount: 2,
children: List.generate(
1000,
(index) => Card(
child: Text('卡片 $index'),
),
),
)
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)