在这里插入图片描述

案例概述

本案例展示如何使用 ChipFilterChip 创建标签和选择界面。芯片是一种紧凑的组件,用于表示标签、选项、过滤条件等。在现代应用中,芯片广泛应用于下列场景:

  • 标签管理:文章标签、产品分类、技能标签等
  • 多选择器:用户可以一次选择多个选项,常见于搜索、筛选场景
  • 上下文信息:显示上下文相关的信息或分类
  • 实时反馈:支持即时的选择反馈和加载状态

Flutter 提供了多种芯片组件,包括基础 Chip、可选择的 FilterChip、可删除的 InputChip 和可执行操作的 ActionChip。在企业应用中,芯片需要处理动态数据、多选择、批量操作等复杂需求。

此外,芯片还应支持响应式设计以適应不同屏幕尺寸、提供无障碍支持以满足屏幕阅读器需求、支持键盘导航等功能。在 PC 端应用中,芯片需要充分利用屏幕空间,提供清晰的选择反馈。

核心概念

1. Chip(基础芯片)

基础芯片是最简单的芯片类型,主要用于显示标签或信息。其主要特点包括:

  • label 参数:芯片显示的文本内容,通常是一个 Text 组件
  • avatar 参数:可选的头像或图标,显示在标签左侧
  • onDeleted 回调:当用户点击删除按钮时触发,允许删除该芯片
  • backgroundColor:芯片的背景颜色
  • deleteIcon:自定义删除按钮的图标
  • labelPadding:标签的内边距

2. FilterChip(可选择芯片)

可选择芯片允许用户选择或取消选择,常用于多选择场景。其主要特点包括:

  • selected 参数:布尔值,表示芯片是否被选中
  • onSelected 回调:当用户点击芯片时触发,返回新的选择状态
  • selectedColor:选中时的背景颜色
  • showCheckmark:是否显示选中标记
  • side:芯片边框的样式
  • avatar 参数:可选的头像或图标

3. 芯片列表与布局

芯片通常使用 Wrap 组件进行布局,以支持自动换行。其主要特点包括:

  • Wrap 布局:自动换行布局,当芯片超出屏幕宽度时自动换到下一行
  • spacing 参数:芯片之间的水平间距
  • runSpacing 参数:行之间的垂直间距
  • alignment 参数:芯片在行中的对齐方式
  • runAlignment 参数:行在容器中的对齐方式
  • crossAxisAlignment 参数:交叉轴对齐方式

代码详解

1. 基础芯片实现

最简单的芯片实现,直接显示标签文本。这种方式适合用于展示信息或标签,不需要用户交互。

Chip(label: Text('标签'))

这个基础实现创建了一个简单的芯片,显示"标签"文本。芯片会自动应用默认的样式,包括背景颜色、文本颜色等。

2. 带删除功能的芯片

许多应用需要允许用户删除标签或芯片。通过添加 onDeleted 回调,可以实现删除功能。

Chip(
  label: Text('标签'),
  onDeleted: () {
    setState(() {
      _tags.remove('标签');
    });
  },
  deleteIcon: Icon(Icons.close),
)

这个实现在芯片右侧显示一个删除按钮。当用户点击删除按钮时,会触发 onDeleted 回调,从而从列表中移除该芯片。

3. 可选择芯片

可选择芯片允许用户通过点击来选择或取消选择。这在多选择场景中非常有用,如搜索过滤、标签选择等。

FilterChip(
  label: Text('标签'),
  selected: _selectedTags.contains('标签'),
  onSelected: (selected) {
    setState(() {
      if (selected) {
        _selectedTags.add('标签');
      } else {
        _selectedTags.remove('标签');
      }
    });
  },
  selectedColor: Colors.blue,
  checkmarkColor: Colors.white,
)

这个实现创建了一个可选择的芯片。当用户点击芯片时,onSelected 回调会被触发,返回新的选择状态。根据 selected 参数,芯片会改变其外观以反映选择状态。

4. 芯片列表

在实际应用中,通常需要显示多个芯片。使用 Wrap 组件可以自动处理换行,确保芯片在屏幕宽度不足时自动换到下一行。

Wrap(
  spacing: 8,
  runSpacing: 8,
  children: _tags.map((tag) {
    return FilterChip(
      label: Text(tag),
      selected: _selectedTags.contains(tag),
      onSelected: (selected) {
        setState(() {
          if (selected) {
            _selectedTags.add(tag);
          } else {
            _selectedTags.remove(tag);
          }
        });
      },
    );
  }).toList(),
)

这个实现展示了如何创建一个芯片列表。Wrap 组件会自动处理换行,spacing 参数控制芯片之间的水平间距,runSpacing 参数控制行之间的垂直间距。

5. 带头像的芯片

