进阶实战 Flutter for OpenHarmony:NestedScrollView 嵌套滚动系统 - 复杂滚动交互实现

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


一、嵌套滚动系统架构深度解析

在现代移动应用中,复杂的滚动交互是提升用户体验的关键因素。从简单的列表滚动到复杂的头部折叠效果,Flutter 提供了 NestedScrollView 组件来协调多个滚动视图的交互。理解这套架构的底层原理,是构建高性能滚动系统的基础。

📱 1.1 Flutter 嵌套滚动架构

Flutter 的嵌套滚动系统由多个核心层次组成,每一层都有其特定的职责:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                      应用层 (Application Layer)                  │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  NestedScrollView, CustomScrollView, SliverAppBar...    │    │
│  └─────────────────────────────────────────────────────────┘    │
│                              │                                   │
│                              ▼                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │              滚动协调层 (Scroll Coordination Layer)       │    │
│  │  ScrollController, ScrollPosition, ScrollActivity...    │    │
│  └─────────────────────────────────────────────────────────┘    │
│                              │                                   │
│                              ▼                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │              Sliver 层 (Sliver Layer)                     │    │
│  │  SliverList, SliverGrid, SliverPersistentHeader...      │    │
│  └─────────────────────────────────────────────────────────┘    │
│                              │                                   │
│                              ▼                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │              渲染层 (Rendering Layer)                     │    │
│  │  RenderSliver, RenderBox, Viewport...                   │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘

🔬 1.2 NestedScrollView 核心组件详解

Flutter 嵌套滚动系统的核心组件包括以下几个部分:

NestedScrollView(嵌套滚动视图)

NestedScrollView 是协调头部滚动和主体滚动的核心组件,它通过 headerSliverBuilder 和 body 属性将两个滚动区域连接起来。

dart 复制代码
NestedScrollView(
  headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
    return [
      SliverAppBar(
        expandedHeight: 200,
        floating: false,
        pinned: true,
        flexibleSpace: FlexibleSpaceBar(
          title: Text('标题'),
          background: Image.network('url'),
        ),
      ),
    ];
  },
  body: ListView.builder(
    itemCount: 100,
    itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
  ),
)

SliverAppBar(Sliver 应用栏)

SliverAppBar 是可以随滚动折叠和展开的应用栏,支持多种滚动行为配置。

dart 复制代码
SliverAppBar(
  expandedHeight: 200,        // 展开高度
  floating: false,            // 是否浮动
  pinned: true,               // 是否固定
  snap: false,                // 是否吸附
  stretch: true,              // 是否拉伸
  flexibleSpace: FlexibleSpaceBar(
    title: Text('标题'),
    background: Container(color: Colors.blue),
  ),
)

ScrollController(滚动控制器)

ScrollController 用于控制滚动位置和监听滚动事件。

dart 复制代码
final controller = ScrollController();

controller.addListener(() {
  print('滚动位置: ${controller.offset}');
});

controller.animateTo(
  0,
  duration: Duration(milliseconds: 300),
  curve: Curves.easeOut,
);

🎯 1.3 滚动协调原理

NestedScrollView 通过协调器(Coordinator)来管理头部和主体的滚动关系:

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    滚动协调流程                              │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  用户滚动手势                                                 │
│       │                                                      │
│       ▼                                                      │
│  ┌─────────────────────────────────────────────────────┐    │
│  │              NestedScrollViewCoordinator            │    │
│  │  ┌─────────────────┐    ┌─────────────────────┐    │    │
│  │  │  Header Scroll  │ -> │   Body Scroll       │    │    │
│  │  │  (头部滚动)      │    │   (主体滚动)         │    │    │
│  │  └─────────────────┘    └─────────────────────┘    │    │
│  └─────────────────────────────────────────────────────┘    │
│       │                                                      │
│       ▼                                                      │
│  滚动分配策略:                                               │
│  1. 头部未完全折叠时 -> 滚动头部                              │
│  2. 头部完全折叠后 -> 滚动主体                                │
│  3. 向上滚动时 -> 先滚动主体,再展开头部                       │
│                                                              │
└─────────────────────────────────────────────────────────────┘

滚动行为类型:

属性 说明 效果
pinned 是否固定在顶部 折叠后保持在顶部
floating 是否浮动 向下滚动时立即显示
snap 是否吸附 自动吸附到展开/折叠状态
stretch 是否支持拉伸 过度滚动时拉伸头部

