要实现首页头部(搜索栏 + 分类栏)固定、商品列表滚动 的效果,核心是将「固定头部」和「可滚动商品列表」拆分,通过 Column + Expanded + ListView 组合实现,或使用 CustomScrollView + SliverAppBar(更优雅)。以下是两种实现方案,优先推荐第二种(Sliver 方案,贴合 Material 设计且性能更优)。
方案 1:基础方案(Column + Expanded)
原理:将固定头部放在 Column 的顶部,商品列表用 Expanded 包裹(占满剩余空间),仅列表部分滚动,头部保持固定。
修改 home_page.dart 中 _buildHomeContent 方法:
dart
less
// 首页内容构建(固定头部版)
Widget _buildHomeContent() {
return Column(
children: [
// 👇 固定头部:搜索栏 + 分类栏
Column(
children: [
// 顶部搜索栏(固定)
Container(
color: const Color(0xFFFC6721),
padding: const EdgeInsets.fromLTRB(10, 20, 10, 10),
child: Column(
children: [
Row(
children: [
Expanded(
child: Container(
height: 38,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(19),
),
child: const Row(
children: [
SizedBox(width: 10),
Icon(Icons.search, size: 18, color: Colors.grey),
SizedBox(width: 5),
Text(
'搜索你想要的宝贝',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
),
),
const SizedBox(width: 10),
const Icon(Icons.camera_alt, color: Colors.white, size: 22),
],
),
],
),
),
// 分类入口(固定)
Container(
height: 100,
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 5),
child: GridView.count(
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 8,
children: _categoryList.map((item) {
return CategoryItem(
icon: item['icon']!,
title: item['title']!,
);
}).toList(),
),
),
],
),
// 👇 可滚动商品列表(占满剩余空间)
Expanded(
child: ListView(
physics: const BouncingScrollPhysics(), // 弹性滚动
children: [
const SizedBox(height: 5),
// 推荐商品列表
Column(
children: _goodsList.map((goods) {
return GoodsItem(
imageUrl: goods['image']!,
title: goods['title']!,
price: goods['price']!,
location: goods['location']!,
time: goods['time']!,
);
}).toList(),
),
],
),
),
],
);
}
方案 2:优雅方案(CustomScrollView + Sliver)
原理:使用 CustomScrollView 管理滚动区域,通过 SliverPersistentHeader 实现头部固定,SliverList 承载商品列表,性能更优且支持头部吸顶 / 折叠扩展(如需)。
步骤 1:封装固定头部为 Sliver 组件
在 home_page.dart 中新增头部组件:
dart
less
// 固定头部组件(Sliver版)
Widget _buildFixedHeader() {
return SliverPersistentHeader(
pinned: true, // 固定在顶部(关键)
floating: false,
delegate: _SliverHeaderDelegate(
minHeight: 138, // 头部最小高度(搜索栏38+分类栏100)
maxHeight: 138, // 头部最大高度(固定高度,不折叠)
child: Column(
children: [
// 搜索栏
Container(
color: const Color(0xFFFC6721),
padding: const EdgeInsets.fromLTRB(10, 20, 10, 10),
child: Row(
children: [
Expanded(
child: Container(
height: 38,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(19),
),
child: const Row(
children: [
SizedBox(width: 10),
Icon(Icons.search, size: 18, color: Colors.grey),
SizedBox(width: 5),
Text(
'搜索你想要的宝贝',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
),
),
const SizedBox(width: 10),
const Icon(Icons.camera_alt, color: Colors.white, size: 22),
],
),
),
// 分类栏
Container(
height: 100,
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 5),
child: GridView.count(
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 8,
children: _categoryList.map((item) {
return CategoryItem(
icon: item['icon']!,
title: item['title']!,
);
}).toList(),
),
),
],
),
),
);
}
// 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 minHeight != oldDelegate.minHeight ||
maxHeight != oldDelegate.maxHeight ||
child != oldDelegate.child;
}
}
步骤 2:修改 _buildHomeContent 为 CustomScrollView
dart
less
// 首页内容构建(Sliver固定头部版)
Widget _buildHomeContent() {
return CustomScrollView(
physics: const BouncingScrollPhysics(), // 弹性滚动
slivers: [
// 👇 固定头部
_buildFixedHeader(),
// 👇 商品列表
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final goods = _goodsList[index];
return GoodsItem(
imageUrl: goods['image']!,
title: goods['title']!,
price: goods['price']!,
location: goods['location']!,
time: goods['time']!,
);
},
childCount: _goodsList.length,
// 列表顶部间距
addFirstChildIndex: 0,
addAutomaticKeepAlives: true, // 保持列表项状态
),
),
],
);
}
核心效果说明
两种方案均实现:
- 搜索栏 + 分类栏固定在屏幕顶部,不会随商品列表滚动;
- 仅商品列表区域可滚动,滚动时头部始终可见;
- 方案 1 更简单(适合新手),方案 2 更优雅(Flutter 官方推荐的滚动布局方案,支持后续扩展头部折叠 / 吸顶动画)。
额外优化(可选)
- 头部阴影:给固定头部添加底部阴影,增强层次感:
dart
less
// 在固定头部外层添加Container
Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.grey.shade200,
blurRadius: 3,
offset: const Offset(0, 2),
),
],
),
child: // 原头部内容
)
- 适配刘海屏:在搜索栏顶部添加安全区域:
dart
less
// 搜索栏外层包裹SafeArea
SafeArea(
top: true,
child: Container(
color: const Color(0xFFFC6721),
padding: const EdgeInsets.fromLTRB(10, 10, 10, 10), // 移除原20px顶部padding
// 原搜索栏内容
),
)
运行修改后的代码,即可看到头部完全固定,商品列表滚动时头部不会跟随移动,完美还原闲鱼 APP 的固定头部效果。