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<WhiteNoiseItem> _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<StatefulWidget> createState() => _CustomWhiteNoiseState();
}

class _CustomWhiteNoiseState extends State<CustomWhiteNoise> {

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

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

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

  // 准备白噪音数据
  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: '生活'),

  ];





  @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<WhiteNoiseItem> _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; //这个对象的哈希码等于它的名字的哈希码
}
相关推荐
Geek_Vison6 小时前
保险APP已经用了Flutter,还能接入小程序吗?
flutter·小程序·uni-app·mpaas·小程序容器
FungLeo7 小时前
成为全栈·Flutter App 篇·go_router 与四 Tab App Shell:保留导航状态、深链和登录回跳
flutter·移动开发·成为全栈·go_router·路由设计·深链接
m0_738185827 小时前
Flutter 鸿蒙化实战:screen_protector 适配 OpenHarmony,防截屏/防泄露
flutter·华为·harmonyos·鸿蒙
梦想的颜色12 小时前
【编程实战】AI 时代 APP 开发全栈硬核指南:技术选型 + AI 架构 + 模型落地全维度决策
python·flutter·react native·react.js·ai·桌面应用·milvus
yuanlaile1 天前
2026年全新Flutter 跨平台开发全栈学习资料整理
flutter·getx·鸿蒙 next·flutter开发鸿蒙·flutter入门实战·flutter全栈
恋猫de小郭2 天前
Genkit Dart 1.0 发布,Flutter 原生的 AI Agent 终于完整了
android·前端·flutter
第10086个小白2 天前
flutter桌面端打包方式(Inno Setup Compiler)版本
前端·flutter
陆断枫2 天前
Flutter 3.47 动画引擎盘点:Impeller 全平台,和那些"新特性"
flutter
程序员老刘2 天前
Flet 1.0 思路很好,可惜来晚了
flutter·ai编程·客户端
m0_738185823 天前
Flutter 鸿蒙化实战:qr_code_scanner_plus 适配 OpenHarmony,二维码扫描
flutter·华为·harmonyos·鸿蒙