二、基础嵌套滚动实现

基础嵌套滚动包括头部折叠效果、吸顶导航栏和简单的联动滚动。这些是构建复杂滚动界面的基础。

👆 2.1 基础头部折叠效果

头部折叠效果是最常见的嵌套滚动场景,通过 SliverAppBar 实现。

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

/// 基础头部折叠示例
class BasicCollapseHeaderDemo extends StatelessWidget {
  const BasicCollapseHeaderDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
          return [
            SliverAppBar(
              expandedHeight: 200,
              floating: false,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                title: const Text('头部折叠效果'),
                background: Container(
                  decoration: const BoxDecoration(
                    gradient: LinearGradient(
                      begin: Alignment.topLeft,
                      end: Alignment.bottomRight,
                      colors: [Colors.blue, Colors.purple],
                    ),
                  ),
                  child: const Center(
                    child: Icon(Icons.star, size: 80, color: Colors.white),
                  ),
                ),
              ),
            ),
          ];
        },
        body: ListView.builder(
          padding: const EdgeInsets.all(16),
          itemCount: 50,
          itemBuilder: (context, index) {
            return Card(
              margin: const EdgeInsets.only(bottom: 12),
              child: ListTile(
                leading: CircleAvatar(
                  backgroundColor: Colors.primaries[index % Colors.primaries.length],
                  child: Text('${index + 1}'),
                ),
                title: Text('列表项 ${index + 1}'),
                subtitle: Text('这是第 ${index + 1} 个列表项的描述'),
                trailing: const Icon(Icons.chevron_right),
              ),
            );
          },
        ),
      ),
    );
  }
}

🔄 2.2 吸顶导航栏

吸顶导航栏在头部折叠后保持在顶部,提供快速导航功能。

dart 复制代码
/// 吸顶导航栏示例
class PinnedTabBarDemo extends StatefulWidget {
  const PinnedTabBarDemo({super.key});

  @override
  State<PinnedTabBarDemo> createState() => _PinnedTabBarDemoState();
}

class _PinnedTabBarDemoState extends State<PinnedTabBarDemo>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
          return [
            SliverAppBar(
              expandedHeight: 180,
              floating: false,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                title: const Text('吸顶导航栏'),
                background: Container(
                  decoration: const BoxDecoration(
                    gradient: LinearGradient(
                      colors: [Colors.teal, Colors.cyan],
                    ),
                  ),
                ),
              ),
            ),
            SliverPersistentHeader(
              pinned: true,
              delegate: _SliverTabBarDelegate(
                TabBar(
                  controller: _tabController,
                  tabs: const [
                    Tab(text: '推荐'),
                    Tab(text: '热门'),
                    Tab(text: '最新'),
                  ],
                  indicatorColor: Colors.teal,
                  labelColor: Colors.teal,
                  unselectedLabelColor: Colors.grey,
                ),
              ),
            ),
          ];
        },
        body: TabBarView(
          controller: _tabController,
          children: [
            _buildTabPage('推荐内容', Colors.red),
            _buildTabPage('热门内容', Colors.orange),
            _buildTabPage('最新内容', Colors.green),
          ],
        ),
      ),
    );
  }

  Widget _buildTabPage(String title, Color color) {
    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemCount: 20,
      itemBuilder: (context, index) {
        return Card(
          margin: const EdgeInsets.only(bottom: 12),
          color: color.withOpacity(0.1),
          child: ListTile(
            leading: Container(
              width: 50,
              height: 50,
              decoration: BoxDecoration(
                color: color.withOpacity(0.3),
                borderRadius: BorderRadius.circular(8),
              ),
            ),
            title: Text('$title - ${index + 1}'),
            subtitle: Text('这是 $title 的第 ${index + 1} 项'),
          ),
        );
      },
    );
  }
}

class _SliverTabBarDelegate extends SliverPersistentHeaderDelegate {
  final TabBar tabBar;

  _SliverTabBarDelegate(this.tabBar);

  @override
  double get minExtent => tabBar.preferredSize.height;

  @override
  double get maxExtent => tabBar.preferredSize.height;

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
      color: Colors.white,
      child: tabBar,
    );
  }

  @override
  bool shouldRebuild(_SliverTabBarDelegate oldDelegate) {
    return false;
  }
}

🌊 2.3 浮动头部效果

