在这里插入图片描述

引言

筛选功能帮助用户精确查找符合条件的内容。本篇将实现多条件筛选页面,包含类型、人数、价格和评分筛选。通过合理的筛选设计,用户可以快速缩小搜索范围,找到最符合需求的剧本或店铺。筛选功能是电商和内容应用的核心功能之一,良好的筛选体验能显著提升用户满意度和转化率。

功能设计

筛选页面包含:

  • 剧本类型多选
  • 人数多选
  • 价格范围滑块
  • 最低评分滑块
  • 重置和确定按钮

设计思路

筛选功能的设计遵循了渐进式过滤的原则。用户可以同时应用多个筛选条件,系统会实时显示当前的筛选状态。我们使用不同的组件来处理不同类型的筛选:离散选项(类型、人数)使用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

Logo

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

更多推荐