flutter---自定义白噪音UI

效果图
详细解释

这页面有横向导航栏供用户选择,左右滑动查看所有分类,可以选择三个子项,当已选满3个时,再选择新的音效会自动 替换最早选择的那一个。

哈希码

1.概念:类似身份证号码,在Dart中,所有对象都继承自 Object ,而 Object 类里就有一个默认的 hashCode 属性。所以每个对象都有一个哈希码。

2.默认的哈希码是怎么生成的?

Dart 复制代码
// Dart的默认hashCode生成(简化理解):
int get hashCode {
  return 基于内存地址的随机数;
}

3.作用:一般用来比较两个对象是否相同

数据结构
Dart 复制代码
WhiteNoiseItem {
  String name;      // 白噪音名称
  String icon;      // 图标emoji
  String category;  // 所属分类
}
数据流向
Dart 复制代码
静态数据 (_whiteNoiseItems)
    ↓
用户选择分类 (_selectedCategoryIndex)
    ↓
筛选显示 (_getFilteredItems())
    ↓
用户选择/取消 (_selectedNoises)
    ↓
UI更新 (setState)
业务逻辑层
Dart 复制代码
1. 选择逻辑
最大支持选择3个白噪音

采用循环替换策略:超过3个时替换最早选择的一项

点击已选项会移除

2. 分类筛选
"全部"显示所有数据

其他分类显示对应数据

使用List.where()进行数据筛选

3. 比较去重
分层结构
Dart 复制代码
CustomWhiteNoise (StatefulWidget)
├── 数据层 (Model)
│   └── WhiteNoiseItem
├── 业务逻辑层 (ViewModel)
│   ├── _getFilteredItems()
│   ├── _onNoiseSelected()
│   └── _removeSelectedNoise()
├── 视图层 (View)
│   ├── _buildCategoryTabs()
│   ├── _buildWhiteNoiseGrid()
│   └── _buildSelectedNoisesDisplay()
└── 组件层 (Components)
    ├── _buildNoiseItem()
    ├── _buildSelectedNoiseItem()
    └── _buildEmptySlot()
实现步骤

1.定义变量:分类标签组、标记用户选中的子项、存储选中的白噪音列表

Dart 复制代码
final List<String> _categories = ['全部', '遮噪', '水流声', '大自然', '生活',"冥想"];// 分类标签数据

int _selectedCategoryIndex = 0; //用户选中的目录子项,默认选中全部

// 选中的白噪音列表(最多3个)
List<WhiteNoiseItem> _selectedNoises = [];

2.定义白噪音数据模型

Dart 复制代码
class WhiteNoiseItem {
  final String name; //名称
  final String icon; //图标
  final String category; //所属分类

  //构造函数
  WhiteNoiseItem({
    required this.name,
    required this.icon,
    required this.category,
  });

  ///以下方法防止重复添加(因为白噪音项目最多选中3个),
  @override
  bool operator ==(Object other) => //两个子项是否相等 (operator==表示我要重新定义等于号)
      identical(this, other) || //情况1:完全是同一个对象
          other is WhiteNoiseItem && runtimeType == other.runtimeType && name == other.name; //情况二:都是WhiteNoiseItem类型,且类型完全相同,名字完全相同,则认定是统一对象

  @override
  int get hashCode => name.hashCode; //这个对象的哈希码等于它的名字的哈希码
}

3.准备网格列表数据:白噪音数据