浮动头部在向下滚动时立即显示,适合快速访问导航的场景。

dart 复制代码
/// 浮动头部示例
class FloatingHeaderDemo extends StatelessWidget {
  const FloatingHeaderDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
          return [
            SliverAppBar(
              floating: true,
              snap: true,
              expandedHeight: 120,
              flexibleSpace: FlexibleSpaceBar(
                title: const Text('浮动头部'),
                background: Container(
                  color: Colors.orange,
                  child: const Center(
                    child: Icon(Icons.search, size: 50, color: Colors.white),
                  ),
                ),
              ),
            ),
          ];
        },
        body: ListView.builder(
          itemCount: 100,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text('列表项 ${index + 1}'),
              subtitle: Text('向下滚动时头部会立即出现'),
            );
          },
        ),
      ),
    );
  }
}

三、高级嵌套滚动实现

高级嵌套滚动包括复杂头部布局、多级吸顶、联动滚动和自定义滚动行为。

📊 3.1 复杂头部布局

复杂头部布局包含多个区域,如搜索框、轮播图、分类导航等。

dart 复制代码
/// 复杂头部布局示例
class ComplexHeaderDemo extends StatelessWidget {
  const ComplexHeaderDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
          return [
            SliverAppBar(
              expandedHeight: 300,
              floating: false,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                background: Column(
                  children: [
                    _buildSearchBar(),
                    _buildBanner(),
                    _buildCategoryGrid(),
                  ],
                ),
              ),
            ),
          ];
        },
        body: _buildProductList(),
      ),
    );
  }

  Widget _buildSearchBar() {
    return Container(
      padding: const EdgeInsets.all(16),
      color: Colors.blue,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(20),
        ),
        child: Row(
          children: [
            Icon(Icons.search, color: Colors.grey[400]),
            const SizedBox(width: 8),
            Text('搜索商品', style: TextStyle(color: Colors.grey[400])),
          ],
        ),
      ),
    );
  }

  Widget _buildBanner() {
    return Container(
      height: 150,
      color: Colors.blue[100],
      child: PageView.builder(
        itemCount: 3,
        itemBuilder: (context, index) {
          return Container(
            margin: const EdgeInsets.all(8),
            decoration: BoxDecoration(
              gradient: LinearGradient(
                colors: [
                  Colors.primaries[index % Colors.primaries.length],
                  Colors.primaries[(index + 1) % Colors.primaries.length],
                ],
              ),
              borderRadius: BorderRadius.circular(12),
            ),
            child: Center(
              child: Text(
                '轮播图 ${index + 1}',
                style: const TextStyle(
                  color: Colors.white,
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          );
        },
      ),
    );
  }

  Widget _buildCategoryGrid() {
    final categories = [
      {'icon': Icons.phone, 'name': '手机'},
      {'icon': Icons.computer, 'name': '电脑'},
      {'icon': Icons.tv, 'name': '电视'},
      {'icon': Icons.headphones, 'name': '耳机'},
      {'icon': Icons.camera, 'name': '相机'},
      {'icon': Icons.watch, 'name': '手表'},
      {'icon': Icons.sports_esports, 'name': '游戏'},
      {'icon': Icons.more_horiz, 'name': '更多'},
    ];

    return Container(
      height: 100,
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: categories.map((cat) {
          return Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Container(
                width: 44,
                height: 44,
                decoration: BoxDecoration(
                  color: Colors.blue.withOpacity(0.1),
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Icon(cat['icon'] as IconData, color: Colors.blue),
              ),
              const SizedBox(height: 4),
              Text(cat['name'] as String, style: const TextStyle(fontSize: 12)),
            ],
          );
        }).toList(),
      ),
    );
  }

  Widget _buildProductList() {
    return GridView.builder(
      padding: const EdgeInsets.all(8),
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        childAspectRatio: 0.75,
        crossAxisSpacing: 8,
        mainAxisSpacing: 8,
      ),
      itemCount: 20,
      itemBuilder: (context, index) {
        return Card(
          clipBehavior: Clip.antiAlias,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Expanded(
                child: Container(
                  color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.3),
                  child: Center(
                    child: Icon(
                      Icons.shopping_bag,
                      size: 50,
                      color: Colors.primaries[index % Colors.primaries.length],
                    ),
                  ),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(8),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      '商品 ${index + 1}',
                      style: const TextStyle(fontWeight: FontWeight.bold),
                    ),
                    const SizedBox(height: 4),
                    Text(
                      '¥${(index + 1) * 99}',
                      style: TextStyle(
                        color: Colors.red[700],
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

📝 3.2 多级吸顶效果

多级吸顶效果实现多个区域的吸顶,如分类导航和筛选栏。

dart 复制代码
/// 多级吸顶示例
class MultiLevelPinnedDemo extends StatefulWidget {
  const MultiLevelPinnedDemo({super.key});

  @override
  State<MultiLevelPinnedDemo> createState() => _MultiLevelPinnedDemoState();
}

class _MultiLevelPinnedDemoState extends State<MultiLevelPinnedDemo>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;
  final List<String> _categories = ['全部', '数码', '服装', '食品', '家居'];
  final List<String> _filters = ['综合', '销量', '价格', '新品'];

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: _categories.length, vsync: this);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
          return [
            SliverAppBar(
              expandedHeight: 150,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                title: const Text('多级吸顶'),
                background: Container(
                  decoration: const BoxDecoration(
                    gradient: LinearGradient(
                      colors: [Colors.indigo, Colors.purple],
                    ),
                  ),
                ),
              ),
            ),
            SliverPersistentHeader(
              pinned: true,
              delegate: _CategoryTabBarDelegate(
                TabBar(
                  controller: _tabController,
                  isScrollable: true,
                  tabs: _categories.map((c) => Tab(text: c)).toList(),
                  indicatorColor: Colors.indigo,
                  labelColor: Colors.indigo,
                  unselectedLabelColor: Colors.grey,
                ),
              ),
            ),
            SliverPersistentHeader(
              pinned: true,
              delegate: _FilterBarDelegate(_filters),
            ),
          ];
        },
        body: _buildProductGrid(),
      ),
    );
  }

  Widget _buildProductGrid() {
    return GridView.builder(
      padding: const EdgeInsets.all(8),
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        childAspectRatio: 0.8,
        crossAxisSpacing: 8,
        mainAxisSpacing: 8,
      ),
      itemCount: 30,
      itemBuilder: (context, index) {
        return Card(
          child: Column(
            children: [
              Expanded(
                child: Container(
                  color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.2),
                  child: Center(
                    child: Text('商品 ${index + 1}'),
                  ),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(8),
                child: Text('¥${(index + 1) * 50}'),
              ),
            ],
          ),
        );
      },
    );
  }
}