某些应用需要在芯片中显示头像或图标。通过 avatar 参数,可以在标签左侧添加头像或图标。

FilterChip(
  avatar: CircleAvatar(
    backgroundColor: Colors.blue,
    child: Text('A'),
  ),
  label: Text('用户A'),
  selected: _selectedUsers.contains('用户A'),
  onSelected: (selected) {
    setState(() {
      if (selected) {
        _selectedUsers.add('用户A');
      } else {
        _selectedUsers.remove('用户A');
      }
    });
  },
)

这个实现在芯片左侧显示一个圆形头像。头像可以是任何 Widget,如 CircleAvatar、Image 等。

高级话题:芯片的企业级应用

1. 动态/响应式设计与多屏幕适配

在企业应用中,芯片需要在不同屏幕尺寸下提供一致的用户体验。对于移动设备,芯片应该较小以节省空间;对于 PC 端,可以使用更大的芯片以提高可见性。响应式设计确保芯片在任何设备上都清晰可见。

class ResponsiveChipList extends StatelessWidget {
  final List<String> tags;
  final Set<String> selectedTags;
  final Function(String, bool) onTagSelected;
  
  const ResponsiveChipList({
    required this.tags,
    required this.selectedTags,
    required this.onTagSelected,
  });
  
  
  Widget build(BuildContext context) {
    final screenWidth = MediaQuery.of(context).size.width;
    final isMobile = screenWidth < 600;
    final isTablet = screenWidth < 1200;
    
    double spacing;
    double fontSize;
    
    if (isMobile) {
      spacing = 4;
      fontSize = 12;
    } else if (isTablet) {
      spacing = 8;
      fontSize = 14;
    } else {
      spacing = 12;
      fontSize = 16;
    }
    
    return Wrap(
      spacing: spacing,
      runSpacing: spacing,
      children: tags.map((tag) {
        return FilterChip(
          label: Text(tag, style: TextStyle(fontSize: fontSize)),
          selected: selectedTags.contains(tag),
          onSelected: (selected) => onTagSelected(tag, selected),
        );
      }).toList(),
    );
  }
}

这个实现根据屏幕宽度动态调整芯片的间距和文字大小,确保在所有设备上都有最佳的视觉效果。

2. 动画与过渡效果

为芯片添加动画效果可以提升用户体验。通过 AnimatedBuilder 和 AnimationController,可以实现芯片的平滑动画。

class AnimatedChipWidget extends StatefulWidget {
  final String label;
  final bool selected;
  final Function(bool) onSelected;
  
  const AnimatedChipWidget({
    required this.label,
    required this.selected,
    required this.onSelected,
  });
  
  
  State<AnimatedChipWidget> createState() => _AnimatedChipWidgetState();
}

class _AnimatedChipWidgetState extends State<AnimatedChipWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;
  
  
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: 300),
      vsync: this,
    );
    _scaleAnimation = Tween<double>(begin: 1.0, end: 1.1).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
  }
  
  
  void didUpdateWidget(AnimatedChipWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.selected != widget.selected) {
      if (widget.selected) {
        _controller.forward();
      } else {
        _controller.reverse();
      }
    }
  }
  
  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  
  Widget build(BuildContext context) {
    return ScaleTransition(
      scale: _scaleAnimation,
      child: FilterChip(
        label: Text(widget.label),
        selected: widget.selected,
        onSelected: widget.onSelected,
      ),
    );
  }
}

这个实现使用 ScaleTransition 提供平滑的缩放效果,使芯片的选择状态变化看起来更加自然流畅。

3. 搜索/过滤/排序功能

在显示多个芯片时,可能需要根据搜索条件、过滤条件或排序方式进行动态调整。

class ChipFilterManager {
  List<String> _allTags;
  List<String> _filteredTags;
  
  String _searchQuery = '';
  String _sortBy = 'name';
  bool _sortAscending = true;
  
  ChipFilterManager(this._allTags) : _filteredTags = _allTags;
  
  void setSearchQuery(String query) {
    _searchQuery = query;
    _applyFilters();
  }
  
  void setSortBy(String sortBy, bool ascending) {
    _sortBy = sortBy;
    _sortAscending = ascending;
    _applyFilters();
  }
  
  void _applyFilters() {
    _filteredTags = _allTags.where((tag) {
      return _searchQuery.isEmpty ||
          tag.toLowerCase().contains(_searchQuery.toLowerCase());
    }).toList();
    
    _filteredTags.sort((a, b) {
      int comparison = 0;
      switch (_sortBy) {
        case 'name':
          comparison = a.compareTo(b);
          break;
        case 'length':
          comparison = a.length.compareTo(b.length);
          break;
      }
      return _sortAscending ? comparison : -comparison;
    });
  }
  
