Flutter for OpenHarmony 剧本杀组队App实战:筛选功能实现
摘要 本文介绍了Flutter多条件筛选页面的设计与实现。页面包含剧本类型、人数、价格范围和最低评分四种筛选方式,使用FilterChip组件实现多选功能,RangeSlider处理价格范围选择,Slider控制评分阈值。通过状态管理实时更新筛选条件,提供重置按钮方便用户重新选择。文章详细讲解了数据结构设计、UI布局和交互逻辑,展示了如何构建一个用户友好的筛选系统,帮助用户快速找到符合需求的剧本或

引言
筛选功能帮助用户精确查找符合条件的内容。本篇将实现多条件筛选页面,包含类型、人数、价格和评分筛选。通过合理的筛选设计,用户可以快速缩小搜索范围,找到最符合需求的剧本或店铺。筛选功能是电商和内容应用的核心功能之一,良好的筛选体验能显著提升用户满意度和转化率。
功能设计
筛选页面包含:
- 剧本类型多选
- 人数多选
- 价格范围滑块
- 最低评分滑块
- 重置和确定按钮
设计思路
筛选功能的设计遵循了渐进式过滤的原则。用户可以同时应用多个筛选条件,系统会实时显示当前的筛选状态。我们使用不同的组件来处理不同类型的筛选:离散选项(类型、人数)使用FilterChip,连续范围(价格、评分)使用滑块。这种设计既直观易用,又能处理复杂的筛选需求。
数据结构设计
我们使用Set集合存储多选项,这样可以自动去重,避免重复选择。对于范围值,使用RangeValues和double类型分别表示。这种设计使状态管理清晰,易于序列化和传递给后端API。
核心代码实现
第一部分:导入和页面结构
筛选功能的实现需要导入Flutter的Material组件库和GetX路由库。我们使用StatefulWidget来管理筛选状态,包括多选项的选择状态、价格范围和评分范围。通过状态管理,我们可以实时更新UI,让用户看到当前的筛选条件。这种设计使筛选功能具有良好的交互反馈。
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class FilterPage extends StatefulWidget {
FilterPage({super.key});
State<FilterPage> createState() => _FilterPageState();
}
class _FilterPageState extends State<FilterPage> {
final Set<String> _selectedTypes = {};
final Set<String> _selectedPlayers = {};
RangeValues _priceRange = const RangeValues(50, 150);
double _minRating = 8.0;
final List<String> _types = ['情感本', '恐怖本', '机制本', '欢乐本', '硬核本', '阵营本'];
final List<String> _players = ['4人', '5人', '6人', '7人', '8人', '8人以上'];
状态变量详解
_selectedTypes和_selectedPlayers使用Set集合存储,这是处理多选的最佳方式。Set自动去重,避免了重复选择的问题。RangeValues用于表示价格范围的起始和结束值,初始值设为50到150元。_minRating表示最低评分,初始值为8.0分。_types和_players列表定义了可选的筛选项,这些数据可以从后端API动态获取。
页面布局结构
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('筛选'),
actions: [
TextButton(
onPressed: _reset,
child: const Text('重置', style: TextStyle(color: Colors.white)),
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppBar和重置按钮
AppBar中的重置按钮允许用户一键清除所有筛选条件,恢复到初始状态。这是一个重要的交互功能,用户可能需要重新开始筛选。SingleChildScrollView包装整个body,确保当筛选项很多时,页面仍然可以滚动。padding设置了内边距,使内容不会贴边显示。
// 剧本类型
_buildSection(
'剧本类型',
Wrap(
spacing: 8,
runSpacing: 8,
children: _types.map((t) => FilterChip(
label: Text(t),
selected: _selectedTypes.contains(t),
selectedColor: const Color(0xFF6B4EFF).withOpacity(0.2),
onSelected: (v) => setState(() => v ? _selectedTypes.add(t) : _selectedTypes.remove(t)),
)).toList(),
),
),
剧本类型筛选
剧本类型筛选使用FilterChip组件,这是Material Design中专门为筛选设计的组件。Wrap组件实现自动换行,使多个筛选项能自适应屏幕宽度。selected属性根据_selectedTypes是否包含该类型来决定芯片是否被选中。onSelected回调在用户点击时触发,根据v的值来添加或移除该类型。selectedColor设置为紫色半透明,提供清晰的视觉反馈。
// 人数
_buildSection(
'人数',
Wrap(
spacing: 8,
runSpacing: 8,
children: _players.map((p) => FilterChip(
label: Text(p),
selected: _selectedPlayers.contains(p),
selectedColor: const Color(0xFF6B4EFF).withOpacity(0.2),
onSelected: (v) => setState(() => v ? _selectedPlayers.add(p) : _selectedPlayers.remove(p)),
)).toList(),
),
),
人数筛选
人数筛选的实现与类型筛选完全相同,都使用FilterChip和Wrap组件。这种一致的设计使用户能快速理解和使用筛选功能。人数选项包括4人到8人以上的多个选择,用户可以根据自己的需求选择一个或多个人数范围。
// 价格范围
_buildSection(
'价格范围',
Column(
children: [
RangeSlider(
values: _priceRange,
min: 0,
max: 300,
divisions: 30,
activeColor: const Color(0xFF6B4EFF),
labels: RangeLabels('¥${_priceRange.start.toInt()}', '¥${_priceRange.end.toInt()}'),
onChanged: (v) => setState(() => _priceRange = v),
),
Text('¥${_priceRange.start.toInt()} - ¥${_priceRange.end.toInt()}'),
],
),
),
价格范围滑块
RangeSlider是处理范围值的理想组件。min和max定义了滑块的范围(0到300元),divisions将范围分为30个等份,使滑块移动更加平滑。labels属性显示当前选中的起始和结束值,用户可以实时看到价格范围。activeColor设置为应用主题色,提供一致的视觉风格。下方的Text组件显示当前选中的价格范围,为用户提供额外的信息反馈。
// 最低评分
_buildSection(
'最低评分',
Column(
children: [
Slider(
value: _minRating,
min: 5,
max: 10,
divisions: 10,
activeColor: const Color(0xFF6B4EFF),
label: _minRating.toString(),
onChanged: (v) => setState(() => _minRating = v),
),
Text('${_minRating.toStringAsFixed(1)}分以上'),
],
),
),
最低评分滑块
最低评分使用单值Slider组件,范围从5分到10分。divisions设为10,使滑块可以精确到0.1分。label显示当前选中的评分值。下方的Text使用toStringAsFixed(1)方法格式化评分,确保显示一位小数。这个筛选条件帮助用户只查看评分较高的内容,提高了内容质量。
],
),
),
bottomSheet: Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Colors.white,
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 4)],
),
child: SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: () {
Get.back();
Get.snackbar('筛选', '已应用筛选条件');
},
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF6B4EFF)),
child: const Text('确定', style: TextStyle(color: Colors.white)),
),
),
),
);
}
底部确定按钮
bottomSheet将确定按钮固定在屏幕底部,确保用户无论滚动到哪里都能看到。Container的decoration设置了白色背景和阴影,使按钮区域与内容区域分离。ElevatedButton使用应用主题色,宽度设为double.infinity使其充满整个宽度。点击确定按钮时,使用Get.back()返回上一页,并显示一个Snackbar提示用户筛选条件已应用。
Widget _buildSection(String title, Widget child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
),
child,
const SizedBox(height: 8),
],
);
}
通用Section构建方法
_buildSection是一个通用的辅助方法,用于构建每个筛选部分。它接收标题和内容组件作为参数,返回一个统一格式的Column。这种设计避免了代码重复,使代码更加DRY(Don’t Repeat Yourself)。标题使用粗体和较大的字体,与内容区分。Padding和SizedBox提供了适当的间距,使整个页面看起来整洁有序。
void _reset() => setState(() {
_selectedTypes.clear();
_selectedPlayers.clear();
_priceRange = const RangeValues(50, 150);
_minRating = 8.0;
});
}
重置功能实现
_reset方法清除所有筛选条件,恢复到初始状态。使用Set的clear()方法清空多选项,重新赋值RangeValues和double来重置范围值。这个方法通过setState触发UI重建,用户能立即看到所有筛选条件被清除。
技术要点详解
1. FilterChip组件的应用
FilterChip是Material Design中专门为筛选设计的组件。与普通的Chip不同,FilterChip支持selected状态,可以清晰地显示用户的选择。通过设置selectedColor属性,我们可以为选中状态提供视觉反馈。FilterChip的onSelected回调提供了一个布尔值,表示用户是否选中了该芯片。这使得多选逻辑变得简单直观。
2. Set集合的优势
使用Set存储多选项比使用List更合适。Set自动去重,避免了重复选择的问题。Set的contains()方法用于检查是否包含某个元素,add()和remove()方法用于添加和移除元素。这些操作都是O(1)时间复杂度,性能优于List。Set还支持clear()方法一键清空所有元素。
3. RangeSlider的使用
RangeSlider用于选择一个范围内的值。它有两个拖动点,分别表示范围的起始和结束。min和max定义了范围的边界,divisions将范围分成若干等份,使滑块移动更加平滑。labels属性显示当前选中的值,为用户提供实时反馈。RangeValues是一个包含start和end两个属性的类,用于表示范围值。
4. Slider的精确控制
Slider用于选择单个值。divisions属性控制精度,例如divisions: 10表示可以精确到0.1。label属性显示当前值,用户拖动滑块时会看到实时更新的标签。通过设置min和max,我们可以定义值的范围。Slider的onChanged回调在用户拖动时不断触发,使用setState更新状态。
5. Wrap组件的自适应布局
Wrap组件会根据可用宽度自动换行排列子组件。spacing控制水平间距,runSpacing控制竖直间距。相比Column或Row,Wrap能更灵活地处理不确定数量的子组件。当屏幕宽度变化时,Wrap会自动重新排列子组件,提供响应式的布局。
6. bottomSheet的固定位置
bottomSheet将组件固定在屏幕底部,即使用户滚动内容也不会移动。这对于确定按钮等重要操作很有用。通过设置decoration的boxShadow,我们可以为bottomSheet添加阴影,使其与内容区域分离。
高级功能实现
筛选条件的序列化和传递
为了将筛选条件传递给搜索或列表页面,我们可以创建一个FilterModel类:
class FilterModel {
final Set<String> types;
final Set<String> players;
final RangeValues priceRange;
final double minRating;
FilterModel({
required this.types,
required this.players,
required this.priceRange,
required this.minRating,
});
Map<String, dynamic> toJson() {
return {
'types': types.toList(),
'players': players.toList(),
'priceStart': priceRange.start,
'priceEnd': priceRange.end,
'minRating': minRating,
};
}
factory FilterModel.fromJson(Map<String, dynamic> json) {
return FilterModel(
types: Set.from(json['types'] ?? []),
players: Set.from(json['players'] ?? []),
priceRange: RangeValues(
(json['priceStart'] ?? 50).toDouble(),
(json['priceEnd'] ?? 150).toDouble(),
),
minRating: (json['minRating'] ?? 8.0).toDouble(),
);
}
}
这个模型类提供了toJson()和fromJson()方法,使筛选条件可以序列化为JSON格式,便于存储和传递。
筛选条件的保存和恢复
可以使用shared_preferences保存用户的筛选偏好:
Future<void> _saveFilter() async {
final prefs = await SharedPreferences.getInstance();
final filter = FilterModel(
types: _selectedTypes,
players: _selectedPlayers,
priceRange: _priceRange,
minRating: _minRating,
);
await prefs.setString('last_filter', jsonEncode(filter.toJson()));
}
Future<void> _loadFilter() async {
final prefs = await SharedPreferences.getInstance();
final filterJson = prefs.getString('last_filter');
if (filterJson != null) {
final filter = FilterModel.fromJson(jsonDecode(filterJson));
setState(() {
_selectedTypes.addAll(filter.types);
_selectedPlayers.addAll(filter.players);
_priceRange = filter.priceRange;
_minRating = filter.minRating;
});
}
}
这样用户下次打开筛选页面时,会看到上次的筛选条件。
动态筛选选项
筛选选项可以从后端API动态获取:
void initState() {
super.initState();
_loadFilterOptions();
}
Future<void> _loadFilterOptions() async {
try {
final response = await http.get(Uri.parse('/api/filter-options'));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
setState(() {
_types = List<String>.from(data['types']);
_players = List<String>.from(data['players']);
});
}
} catch (e) {
print('Error loading filter options: $e');
}
}
这使筛选选项可以根据后端数据动态更新。
筛选结果的实时预览
可以在筛选页面显示当前筛选条件下的结果数量:
Widget _buildResultCount() {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF6B4EFF).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'符合条件的结果:${_getResultCount()}个',
style: const TextStyle(color: Color(0xFF6B4EFF)),
),
);
}
int _getResultCount() {
// 根据筛选条件计算结果数量
// 这里应该调用API或本地数据库查询
return 42; // 示例值
}
这个功能让用户能实时看到筛选条件的效果。
功能扩展建议
1. 筛选历史记录
保存用户最近使用过的筛选条件,用户可以快速应用之前的筛选。
2. 筛选条件的分享
允许用户分享当前的筛选条件给其他用户,通过深链接实现。
3. 高级筛选
添加更多筛选维度,如地区、营业时间、特殊标签等。
4. 筛选条件的可视化
使用图表或其他可视化方式展示筛选条件的分布情况。
5. 智能推荐
根据用户的筛选历史,推荐可能感兴趣的筛选条件。
常见问题解答
Q: 如何实现筛选条件的联动?
A: 可以在onChanged回调中检查其他条件,根据需要禁用某些选项:
onSelected: (v) {
setState(() {
if (v) {
_selectedTypes.add(t);
// 如果选择了某个类型,自动调整价格范围
if (t == '高端本') {
_priceRange = const RangeValues(200, 300);
}
} else {
_selectedTypes.remove(t);
}
});
}
Q: 如何处理筛选条件过多的情况?
A: 可以使用分类标签页或可折叠的Section来组织筛选条件:
ExpansionTile(
title: const Text('高级筛选'),
children: [
// 更多筛选选项
],
)
Q: 如何验证筛选条件的有效性?
A: 在应用筛选前进行验证:
bool _validateFilter() {
if (_priceRange.start >= _priceRange.end) {
Get.snackbar('错误', '价格范围无效');
return false;
}
return true;
}
小结
本篇实现了一个功能完整的筛选页面,支持多选、范围选择等多种筛选方式。通过合理使用FilterChip、Slider等组件,我们创建了一个用户友好、交互流畅的筛选体验。筛选功能是电商和内容应用的核心功能,良好的筛选设计能显著提升用户体验和转化率。本篇提供的代码和设计思路可以作为基础,根据实际需求进行扩展和优化。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)