class _CategoryTabBarDelegate extends SliverPersistentHeaderDelegate {
  final TabBar tabBar;

  _CategoryTabBarDelegate(this.tabBar);

  @override
  double get minExtent => tabBar.preferredSize.height;

  @override
  double get maxExtent => tabBar.preferredSize.height;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(color: Colors.white, child: tabBar);
  }

  @override
  bool shouldRebuild(_CategoryTabBarDelegate oldDelegate) => false;
}

class _FilterBarDelegate extends SliverPersistentHeaderDelegate {
  final List<String> filters;

  _FilterBarDelegate(this.filters);

  @override
  double get minExtent => 50;

  @override
  double get maxExtent => 50;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
      color: Colors.grey[100],
      padding: const EdgeInsets.symmetric(horizontal: 8),
      child: Row(
        children: filters.map((f) {
          return Padding(
            padding: const EdgeInsets.symmetric(horizontal: 12),
            child: Text(f, style: const TextStyle(fontSize: 14)),
          );
        }).toList(),
      ),
    );
  }

  @override
  bool shouldRebuild(_FilterBarDelegate oldDelegate) => false;
}

🔄 3.3 联动滚动效果

联动滚动实现多个滚动区域的同步滚动,如左右联动列表。

dart 复制代码
/// 联动滚动示例
class LinkedScrollDemo extends StatefulWidget {
  const LinkedScrollDemo({super.key});

  @override
  State<LinkedScrollDemo> createState() => _LinkedScrollDemoState();
}

class _LinkedScrollDemoState extends State<LinkedScrollDemo> {
  final List<String> _categories = List.generate(15, (i) => '分类 ${i + 1}');
  final Map<int, List<String>> _products = {};
  int _selectedIndex = 0;

