Flutter开发求助:实现固定顶部的Sliver水平导航栏及选中动效
实现滚动固定的水平导航栏(覆盖图片底部+选中动效)
以下是符合需求的完整实现代码,基于你已有的CustomScrollView和sliver_tools库进行改造:
import 'package:find_your_event/models/festival.dart'; import 'package:flutter/material.dart'; import 'package:sliver_tools/sliver_tools.dart'; class FestivalInformation extends StatefulWidget { const FestivalInformation({required this.festival, super.key}); final Festival festival; @override State<FestivalInformation> createState() => _FestivalInformationState(); } class _FestivalInformationState extends State<FestivalInformation> { // 跟踪选中的导航项索引 int _selectedIndex = 0; // 导航项列表(可根据需求修改) final List<String> _tabs = ['About', 'Lineup', 'Tickets', 'FAQ']; // 用于获取导航项位置/宽度的全局Key final List<GlobalKey> _tabKeys = List.generate(4, (_) => GlobalKey()); @override Widget build(BuildContext context) { return CustomScrollView( slivers: [ // 顶部固定的Medium样式SliverAppBar SliverAppBar.medium( pinned: true, title: Text(widget.festival.name), leading: const Icon(Icons.arrow_back), ), MultiSliver( pushPinnedChildren: true, // 让固定导航栏推挤下方内容,避免遮挡 children: [ // 叠加图片与导航栏的SliverStack SliverStack( insetOnOverlap: false, children: [ // 横幅图片 SliverToBoxAdapter( child: Image.network( widget.festival.bannerURL, width: double.infinity, height: 250, fit: BoxFit.cover, ), ), // 固定在图片底部、滚动后吸附到顶部的导航栏 SliverPinnedHeader( child: Container( height: 60, padding: const EdgeInsets.symmetric(horizontal: 16), // 渐变背景让图片到导航栏过渡自然 decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.transparent, Colors.white], ), ), child: Stack( children: [ // 水平导航项 Row( children: List.generate(_tabs.length, (index) { return Expanded( child: GestureDetector( key: _tabKeys[index], onTap: () => setState(() => _selectedIndex = index), child: Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Text( _tabs[index], textAlign: TextAlign.center, style: TextStyle( fontWeight: _selectedIndex == index ? FontWeight.bold : FontWeight.normal, color: _selectedIndex == index ? Colors.black : Colors.grey[600], ), ), ), ), ); }), ), // 选中项下方的粉色移动条 AnimatedPositioned( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, left: _getTabLeftPosition(_selectedIndex), bottom: 0, width: _getTabWidth(_selectedIndex), child: Container( height: 3, color: Colors.pink, ), ), ], ), ), ), ], ), // 下方内容区域(可根据选中标签切换内容) SliverList( delegate: SliverChildBuilderDelegate( (context, index) { return Container( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _tabs[_selectedIndex], style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), Text( '这是${_tabs[_selectedIndex]}标签下的内容,滚动时导航栏会固定在顶部。', style: const TextStyle(fontSize: 16, height: 1.5), ), // 模拟长内容,方便测试滚动效果 ...List.generate(20, (i) => Text('内容行 ${i+1}', style: const TextStyle(fontSize: 16))), ], ), ); }, childCount: 1, ), ), ], ), ], ); } // 获取选中导航项的左侧偏移量 double _getTabLeftPosition(int index) { final renderBox = _tabKeys[index].currentContext?.findRenderObject() as RenderBox?; if (renderBox == null) return 0; final offset = renderBox.localToGlobal(Offset.zero); return offset.dx - 16; // 减去容器的水平内边距 } // 获取选中导航项的宽度 double _getTabWidth(int index) { final renderBox = _tabKeys[index].currentContext?.findRenderObject() as RenderBox?; return renderBox?.size.width ?? 0; } }
关键实现细节
- 状态管理:将原无状态组件改为有状态组件,跟踪选中索引和导航项位置,支持动态更新UI
- 导航栏固定:通过
SliverPinnedHeader实现导航栏滚动时吸附到顶部,MultiSliver的pushPinnedChildren属性确保内容不会被导航栏遮挡 - 覆盖图片布局:使用
SliverStack将导航栏叠加在图片底部,配合渐变背景提升视觉过渡效果 - 粉色条动效:通过
AnimatedPositioned结合导航项的实际位置/宽度,实现平滑的移动动画
内容的提问来源于stack exchange,提问作者BradDoesCode
相关产品推荐
相关产品推荐

