如何实现Flutter AppBar底部组件随滚动渐隐渐显(反向SliverAppBar效果)
实现反向滚动行为:固定AppBar,底部组件随滚动隐藏/显示
需求:与SliverAppBar搭配TabBar的默认行为相反——AppBar始终保持可见,底部的Chip栏(或TabBar)在向下滚动时逐渐消失,向上滚动时重新显示。
测试代码(原实现)
import 'package:flutter/material.dart'; void main() { runApp(const TestWidget()); } class TestWidget extends StatelessWidget { const TestWidget({super.key}); @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData.light( useMaterial3: true, ), home: Scaffold( body: CustomScrollView( slivers: [ SliverAppBar( floating: true, pinned: true, snap: true, title: const Text("My App Title"), bottom: PreferredSize( preferredSize: const Size.fromHeight(kToolbarHeight), child: SizedBox( height: kToolbarHeight, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: 25, itemBuilder: (context, index) => FilterChip( label: Text("Chip $index"), onSelected: (_) {}, ), ), ), ), ), SliverList( delegate: SliverChildListDelegate( List.generate( 100, (index) => ListTile( title: Text("Item $index"), ), ), ), ) ], ), ), ); } }
实现思路
原生SliverAppBar的bottom属性会和AppBar绑定,无法单独控制其显示隐藏,因此需要拆分组件并通过滚动监听+动画控制实现需求:
- 将AppBar单独设置为固定顶部(
SliverAppBar(pinned: true),移除bottom属性) - 把底部Chip栏做成独立组件,放入CustomScrollView的Sliver列表中
- 通过
NotificationListener监听滚动事件,判断滚动方向(向上/向下) - 根据滚动方向,用动画控制器控制Chip栏的垂直偏移,实现平滑的滑入/滑出效果
完整实现代码
import 'package:flutter/material.dart'; void main() { runApp(const TestWidget()); } class TestWidget extends StatefulWidget { const TestWidget({super.key}); @override State<TestWidget> createState() => _TestWidgetState(); } class _TestWidgetState extends State<TestWidget> with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation<double> _offsetAnimation; double _previousScrollOffset = 0; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 300), ); _offsetAnimation = Tween<double>(begin: 0, end: kToolbarHeight).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), ); } @override void dispose() { _animationController.dispose(); super.dispose(); } void _handleScroll(ScrollNotification notification) { if (notification is ScrollUpdateNotification) { final currentOffset = notification.metrics.pixels; // 向下滚动且超过AppBar高度时,隐藏底部栏 if (currentOffset > _previousScrollOffset && currentOffset > kToolbarHeight) { if (_animationController.status != AnimationStatus.forward) { _animationController.forward(); } } // 向上滚动时,显示底部栏 else if (currentOffset < _previousScrollOffset) { if (_animationController.status != AnimationStatus.reverse) { _animationController.reverse(); } } _previousScrollOffset = currentOffset; } } @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData.light(useMaterial3: true), home: Scaffold( body: NotificationListener<ScrollNotification>( onNotification: (notification) { _handleScroll(notification); return false; }, child: CustomScrollView( slivers: [ // 固定在顶部的AppBar const SliverAppBar( pinned: true, title: Text("My App Title"), floating: false, snap: false, ), // 可隐藏的Chip栏 SliverToBoxAdapter( child: AnimatedBuilder( animation: _offsetAnimation, builder: (context, child) { return Transform.translate( offset: Offset(0, _offsetAnimation.value), child: child, ); }, child: SizedBox( height: kToolbarHeight, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: 25, itemBuilder: (context, index) => FilterChip( label: Text("Chip $index"), onSelected: (_) {}, ), ), ), ), ), // 内容列表 SliverList( delegate: SliverChildListDelegate( List.generate( 100, (index) => ListTile(title: Text("Item $index")), ), ), ), ], ), ), ), ); } }
关键细节说明
- 滚动触发优化:仅当滚动偏移超过AppBar高度时才触发隐藏,避免初始滚动时的误操作
- 平滑动画:使用
AnimationController和CurvedAnimation实现自然的滑入滑出效果,避免生硬的显示/隐藏 - 状态管理:通过
StatefulWidget维护动画控制器和滚动偏移量,确保状态正确更新
内容的提问来源于stack exchange,提问作者astubenbord
相关产品推荐
相关产品推荐

