
欢迎加入开源鸿蒙跨平台社区: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 性能优化建议
- 使用 const 构造函数:对于不变的组件使用 const 构造函数
- 避免过度重建:使用 AutomaticKeepAliveClientMixin 保持状态
- 合理使用 Sliver:根据场景选择合适的 Sliver 组件
- 控制列表项数量:使用 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 | 左右联动列表 |
参考资料
💡 提示:嵌套滚动是复杂界面的核心技术,合理使用可以显著提升用户体验。建议根据具体场景选择合适的滚动策略,并注意性能优化。