Flutter实现固定尺寸Chip筛选+水平滚动人员卡片功能
Flutter 职业筛选+水平人员列表实现方案
核心实现逻辑
- 统一Chip宽度:页面初始化阶段遍历所有职业分类文本,通过
TextPainter计算最长文本的渲染宽度,叠加Chip水平内边距作为所有Chip的固定宽度,避免长短文本导致Chip尺寸参差不齐。 - 多标签匹配筛选:人员数据模型支持绑定多个职业标签,选中Chip时遍历人员列表,匹配「自身标签集合包含当前选中职业」的条目,默认截取前6条做展示,剩余条目通过末尾的「查看全部」按钮触发全量加载。
- 水平滚动卡片列表:使用
ListView配置scrollDirection: Axis.horizontal实现横向滚动,每个人员条目采用固定宽高的卡片布局,依次展示圆形头像、姓名、核心信息、星级评分,列表最后一个位置固定渲染「查看全部」操作按钮。
完整代码实现
1. 基础数据定义
// 人员数据模型 class Person { final String name; final String avatar; final List<String> tags; // 支持单人员绑定多个职业标签 final double score; final String desc; Person({ required this.name, required this.avatar, required this.tags, required this.score, required this.desc, }); } // 职业标签列表 const List<String> jobTags = ['设计师', '前端开发', '后端开发', '产品经理', '运维工程师', 'UI交互']; // 模拟人员数据 final List<Person> mockPersonList = [ Person(name: '张三', avatar: 'https://picsum.photos/200', tags: ['设计师', 'UI交互'], score: 4.8, desc: '5年互联网设计经验'), Person(name: '李四', avatar: 'https://picsum.photos/201', tags: ['前端开发'], score: 4.6, desc: '精通Flutter、Vue开发'), Person(name: '王五', avatar: 'https://picsum.photos/202', tags: ['后端开发', '运维工程师'], score: 4.9, desc: '8年服务端架构经验'), Person(name: '赵六', avatar: 'https://picsum.photos/203', tags: ['产品经理'], score: 4.5, desc: '擅长B端产品设计'), Person(name: '孙七', avatar: 'https://picsum.photos/204', tags: ['UI交互', '产品经理'], score: 4.7, desc: '用户体验设计专家'), Person(name: '周八', avatar: 'https://picsum.photos/205', tags: ['前端开发', '后端开发'], score: 4.8, desc: '全栈开发工程师'), Person(name: '吴九', avatar: 'https://picsum.photos/206', tags: ['运维工程师'], score: 4.4, desc: '云原生运维专家'), Person(name: '郑十', avatar: 'https://picsum.photos/207', tags: ['设计师'], score: 4.9, desc: '品牌视觉设计专家'), ];
2. HomeScreen核心逻辑
class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { String? selectedJob; late double unifiedChipWidth; final double chipHorizontalPadding = 16; @override void initState() { super.initState(); // 首帧渲染完成后计算统一Chip宽度 WidgetsBinding.instance.addPostFrameCallback((_) { _calculateUnifiedChipWidth(); }); } // 计算最长文本对应Chip的统一宽度 void _calculateUnifiedChipWidth() { double maxTextWidth = 0; const textStyle = TextStyle(fontSize: 14); for (final tag in jobTags) { final textPainter = TextPainter( text: TextSpan(text: tag, style: textStyle), maxLines: 1, textDirection: TextDirection.ltr, )..layout(); if (textPainter.width > maxTextWidth) { maxTextWidth = textPainter.width; } } // 总宽度 = 最长文本宽度 + 左右内边距 + Chip自带边距冗余 setState(() { unifiedChipWidth = maxTextWidth + chipHorizontalPadding * 2 + 16; }); } // 根据选中职业筛选匹配人员 List<Person> _getFilteredPersons() { if (selectedJob == null) return []; final matched = mockPersonList.where((p) => p.tags.contains(selectedJob)).toList(); // 默认取前6条展示 return matched.take(6).toList(); } @override Widget build(BuildContext context) { final filteredList = _getFilteredPersons(); return Scaffold( appBar: AppBar(title: const Text('人员列表')), body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 职业筛选Chip区域 Padding( padding: const EdgeInsets.all(16), child: Wrap( spacing: 12, runSpacing: 12, children: jobTags.map((tag) { final isSelected = selectedJob == tag; return SizedBox( width: unifiedChipWidth, child: ChoiceChip( label: Center(child: Text(tag)), selected: isSelected, onSelected: (val) { setState(() { selectedJob = val ? tag : null; }); }, padding: EdgeInsets.symmetric(horizontal: chipHorizontalPadding), ), ); }).toList(), ), ), // 水平滚动人员列表区域 if (selectedJob != null) SizedBox( height: 180, child: ListView.builder( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: filteredList.length + 1, // +1为末尾的查看全部按钮 itemBuilder: (context, index) { if (index == filteredList.length) { return _buildViewAllCard(); } final person = filteredList[index]; return _buildPersonCard(person); }, ), ) ], ), ); } // 人员卡片组件 Widget _buildPersonCard(Person person) { return Container( width: 140, margin: const EdgeInsets.only(right: 12), child: Card( elevation: 2, child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ CircleAvatar( radius: 32, backgroundImage: NetworkImage(person.avatar), ), const SizedBox(height: 8), Text(person.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)), const SizedBox(height: 4), Text(person.desc, style: const TextStyle(fontSize: 12, color: Colors.grey), maxLines: 1, overflow: TextOverflow.ellipsis), const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.star, color: Colors.amber, size: 16), const SizedBox(width: 2), Text(person.score.toString(), style: const TextStyle(fontSize: 13)), ], ) ], ), ), ), ); } // 查看全部按钮卡片 Widget _buildViewAllCard() { return InkWell( onTap: () { // 在此处补充跳转全量人员列表的逻辑 }, child: Container( width: 140, margin: const EdgeInsets.only(right: 12), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey[300]!) ), child: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.arrow_forward, size: 32, color: Colors.blueAccent), SizedBox(height: 8), Text('查看全部', style: TextStyle(color: Colors.blueAccent, fontWeight: FontWeight.w500)) ], ), ), ); } }
效果说明
- 所有Chip尺寸完全统一,后续新增职业标签时无需手动调整宽度,逻辑会自动计算适配
- 选中职业后自动过滤匹配人员,支持一人多标签的匹配规则,默认最多展示6条数据
- 水平列表滚动流畅,卡片信息完整,末尾「查看全部」按钮可直接扩展跳转逻辑
- 取消Chip选中时,人员列表自动隐藏,交互符合常规使用习惯
内容的提问来源于stack exchange,提问作者Sachin Rajput
相关产品推荐
相关产品推荐