  final ScrollController _categoryController = ScrollController();
  final ScrollController _productController = ScrollController();

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < _categories.length; i++) {
      _products[i] = List.generate(10, (j) => '${_categories[i]} - 商品 ${j + 1}');
    }
  }

  @override
  void dispose() {
    _categoryController.dispose();
    _productController.dispose();
    super.dispose();
  }

  void _onCategoryTap(int index) {
    setState(() => _selectedIndex = index);
    _productController.animateTo(
      0,
      duration: const Duration(milliseconds: 300),
      curve: Curves.easeOut,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('联动滚动')),
      body: Row(
        children: [
          SizedBox(
            width: 100,
            child: ListView.builder(
              controller: _categoryController,
              itemCount: _categories.length,
              itemBuilder: (context, index) {
                final isSelected = index == _selectedIndex;
                return InkWell(
                  onTap: () => _onCategoryTap(index),
                  child: Container(
                    padding: const EdgeInsets.symmetric(vertical: 16),
                    decoration: BoxDecoration(
                      color: isSelected ? Colors.white : Colors.grey[100],
                      border: Border(
                        left: BorderSide(
                          color: isSelected ? Colors.blue : Colors.transparent,
                          width: 3,
                        ),
                      ),
                    ),
                    child: Text(
                      _categories[index],
                      textAlign: TextAlign.center,
                      style: TextStyle(
                        color: isSelected ? Colors.blue : Colors.black,
                        fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
                      ),
                    ),
                  ),
                );
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Colors.white,
              child: ListView.builder(
                controller: _productController,
                padding: const EdgeInsets.all(8),
                itemCount: _products[_selectedIndex]?.length ?? 0,
                itemBuilder: (context, index) {
                  return Card(
                    margin: const EdgeInsets.only(bottom: 8),
                    child: ListTile(
                      leading: Container(
                        width: 50,
                        height: 50,
                        color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.3),
                      ),
                      title: Text(_products[_selectedIndex]![index]),
                      trailing: const Icon(Icons.add_shopping_cart),
                    ),
                  );
                },
              ),
            ),
          ),
        ],
      ),
    );
  }
}

四、完整示例:嵌套滚动系统

下面是一个完整的嵌套滚动系统示例,整合了所有滚动效果:

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

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
        useMaterial3: true,
      ),
      home: const NestedScrollHomePage(),
    );
  }
}

class NestedScrollHomePage extends StatelessWidget {
  const NestedScrollHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('📜 NestedScrollView 嵌套滚动系统')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _buildSectionCard(context, title: '基础折叠头部', description: 'SliverAppBar 折叠效果', icon: Icons.expand, color: Colors.blue, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const BasicCollapseDemo()))),
          _buildSectionCard(context, title: '吸顶导航栏', description: 'TabBar 吸顶效果', icon: Icons.pin, color: Colors.teal, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const PinnedTabBarDemo()))),
          _buildSectionCard(context, title: '浮动头部', description: '快速访问导航', icon: Icons.open_in_full, color: Colors.orange, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const FloatingHeaderDemo()))),
          _buildSectionCard(context, title: '复杂头部布局', description: '多区域头部', icon: Icons.dashboard, color: Colors.purple, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ComplexHeaderDemo()))),
          _buildSectionCard(context, title: '多级吸顶', description: '多层级固定', icon: Icons.layers, color: Colors.indigo, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const MultiLevelPinnedDemo()))),
          _buildSectionCard(context, title: '联动滚动', description: '左右联动列表', icon: Icons.sync_alt, color: Colors.cyan, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const LinkedScrollDemo()))),
        ],
      ),
    );
  }

  Widget _buildSectionCard(BuildContext context, {required String title, required String description, required IconData icon, required Color color, required VoidCallback onTap}) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(12),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              Container(width: 56, height: 56, decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12)), child: Icon(icon, color: color, size: 28)),
              const SizedBox(width: 16),
              Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 4), Text(description, style: TextStyle(fontSize: 13, color: Colors.grey[600]))])),
              Icon(Icons.chevron_right, color: Colors.grey[400]),
            ],
          ),
        ),
      ),
    );
  }
}

