Flutter 滚动控件深度解析

85 阅读9分钟

通过丰富的图表、对比分析和实际案例,全面掌握 Flutter 滚动控件的使用技巧

Flutter Scrollable Widgets Version License

📊 文章概览

章节内容难度等级
ListView 详解列表视图控件⭐⭐⭐
GridView 详解网格视图控件⭐⭐⭐
CustomScrollView自定义滚动视图⭐⭐⭐⭐
Sliver 系列控件Sliver 组件详解⭐⭐⭐⭐
性能优化滚动性能优化⭐⭐⭐⭐

🎯 学习目标

  • ✅ 掌握各种滚动控件的核心概念和使用方法
  • ✅ 学会 ScrollController 的配置和控制
  • ✅ 理解 Sliver 系列控件的高级用法
  • ✅ 能够实现复杂的滚动界面
  • ✅ 掌握性能优化和最佳实践

📋 目录导航

🎯 快速导航

📋 概述

Flutter 提供了丰富的滚动控件来处理超出屏幕范围的内容。本文将详细介绍 ListView、GridView、CustomScrollView、Sliver 系列控件以及 PageView 的使用方法和最佳实践。

🏗️ 滚动控件架构图

graph TD
    A[Flutter ScrollView System] --> B[Basic ScrollViews]
    A --> C[Advanced ScrollViews]
    A --> D[Scroll Controllers]
    A --> E[Scroll Physics]

    B --> F[ListView]
    B --> G[GridView]
    B --> H[SingleChildScrollView]
    B --> I[PageView]

    C --> J[CustomScrollView]
    C --> K[Sliver Components]
    C --> L[NestedScrollView]

    D --> M[ScrollController]
    D --> N[ScrollNotification]
    D --> O[ScrollPosition]

    E --> P[ClampingScrollPhysics]
    E --> Q[BouncingScrollPhysics]
    E --> R[Custom Physics]

    K --> S[SliverList]
    K --> T[SliverGrid]
    K --> U[SliverAppBar]
    K --> V[SliverToBoxAdapter]
    K --> W[SliverPadding]
    K --> X[SliverFillRemaining]

📊 滚动控件特性对比

控件类型主要用途性能灵活性复杂度适用场景
ListView垂直列表⭐⭐⭐⭐⭐⭐⭐⭐⭐简单列表
GridView网格布局⭐⭐⭐⭐⭐⭐⭐⭐⭐图片网格
CustomScrollView自定义滚动⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐复杂布局
PageView页面切换⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐引导页、轮播
Sliver 系列高级滚动⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐特殊需求

ListView 详解

基础用法

// 基础 ListView
class BasicListView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        ListTile(
          leading: Icon(Icons.person),
          title: Text('用户1'),
          subtitle: Text('在线'),
          trailing: Icon(Icons.more_vert),
        ),
        ListTile(
          leading: Icon(Icons.person),
          title: Text('用户2'),
          subtitle: Text('离线'),
          trailing: Icon(Icons.more_vert),
        ),
        // 更多项目...
      ],
    );
  }
}

// ListView.builder - 适用于大量数据
class BuilderListView extends StatelessWidget {
  final List<String> items = List.generate(1000, (index) => '项目 $index');

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) {
        return Card(
          margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
          child: ListTile(
            leading: CircleAvatar(
              child: Text('${index + 1}'),
            ),
            title: Text(items[index]),
            subtitle: Text('描述信息 $index'),
            onTap: () {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('点击了 ${items[index]}')),
              );
            },
          ),
        );
      },
    );
  }
}

## CustomScrollView 和 Sliver 系列

### CustomScrollView 基础用法

