Flutter---Listview横向滚动列表(2)

目标:实现两个横向滚动列表的选择的互斥,就是说这两个列表中,所有的元素都是互斥的存在,每次只能选中一个。

效果图

实现步骤

1.定义枚举类型:水果和花朵

Dart 复制代码
enum ItemType{fruit,flower}

2.定义数据类,虽然这个名字是水果类,但是其实是通用的,因为加了类型判断在里面

Dart 复制代码
class FruitItem{
  final String title;
  final String image;
  final ItemType type;
  bool isSelected;

  FruitItem({
    required this.title,
    required this.image,
    required this.type,
    this.isSelected = false,
  });
}

3.定义数据,数据可以放在同一个列表里面,因为可以用传入类型区分这两种数据。

Dart 复制代码
  //使用自定义类
  final List<FruitItem> _items = [
    //水果项目
    FruitItem(title: '苹果', image: 'assets/images/apple.png',type: ItemType.fruit,isSelected: false),
    FruitItem(title: '香蕉', image: 'assets/images/banana.png',type: ItemType.fruit,isSelected: false),
    FruitItem(title: '樱桃', image: 'assets/images/cherry.png',type: ItemType.fruit,isSelected: false),

    //花朵项目
    FruitItem(title: '玫瑰', image: 'assets/images/rose.png',type: ItemType.flower,isSelected: false),
    FruitItem(title: '郁金香', image: 'assets/images/tulip.png',type: ItemType.flower,isSelected: false),
    FruitItem(title: '向日葵', image: 'assets/images/sunflower.png',type: ItemType.flower,isSelected: false),
  ];

4.定义一个整数来标识当前用户选中的索引

Dart 复制代码
  int? _selectedIndex;

5.分出两个列表出来显示UI

Dart 复制代码
  List<FruitItem> get fruitItems => _items.where((item) => item.type == ItemType.fruit).toList();

  List<FruitItem> get flowerItems => _items.where((item) => item.type == ItemType.flower).toList();

6.定义UI:背景渐变

Dart 复制代码
        Container(
            width: double.infinity,
            height: double.infinity,
            decoration: BoxDecoration(
              gradient: LinearGradient(
                begin: Alignment.topCenter,
                end: Alignment.bottomCenter,
                colors: [
                  Colors.green.withOpacity(0.9),
                  Colors.white,
                ],
              ),
            ),
          ),

7.定义总标题