  List<String> get filteredTags => _filteredTags;
}

4. 选择与批量操作

在任务管理应用中,用户可能需要选择多个芯片进行批量操作,如批量删除、批量标记等。

class SelectableChipListWidget extends StatefulWidget {
  final List<String> tags;
  
  const SelectableChipListWidget({required this.tags});
  
  
  State<SelectableChipListWidget> createState() => _SelectableChipListWidgetState();
}

class _SelectableChipListWidgetState extends State<SelectableChipListWidget> {
  final Set<String> _selectedTags = {};
  
  void _toggleTagSelection(String tag) {
    setState(() {
      if (_selectedTags.contains(tag)) {
        _selectedTags.remove(tag);
      } else {
        _selectedTags.add(tag);
      }
    });
  }
  
  void _batchDeleteSelected() {
    print('删除 ${_selectedTags.length} 个标签');
    setState(() {
      _selectedTags.clear();
    });
  }
  
  
  Widget build(BuildContext context) {
    return Column(
      children: [
        if (_selectedTags.isNotEmpty)
          Container(
            padding: EdgeInsets.all(16),
            color: Colors.blue.shade50,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text('已选择 ${_selectedTags.length} 个标签'),
                ElevatedButton(
                  onPressed: _batchDeleteSelected,
                  child: Text('批量删除'),
                ),
              ],
            ),
          ),
        Expanded(
          child: Wrap(
            spacing: 8,
            runSpacing: 8,
            children: widget.tags.map((tag) {
              final isSelected = _selectedTags.contains(tag);
              return FilterChip(
                label: Text(tag),
                selected: isSelected,
                onSelected: (_) => _toggleTagSelection(tag),
                backgroundColor: isSelected ? Colors.blue.shade100 : null,
              );
            }).toList(),
          ),
        ),
      ],
    );
  }
}

5. 加载与缓存策略

对于需要从网络加载数据的芯片列表,应该实现缓存机制以避免重复加载。

class ChipDataCache {
  final Map<String, List<String>> _cache = {};
  bool _isLoading = false;
  
  Future<List<String>> loadChips(String category) async {
    if (_cache.containsKey(category)) {
      return _cache[category]!;
    }
    
    _isLoading = true;
    try {
      await Future.delayed(Duration(seconds: 1));
      
      final chips = [
        'Flutter',
        'Dart',
        'Mobile',
        'Web',
        'Desktop',
      ];
      
      _cache[category] = chips;
      return chips;
    } finally {
      _isLoading = false;
    }
  }
  
  void clearCache() {
    _cache.clear();
  }
  
  bool get isLoading => _isLoading;
}

6. 键盘导航与快捷键

为了提高 PC 端的可用性,应该支持键盘导航。用户可以使用 Tab 键在芯片之间导航,使用 Space 键选择芯片。

class KeyboardNavigableChipList extends StatefulWidget {
  final List<String> tags;
  
  const KeyboardNavigableChipList({required this.tags});
  
  
  State<KeyboardNavigableChipList> createState() => _KeyboardNavigableChipListState();
}

class _KeyboardNavigableChipListState extends State<KeyboardNavigableChipList> {
  int _focusedIndex = 0;
  final Set<String> _selectedTags = {};
  late FocusNode _focusNode;
  
  
  void initState() {
    super.initState();
    _focusNode = FocusNode();
  }
  
  
  void dispose() {
    _focusNode.dispose();
    super.dispose();
  }
  
  void _handleKeyEvent(RawKeyEvent event) {
    if (event.isKeyPressed(LogicalKeyboardKey.arrowRight)) {
      setState(() {
        _focusedIndex = (_focusedIndex + 1) % widget.tags.length;
      });
    } else if (event.isKeyPressed(LogicalKeyboardKey.arrowLeft)) {
      setState(() {
        _focusedIndex = (_focusedIndex - 1 + widget.tags.length) % widget.tags.length;
      });
    } else if (event.isKeyPressed(LogicalKeyboardKey.space)) {
      setState(() {
        final tag = widget.tags[_focusedIndex];
        if (_selectedTags.contains(tag)) {
          _selectedTags.remove(tag);
        } else {
          _selectedTags.add(tag);
        }
      });
    }
  }
  
  
  Widget build(BuildContext context) {
    return RawKeyboardListener(
      focusNode: _focusNode,
      onKey: _handleKeyEvent,
      child: Wrap(
        spacing: 8,
        runSpacing: 8,
        children: List.generate(widget.tags.length, (index) {
          final tag = widget.tags[index];
          final isFocused = index == _focusedIndex;
          final isSelected = _selectedTags.contains(tag);
          
          return Container(
            decoration: BoxDecoration(
              border: isFocused ? Border.all(color: Colors.blue, width: 2) : null,
            ),
            child: FilterChip(
              label: Text(tag),
              selected: isSelected,
              onSelected: (_) {
                setState(() {
                  if (isSelected) {
                    _selectedTags.remove(tag);
                  } else {
                    _selectedTags.add(tag);
                  }
                });
              },
            ),
          );
        }),
      ),
    );
  }
}