class BasicCollapseDemo extends StatelessWidget {
  const BasicCollapseDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) {
          return [
            SliverAppBar(
              expandedHeight: 200,
              floating: false,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                title: const Text('基础折叠头部'),
                background: Container(
                  decoration: const BoxDecoration(gradient: LinearGradient(colors: [Colors.blue, Colors.purple])),
                  child: const Center(child: Icon(Icons.expand, size: 80, color: Colors.white)),
                ),
              ),
            ),
          ];
        },
        body: ListView.builder(
          padding: const EdgeInsets.all(16),
          itemCount: 30,
          itemBuilder: (context, index) => Card(
            margin: const EdgeInsets.only(bottom: 12),
            child: ListTile(
              leading: CircleAvatar(backgroundColor: Colors.primaries[index % Colors.primaries.length], child: Text('${index + 1}')),
              title: Text('列表项 ${index + 1}'),
              subtitle: Text('这是第 ${index + 1} 个列表项'),
            ),
          ),
        ),
      ),
    );
  }
}

class PinnedTabBarDemo extends StatefulWidget {
  const PinnedTabBarDemo({super.key});
  @override
  State<PinnedTabBarDemo> createState() => _PinnedTabBarDemoState();
}

class _PinnedTabBarDemoState extends State<PinnedTabBarDemo> with SingleTickerProviderStateMixin {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) {
          return [
            SliverAppBar(
              expandedHeight: 180,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                title: const Text('吸顶导航栏'),
                background: Container(decoration: const BoxDecoration(gradient: LinearGradient(colors: [Colors.teal, Colors.cyan]))),
              ),
            ),
            SliverPersistentHeader(
              pinned: true,
              delegate: _TabBarDelegate(TabBar(controller: _tabController, tabs: const [Tab(text: '推荐'), Tab(text: '热门'), Tab(text: '最新')], indicatorColor: Colors.teal, labelColor: Colors.teal)),
            ),
          ];
        },
        body: TabBarView(
          controller: _tabController,
          children: [
            _buildTabPage('推荐'),
            _buildTabPage('热门'),
            _buildTabPage('最新'),
          ],
        ),
      ),
    );
  }

  Widget _buildTabPage(String title) {
    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemCount: 20,
      itemBuilder: (context, index) => Card(
        margin: const EdgeInsets.only(bottom: 12),
        child: ListTile(
          leading: Container(width: 50, height: 50, decoration: BoxDecoration(color: Colors.teal.withOpacity(0.2), borderRadius: BorderRadius.circular(8))),
          title: Text('$title - ${index + 1}'),
          subtitle: Text('这是 $title 内容'),
        ),
      ),
    );
  }
}

class _TabBarDelegate extends SliverPersistentHeaderDelegate {
  final TabBar tabBar;
  _TabBarDelegate(this.tabBar);

  @override
  double get minExtent => tabBar.preferredSize.height;
  @override
  double get maxExtent => tabBar.preferredSize.height;
  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => Container(color: Colors.white, child: tabBar);
  @override
  bool shouldRebuild(_TabBarDelegate oldDelegate) => false;
}

class FloatingHeaderDemo extends StatelessWidget {
  const FloatingHeaderDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) {
          return [
            SliverAppBar(
              floating: true,
              snap: true,
              expandedHeight: 120,
              flexibleSpace: FlexibleSpaceBar(
                title: const Text('浮动头部'),
                background: Container(color: Colors.orange, child: const Center(child: Icon(Icons.search, size: 50, color: Colors.white))),
              ),
            ),
          ];
        },
        body: ListView.builder(
          itemCount: 50,
          itemBuilder: (context, index) => ListTile(
            title: Text('列表项 ${index + 1}'),
            subtitle: const Text('向下滚动时头部会立即出现'),
          ),
        ),
      ),
    );
  }
}

