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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 05:21:38