```dart
// CustomScrollView 组合多种滚动组件
class CustomScrollViewExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        // 可折叠的应用栏
        SliverAppBar(
          expandedHeight: 200,
          floating: false,
          pinned: true,
          flexibleSpace: FlexibleSpaceBar(
            title: Text('自定义滚动视图'),
            background: Container(
              decoration: BoxDecoration(
                gradient: LinearGradient(
                  begin: Alignment.topLeft,
                  end: Alignment.bottomRight,
                  colors: [Colors.blue, Colors.purple],
                ),
              ),
            ),
          ),
        ),

        // 固定内容
        SliverToBoxAdapter(
          child: Container(
            height: 100,
            color: Colors.amber[100],
            child: Center(
              child: Text(
                '固定内容区域',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
            ),
          ),
        ),

        // 列表
        SliverList(
          delegate: SliverChildBuilderDelegate(
            (context, index) {
              return ListTile(
                leading: CircleAvatar(
                  child: Text('${index + 1}'),
                ),
                title: Text('列表项 ${index + 1}'),
                subtitle: Text('这是第 ${index + 1} 个列表项'),
              );
            },
            childCount: 10,
          ),
        ),

        // 网格
        SliverGrid(
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 2,
            crossAxisSpacing: 10,
            mainAxisSpacing: 10,
          ),
          delegate: SliverChildBuilderDelegate(
            (context, index) {
              return Container(
                decoration: BoxDecoration(
                  color: Colors.teal[100 * (index % 9 + 1)],
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Center(
                  child: Text(
                    '网格 $index',
                    style: TextStyle(fontWeight: FontWeight.bold),
                  ),
                ),
              );
            },
            childCount: 6,
          ),
        ),

        // 填充剩余空间
        SliverFillRemaining(
          child: Container(
            color: Colors.grey[100],
            child: Center(
              child: Text(
                '填充剩余空间',
                style: TextStyle(fontSize: 16),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

PageView 详解

基础 PageView

// 基础页面视图
class BasicPageView extends StatefulWidget {
  @override
  _BasicPageViewState createState() => _BasicPageViewState();
}

class _BasicPageViewState extends State<BasicPageView> {
  PageController _pageController = PageController();
  int _currentPage = 0;

  final List<PageData> pages = [
    PageData(
      title: '欢迎使用',
      description: '这是一个功能强大的应用',
      color: Colors.blue,
      icon: Icons.star,
    ),
    PageData(
      title: '简单易用',
      description: '直观的用户界面设计',
      color: Colors.green,
      icon: Icons.thumb_up,
    ),
    PageData(
      title: '开始体验',
      description: '立即开始您的旅程',
      color: Colors.orange,
      icon: Icons.rocket_launch,
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Expanded(
            child: PageView.builder(
              controller: _pageController,
              onPageChanged: (index) {
                setState(() {
                  _currentPage = index;
                });
              },
              itemCount: pages.length,
              itemBuilder: (context, index) {
                final page = pages[index];
                return Container(
                  decoration: BoxDecoration(
                    gradient: LinearGradient(
                      begin: Alignment.topCenter,
                      end: Alignment.bottomCenter,
                      colors: [
                        page.color.withOpacity(0.8),
                        page.color.withOpacity(0.6),
                      ],
                    ),
                  ),
                  child: SafeArea(
                    child: Padding(
                      padding: EdgeInsets.all(32),
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Icon(
                            page.icon,
                            size: 100,
                            color: Colors.white,
                          ),
                          SizedBox(height: 32),
                          Text(
                            page.title,
                            style: TextStyle(
                              fontSize: 28,
                              fontWeight: FontWeight.bold,
                              color: Colors.white,
                            ),
                            textAlign: TextAlign.center,
                          ),
                          SizedBox(height: 16),
                          Text(
                            page.description,
                            style: TextStyle(
                              fontSize: 16,
                              color: Colors.white.withOpacity(0.9),
                            ),
                            textAlign: TextAlign.center,
                          ),
                        ],
                      ),
                    ),
                  ),
                );
              },
            ),
          ),

          // 页面指示器
          Container(
            padding: EdgeInsets.all(20),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: List.generate(
                pages.length,
                (index) => Container(
                  margin: EdgeInsets.symmetric(horizontal: 4),
                  width: _currentPage == index ? 12 : 8,
                  height: 8,
                  decoration: BoxDecoration(
                    color: _currentPage == index
                        ? pages[_currentPage].color
                        : Colors.grey[300],
                    borderRadius: BorderRadius.circular(4),
                  ),
                ),
              ),
            ),
          ),

          // 导航按钮
          Padding(
            padding: EdgeInsets.all(20),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                if (_currentPage > 0)
                  TextButton(
                    onPressed: () {
                      _pageController.previousPage(
                        duration: Duration(milliseconds: 300),
                        curve: Curves.easeInOut,
                      );
                    },
                    child: Text('上一页'),
                  )
                else
                  SizedBox(width: 80),

                if (_currentPage < pages.length - 1)
                  ElevatedButton(
                    onPressed: () {
                      _pageController.nextPage(
                        duration: Duration(milliseconds: 300),
                        curve: Curves.easeInOut,
                      );
                    },
                    child: Text('下一页'),
                  )
                else
                  ElevatedButton(
                    onPressed: () {
                      // 完成引导
                      Navigator.of(context).pushReplacementNamed('/home');
                    },
                    child: Text('开始使用'),
                  ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _pageController.dispose();
    super.dispose();
  }
}

class PageData {
  final String title;
  final String description;
  final Color color;
  final IconData icon;

  PageData({
    required this.title,
    required this.description,
    required this.color,
    required this.icon,
  });
}

高级 PageView 用法

// 卡片式 PageView
class CardPageView extends StatefulWidget {
  @override
  _CardPageViewState createState() => _CardPageViewState();
}

class _CardPageViewState extends State<CardPageView> {
  PageController _pageController = PageController(
    viewportFraction: 0.8, // 显示部分相邻页面
    initialPage: 0,
  );

  int _currentPage = 0;

  final List<CardData> cards = List.generate(10, (index) => CardData(
    title: '卡片 ${index + 1}',
    subtitle: '这是第 ${index + 1} 张卡片',
    color: Colors.primaries[index % Colors.primaries.length],
  ));

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('卡片式 PageView')),
      body: Column(
        children: [
          SizedBox(height: 20),
          Container(
            height: 300,
            child: PageView.builder(
              controller: _pageController,
              onPageChanged: (index) {
                setState(() {
                  _currentPage = index;
                });
              },
              itemCount: cards.length,
              itemBuilder: (context, index) {
                final card = cards[index];
                return AnimatedBuilder(
                  animation: _pageController,
                  builder: (context, child) {
                    double value = 1.0;
                    if (_pageController.position.haveDimensions) {
                      value = _pageController.page! - index;
                      value = (1 - (value.abs() * 0.3)).clamp(0.0, 1.0);
                    }

                    return Center(
                      child: SizedBox(
                        height: Curves.easeOut.transform(value) * 300,
                        width: Curves.easeOut.transform(value) * 250,
                        child: child,
                      ),
                    );
                  },
                  child: Container(
                    margin: EdgeInsets.symmetric(horizontal: 10),
                    decoration: BoxDecoration(
                      color: card.color,
                      borderRadius: BorderRadius.circular(16),
                      boxShadow: [
                        BoxShadow(
                          color: Colors.black26,
                          blurRadius: 10,
                          offset: Offset(0, 5),
                        ),
                      ],
                    ),
                    child: Center(
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Text(
                            card.title,
                            style: TextStyle(
                              fontSize: 24,
                              fontWeight: FontWeight.bold,
                              color: Colors.white,
                            ),
                          ),
                          SizedBox(height: 10),
                          Text(
                            card.subtitle,
                            style: TextStyle(
                              fontSize: 16,
                              color: Colors.white.withOpacity(0.8),
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                );
              },
            ),
          ),

          SizedBox(height: 20),

          // 页面指示器
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: List.generate(
              cards.length,
              (index) => Container(
                margin: EdgeInsets.symmetric(horizontal: 3),
                width: _currentPage == index ? 12 : 8,
                height: 8,
                decoration: BoxDecoration(
                  color: _currentPage == index
                      ? cards[_currentPage].color
                      : Colors.grey[300],
                  borderRadius: BorderRadius.circular(4),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _pageController.dispose();
    super.dispose();
  }
}

class CardData {
  final String title;
  final String subtitle;
  final Color color;

  CardData({
    required this.title,
    required this.subtitle,
    required this.color,
  });
}

滚动监听和控制

ScrollController 使用

// 滚动控制器示例
class ScrollControllerExample extends StatefulWidget {
  @override
  _ScrollControllerExampleState createState() => _ScrollControllerExampleState();
}

class _ScrollControllerExampleState extends State<ScrollControllerExample> {
  ScrollController _scrollController = ScrollController();
  bool _showBackToTop = false;
  double _scrollProgress = 0.0;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_scrollListener);
  }

  void _scrollListener() {
    setState(() {
      // 计算滚动进度
      if (_scrollController.position.maxScrollExtent > 0) {
        _scrollProgress = _scrollController.offset /
            _scrollController.position.maxScrollExtent;
      }

      // 显示/隐藏回到顶部按钮
      _showBackToTop = _scrollController.offset > 200;
    });
  }

  void _scrollToTop() {
    _scrollController.animateTo(
      0,
      duration: Duration(milliseconds: 500),
      curve: Curves.easeInOut,
    );
  }

  void _scrollToBottom() {
    _scrollController.animateTo(
      _scrollController.position.maxScrollExtent,
      duration: Duration(milliseconds: 500),
      curve: Curves.easeInOut,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('滚动控制器示例'),
        bottom: PreferredSize(
          preferredSize: Size.fromHeight(4),
          child: LinearProgressIndicator(
            value: _scrollProgress,
            backgroundColor: Colors.transparent,
            valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
          ),
        ),
      ),
      body: Stack(
        children: [
          ListView.builder(
            controller: _scrollController,
            itemCount: 100,
            itemBuilder: (context, index) {
              return Card(
                margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
                child: ListTile(
                  leading: CircleAvatar(
                    child: Text('${index + 1}'),
                  ),
                  title: Text('项目 ${index + 1}'),
                  subtitle: Text('这是第 ${index + 1} 个项目的描述'),
                  trailing: Icon(Icons.arrow_forward_ios),
                ),
              );
            },
          ),

          // 滚动控制按钮
          Positioned(
            right: 16,
            bottom: 80,
            child: Column(
              children: [
                if (_showBackToTop)
                  FloatingActionButton(
                    mini: true,
                    onPressed: _scrollToTop,
                    child: Icon(Icons.keyboard_arrow_up),
                    heroTag: 'top',
                  ),
                SizedBox(height: 8),
                FloatingActionButton(
                  mini: true,
                  onPressed: _scrollToBottom,
                  child: Icon(Icons.keyboard_arrow_down),
                  heroTag: 'bottom',
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _scrollController.removeListener(_scrollListener);
    _scrollController.dispose();
    super.dispose();
  }
}

ScrollNotification 监听

// 滚动通知监听
class ScrollNotificationExample extends StatefulWidget {
  @override
  _ScrollNotificationExampleState createState() => _ScrollNotificationExampleState();
}

class _ScrollNotificationExampleState extends State<ScrollNotificationExample> {
  String _scrollInfo = '滚动信息将在这里显示';
  bool _isScrolling = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('滚动通知监听'),
      ),
      body: Column(
        children: [
          Container(
            padding: EdgeInsets.all(16),
            color: _isScrolling ? Colors.blue[50] : Colors.grey[50],
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  '滚动状态: ${_isScrolling ? "滚动中" : "静止"}',
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    color: _isScrolling ? Colors.blue : Colors.grey[600],
                  ),
                ),
                SizedBox(height: 8),
                Text(_scrollInfo),
              ],
            ),
          ),

          Expanded(
            child: NotificationListener<ScrollNotification>(
              onNotification: (ScrollNotification notification) {
                setState(() {
                  if (notification is ScrollStartNotification) {
                    _isScrolling = true;
                    _scrollInfo = '开始滚动 - 滚动方向: ${notification.dragDetails?.globalPosition}';
                  } else if (notification is ScrollUpdateNotification) {
                    _scrollInfo = '滚动中 - 当前位置: ${notification.metrics.pixels.toStringAsFixed(1)}, '
                        '最大滚动: ${notification.metrics.maxScrollExtent.toStringAsFixed(1)}, '
                        '滚动比例: ${(notification.metrics.pixels / notification.metrics.maxScrollExtent * 100).toStringAsFixed(1)}%';
                  } else if (notification is ScrollEndNotification) {
                    _isScrolling = false;
                    _scrollInfo = '滚动结束 - 最终位置: ${notification.metrics.pixels.toStringAsFixed(1)}';
                  } else if (notification is OverscrollNotification) {
                    _scrollInfo = '过度滚动 - 过度量: ${notification.overscroll.toStringAsFixed(1)}';
                  }
                });
                return true; // 返回 true 表示消费了这个通知
              },
              child: ListView.builder(
                itemCount: 50,
                itemBuilder: (context, index) {
                  return Container(
                    height: 80,
                    margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
                    decoration: BoxDecoration(
                      color: Colors.blue[50],
                      borderRadius: BorderRadius.circular(8),
                      border: Border.all(color: Colors.blue[200]!),
                    ),
                    child: Center(
                      child: Text(
                        '项目 ${index + 1}',
                        style: TextStyle(
                          fontSize: 16,
                          fontWeight: FontWeight.w500,
                        ),
                      ),
                    ),
                  );
                },
              ),
            ),
          ),
        ],
      ),
    );
  }
}

性能优化

ListView 性能优化

// 优化的 ListView 实现
class OptimizedListView extends StatelessWidget {
  final List<String> items;

  const OptimizedListView({Key? key, required this.items}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      // 1. 设置合适的 itemExtent 提高性能
      itemExtent: 80.0,

      // 2. 启用缓存扩展
      cacheExtent: 200.0,

      // 3. 使用 addAutomaticKeepAlives: false 减少内存占用
      addAutomaticKeepAlives: false,
      addRepaintBoundaries: true,
      addSemanticIndexes: true,

      itemCount: items.length,
      itemBuilder: (context, index) {
        // 4. 使用 RepaintBoundary 包装复杂项目
        return RepaintBoundary(
          child: _OptimizedListItem(
            key: ValueKey(items[index]),
            title: items[index],
            index: index,
          ),
        );
      },
    );
  }
}

class _OptimizedListItem extends StatelessWidget {
  final String title;
  final int index;

  const _OptimizedListItem({
    Key? key,
    required this.title,
    required this.index,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 80,
      margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(8),
        boxShadow: [
          BoxShadow(
            color: Colors.grey.withOpacity(0.1),
            blurRadius: 4,
            offset: Offset(0, 2),
          ),
        ],
      ),
      child: ListTile(
        leading: CircleAvatar(
          child: Text('${index + 1}'),
        ),
        title: Text(title),
        subtitle: Text('优化的列表项 $index'),
        trailing: Icon(Icons.arrow_forward_ios),
      ),
    );
  }
}

懒加载和分页

// 懒加载列表
class LazyLoadListView extends StatefulWidget {
  @override
  _LazyLoadListViewState createState() => _LazyLoadListViewState();
}

class _LazyLoadListViewState extends State<LazyLoadListView> {
  List<String> _items = [];
  bool _isLoading = false;
  bool _hasMore = true;
  ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    _loadMoreItems();
    _scrollController.addListener(_scrollListener);
  }

  void _scrollListener() {
    if (_scrollController.position.pixels >=
        _scrollController.position.maxScrollExtent - 200) {
      _loadMoreItems();
    }
  }

  Future<void> _loadMoreItems() async {
    if (_isLoading || !_hasMore) return;

    setState(() {
      _isLoading = true;
    });

    // 模拟网络请求
    await Future.delayed(Duration(seconds: 1));

    final newItems = List.generate(20, (index) =>
        '项目 ${_items.length + index + 1}');

    setState(() {
      _items.addAll(newItems);
      _isLoading = false;

      // 模拟数据加载完毕
      if (_items.length >= 100) {
        _hasMore = false;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('懒加载列表')),
      body: RefreshIndicator(
        onRefresh: () async {
          setState(() {
            _items.clear();
            _hasMore = true;
          });
          await _loadMoreItems();
        },
        child: ListView.builder(
          controller: _scrollController,
          itemCount: _items.length + (_hasMore ? 1 : 0),
          itemBuilder: (context, index) {
            if (index == _items.length) {
              // 加载指示器
              return Container(
                padding: EdgeInsets.all(16),
                alignment: Alignment.center,
                child: _isLoading
                    ? CircularProgressIndicator()
                    : Text('没有更多数据了'),
              );
            }

            return ListTile(
              leading: CircleAvatar(
                child: Text('${index + 1}'),
              ),
              title: Text(_items[index]),
              subtitle: Text('懒加载项目 $index'),
            );
          },
        ),
      ),
    );
  }

  @override
  void dispose() {
    _scrollController.removeListener(_scrollListener);
    _scrollController.dispose();
    super.dispose();
  }
}

实际应用场景

聊天界面

// 聊天界面实现
class ChatListView extends StatefulWidget {
  @override
  _ChatListViewState createState() => _ChatListViewState();
}

class _ChatListViewState extends State<ChatListView> {
  List<ChatMessage> _messages = [];
  ScrollController _scrollController = ScrollController();
  TextEditingController _textController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _loadInitialMessages();
  }

  void _loadInitialMessages() {
    _messages = [
      ChatMessage(text: '你好!', isMe: false, timestamp: DateTime.now().subtract(Duration(minutes: 5))),
      ChatMessage(text: '你好,有什么可以帮助你的吗?', isMe: true, timestamp: DateTime.now().subtract(Duration(minutes: 4))),
      ChatMessage(text: '我想了解一下Flutter的滚动控件', isMe: false, timestamp: DateTime.now().subtract(Duration(minutes: 3))),
      ChatMessage(text: 'Flutter提供了丰富的滚动控件,包括ListView、GridView、CustomScrollView等', isMe: true, timestamp: DateTime.now().subtract(Duration(minutes: 2))),
    ];
    setState(() {});
  }

  void _sendMessage() {
    if (_textController.text.trim().isEmpty) return;

    final message = ChatMessage(
      text: _textController.text,
      isMe: false,
      timestamp: DateTime.now(),
    );

    setState(() {
      _messages.add(message);
    });

    _textController.clear();

    // 滚动到底部
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _scrollController.animateTo(
        _scrollController.position.maxScrollExtent,
        duration: Duration(milliseconds: 300),
        curve: Curves.easeOut,
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('聊天'),
        backgroundColor: Colors.blue,
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              controller: _scrollController,
              padding: EdgeInsets.all(16),
              itemCount: _messages.length,
              itemBuilder: (context, index) {
                final message = _messages[index];
                return _ChatBubble(message: message);
              },
            ),
          ),

          // 输入框
          Container(
            padding: EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: Colors.white,
              boxShadow: [
                BoxShadow(
                  color: Colors.grey.withOpacity(0.2),
                  blurRadius: 4,
                  offset: Offset(0, -2),
                ),
              ],
            ),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _textController,
                    decoration: InputDecoration(
                      hintText: '输入消息...',
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(24),
                      ),
                      contentPadding: EdgeInsets.symmetric(
                        horizontal: 16,
                        vertical: 8,
                      ),
                    ),
                    onSubmitted: (_) => _sendMessage(),
                  ),
                ),
                SizedBox(width: 8),
                FloatingActionButton(
                  mini: true,
                  onPressed: _sendMessage,
                  child: Icon(Icons.send),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _scrollController.dispose();
    _textController.dispose();
    super.dispose();
  }
}

class _ChatBubble extends StatelessWidget {
  final ChatMessage message;

  const _ChatBubble({required this.message});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 4),
      child: Row(
        mainAxisAlignment: message.isMe
            ? MainAxisAlignment.end
            : MainAxisAlignment.start,
        children: [
          if (!message.isMe) ..[
            CircleAvatar(
              radius: 16,
              child: Icon(Icons.person, size: 16),
            ),
            SizedBox(width: 8),
          ],

          Flexible(
            child: Container(
              padding: EdgeInsets.symmetric(horizontal: 16, vertical: 10),
              decoration: BoxDecoration(
                color: message.isMe ? Colors.blue : Colors.grey[200],
                borderRadius: BorderRadius.circular(18),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    message.text,
                    style: TextStyle(
                      color: message.isMe ? Colors.white : Colors.black87,
                    ),
                  ),
                  SizedBox(height: 4),
                  Text(
                    '${message.timestamp.hour}:${message.timestamp.minute.toString().padLeft(2, '0')}',
                    style: TextStyle(
                      fontSize: 12,
                      color: message.isMe
                          ? Colors.white.withOpacity(0.7)
                          : Colors.grey[600],
                    ),
                  ),
                ],
              ),
            ),
          ),

          if (message.isMe) ..[
            SizedBox(width: 8),
            CircleAvatar(
              radius: 16,
              backgroundColor: Colors.blue,
              child: Icon(Icons.person, size: 16, color: Colors.white),
            ),
          ],
        ],
      ),
    );
  }
}

class ChatMessage {
  final String text;
  final bool isMe;
  final DateTime timestamp;

  ChatMessage({
    required this.text,
    required this.isMe,
    required this.timestamp,
  });
}

最佳实践

1. 性能优化建议

  • 使用 ListView.builder 而不是 ListView 来处理大量数据
  • 设置 itemExtent 当所有项目高度相同时,可以显著提高性能
  • 使用 RepaintBoundary 包装复杂的列表项,避免不必要的重绘
  • 合理设置 cacheExtent 控制缓存区域大小
  • 避免在 itemBuilder 中进行复杂计算

2. 内存管理

  • 及时释放 ScrollController
  • 使用 addAutomaticKeepAlives: false 减少内存占用
  • 实现懒加载和分页 避免一次性加载大量数据

3. 用户体验

  • 添加加载指示器 让用户了解数据加载状态
  • 实现下拉刷新 提供数据更新功能
  • 合理的滚动动画 使用适当的 Curve 和 Duration
  • 提供滚动位置指示 如进度条或回到顶部按钮

4. 响应式设计

  • 根据屏幕尺寸调整列数 在 GridView 中使用 LayoutBuilder
  • 适配不同设备 考虑平板和手机的不同显示需求
  • 处理横竖屏切换 保持滚动位置和状态

智能下拉刷新

class SmartRefreshList extends StatefulWidget {
  final List<dynamic> items;
  final Future<List<dynamic>> Function() onRefresh;
  final Future<List<dynamic>> Function() onLoadMore;
  final Widget Function(BuildContext, int) itemBuilder;
  final bool hasMore;

  const SmartRefreshList({
    Key? key,
    required this.items,
    required this.onRefresh,
    required this.onLoadMore,
    required this.itemBuilder,
    this.hasMore = true,
  }) : super(key: key);

  @override
  _SmartRefreshListState createState() => _SmartRefreshListState();
}

class _SmartRefreshListState extends State<SmartRefreshList> {
  final ScrollController _scrollController = ScrollController();
  bool _isLoadingMore = false;
  bool _isRefreshing = false;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  void _onScroll() {
    if (_scrollController.position.pixels >=
        _scrollController.position.maxScrollExtent - 200) {
      if (!_isLoadingMore && widget.hasMore) {
        _loadMore();
      }
    }
  }

  Future<void> _refresh() async {
    if (_isRefreshing) return;
  
    setState(() => _isRefreshing = true);
    try {
      await widget.onRefresh();
    } finally {
      setState(() => _isRefreshing = false);
    }
  }

  Future<void> _loadMore() async {
    if (_isLoadingMore) return;
  
    setState(() => _isLoadingMore = true);
    try {
      await widget.onLoadMore();
    } finally {
      setState(() => _isLoadingMore = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: _refresh,
      child: ListView.builder(
        controller: _scrollController,
        physics: AlwaysScrollableScrollPhysics(),
        itemCount: widget.items.length + (widget.hasMore ? 1 : 0),
        itemBuilder: (context, index) {
          if (index == widget.items.length) {
            return _buildLoadMoreIndicator();
          }
          return widget.itemBuilder(context, index);
        },
      ),
    );
  }

  Widget _buildLoadMoreIndicator() {
    if (!widget.hasMore) {
      return Container(
        padding: EdgeInsets.all(16),
        alignment: Alignment.center,
        child: Text(
          '没有更多数据了',
          style: TextStyle(
            color: Colors.grey[600],
            fontSize: 14,
          ),
        ),
      );
    }

    return Container(
      padding: EdgeInsets.all(16),
      alignment: Alignment.center,
      child: _isLoadingMore
          ? Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                SizedBox(
                  width: 16,
                  height: 16,
                  child: CircularProgressIndicator(strokeWidth: 2),
                ),
                SizedBox(width: 8),
                Text('加载中...'),
              ],
            )
          : Text(
              '上拉加载更多',
              style: TextStyle(color: Colors.grey[600]),
            ),
    );
  }
}

瀑布流布局

class WaterfallGrid extends StatelessWidget {
  final List<dynamic> items;
  final Widget Function(BuildContext, int) itemBuilder;
  final int crossAxisCount;
  final double mainAxisSpacing;
  final double crossAxisSpacing;
  final EdgeInsets padding;

  const WaterfallGrid({
    Key? key,
    required this.items,
    required this.itemBuilder,
    this.crossAxisCount = 2,
    this.mainAxisSpacing = 8.0,
    this.crossAxisSpacing = 8.0,
    this.padding = const EdgeInsets.all(8.0),
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: padding,
      child: MasonryGridView.count(
        crossAxisCount: crossAxisCount,
        mainAxisSpacing: mainAxisSpacing,
        crossAxisSpacing: crossAxisSpacing,
        itemCount: items.length,
        itemBuilder: itemBuilder,
      ),
    );
  }
}

// 使用示例
class WaterfallDemo extends StatelessWidget {
  final List<Map<String, dynamic>> photos = [
    {'url': 'https://picsum.photos/200/300', 'height': 300.0},
    {'url': 'https://picsum.photos/200/250', 'height': 250.0},
    {'url': 'https://picsum.photos/200/400', 'height': 400.0},
    // 更多图片数据...
  ];

  @override
  Widget build(BuildContext context) {
    return WaterfallGrid(
      items: photos,
      crossAxisCount: 2,
      itemBuilder: (context, index) {
        final photo = photos[index];
        return Card(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              ClipRRect(
                borderRadius: BorderRadius.vertical(top: Radius.circular(8)),
                child: Image.network(
                  photo['url'],
                  height: photo['height'],
                  width: double.infinity,
                  fit: BoxFit.cover,
                ),
              ),
              Padding(
                padding: EdgeInsets.all(8),
                child: Text(
                  '图片 ${index + 1}',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

滚动视差效果

class ParallaxScrollView extends StatefulWidget {
  final List<Widget> children;
  final String backgroundImage;
  final double parallaxFactor;

  const ParallaxScrollView({
    Key? key,
    required this.children,
    required this.backgroundImage,
    this.parallaxFactor = 0.5,
  }) : super(key: key);

  @override
  _ParallaxScrollViewState createState() => _ParallaxScrollViewState();
}

class _ParallaxScrollViewState extends State<ParallaxScrollView> {
  final ScrollController _scrollController = ScrollController();
  double _scrollOffset = 0.0;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      setState(() {
        _scrollOffset = _scrollController.offset;
      });
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        // 背景视差层
        Positioned(
          top: -_scrollOffset * widget.parallaxFactor,
          left: 0,
          right: 0,
          child: Container(
            height: MediaQuery.of(context).size.height + 200,
            decoration: BoxDecoration(
              image: DecorationImage(
                image: NetworkImage(widget.backgroundImage),
                fit: BoxFit.cover,
              ),
            ),
          ),
        ),
        // 内容层
        CustomScrollView(
          controller: _scrollController,
          slivers: [
            SliverToBoxAdapter(
              child: Container(
                height: 300,
                color: Colors.transparent,
              ),
            ),
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  return Container(
                    margin: EdgeInsets.all(8),
                    padding: EdgeInsets.all(16),
                    decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.circular(12),
                      boxShadow: [
                        BoxShadow(
                          color: Colors.black.withOpacity(0.1),
                          blurRadius: 8,
                          offset: Offset(0, 2),
                        ),
                      ],
                    ),
                    child: widget.children[index],
                  );
                },
                childCount: widget.children.length,
              ),
            ),
          ],
        ),
      ],
    );
  }
}

🚀 滚动性能优化进阶

虚拟化长列表

class VirtualizedList extends StatefulWidget {
  final int itemCount;
  final double itemHeight;
  final Widget Function(BuildContext, int) itemBuilder;
  final int cacheExtent;

  const VirtualizedList({
    Key? key,
    required this.itemCount,
    required this.itemHeight,
    required this.itemBuilder,
    this.cacheExtent = 5,
  }) : super(key: key);

  @override
  _VirtualizedListState createState() => _VirtualizedListState();
}

class _VirtualizedListState extends State<VirtualizedList> {
  final ScrollController _scrollController = ScrollController();
  final Map<int, Widget> _cachedWidgets = {};
  int _firstVisibleIndex = 0;
  int _lastVisibleIndex = 0;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_updateVisibleRange);
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  void _updateVisibleRange() {
    final viewportHeight = MediaQuery.of(context).size.height;
    final scrollOffset = _scrollController.offset;
  
    final newFirstIndex = (scrollOffset / widget.itemHeight).floor()
        .clamp(0, widget.itemCount - 1);
    final newLastIndex = ((scrollOffset + viewportHeight) / widget.itemHeight).ceil()
        .clamp(0, widget.itemCount - 1);
  
    if (newFirstIndex != _firstVisibleIndex || newLastIndex != _lastVisibleIndex) {
      setState(() {
        _firstVisibleIndex = newFirstIndex;
        _lastVisibleIndex = newLastIndex;
      });
    
      // 清理缓存
      _cleanupCache();
    }
  }

  void _cleanupCache() {
    final keysToRemove = <int>[];
    for (final key in _cachedWidgets.keys) {
      if (key < _firstVisibleIndex - widget.cacheExtent ||
          key > _lastVisibleIndex + widget.cacheExtent) {
        keysToRemove.add(key);
      }
    }
    for (final key in keysToRemove) {
      _cachedWidgets.remove(key);
    }
  }

  Widget _buildItem(int index) {
    if (!_cachedWidgets.containsKey(index)) {
      _cachedWidgets[index] = widget.itemBuilder(context, index);
    }
    return _cachedWidgets[index]!;
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _scrollController,
      itemCount: widget.itemCount,
      itemExtent: widget.itemHeight,
      cacheExtent: widget.cacheExtent * widget.itemHeight,
      itemBuilder: (context, index) {
        if (index >= _firstVisibleIndex - widget.cacheExtent &&
            index <= _lastVisibleIndex + widget.cacheExtent) {
          return _buildItem(index);
        }
        return SizedBox(height: widget.itemHeight);
      },
    );
  }
}

总结

Flutter 的滚动控件提供了强大而灵活的解决方案:

  1. ListView - 适用于线性列表,支持动态和静态内容
  2. GridView - 适用于网格布局,支持响应式设计
  3. CustomScrollView + Sliver - 适用于复杂的滚动效果和组合布局
  4. PageView - 适用于页面切换和引导界面
  5. 滚动监听 - 提供丰富的滚动状态和控制能力

核心组件回顾

  • ListView:线性列表的首选方案,支持懒加载和高性能渲染
  • GridView:网格布局的标准实现,适合展示图片和卡片
  • CustomScrollView:复杂滚动场景的终极解决方案
  • ScrollController:滚动控制的核心工具,提供精确的滚动控制

性能优化要点

  1. 懒加载策略:只渲染可见区域的内容,减少内存占用
  2. 缓存机制:合理缓存已渲染的组件,避免重复构建
  3. 虚拟化技术:对于超大数据集,使用虚拟化技术提升性能
  4. 图片优化:使用合适的图片格式和尺寸,避免内存溢出

用户体验设计

  1. 流畅滚动:确保60fps的滚动体验,避免卡顿
  2. 加载反馈:提供清晰的加载状态指示
  3. 边界处理:优雅处理列表边界和空状态
  4. 手势响应:支持多种滚动手势和交互方式

推荐工具和库

  • flutter_staggered_grid_view:瀑布流和不规则网格布局
  • pull_to_refresh:强大的下拉刷新组件
  • scrollable_positioned_list:可精确定位的滚动列表
  • sticky_headers:粘性头部组件

通过合理使用这些控件和优化技巧,可以创建流畅、高性能的滚动界面,提供优秀的用户体验。


下一步学习: TabBar 和 PageView 详解

高级 Sliver 用法

// 自定义 Sliver 组件
class SliverHeaderDelegate extends SliverPersistentHeaderDelegate {
  final double minHeight;
  final double maxHeight;
  final Widget child;

  SliverHeaderDelegate({
    required this.minHeight,
    required this.maxHeight,
    required this.child,
  });

  @override
  double get minExtent => minHeight;

  @override
  double get maxExtent => maxHeight;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return SizedBox.expand(child: child);
  }

  @override
  bool shouldRebuild(SliverHeaderDelegate oldDelegate) {
    return maxHeight != oldDelegate.maxHeight ||
        minHeight != oldDelegate.minHeight ||
        child != oldDelegate.child;
  }
}

// 使用自定义 Sliver
class AdvancedSliverExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        // 自定义持久化头部
        SliverPersistentHeader(
          pinned: true,
          delegate: SliverHeaderDelegate(
            minHeight: 60,
            maxHeight: 120,
            child: Container(
              decoration: BoxDecoration(
                gradient: LinearGradient(
                  colors: [Colors.orange, Colors.red],
                ),
              ),
              child: Center(
                child: Text(
                  '持久化头部',
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
            ),
          ),
        ),

        // 带内边距的 Sliver
        SliverPadding(
          padding: EdgeInsets.all(16),
          sliver: SliverList(
            delegate: SliverChildListDelegate([
              Card(
                child: Padding(
                  padding: EdgeInsets.all(16),
                  child: Text('带内边距的卡片 1'),
                ),
              ),
              Card(
                child: Padding(
                  padding: EdgeInsets.all(16),
                  child: Text('带内边距的卡片 2'),
                ),
              ),
            ]),
          ),
        ),

        // 动态 Sliver 列表
        SliverList(
          delegate: SliverChildBuilderDelegate(
            (context, index) {
              return Container(
                height: 80,
                margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
                decoration: BoxDecoration(
                  color: Colors.blue[50],
                  borderRadius: BorderRadius.circular(8),
                  border: Border.all(color: Colors.blue[200]!),
                ),
                child: Center(
                  child: Text(
                    '动态项目 ${index + 1}',
                    style: TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.w500,
                    ),
                  ),
                ),
              );
            },
            childCount: 20,
          ),
        ),
      ],
    );
  }
}

ListView 高级用法

// 分组 ListView
class GroupedListView extends StatelessWidget {
  final Map<String, List<String>> groupedData = {
    '今天': ['消息1', '消息2', '消息3'],
    '昨天': ['消息4', '消息5'],
    '更早': ['消息6', '消息7', '消息8', '消息9'],
  };

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: _getTotalItemCount(),
      itemBuilder: (context, index) {
        final item = _getItemAtIndex(index);

        if (item['isHeader']) {
          return Container(
            padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            color: Colors.grey[100],
            child: Text(
              item['data'],
              style: TextStyle(
                fontWeight: FontWeight.bold,
                color: Colors.grey[600],
              ),
            ),
          );
        } else {
          return ListTile(
            leading: Icon(Icons.message),
            title: Text(item['data']),
            subtitle: Text('详细内容...'),
          );
        }
      },
    );
  }

  int _getTotalItemCount() {
    int count = 0;
    groupedData.forEach((key, value) {
      count += 1 + value.length; // 1 for header + items
    });
    return count;
  }

  Map<String, dynamic> _getItemAtIndex(int index) {
    int currentIndex = 0;

    for (String group in groupedData.keys) {
      if (currentIndex == index) {
        return {'isHeader': true, 'data': group};
      }
      currentIndex++;

      for (String item in groupedData[group]!) {
        if (currentIndex == index) {
          return {'isHeader': false, 'data': item};
        }
        currentIndex++;
      }
    }

    return {'isHeader': false, 'data': ''};
  }
}

GridView 详解

基础用法

// 固定列数的网格
class BasicGridView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GridView.count(
      crossAxisCount: 2,
      crossAxisSpacing: 10,
      mainAxisSpacing: 10,
      padding: EdgeInsets.all(16),
      children: List.generate(20, (index) {
        return Container(
          decoration: BoxDecoration(
            color: Colors.blue[100 * (index % 9 + 1)],
            borderRadius: BorderRadius.circular(8),
          ),
          child: Center(
            child: Text(
              '项目 $index',
              style: TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        );
      }),
    );
  }
}

// 动态网格 - 适用于大量数据
class DynamicGridView extends StatelessWidget {
  final List<GridItem> items = List.generate(100, (index) => GridItem(
    id: index,
    title: '商品 $index',
    price: (index + 1) * 10.0,
    imageUrl: 'https://picsum.photos/200/200?random=$index',
  ));

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        crossAxisSpacing: 10,
        mainAxisSpacing: 10,
        childAspectRatio: 0.8,
      ),
      padding: EdgeInsets.all(16),
      itemCount: items.length,
      itemBuilder: (context, index) {
        final item = items[index];
        return Card(
          elevation: 4,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Expanded(
                child: Container(
                  width: double.infinity,
                  decoration: BoxDecoration(
                    color: Colors.grey[300],
                    borderRadius: BorderRadius.vertical(
                      top: Radius.circular(4),
                    ),
                  ),
                  child: Icon(
                    Icons.image,
                    size: 50,
                    color: Colors.grey[600],
                  ),
                ),
              ),
              Padding(
                padding: EdgeInsets.all(8),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      item.title,
                      style: TextStyle(
                        fontWeight: FontWeight.bold,
                        fontSize: 14,
                      ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                    SizedBox(height: 4),
                    Text(
                      ${item.price.toStringAsFixed(2)}',
                      style: TextStyle(
                        color: Colors.red,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

class GridItem {
  final int id;
  final String title;
  final double price;
  final String imageUrl;

  GridItem({
    required this.id,
    required this.title,
    required this.price,
    required this.imageUrl,
  });
}

响应式网格

// 响应式网格布局
class ResponsiveGridView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        // 根据屏幕宽度计算列数
        int crossAxisCount;
        if (constraints.maxWidth < 600) {
          crossAxisCount = 2;
        } else if (constraints.maxWidth < 900) {
          crossAxisCount = 3;
        } else {
          crossAxisCount = 4;
        }

        return GridView.builder(
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: crossAxisCount,
            crossAxisSpacing: 10,
            mainAxisSpacing: 10,
            childAspectRatio: 1.2,
          ),
          padding: EdgeInsets.all(16),
          itemCount: 50,
          itemBuilder: (context, index) {
            return Card(
              child: Container(
                padding: EdgeInsets.all(16),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Icon(
                      Icons.star,
                      size: 40,
                      color: Colors.amber,
                    ),
                    SizedBox(height: 8),
                    Text(
                      '项目 $index',
                      style: TextStyle(fontWeight: FontWeight.bold),
                    ),
                  ],
                ),
              ),
            );
          },
        );
      },
    );
  }
}