class ComplexHeaderDemo extends StatelessWidget {
  const ComplexHeaderDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) {
          return [
            SliverAppBar(
              expandedHeight: 320,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                background: Column(
                  children: [
                    _buildSearchBar(),
                    _buildBanner(),
                    _buildCategories(),
                  ],
                ),
              ),
            ),
          ];
        },
        body: _buildProductGrid(),
      ),
    );
  }

  Widget _buildSearchBar() {
    return Container(
      padding: const EdgeInsets.all(16),
      color: Colors.purple,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
        decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)),
        child: Row(children: [Icon(Icons.search, color: Colors.grey[400]), const SizedBox(width: 8), Text('搜索商品', style: TextStyle(color: Colors.grey[400]))]),
      ),
    );
  }

  Widget _buildBanner() {
    return SizedBox(
      height: 140,
      child: PageView.builder(
        itemCount: 3,
        itemBuilder: (context, index) => Container(
          margin: const EdgeInsets.all(8),
          decoration: BoxDecoration(gradient: LinearGradient(colors: [Colors.primaries[index % Colors.primaries.length], Colors.primaries[(index + 1) % Colors.primaries.length]]), borderRadius: BorderRadius.circular(12)),
          child: Center(child: Text('轮播图 ${index + 1}', style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold))),
        ),
      ),
    );
  }

  Widget _buildCategories() {
    final cats = [Icons.phone, Icons.computer, Icons.tv, Icons.headphones, Icons.camera, Icons.watch, Icons.sports_esports, Icons.more_horiz];
    final names = ['手机', '电脑', '电视', '耳机', '相机', '手表', '游戏', '更多'];
    return SizedBox(
      height: 90,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: List.generate(cats.length, (i) => Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Container(width: 44, height: 44, decoration: BoxDecoration(color: Colors.purple.withOpacity(0.1), borderRadius: BorderRadius.circular(12)), child: Icon(cats[i], color: Colors.purple)),
            const SizedBox(height: 4),
            Text(names[i], style: const TextStyle(fontSize: 12)),
          ],
        )),
      ),
    );
  }

  Widget _buildProductGrid() {
    return GridView.builder(
      padding: const EdgeInsets.all(8),
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 0.75, crossAxisSpacing: 8, mainAxisSpacing: 8),
      itemCount: 20,
      itemBuilder: (context, index) => Card(
        clipBehavior: Clip.antiAlias,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Expanded(child: Container(color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.3), child: const Center(child: Icon(Icons.shopping_bag, size: 50)))),
            Padding(padding: const EdgeInsets.all(8), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text('商品 ${index + 1}', style: const TextStyle(fontWeight: FontWeight.bold)), Text('¥${(index + 1) * 99}', style: TextStyle(color: Colors.red[700], fontWeight: FontWeight.bold))])),
          ],
        ),
      ),
    );
  }
}

class MultiLevelPinnedDemo extends StatefulWidget {
  const MultiLevelPinnedDemo({super.key});
  @override
  State<MultiLevelPinnedDemo> createState() => _MultiLevelPinnedDemoState();
}

class _MultiLevelPinnedDemoState extends State<MultiLevelPinnedDemo> with SingleTickerProviderStateMixin {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 5, vsync: this);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) {
          return [
            SliverAppBar(
              expandedHeight: 150,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                title: const Text('多级吸顶'),
                background: Container(decoration: const BoxDecoration(gradient: LinearGradient(colors: [Colors.indigo, Colors.purple]))),
              ),
            ),
            SliverPersistentHeader(
              pinned: true,
              delegate: _TabBarDelegate(TabBar(controller: _tabController, isScrollable: true, tabs: const [Tab(text: '全部'), Tab(text: '数码'), Tab(text: '服装'), Tab(text: '食品'), Tab(text: '家居')], indicatorColor: Colors.indigo, labelColor: Colors.indigo)),
            ),
            SliverPersistentHeader(
              pinned: true,
              delegate: _FilterBarDelegate(['综合', '销量', '价格', '新品']),
            ),
          ];
        },
        body: GridView.builder(
          padding: const EdgeInsets.all(8),
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 0.8, crossAxisSpacing: 8, mainAxisSpacing: 8),
          itemCount: 20,
          itemBuilder: (context, index) => Card(
            child: Column(
              children: [
                Expanded(child: Container(color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.2), child: Center(child: Text('商品 ${index + 1}')))),
                Padding(padding: const EdgeInsets.all(8), child: Text('¥${(index + 1) * 50}')),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class _FilterBarDelegate extends SliverPersistentHeaderDelegate {
  final List<String> filters;
  _FilterBarDelegate(this.filters);

  @override
  double get minExtent => 50;
  @override
  double get maxExtent => 50;
  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
      color: Colors.grey[100],
      padding: const EdgeInsets.symmetric(horizontal: 8),
      child: Row(children: filters.map((f) => Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Text(f, style: const TextStyle(fontSize: 14)))).toList()),
    );
  }
  @override
  bool shouldRebuild(_FilterBarDelegate oldDelegate) => false;
}

class LinkedScrollDemo extends StatefulWidget {
  const LinkedScrollDemo({super.key});
  @override
  State<LinkedScrollDemo> createState() => _LinkedScrollDemoState();
}