Dart 复制代码
  // 准备白噪音数据
  final List<WhiteNoiseItem> _whiteNoiseItems = [

    // 水流声类别
    WhiteNoiseItem(name: '溪流', icon: '🏞️', category: '水流声'),
    WhiteNoiseItem(name: '海浪', icon: '🌊', category: '水流声'),
    WhiteNoiseItem(name: '瀑布', icon: '💦', category: '水流声'),
    WhiteNoiseItem(name: '泉水', icon: '💧', category: '水流声'),
    WhiteNoiseItem(name: '河流', icon: '🌊', category: '水流声'),
    WhiteNoiseItem(name: '雨滴', icon: '🌧️', category: '水流声'),

    // 大自然类别
    WhiteNoiseItem(name: '风声', icon: '💨', category: '大自然'),
    WhiteNoiseItem(name: '森林', icon: '🌲', category: '大自然'),
    WhiteNoiseItem(name: '鸟鸣', icon: '🐦', category: '大自然'),
    WhiteNoiseItem(name: '蝉鸣', icon: '🦗', category: '大自然'),
    WhiteNoiseItem(name: '蛙声', icon: '🐸', category: '大自然'),
    WhiteNoiseItem(name: '雷声', icon: '⚡', category: '大自然'),
    WhiteNoiseItem(name: '雪落', icon: '❄️', category: '大自然'),
    WhiteNoiseItem(name: '落叶', icon: '🍂', category: '大自然'),

    // 生活类别
    WhiteNoiseItem(name: '篝火', icon: '🔥', category: '生活'),
    WhiteNoiseItem(name: '钟声', icon: '⏰', category: '生活'),
    WhiteNoiseItem(name: '城市', icon: '🏙️', category: '生活'),
    WhiteNoiseItem(name: '火车', icon: '🚂', category: '生活'),
    WhiteNoiseItem(name: '咖啡', icon: '☕', category: '生活'),
    WhiteNoiseItem(name: '键盘', icon: '⌨️', category: '生活'),
    WhiteNoiseItem(name: '风扇', icon: '🌀', category: '生活'),
    WhiteNoiseItem(name: '心跳', icon: '💓', category: '生活'),

    // 遮噪类别
    WhiteNoiseItem(name: '白噪', icon: '📡', category: '遮噪'),
    WhiteNoiseItem(name: '粉噪', icon: '🎵', category: '遮噪'),
    WhiteNoiseItem(name: '棕噪', icon: '🔊', category: '遮噪'),
    WhiteNoiseItem(name: '紫噪', icon: '🎶', category: '遮噪'),
    WhiteNoiseItem(name: '灰噪', icon: '🔇', category: '遮噪'),

    // 冥想类别
    WhiteNoiseItem(name: '禅钟', icon: '🛎️', category: '冥想'),
    WhiteNoiseItem(name: '钵音', icon: '🥣', category: '冥想'),
    WhiteNoiseItem(name: '经诵', icon: '📿', category: '冥想'),
    WhiteNoiseItem(name: '风铃', icon: '🎐', category: '冥想'),
    WhiteNoiseItem(name: '静心', icon: '🧘', category: '冥想'),
    WhiteNoiseItem(name: '呼吸', icon: '🌬️', category: '冥想'),


    // 动物声音
    WhiteNoiseItem(name: '猫咪', icon: '🐱', category: '生活'),
    WhiteNoiseItem(name: '狗狗', icon: '🐶', category: '生活'),
    WhiteNoiseItem(name: '鸟儿', icon: '🐦', category: '大自然'),
    WhiteNoiseItem(name: '海豚', icon: '🐬', category: '大自然'),
    WhiteNoiseItem(name: '鲸鱼', icon: '🐋', category: '大自然'),
    WhiteNoiseItem(name: '狗吠', icon: '🐕', category: '生活'),

    // 天气声音
    WhiteNoiseItem(name: '暴雨', icon: '⛈️', category: '大自然'),
    WhiteNoiseItem(name: '细雨', icon: '🌦️', category: '大自然'),
    WhiteNoiseItem(name: '风雪', icon: '🌨️', category: '大自然'),
    WhiteNoiseItem(name: '晴天', icon: '☀️', category: '大自然'),

    // 乐器声音
    WhiteNoiseItem(name: '钢琴', icon: '🎹', category: '生活'),
    WhiteNoiseItem(name: '吉他', icon: '🎸', category: '生活'),
    WhiteNoiseItem(name: '笛子', icon: '🎵', category: '生活'),
    WhiteNoiseItem(name: '鼓声', icon: '🥁', category: '生活'),

  ];

4.构建页面架构,渐变的底面、分类的标签栏、白噪音网格列表

