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

效果图

详细介绍:这是一个使用ListView.builder来建造的横向滚动水果数据列表,当点击的时候会有一个选中图标和选中边框,并且文字的颜色也会发生更改。

实现步骤:

1.准备数据,并且定义一个值为当前选中索引

Dart 复制代码
  final List<Map<String, dynamic>> _items = [
    {'title': '苹果', 'image': 'assets/images/apple.png', 'isSelected': false},
    {'title': '香蕉', 'image': 'assets/images/banana.png', 'isSelected': false},
    {'title': '樱桃', 'image': 'assets/images/cherry.png', 'isSelected': false},
    {'title': '芒果', 'image': 'assets/images/mango.png', 'isSelected': false},
  ];

// 当前选中的索引
  int? _selectedIndex;

或者可以自定义数据类来使用

Dart 复制代码
//定义水果数据类
class FruitItem{
  final String title;
  final String image;
  bool isSelected;

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



  // 使用自定义类
  final List<FruitItem> _items = [
    FruitItem(title: '苹果', image: 'assets/images/apple.png'),
    FruitItem(title: '香蕉', image: 'assets/images/banana.png'),
    FruitItem(title: '樱桃', image: 'assets/images/cherry.png'),
    FruitItem(title: '芒果', image: 'assets/images/mango.png'),
  ];


// 当前选中的索引
  int? _selectedIndex;

2.定义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,
                ],
              ),
            ),
          ),

3.设置标题

