Flutter & OpenHarmony PC 端适配:芯片列表(Chip List)
摘要 本案例展示了Flutter中Chip和FilterChip组件的使用方法,适用于标签管理和多选场景。核心组件包括基础Chip(显示标签信息)、带删除功能的Chip(支持用户删除)和FilterChip(实现多选功能)。通过Wrap布局可实现自动换行的芯片列表,并支持响应式设计以适应不同屏幕尺寸。高级应用涵盖动态调整(根据设备尺寸优化显示)、动画效果(提升用户体验)以及企业级功能(如批量操作和
案例概述
本案例展示如何使用 Chip 和 FilterChip 创建标签和选择界面。芯片是一种紧凑的组件,用于表示标签、选项、过滤条件等。在现代应用中,芯片广泛应用于下列场景:
- 标签管理:文章标签、产品分类、技能标签等
- 多选择器:用户可以一次选择多个选项,常见于搜索、筛选场景
- 上下文信息:显示上下文相关的信息或分类
- 实时反馈:支持即时的选择反馈和加载状态
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 端适配要点
- 屏幕宽度检测:根据不同屏幕宽度调整芯片间距和大小
- 响应式布局:在 PC 端使用更大的间距以提高可见性
- 键盘导航:支持 Tab、方向键和 Space 键操作
- 鼠标交互:支持悬停效果和点击反馈
- 无障碍支持:为屏幕阅读器提供完整的语义标签
实际应用场景
- 标签管理:文章标签、产品标签、技能标签
- 多选择器:搜索过滤、条件筛选
- 用户标记:用户角色、权限标记
- 分类展示:内容分类、产品分类
扩展建议
- 支持芯片的拖拽排序
- 实现芯片的搜索和自动完成
- 添加芯片的分组显示
- 支持芯片的动态加载
- 实现芯片的历史记录
总结
芯片是展示标签和选项的有效方式。通过合理的设计和实现,可以创建出功能完整、用户体验良好的芯片系统。在 PC 端应用中,充分利用屏幕空间、提供键盘导航和无障碍支持是关键。
更多推荐



所有评论(0)