Dart 复制代码
     Container(
        width: double.infinity,
        height: double.infinity,
        decoration: const BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [
              Color(0xFF060618),
              Color(0xFF070A23),
            ],
          ),
        ),
        child: Column(
          children: [

            // 分类标签栏
            _buildCategoryTabs(),
            const SizedBox(height: 20),

            // 白噪音网格列表
            Expanded(
              child: _buildWhiteNoiseGrid(),
            ),

          ],
        ),
      ),

5.构建分类标签栏:设定高为20,宽为无限大的区域里面设置横向滚动列表

Dart 复制代码
 Widget _buildCategoryTabs() {
    return Container( //构建标签栏的整个区域
      height: 20,
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: ListView.builder(
        scrollDirection: Axis.horizontal, //横向滚动
        itemCount: _categories.length, //子项长度
        itemBuilder: (context, index) { //自动传入子项构建
          final isSelected = _selectedCategoryIndex == index; //判断当前标签是否选中
          return Container(
            margin: const EdgeInsets.only(right: 12),
            child: GestureDetector(
              onTap: () {
                setState(() {
                  _selectedCategoryIndex = index; //更新选中状态,比如用户选中第二项的时候,这个列表的UI将重新构建,绘制选中第二项的UI
                });
              },
              child: Container( //列表的单个子项UI
                padding: const EdgeInsets.symmetric(horizontal: 20),
                decoration: BoxDecoration(
                  color: isSelected ? const Color(0xFF4A90E2) :  Colors.white.withOpacity(0.1),
                  borderRadius: BorderRadius.circular(10),
                ),
                child: Center(
                  child: Text(
                    _categories[index], //显示分类名称
                    style: TextStyle(
                      color: isSelected ? Colors.white : Colors.white.withOpacity(0.7),
                      fontSize: 10,
                      fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
                    ),
                  ),
                ),
              ),
            ),
          );
        },
      ),
    );
  }