Dart 复制代码
        Positioned(
            top: 50,
            left: 20,
            child: Text(
              '推荐水果',
              style: TextStyle(
                color: Colors.white,
                fontSize: 24,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),

4.设置横向滚动列表

Dart 复制代码
        Positioned(
            top: 100,
            left: 0,
            right: 0,
            height: 180, // 列表高度
            child: _buildHorizontalList(),
          ),


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

        final item = _items[index]; //用index取_items的对象数据
        final isSelected = _selectedIndex == index; // 计算选中状态

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

5.处理选中逻辑

Dart 复制代码
  // 选中状态管理逻辑
  void _onItemTap(int index) {
    print('点击了: ${_items[index]['title']}');
    // 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 ? '被选中' : '取消选中'}');
  }

6.设置通用的功能小卡片

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 复制代码
┌─────────────────────────────────────────┐
│   UI层 (Presentation Layer)             │
│  ┌─────────────────────────────────────┐ │
│  │  Widget树 (Widget Tree)             │ │
│  │  • Scaffold                         │ │
│  │  • Stack                            │ │
│  │  • Positioned                        │ │
│  │  • ListView.builder                 │ │
│  └─────────────────────────────────────┘ │
├─────────────────────────────────────────┤
│  状态管理层 (State Management Layer)     │
│  ┌─────────────────────────────────────┐ │
│  │  StatefulWidget + setState()        │ │
│  │  • _items (数据状态)                 │ │
│  │  • _selectedIndex (选中状态)         │ │
│  └─────────────────────────────────────┘ │
├─────────────────────────────────────────┤
│  数据层 (Data Layer)                    │
│  ┌─────────────────────────────────────┐ │
│  │  List<Map<String, dynamic>> _items   │ │
│  │  • 静态数据                          │ │
│  │  • 内存存储                          │ │
│  └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘

组件架构

Dart 复制代码
SleepPage (StatefulWidget)
├── _SleepPage (State)
    ├── _items (数据源)
    ├── _selectedIndex (状态)
    ├── build() (UI构建)
    ├── _buildHorizontalList() (列表构建)
    ├── _onItemTap() (交互处理)
    └── buildItem() (组件复用)

布局

Dart 复制代码
// Stack布局计算
Stack(
  children: [
    Container(...), // 背景层 - 最底层
    Positioned(...), // 标题层 - 中间层  
    Positioned(...), // 列表层 - 最上层
  ],
)

// ListView动态构建
ListView.builder(
  itemCount: _items.length, // 数据驱动
  itemBuilder: (context, index) {
    return buildItem(...); // 按需构建
  },
)

条件渲染

Dart 复制代码
// 边框条件渲染
border: isSelected ? Border.all(...) : null

// 图标条件渲染  
if (isSelected) Positioned(...)

// 文字样式条件渲染
color: isSelected ? Colors.green : Colors.white
fontWeight: isSelected ? FontWeight.bold : FontWeight.w500

数据流

Dart 复制代码
用户点击
    ↓
InkWell.onTap 
    ↓  
callback() → _onItemTap(index)
    ↓
setState() → 状态变更
    ↓
Widget.rebuild() → UI更新
    ↓  
_handleItemSelection() → 事件回调

为什么callback能够直到当前用户点击的是哪个子项?因为闭包捕获机制。

Dart 复制代码
==================================================================
callback 是如何传递 index 的?

itemBuilder: (context, index) {
  final item = _items[index];
  final isSelected = _selectedIndex == index;
  
  return buildItem(
    title: item['title']!,
    image: item['image']!,
    isSelected: isSelected,
    callback: () {  // ⭐️ 关键:这里创建了一个闭包
      _onItemTap(index);  // 闭包捕获了当前的 index
    },
  );
}
===============================================================

什么是闭包?

// 每次调用 itemBuilder 时,都会创建一个新的函数
callback: () {
  _onItemTap(index);  // 这个函数"记住"了创建时的 index 值
}

===============================================================

执行过程

// 第1次构建 (index = 0)
callback = () { _onItemTap(0); }  // 闭包捕获 index=0

// 第2次构建 (index = 1)  
callback = () { _onItemTap(1); }  // 闭包捕获 index=1

// 第3次构建 (index = 2)
callback = () { _onItemTap(2); }  // 闭包捕获 index=2

===============================================================

具体的数据流路径

Dart 复制代码
// ========== 点击苹果的执行路径 ==========

// 用户手指触摸屏幕苹果卡片区域
↓
// Flutter 检测到触摸事件,触发 InkWell 的 onTap
InkWell(
  onTap: callback,  // ⭐️ 起点:这里被触发
)
↓
// 执行 callback(),实际执行的是闭包函数
callback()  // 实际是:() { _onItemTap(0); }
↓
// 调用 _onItemTap 方法,传入 index=0
_onItemTap(0)  // ⭐️ 进入点击处理方法
↓
// 第1步:打印日志
print('点击了: ${_items[0]['title']}')  // 输出:点击了: 苹果
↓
// 第2步:更新状态
setState(() {
  // 互斥逻辑:如果点击的是已选中的项目,则取消选中
  if (_selectedIndex == index) {  // _selectedIndex == 0 ?
    _selectedIndex = null;
  } else {
    _selectedIndex = index;       // _selectedIndex = 0
  }
})
↓
// Flutter 收到 setState() 信号,标记需要重建
↓
// 第3步:调用 build() 方法重新构建UI
@override
Widget build(BuildContext context) {
  return Scaffold(...);  // ⭐️ 重新构建整个页面
}
↓
// 第4步:重新构建 _buildHorizontalList()
Positioned(
  child: _buildHorizontalList(),  // ⭐️ 重新构建列表
)
↓
// 第5步:ListView.builder 重新构建所有项目
ListView.builder(
  itemBuilder: (context, index) {
    // 为每个索引重新构建项目
    // index=0: 苹果(重新计算 isSelected)
    // index=1: 香蕉(重新计算 isSelected)  
    // index=2: 樱桃(重新计算 isSelected)
    // index=3: 芒果(重新计算 isSelected)
  }
)
↓
// 第6步:为苹果项目重新计算选中状态
final isSelected = _selectedIndex == index;  // 0 == 0 → true
↓
// 第7步:重新构建苹果卡片(使用新的 isSelected=true)
return buildItem(
  title: '苹果',
  image: 'assets/images/apple.png', 
  isSelected: true,  // ⭐️ 现在为选中状态
  callback: () { _onItemTap(0); }
)
↓
// 第8步:buildItem 使用新的选中状态渲染
// - 显示绿色边框
// - 显示对勾图标  
// - 文字变为绿色
↓
// 第9步:处理回调事件
_handleItemSelection(0, true)  // ⭐️ 传递选中事件
↓
// 第10步:打印最终状态
print('项目 0 被选中')  // 输出:项目 0 被选中

视觉更新
- 苹果: 显示绿色边框 + 对勾图标 + 绿色文字
- 其他: 无边框 + 无图标 + 白色文字

代码实例

Dart 复制代码
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';


//定义水果数据类
// class FruitItem{
//   final String title;
//   final String image;
//   bool isSelected;
//
//   FruitItem({
//     required this.title,
//     required this.image,
//     this.isSelected = false,
//   });
// }


class SleepPage extends StatefulWidget {
  const SleepPage({super.key});

  @override
  State<StatefulWidget> createState() => _SleepPage();
}

class _SleepPage extends State<SleepPage> {

  //1.准备数据
  final List<Map<String, dynamic>> _items = [
    {'title': '苹果', 'image': 'assets/images/apple.png', 'isSelected': false},
    {'title': '香蕉', 'image': 'assets/images/banana.png', 'isSelected': false},
    {'title': '樱桃', 'image': 'assets/images/cherry.png', 'isSelected': false},
    {'title': '芒果', 'image': 'assets/images/mango.png', 'isSelected': false},
  ];

  // 使用自定义类
  // final List<FruitItem> _items = [
  //   FruitItem(title: '苹果', image: 'assets/images/apple.png'),
  //   FruitItem(title: '香蕉', image: 'assets/images/banana.png'),
  //   FruitItem(title: '樱桃', image: 'assets/images/cherry.png'),
  //   FruitItem(title: '芒果', image: 'assets/images/mango.png'),
  // ];


  // 当前选中的索引
  int? _selectedIndex;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.transparent,
      body: Stack(
        children: [
          // 2. 背景渐变
          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,
                ],
              ),
            ),
          ),

          // 3. 标题
          Positioned(
            top: 50,
            left: 20,
            child: Text(
              '推荐水果',
              style: TextStyle(
                color: Colors.white,
                fontSize: 24,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),

          // 4. 横向滚动列表(占位置)
          Positioned(
            top: 100,
            left: 0,
            right: 0,
            height: 180, // 列表高度
            child: _buildHorizontalList(),
          ),


        ],
      ),
    );
  }

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

        final item = _items[index]; //用index取_items的对象数据
        final isSelected = _selectedIndex == index; // 计算选中状态  计算_selectedIndex是否等于 index?

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

  // 选中状态管理逻辑
  void _onItemTap(int index) {
    print('点击了: ${_items[index]['title']}');
    // 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 ? '被选中' : '取消选中'}');
  }

  // 功能小卡片
  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,
            ),
          ),
        ],
      ),
    );
  }
}
相关推荐
XI锐真的烦5 小时前
Flutter Windows 下“Running Gradle task ‘assembleDebug‘...” 卡住一整天的终极解决办法
windows·flutter
苦逼的搬砖工14 小时前
基于 easy_rxdart 的轻量响应式与状态管理架构实践
android·flutter
SoaringHeart15 小时前
Flutter组件封装:标签拖拽排序 NDragSortWrap
前端·flutter
天天开发18 小时前
Flutter每日库: local_auth本地设备验证插件
flutter
天天开发19 小时前
Flutter每日库: logger自定义日志格式并输出到文件
flutter
美摄科技19 小时前
为什么选择Flutter美颜SDK?
flutter
程序员老刘1 天前
跨平台开发地图:客户端技术选型指南 | 2025年11月 |(Valdi 加入战场)
flutter·react native·客户端
西西学代码2 天前
Flutter---Listview横向滚动列表(2)
linux·运维·flutter