7. 无障碍支持与屏幕阅读器

为了确保应用对所有用户都可用,应该为芯片添加适当的无障碍标签。

class AccessibleChipWidget extends StatelessWidget {
  final String label;
  final bool selected;
  final Function(bool) onSelected;
  
  const AccessibleChipWidget({
    required this.label,
    required this.selected,
    required this.onSelected,
  });
  
  
  Widget build(BuildContext context) {
    return Semantics(
      label: '$label ${selected ? "已选择" : "未选择"}',
      button: true,
      enabled: true,
      onTap: () => onSelected(!selected),
      child: FilterChip(
        label: Semantics(
          label: '标签',
          child: Text(label),
        ),
        selected: selected,
        onSelected: onSelected,
      ),
    );
  }
}

8. 样式自定义与主题适配

不同的应用可能需要不同的芯片样式。通过创建可配置的主题类,可以轻松适配不同的设计风格。

class ChipTheme {
  final Color backgroundColor;
  final Color selectedColor;
  final Color textColor;
  final Color selectedTextColor;
  final EdgeInsets padding;
  final double borderRadius;
  
  const ChipTheme({
    this.backgroundColor = const Color(0xFFE0E0E0),
    this.selectedColor = Colors.blue,
    this.textColor = Colors.black87,
    this.selectedTextColor = Colors.white,
    this.padding = const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
    this.borderRadius = 16,
  });
  
  static ChipTheme light() {
    return ChipTheme(
      backgroundColor: Colors.grey.shade300,
      selectedColor: Colors.blue,
      textColor: Colors.black87,
      selectedTextColor: Colors.white,
    );
  }
  
  static ChipTheme dark() {
    return ChipTheme(
      backgroundColor: Colors.grey.shade700,
      selectedColor: Colors.blue.shade300,
      textColor: Colors.white,
      selectedTextColor: Colors.black,
    );
  }
}

9. 数据持久化与导出

在某些应用中,可能需要保存用户的芯片选择以便后续查看或分析。

class ChipDataExporter {
  static String exportToJSON(Set<String> selectedTags) {
    final jsonList = selectedTags.toList();
    return jsonEncode(jsonList);
  }
  
  static String exportToCSV(Set<String> selectedTags) {
    return selectedTags.join(',');
  }
}

10. 单元测试与集成测试

为了确保芯片功能的正确性,应该编写全面的测试用例。

void main() {
  group('Chip Tests', () {
    test('芯片选择', () {
      final selectedTags = <String>{};
      expect(selectedTags.length, 0);
      selectedTags.add('标签1');
      expect(selectedTags.length, 1);
      expect(selectedTags.contains('标签1'), true);
    });
    
    test('芯片过滤', () {
      final tags = ['Flutter', 'Dart', 'Mobile', 'Web'];
      final filtered = tags.where((tag) => tag.contains('l')).toList();
      expect(filtered.length, 2);
    });
    
    test('芯片排序', () {
      final tags = ['Zebra', 'Apple', 'Banana'];
      tags.sort();
      expect(tags[0], 'Apple');
      expect(tags[2], 'Zebra');
    });
  });
  
  testWidgets('Chip Widget 集成测试', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Chip(label: Text('测试标签')),
        ),
      ),
    );
    
    expect(find.text('测试标签'), findsOneWidget);
  });
}

OpenHarmony PC 端适配要点

  1. 屏幕宽度检测:根据不同屏幕宽度调整芯片间距和大小
  2. 响应式布局:在 PC 端使用更大的间距以提高可见性
  3. 键盘导航:支持 Tab、方向键和 Space 键操作
  4. 鼠标交互:支持悬停效果和点击反馈
  5. 无障碍支持:为屏幕阅读器提供完整的语义标签

实际应用场景

  1. 标签管理:文章标签、产品标签、技能标签
  2. 多选择器:搜索过滤、条件筛选
  3. 用户标记:用户角色、权限标记
  4. 分类展示:内容分类、产品分类

扩展建议

  1. 支持芯片的拖拽排序
  2. 实现芯片的搜索和自动完成
  3. 添加芯片的分组显示
  4. 支持芯片的动态加载
  5. 实现芯片的历史记录

总结

芯片是展示标签和选项的有效方式。通过合理的设计和实现,可以创建出功能完整、用户体验良好的芯片系统。在 PC 端应用中,充分利用屏幕空间、提供键盘导航和无障碍支持是关键。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新

更多推荐