class _LinkedScrollDemoState extends State<LinkedScrollDemo> {
  final List<String> _categories = List.generate(15, (i) => '分类 ${i + 1}');
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('联动滚动')),
      body: Row(
        children: [
          SizedBox(
            width: 100,
            child: ListView.builder(
              itemCount: _categories.length,
              itemBuilder: (context, index) => InkWell(
                onTap: () => setState(() => _selectedIndex = index),
                child: Container(
                  padding: const EdgeInsets.symmetric(vertical: 16),
                  decoration: BoxDecoration(
                    color: index == _selectedIndex ? Colors.white : Colors.grey[100],
                    border: Border(left: BorderSide(color: index == _selectedIndex ? Colors.cyan : Colors.transparent, width: 3)),
                  ),
                  child: Text(_categories[index], textAlign: TextAlign.center, style: TextStyle(color: index == _selectedIndex ? Colors.cyan : Colors.black, fontWeight: index == _selectedIndex ? FontWeight.bold : FontWeight.normal)),
                ),
              ),
            ),
          ),
          Expanded(
            child: Container(
              color: Colors.white,
              child: ListView.builder(
                padding: const EdgeInsets.all(8),
                itemCount: 15,
                itemBuilder: (context, index) => Card(
                  margin: const EdgeInsets.only(bottom: 8),
                  child: ListTile(
                    leading: Container(width: 50, height: 50, color: Colors.cyan.withOpacity(0.3)),
                    title: Text('${_categories[_selectedIndex]} - 商品 ${index + 1}'),
                    trailing: const Icon(Icons.add_shopping_cart),
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

五、最佳实践与性能优化

🎨 5.1 性能优化建议

  1. 使用 const 构造函数:对于不变的组件使用 const 构造函数
  2. 避免过度重建:使用 AutomaticKeepAliveClientMixin 保持状态
  3. 合理使用 Sliver:根据场景选择合适的 Sliver 组件
  4. 控制列表项数量:使用 ListView.builder 替代 ListView

🔧 5.2 滚动监听优化

dart 复制代码
NotificationListener<ScrollNotification>(
  onNotification: (notification) {
    if (notification is ScrollUpdateNotification) {
      // 处理滚动更新
    }
    return false;
  },
  child: ListView(...),
)

📱 5.3 OpenHarmony 适配

在 OpenHarmony 平台上,需要注意:

  • 处理手势冲突
  • 优化滚动性能
  • 适配不同屏幕尺寸

六、总结

本文详细介绍了 Flutter for OpenHarmony 的 NestedScrollView 嵌套滚动系统,包括:

组件类型 核心技术 应用场景
基础折叠头部 SliverAppBar 通用头部折叠
吸顶导航栏 SliverPersistentHeader TabBar 吸顶
浮动头部 floating + snap 快速访问导航
复杂头部布局 多组件组合 电商首页
多级吸顶 多个 SliverPersistentHeader 分类筛选
联动滚动 ScrollController 左右联动列表

参考资料


💡 提示:嵌套滚动是复杂界面的核心技术,合理使用可以显著提升用户体验。建议根据具体场景选择合适的滚动策略,并注意性能优化。

相关推荐
lili-felicity2 小时前
进阶实战 Flutter for OpenHarmony:PageView 无限轮播系统 - 轮播交互优化实现
flutter·交互
lili-felicity3 小时前
进阶实战 Flutter for OpenHarmony:flutter_slidable 第三方库实战 - 列表滑动
flutter
啥都想学点3 小时前
第1天:搭建 flutter 和 Android 环境
android·flutter
蓝帆傲亦3 小时前
Vue.js 大数据处理全景解析:从加载策略到渲染优化的完全手册
前端·vue.js·flutter
九狼3 小时前
Flutter Riverpod + MVI 状态管理实现的提示词优化器
前端·flutter·github
阿林来了4 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 总结与未来展望
flutter
早點睡39013 小时前
进阶实战 Flutter for OpenHarmony:CustomPainter 组件实战 - 自定义绘图与画板
flutter
早點睡39014 小时前
进阶实战 Flutter for OpenHarmony:video_player 第三方库实战
flutter
键盘鼓手苏苏17 小时前
Flutter for OpenHarmony:csslib 强力 CSS 样式解析器,构建自定义渲染引擎的基石(Dart 官方解析库) 深度解析与鸿蒙适配指南
css·flutter·harmonyos