注\]用户点击其他子项的数据流 ```Dart 1. 用户点击第二个标签(index = 1) 2. setState被调用 3. _selectedCategoryIndex 的值从 0 → 1 4. Flutter标记此Widget为"脏"(需要重建) 5. 框架调度重建任务 6. _buildCategoryTabs() 方法被重新调用 7. ListView.builder 重新构建所有标签项 8. 每个标签项根据新的 _selectedCategoryIndex 判断选中状态 ``` 6.获取筛选后的音效列表 ```Dart List _getFilteredItems() { // 1. 先获取当前选中的分类索引 final selectedIndex = _selectedCategoryIndex; // 2. 判断是否选择了"全部"分类 final isAllSelected = selectedIndex == 0; // 3. 如果选择了"全部",直接使用所有数据 if (isAllSelected) { return _whiteNoiseItems; } // 4. 否则,筛选对应分类的数据 else { // 获取选中的分类名称 final selectedCategoryName = _categories[selectedIndex]; // 筛选数据:只保留分类匹配的项 return _whiteNoiseItems.where((item) { //where()筛选方法,(item)列表中的每个音效 return item.category == selectedCategoryName; //子项的分类等于选中的分类 }).toList(); //.toList 把筛选出来的东西打包成列表 } } ``` 7.构建白噪音网格列表 ```Dart Widget _buildWhiteNoiseGrid() { //获取筛选后的列表 final filteredItems = _getFilteredItems(); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ // 网格列表 Expanded( child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, //每行显示4个 crossAxisSpacing: 15, //列间距 mainAxisSpacing: 25, //行间距 childAspectRatio: 0.9, //子项宽高比 ), itemCount: filteredItems.length, //动态数据长度 itemBuilder: (context, index) { //构建每个网格项 final item = filteredItems[index]; //获取当前目录子项的数据 final isSelected = _selectedNoises.contains(item); //检查是否已经被选中:检查当前这个音效(item),是否在已选音效列表(_selectedNoises)中,根据返回的bool值UI做相应的更改 return _buildNoiseItem(item, index, isSelected); //传入、下标、选中状态 }, ), ), // 选中的白噪音显示区域 if (_selectedNoises.isNotEmpty) _buildSelectedNoisesDisplay(), ], ), ); } ``` \[注\]contains是怎么跟已经点击的子项做比较的 ```Dart ==========================调用链========================= // 你写的代码: _selectedNoises.contains(item) // Dart内部实际上执行: _selectedNoises.contains(item) { // 内部会调用: item.hashCode; // ⭐ 调用你的hashCode getter item == otherItem; // ⭐ 调用你的operator==方法 } 规定:所有"比较两个对象是否相等"的操作,必须使用对象的==方法 =======================完整的调用链===================== 用户操作 ↓ _selectedNoises.contains(item) // 你写的代码 ↓ Dart的List.contains()方法执行 ↓ 内部循环:for each element in list ↓ 调用:element == item ⭐ 这里调用你的operator== ↓ 你的operator==方法执行:比较name ↓ 返回比较结果 ↓ contains() 返回 true/false ``` 8.构建单个白噪音项 ```Dart Widget _buildNoiseItem(WhiteNoiseItem item, int index, bool isSelected) { return GestureDetector( //子项点击事件 onTap: () { _onNoiseSelected(item, isSelected); }, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // 图标 Container( width: 55, height: 55, decoration: BoxDecoration( color: isSelected ? const Color(0xFF2178E3) : Colors.white.withOpacity(0.1), borderRadius: BorderRadius.circular(30), ), child: Center( child: Text( item.icon, style: const TextStyle(fontSize: 24), ), ), ), const SizedBox(height: 8), // 名称 Text( item.name, style: TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), ], ), ); } ``` 9.白噪音选中处理 ```Dart void _onNoiseSelected(WhiteNoiseItem item, bool isSelected) { setState(() { if (isSelected) { // 如果已经选中,则移除 _selectedNoises.remove(item); print('⏹️ 移除白噪音: ${item.name}'); } else { // 如果未选中,则添加(循环替换逻辑) if (_selectedNoises.length < 3) { // 如果还有空位,直接添加 _selectedNoises.add(item); print('🎵 添加白噪音: ${item.name}'); } else { // 如果已满3个,替换第一个 final removedItem = _selectedNoises.removeAt(0); _selectedNoises.add(item); print('🔄 替换白噪音: ${removedItem.name} -> ${item.name}'); } } }); // 播放/停止逻辑 if (!isSelected) { print('▶️ 开始播放: ${item.name}'); // TODO: 实现播放逻辑 } else { print('⏹️ 停止播放: ${item.name}'); // TODO: 实现停止播放逻辑 } } ``` \[注\]为什么总是能替换最早的那个子项 ```Dart =====================removeAt============================ // 初始:_selectedNoises = [A, B, C] // 索引:0(A), 1(B), 2(C) _selectedNoises.removeAt(0); // 移除索引0的元素(A) // 结果:_selectedNoises = [B, C] // B自动变成索引0,C变成索引1 ====================替换完整流程=========================== // 初始状态(按添加顺序): _selectedNoises = [ '溪流', // 最早添加(索引0) ⭐ 会被替换 '海浪', // 第二个添加(索引1) '森林' // 最新添加(索引2) ]; // 用户点击第4个音效:'鸟鸣' if (_selectedNoises.length < 3) { // 不满3个 → 直接添加 } else { // 已满3个 → 替换第一个 final removedItem = _selectedNoises.removeAt(0); // 移除'溪流' _selectedNoises.add('鸟鸣'); // 添加'鸟鸣' } // 结果: _selectedNoises = ['海浪', '森林', '鸟鸣']; // '溪流'被移除,'鸟鸣'加在最后 ``` 10.构建选中的白噪音显示区域 ```Dart Widget _buildSelectedNoisesDisplay() { return Container( height: 80, margin: const EdgeInsets.only(bottom: 20), decoration: BoxDecoration( color: Colors.white.withOpacity(0.05), borderRadius: BorderRadius.circular(15), border: Border.all(color: Colors.white.withOpacity(0.1)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // 显示最多3个选中的白噪音 for (int i = 0; i < 3; i++) //i=0,1,2 i < _selectedNoises.length //检查这个位置有没有子项 ? _buildSelectedNoiseItem(_selectedNoises[i], i) //有,则显示子项 : _buildEmptySlot(i), //没有,则显示空位 ], ), ); } ``` 11.构建选中的白噪音项 ```Dart Widget _buildSelectedNoiseItem(WhiteNoiseItem item, int index) { return GestureDetector( onTap: () => _removeSelectedNoise(index), //点击子项本身则移除 child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Stack( children: [ //图标 Container( width: 50, height: 50, decoration: BoxDecoration( color: const Color(0xFF2178E3), borderRadius: BorderRadius.circular(25), ), child: Center( child: Text( item.icon, style: const TextStyle(fontSize: 20), ), ), ), // 红色的移除按钮 Positioned( top: 0, right: 0, child: Container( width: 16, height: 16, decoration: const BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), child: const Icon( Icons.close, color: Colors.white, size: 12, ), ), ), ], ), const SizedBox(height: 4), Text( item.name, style: const TextStyle( color: Colors.white, fontSize: 10, ), ), ], ), ); } ``` 12.构建空槽位 ```Dart Widget _buildEmptySlot(int index) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ //图标 Container( width: 50, height: 50, decoration: BoxDecoration( color: Colors.white.withOpacity(0.1), borderRadius: BorderRadius.circular(25), border: Border.all(color: Colors.white.withOpacity(0.3), width: 1), ), child: const Icon( Icons.add, color: Colors.white54, size: 20, ), ), const SizedBox(height: 4), //标题 Text( '空位 ${index + 1}', style: TextStyle( color: Colors.white.withOpacity(0.5), fontSize: 10, ), ), ], ); } ``` 13.移除选中白噪音子项的方法 ```Dart void _removeSelectedNoise(int index) { setState(() { final removedItem = _selectedNoises.removeAt(index); print('🗑️ 移除白噪音: ${removedItem.name}'); // TODO: 停止播放该白噪音 }); } ``` ###### 代码实例 ```Dart import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:mate/bluetooth/headset.dart'; import '../generated/l10n.dart'; class CustomWhiteNoise extends StatefulWidget { //final Headset device; const CustomWhiteNoise({super.key, //required this.device }); @override State createState() => _CustomWhiteNoiseState(); } class _CustomWhiteNoiseState extends State { final List _categories = ['全部', '遮噪', '水流声', '大自然', '生活',"冥想"];// 分类标签数据 int _selectedCategoryIndex = 0; //用户选中的目录子项,默认选中全部 // 选中的白噪音列表(最多3个) List _selectedNoises = []; // 准备白噪音数据 final List _whiteNoiseItems = [ // 水流声类别 WhiteNoiseItem(name: '溪流', icon: '🏞️', category: '水流声'), WhiteNoiseItem(name: '海浪', icon: '🌊', category: '水流声'), WhiteNoiseItem(name: '瀑布', icon: '💦', category: '水流声'), WhiteNoiseItem(name: '泉水', icon: '💧', category: '水流声'), WhiteNoiseItem(name: '河流', icon: '🌊', category: '水流声'), WhiteNoiseItem(name: '雨滴', icon: '🌧️', category: '水流声'), // 大自然类别 WhiteNoiseItem(name: '风声', icon: '💨', category: '大自然'), WhiteNoiseItem(name: '森林', icon: '🌲', category: '大自然'), WhiteNoiseItem(name: '鸟鸣', icon: '🐦', category: '大自然'), WhiteNoiseItem(name: '蝉鸣', icon: '🦗', category: '大自然'), WhiteNoiseItem(name: '蛙声', icon: '🐸', category: '大自然'), WhiteNoiseItem(name: '雷声', icon: '⚡', category: '大自然'), WhiteNoiseItem(name: '雪落', icon: '❄️', category: '大自然'), WhiteNoiseItem(name: '落叶', icon: '🍂', category: '大自然'), // 生活类别 WhiteNoiseItem(name: '篝火', icon: '🔥', category: '生活'), WhiteNoiseItem(name: '钟声', icon: '⏰', category: '生活'), WhiteNoiseItem(name: '城市', icon: '🏙️', category: '生活'), WhiteNoiseItem(name: '火车', icon: '🚂', category: '生活'), WhiteNoiseItem(name: '咖啡', icon: '☕', category: '生活'), WhiteNoiseItem(name: '键盘', icon: '⌨️', category: '生活'), WhiteNoiseItem(name: '风扇', icon: '🌀', category: '生活'), WhiteNoiseItem(name: '心跳', icon: '💓', category: '生活'), // 遮噪类别 WhiteNoiseItem(name: '白噪', icon: '📡', category: '遮噪'), WhiteNoiseItem(name: '粉噪', icon: '🎵', category: '遮噪'), WhiteNoiseItem(name: '棕噪', icon: '🔊', category: '遮噪'), WhiteNoiseItem(name: '紫噪', icon: '🎶', category: '遮噪'), WhiteNoiseItem(name: '灰噪', icon: '🔇', category: '遮噪'), // 冥想类别 WhiteNoiseItem(name: '禅钟', icon: '🛎️', category: '冥想'), WhiteNoiseItem(name: '钵音', icon: '🥣', category: '冥想'), WhiteNoiseItem(name: '经诵', icon: '📿', category: '冥想'), WhiteNoiseItem(name: '风铃', icon: '🎐', category: '冥想'), WhiteNoiseItem(name: '静心', icon: '🧘', category: '冥想'), WhiteNoiseItem(name: '呼吸', icon: '🌬️', category: '冥想'), // 动物声音 WhiteNoiseItem(name: '猫咪', icon: '🐱', category: '生活'), WhiteNoiseItem(name: '狗狗', icon: '🐶', category: '生活'), WhiteNoiseItem(name: '鸟儿', icon: '🐦', category: '大自然'), WhiteNoiseItem(name: '海豚', icon: '🐬', category: '大自然'), WhiteNoiseItem(name: '鲸鱼', icon: '🐋', category: '大自然'), WhiteNoiseItem(name: '狗吠', icon: '🐕', category: '生活'), // 天气声音 WhiteNoiseItem(name: '暴雨', icon: '⛈️', category: '大自然'), WhiteNoiseItem(name: '细雨', icon: '🌦️', category: '大自然'), WhiteNoiseItem(name: '风雪', icon: '🌨️', category: '大自然'), WhiteNoiseItem(name: '晴天', icon: '☀️', category: '大自然'), // 乐器声音 WhiteNoiseItem(name: '钢琴', icon: '🎹', category: '生活'), WhiteNoiseItem(name: '吉他', icon: '🎸', category: '生活'), WhiteNoiseItem(name: '笛子', icon: '🎵', category: '生活'), WhiteNoiseItem(name: '鼓声', icon: '🥁', category: '生活'), ]; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.transparent, appBar: AppBar( leading: IconButton( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.arrow_back_ios, color: Colors.white) ), title: Text( '自定义白噪音', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), centerTitle: true, backgroundColor: const Color(0xFF060618), ), body: Container( width: double.infinity, height: double.infinity, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Color(0xFF060618), Color(0xFF070A23), ], ), ), child: Column( children: [ // 分类标签栏 _buildCategoryTabs(), const SizedBox(height: 20), // 白噪音网格列表 Expanded( child: _buildWhiteNoiseGrid(), ), ], ), ), ); } //====================================构建分类标签栏============================ Widget _buildCategoryTabs() { return Container( //构建标签栏的整个区域 height: 20, padding: const EdgeInsets.symmetric(horizontal: 16), child: ListView.builder( scrollDirection: Axis.horizontal, //横向滚动 itemCount: _categories.length, //子项长度 itemBuilder: (context, index) { //自动传入子项构建 final isSelected = _selectedCategoryIndex == index; //判断当前标签是否选中 return Container( margin: const EdgeInsets.only(right: 12), child: GestureDetector( onTap: () { setState(() { _selectedCategoryIndex = index; //更新选中状态,比如用户选中第二项的时候,这个列表的UI将重新构建,绘制选中第二项的UI }); }, child: Container( //列表的单个子项UI padding: const EdgeInsets.symmetric(horizontal: 20), decoration: BoxDecoration( color: isSelected ? const Color(0xFF4A90E2) : Colors.white.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: Center( child: Text( _categories[index], //显示分类名称 style: TextStyle( color: isSelected ? Colors.white : Colors.white.withOpacity(0.7), fontSize: 10, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), ), ), ), ); }, ), ); } //==========================获取筛选后的音效列表=============================== List _getFilteredItems() { // 1. 先获取当前选中的分类索引 final selectedIndex = _selectedCategoryIndex; // 2. 判断是否选择了"全部"分类 final isAllSelected = selectedIndex == 0; // 3. 如果选择了"全部",直接使用所有数据 if (isAllSelected) { return _whiteNoiseItems; } // 4. 否则,筛选对应分类的数据 else { // 获取选中的分类名称 final selectedCategoryName = _categories[selectedIndex]; // 筛选数据:只保留分类匹配的项 return _whiteNoiseItems.where((item) { //where()筛选方法,(item)列表中的每个音效 return item.category == selectedCategoryName; //子项的分类等于选中的分类 }).toList(); //.toList 把筛选出来的东西打包成列表 } } //=================================构建白噪音网格================================ Widget _buildWhiteNoiseGrid() { //获取筛选后的列表 final filteredItems = _getFilteredItems(); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ // 网格列表 Expanded( child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, //每行显示4个 crossAxisSpacing: 15, //列间距 mainAxisSpacing: 25, //行间距 childAspectRatio: 0.9, //子项宽高比 ), itemCount: filteredItems.length, //动态数据长度 itemBuilder: (context, index) { //构建每个网格项 final item = filteredItems[index]; //获取当前目录子项的数据 final isSelected = _selectedNoises.contains(item); //检查是否已经被选中:检查当前这个音效(item),是否在已选音效列表(_selectedNoises)中,根据返回的bool值UI做相应的更改 return _buildNoiseItem(item, index, isSelected); //传入子项、下标、选中状态 }, ), ), // 选中的白噪音显示区域 if (_selectedNoises.isNotEmpty) _buildSelectedNoisesDisplay(), ], ), ); } // ========================单个白噪音项====================================== Widget _buildNoiseItem(WhiteNoiseItem item, int index, bool isSelected) { return GestureDetector( //子项点击事件 onTap: () { _onNoiseSelected(item, isSelected); }, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // 图标 Container( width: 55, height: 55, decoration: BoxDecoration( color: isSelected ? const Color(0xFF2178E3) : Colors.white.withOpacity(0.1), borderRadius: BorderRadius.circular(30), ), child: Center( child: Text( item.icon, style: const TextStyle(fontSize: 24), ), ), ), const SizedBox(height: 8), // 名称 Text( item.name, style: TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), ], ), ); } //=============================白噪音选中处理==================================== void _onNoiseSelected(WhiteNoiseItem item, bool isSelected) { setState(() { if (isSelected) { // 如果已经选中,则移除 _selectedNoises.remove(item); print('⏹️ 移除白噪音: ${item.name}'); } else { // 如果未选中,则添加(循环替换逻辑) if (_selectedNoises.length < 3) { // 如果还有空位,直接添加 _selectedNoises.add(item); print('🎵 添加白噪音: ${item.name}'); } else { // 如果已满3个,替换第一个 final removedItem = _selectedNoises.removeAt(0); _selectedNoises.add(item); print('🔄 替换白噪音: ${removedItem.name} -> ${item.name}'); } } }); // 播放/停止逻辑 if (!isSelected) { print('▶️ 开始播放: ${item.name}'); // TODO: 实现播放逻辑 } else { print('⏹️ 停止播放: ${item.name}'); // TODO: 实现停止播放逻辑 } } //============================构建选中的白噪音显示区域============================ Widget _buildSelectedNoisesDisplay() { return Container( height: 80, margin: const EdgeInsets.only(bottom: 20), decoration: BoxDecoration( color: Colors.white.withOpacity(0.05), borderRadius: BorderRadius.circular(15), border: Border.all(color: Colors.white.withOpacity(0.1)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // 显示最多3个选中的白噪音 for (int i = 0; i < 3; i++) //i=0,1,2 i < _selectedNoises.length //检查这个位置有没有子项 ? _buildSelectedNoiseItem(_selectedNoises[i], i) //有,则显示子项 : _buildEmptySlot(i), //没有,则显示空位 ], ), ); } //=============================选中的白噪音项=================================== Widget _buildSelectedNoiseItem(WhiteNoiseItem item, int index) { return GestureDetector( onTap: () => _removeSelectedNoise(index), //点击子项本身则移除 child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Stack( children: [ //图标 Container( width: 50, height: 50, decoration: BoxDecoration( color: const Color(0xFF2178E3), borderRadius: BorderRadius.circular(25), ), child: Center( child: Text( item.icon, style: const TextStyle(fontSize: 20), ), ), ), // 红色的移除按钮 Positioned( top: 0, right: 0, child: Container( width: 16, height: 16, decoration: const BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), child: const Icon( Icons.close, color: Colors.white, size: 12, ), ), ), ], ), const SizedBox(height: 4), Text( item.name, style: const TextStyle( color: Colors.white, fontSize: 10, ), ), ], ), ); } //=======================================构建空槽位============================= Widget _buildEmptySlot(int index) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ //图标 Container( width: 50, height: 50, decoration: BoxDecoration( color: Colors.white.withOpacity(0.1), borderRadius: BorderRadius.circular(25), border: Border.all(color: Colors.white.withOpacity(0.3), width: 1), ), child: const Icon( Icons.add, color: Colors.white54, size: 20, ), ), const SizedBox(height: 4), //标题 Text( '空位 ${index + 1}', style: TextStyle( color: Colors.white.withOpacity(0.5), fontSize: 10, ), ), ], ); } //=======================移除选中的白噪音====================================== void _removeSelectedNoise(int index) { setState(() { final removedItem = _selectedNoises.removeAt(index); print('🗑️ 移除白噪音: ${removedItem.name}'); // TODO: 停止播放该白噪音 }); } } //====================================白噪音数据模型=============================== class WhiteNoiseItem { final String name; //名称 final String icon; //图标 final String category; //所属分类 //构造函数 WhiteNoiseItem({ required this.name, required this.icon, required this.category, }); ///(白噪音项目最多选中3个)以下方法防止重复添加, @override bool operator ==(Object other) => //两个子项是否相等 (operator==表示我要重新定义等于号) identical(this, other) || //情况1:完全是同一个对象 other is WhiteNoiseItem && runtimeType == other.runtimeType && name == other.name; //情况二:都是WhiteNoiseItem类型,且类型完全相同,名字完全相同,则认定是统一对象 @override int get hashCode => name.hashCode; //这个对象的哈希码等于它的名字的哈希码 } ```

相关推荐
肠胃炎1 小时前
Flutter ListView 组件及各种模式
flutter
sunly_1 小时前
Flutter:设备唯一id生成,存储,
flutter
走在路上的菜鸟4 小时前
Android学Dart学习笔记第十节 循环
android·笔记·学习·flutter
小蜜蜂嗡嗡5 小时前
【flutter项目从xcode运行时报错:Undefined symbol: _OBJC_CLASS_$_WeiboSDK】
flutter·cocoa·xcode
走在路上的菜鸟5 小时前
Android学Dart学习笔记第九节 Patterns
android·笔记·学习·flutter
晚霞的不甘6 小时前
跨端一致性与体验统一:构建面向全场景的 Flutter UI 自适应架构
flutter·ui·架构
走在路上的菜鸟6 小时前
Android学Dart学习笔记第十一节 分支
android·笔记·学习·flutter
克喵的水银蛇7 小时前
Flutter 自定义 Widget 实战:封装通用按钮 + 下拉刷新列表
前端·javascript·flutter
2501_915918417 小时前
Flutter 加固方案全解析,从 Dart 层到 IPA 成品的多工具协同防护体系
flutter·macos·ios·小程序·uni-app·cocoa·iphone