Dart 复制代码
        Positioned(
            top: 50,
            left: 150,
            child: Text(
              '播放歌曲',
              style: TextStyle(
                color: Colors.white,
                fontSize: 24,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),

8.定义水果标题和水果列表

Dart 复制代码
          //水果标题
          Positioned(
            top: 120,
            left: 20,
            child: Text(
              '水果歌曲',
              style: TextStyle(
                color: Colors.white,
                fontSize: 20,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),

          // 水果横向滚动列表(占位置)
          Positioned(
            top: 150,
            left: 0,
            right: 0,
            height: 180, // 列表高度
            child: _buildHorizontalList(fruitItems),
          ),

9.定义花朵标题和花朵列表

Dart 复制代码
         //花朵标题
          Positioned(
            top: 320,
            left: 20,
            child: Text(
              '花朵歌曲',
              style: TextStyle(
                color: Colors.white,
                fontSize: 20,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),

          // 花朵横向滚动列表(占位置)
          Positioned(
            top: 350,
            left: 0,
            right: 0,
            height: 180, // 列表高度
            child: _buildHorizontalList(flowerItems),
          ),

10.根据传入的不同数据构建滚动列表

Dart 复制代码
  Widget _buildHorizontalList(List<FruitItem> items) { //根据用户传入的不同的数据显示UI

    return ListView.builder( //ListView.builder按需构建子项
      scrollDirection: Axis.horizontal, // 横向滚动
      padding: const EdgeInsets.symmetric(horizontal: 15), //内边距
      itemCount: items.length, //子项的数量
      itemBuilder: (context, index) { //框架自动传递index:为每个索引位置构建对应的Widget

        final globalIndex = _getGlobalIndex(items[index]); //将局部索引转换为全局索引
        final isSelected = _selectedIndex == globalIndex; // 计算选中状态  计算_selectedIndex是否等于 globalIndex?

        return buildItem(
          title: items[index].title,
          image: items[index].image,
          isSelected: isSelected, // 传递选中状态
          callback: () {
            _onItemTap(globalIndex);
          },
        );
      },
    );
  }

  //根据item对象找到在全局列表中的索引
  //element是item中的每一个元素,意思是元素从头到尾一个一个赋值给element,然后跟传入进来的item去比较
  int _getGlobalIndex(FruitItem item) {
    return _items.indexWhere((element) =>
      element.title == item.title && element.type == item.type);
  }

11.选中事件的逻辑

Dart 复制代码
  void _onItemTap(int index) {
    print('点击了: ${_items[index].title}');

    setState(() {
      // 互斥逻辑:如果点击的是已选中的项目,则取消选中
      if (_selectedIndex == index) {
        _selectedIndex = null;
      } else {
        _selectedIndex = index;  //下标传递
      }
    });

    // 传回点击事件给父组件
    _handleItemSelection(index, _selectedIndex == index);
  }

  // 处理项目选择事件
  void _handleItemSelection(int index, bool isSelected) {
    //print('项目 $index ${isSelected ? '被选中' : '取消选中'}');

    final item = _items[index];
    if (isSelected) {
      print('✅ 选中了【${item.type == ItemType.fruit ? '水果' : '花朵'}】: ${item.title}');
      // 这里可以添加播放音频的逻辑
    } else {
      print('❌ 取消选中: ${item.title}');
      // 这里可以添加停止音频的逻辑
    }
  }

12.定义通用的功能小卡片

Dart 复制代码
  Widget buildItem({
    required String title, //名字
    required String image, //图片
    required bool isSelected, //是否选中
    VoidCallback? callback //传回点击事件
  }) {
    return Container(
      margin: const EdgeInsets.only(right: 15),//外边距
      child: Column(
        children: [
          // 使用 Stack 包装图片和选中图标
          Stack(
            children: [
              // 图片容器
              Container(
                width: 130,
                height: 130,
                decoration: BoxDecoration(
                  color: Colors.white.withOpacity(0.1),
                  borderRadius: BorderRadius.circular(15),
                  boxShadow: [ //阴影效果
                    BoxShadow(
                      color: Colors.black.withOpacity(0.3),
                      blurRadius: 8,
                      offset: const Offset(0, 4),
                    ),
                  ],
                  // 添加选中状态边框
                  border: isSelected ? Border.all(color: Colors.green, width: 3,) : null,
                ),
                child: Material(
                  color: Colors.transparent,
                  child: InkWell(
                    onTap: callback, //点击事件
                    borderRadius: BorderRadius.circular(15),
                    child: ClipRRect( //给图片做裁剪
                      borderRadius: BorderRadius.circular(15),
                      child: Image.asset(
                        image,
                        fit: BoxFit.cover,
                        width: double.infinity,
                        height: double.infinity,
                      ),
                    ),
                  ),
                ),
              ),

              // 选中图标 - 显示在右上角
              if (isSelected)
                Positioned(
                  top: 8,
                  right: 8,
                  child: Container(
                    width: 24,
                    height: 24,
                    decoration: BoxDecoration(
                      color: Colors.green,
                      borderRadius: BorderRadius.circular(12),
                    ),
                    child: const Icon(
                      Icons.check,
                      color: Colors.white,
                      size: 16,
                    ),
                  ),
                ),
            ],
          ),

          // 文字
          const SizedBox(height: 8),
          Text(
            title,
            style: TextStyle(
              color: isSelected ? Colors.green : Colors.white,
              fontSize: 14,
              fontWeight: isSelected ? FontWeight.bold : FontWeight.w500,
            ),
          ),
        ],
      ),
    );
  }

项目架构

Dart 复制代码
数据层
// 数据模型
class FruitItem {
  final String title;
  final String image;
  final ItemType type;
  bool isSelected;
}

// 数据源管理
final List<FruitItem> _items = []; // 统一数据源

状态层
// 状态管理
int? _selectedIndex; // 单一状态源

UI层
// 组件化架构
Widget _buildHorizontalList()  // 列表组件
Widget buildItem()            // 卡片组件

数据流动路径

Dart 复制代码
用户交互 → 事件回调 → 状态更新 → UI重绘 → 用户反馈

从那些方面学习这个项目

Dart 复制代码
基础UI组件
// 学习重点:
- Stack 层叠布局
- Positioned 精确定位  
- ListView.builder 动态列表
- Container 样式装饰
- InkWell 点击交互


状态管理
// 学习重点:
- StatefulWidget 生命周期
- setState() 触发重绘
- 状态提升 (Lifting State Up)
- 单向数据流


组件化设计
// 学习重点:
- 参数化组件 (buildItem方法)
- 回调函数通信
- 关注点分离
- 代码复用


架构模式
// 学习重点:
- 数据与UI分离
- 单一职责原则
- 索引映射算法
- 状态一致性保证

关键点

1.怎么将局部索引转换为全局索引:通过对象本身来找到它在全局列表中的位置

数据情况

Dart 复制代码
// 全局列表(所有项目)
_items = [
  苹果,  // 全局索引0
  香蕉,  // 全局索引1  
  樱桃,  // 全局索引2
  玫瑰,  // 全局索引3
  郁金香,// 全局索引4
  向日葵,// 全局索引5
];

// 水果列表(筛选出的子集)
fruitItems = [
  苹果,  // 局部索引0 → 全局索引0
  香蕉,  // 局部索引1 → 全局索引1
  樱桃,  // 局部索引2 → 全局索引2
];

// 花朵列表(筛选出的子集)  
flowerItems = [
  玫瑰,    // 局部索引0 → 全局索引3
  郁金香,  // 局部索引1 → 全局索引4
  向日葵,  // 局部索引2 → 全局索引5
];

转换过程

Dart 复制代码
// 当用户点击花朵列表的第2项(郁金香)时:

// 步骤1:通过局部索引拿到对象
FruitItem clickedItem = flowerItems[1]; // 拿到"郁金香"对象

// 步骤2:拿着这个对象去全局列表问:
// "郁金香同学,你在全局列表中是第几个?"
int globalIndex = _getGlobalIndex(clickedItem);

// _getGlobalIndex内部:
return _items.indexWhere((element) => 
  element.title == '郁金香' && element.type == ItemType.flower
);

// 执行过程:
// 遍历_items,比较每个元素是不是我们要找的"郁金香"
// [0] 苹果 == 郁金香? ❌
// [1] 香蕉 == 郁金香? ❌  
// [2] 樱桃 == 郁金香? ❌
// [3] 玫瑰 == 郁金香? ❌
// [4] 郁金香 == 郁金香? ✅ → 返回索引4

2.怎么理解用户点击子项后的数据流?

场景1:用户首次点击"郁金香"(花朵列表第2项)

阶段1:点击前的界面构建(_selectedIndex = null)

Dart 复制代码
// 这是用户点击之前的界面构建过程
_buildHorizontalList(flowerItems) {
  // index = 1 (用户将要点击郁金香)
  
  // 坐标转换
  final globalIndex = _getGlobalIndex(flowerItems[1]); // 返回4
  
  // 判断选中状态 ← 此时_selectedIndex还是null!
  final isSelected = _selectedIndex == 4; // null == 4 → false
  
  // 构建UI组件 ← 显示未选中状态
  return buildItem(
    title: "郁金香",
    image: "assets/images/tulip.png", 
    isSelected: false, // 显示灰色边框(未选中)
    callback: () {
      _onItemTap(4); // 设置点击回调
    },
  );
}

此时的界面显示

Dart 复制代码
花朵歌曲: [玫瑰❌, 郁金香❌, 向日葵❌] ← 都显示未选中状态

阶段2:用户点击郁金香

Dart 复制代码
// 用户点击郁金香卡片,触发callback
_onItemTap(4) {
  print('点击了: ${_items[4].title}'); // 输出:点击了: 郁金香
  
  setState(() {
    // _selectedIndex从null变为4
    _selectedIndex = 4;
  });
  
  _handleItemSelection(4, true); // 输出:✅ 选中了【花朵】: 郁金香
}

阶段3:setState触发页面重绘

Dart 复制代码
// setState()后,整个build方法重新执行
// 现在_selectedIndex = 4

_buildHorizontalList(flowerItems) {
  // 重新构建所有项目
  
  // 对于郁金香(index=1):
  final globalIndex = _getGlobalIndex(flowerItems[1]); // 返回4
  final isSelected = _selectedIndex == 4; // 4 == 4 → true
  
  return buildItem(
    title: "郁金香",
    image: "assets/images/tulip.png", 
    isSelected: true, // 现在显示绿色边框(选中)
    callback: () {
      _onItemTap(4);
    },
  );
  
  // 对于其他项目(玫瑰、向日葵):
  // isSelected = _selectedIndex == 其他索引 → false
}

重绘后,页面显示

Dart 复制代码
花朵歌曲: [玫瑰❌, 郁金香✅, 向日葵❌] ← 只有郁金香显示选中状态

完整的时间线

Dart 复制代码
时间点0: _selectedIndex = null
    ↓
界面构建:所有项目显示未选中
    ↓  
用户点击郁金香
    ↓
_onItemTap(4): _selectedIndex = 4
    ↓
setState()触发重绘
    ↓  
界面重新构建:郁金香显示选中,其他未选中

场景2:用户再点击"香蕉"(水果列表第2项)

数据流

Dart 复制代码
// 1. 用户点击水果列表的第2项  
_buildHorizontalList(fruitItems) {
  // index = 1 (局部索引)
  
  // 2. 坐标转换
  final globalIndex = _getGlobalIndex(fruitItems[1]);
  // fruitItems[1] = 香蕉对象
  // _getGlobalIndex内部:在_items中查找"香蕉"
  // 遍历过程:苹果❌ → 香蕉✅ (找到!)
  // 返回:1
  
  // 3. 判断选中状态
  final isSelected = _selectedIndex == 1;
  // _selectedIndex当前是4,4 != 1
  // 所以 isSelected = false
  
  // 4. 构建UI组件
  return buildItem(
    title: "香蕉",
    image: "assets/images/banana.png",
    isSelected: false, // 当前未选中
    callback: () {
      _onItemTap(1); // 点击时传入全局索引1
    },
  );
}

// 5. 用户点击香蕉卡片,触发callback
_onItemTap(1) {
  print('点击了: ${_items[1].title}'); // 输出:点击了: 香蕉
  
  setState(() {
    // 互斥选择:取消之前的选中(4),设置新的选中(1)
    _selectedIndex = 1;
  });
  
  _handleItemSelection(1, true); // 输出:✅ 选中了【水果】: 香蕉
}

界面更新

Dart 复制代码
_selectedIndex = 1

界面显示:
水果歌曲: [苹果❌, 香蕉✅, 樱桃❌] ← 只有香蕉显示绿色边框  
花朵歌曲: [玫瑰❌, 郁金香❌, 向日葵❌] ← 花朵全部取消选中

数据流总结

Dart 复制代码
用户点击 
    ↓
获取局部索引 + 对象
    ↓  
坐标转换:对象 → 全局索引  
    ↓
判断选中状态
    ↓  
构建UI组件(传入回调)
    ↓
用户触发点击 → 执行_onItemTap(全局索引)
    ↓
更新_selectedIndex ← 互斥选择逻辑
    ↓  
setState()触发界面重绘
    ↓
所有列表重新构建,显示新的选中状态
相关推荐
翼龙云_cloud2 小时前
阿里云渠道商:WordPress网站如何使用文件存储NAS?
运维·服务器·阿里云·云计算
未来猫咪花2 小时前
🔥 神奇的 Dart Zone 机制
flutter
q***T5832 小时前
Docker文本处理开发
运维·docker·容器
biubiubiu07062 小时前
给Docker设置代理
运维·docker·容器
h***83933 小时前
Docker测试框架使用指南
运维·docker·容器
Aaron15883 小时前
通用的通感控算存一体化平台设计方案
linux·人工智能·算法·fpga开发·硬件工程·射频工程·基带工程
讨厌下雨的天空3 小时前
缓冲区io
linux·服务器·前端
知南x3 小时前
【Socket消息传递】(1) 嵌入式设备间Socket通信传输图片
linux·fpga开发
7***n753 小时前
Docker镜像瘦身
运维·docker·容器