Flutter实现类似iPhone谷歌地图的SliverAppBar(展开状态专属组件)
实现类似谷歌地图的展开/折叠式SliverAppBar
我刚好之前做过类似的需求,核心就是要精准控制组件只在SliverAppBar展开状态下显示。下面给你两种经过验证的可行方案,你可以根据自己的场景选择:
方案一:通过滚动通知监听展开状态
这个方案是通过监听CustomScrollView的滚动事件,计算SliverAppBar的展开比例,从而动态控制组件的显示与隐藏,实现起来非常直观:
class MapLikeAppBar extends StatefulWidget { const MapLikeAppBar({super.key}); @override State<MapLikeAppBar> createState() => _MapLikeAppBarState(); } class _MapLikeAppBarState extends State<MapLikeAppBar> { bool _isAppBarExpanded = true; final double _expandedHeight = 200; // 自定义展开高度 @override Widget build(BuildContext context) { return Scaffold( body: NotificationListener<ScrollUpdateNotification>( onNotification: (notification) { // 计算当前滚动偏移对应的展开比例 final scrollOffset = notification.metrics.pixels; final expansionRatio = (_expandedHeight - scrollOffset) / _expandedHeight; // 当比例超过0.5时判定为展开状态(阈值可根据需求调整) setState(() { _isAppBarExpanded = expansionRatio > 0.5; }); return false; // 允许通知继续传播 }, child: CustomScrollView( slivers: [ SliverAppBar( expandedHeight: _expandedHeight, pinned: true, // 保持AppBar始终在顶部 flexibleSpace: FlexibleSpaceBar( background: Stack( fit: StackFit.expand, children: [ // 替换成你的地图背景或其他组件 Container(color: Colors.blueGrey[100]), // 仅在展开状态显示的组件(比如搜索框) if (_isAppBarExpanded) Positioned( bottom: 20, left: 20, right: 20, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4)], ), child: const TextField( decoration: InputDecoration( hintText: "搜索地点...", border: InputBorder.none, prefixIcon: Icon(Icons.search), ), ), ), ), ], ), title: const Text("我的地图"), ), ), // 页面主体内容 SliverList( delegate: SliverChildBuilderDelegate( (context, index) => ListTile(title: Text("列表项 ${index + 1}")), childCount: 50, ), ), ], ), ), ); } }
方案说明
- 利用
NotificationListener捕获滚动更新事件,计算展开比例来判断AppBar状态 - 优点:逻辑简单易懂,容易自定义阈值
- 小缺点:滚动时会频繁触发
setState,但对于大多数场景性能影响可以忽略
方案二:用LayoutBuilder实时获取布局约束
这个方案更高效,不需要维护状态,直接通过LayoutBuilder获取FlexibleSpaceBar的实时高度来判断展开状态:
class MapLikeAppBar extends StatelessWidget { const MapLikeAppBar({super.key}); final double _expandedHeight = 200; @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( slivers: [ SliverAppBar( expandedHeight: _expandedHeight, pinned: true, flexibleSpace: FlexibleSpaceBar( background: LayoutBuilder( builder: (context, constraints) { // 获取当前FlexibleSpaceBar的可用高度 final currentHeight = constraints.maxHeight; // 默认折叠状态的AppBar高度是kToolbarHeight(56),加上20的阈值判断展开 final isExpanded = currentHeight > kToolbarHeight + 20; return Stack( fit: StackFit.expand, children: [ Container(color: Colors.blueGrey[100]), // 用AnimatedOpacity实现平滑过渡效果 AnimatedOpacity( opacity: isExpanded ? 1 : 0, duration: const Duration(milliseconds: 200), child: Positioned( bottom: 20, left: 20, right: 20, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4)], ), child: const TextField( decoration: InputDecoration( hintText: "搜索地点...", border: InputBorder.none, prefixIcon: Icon(Icons.search), ), ), ), ), ), ], ); }, ), title: const Text("我的地图"), ), ), SliverList( delegate: SliverChildBuilderDelegate( (context, index) => ListTile(title: Text("列表项 ${index + 1}")), childCount: 50, ), ), ], ), ); } }
方案说明
- 利用
LayoutBuilder获取FlexibleSpaceBar的实时布局约束,通过高度对比判断展开状态 - 搭配
AnimatedOpacity可以实现组件的平滑淡入淡出,更接近谷歌地图的体验 - 优点:无状态组件,性能更优,只有布局变化时才会触发重绘
额外优化建议
- 如果你需要更细腻的过渡效果,可以用
AnimatedContainer替代AnimatedOpacity,实现位置或大小的动画 - 阈值可以根据需求调整,比如想让组件在AppBar展开到70%时显示,就用
(_expandedHeight * 0.7)作为判断值 - 可以把判断逻辑封装成一个工具方法,方便在多个地方复用
内容的提问来源于stack exchange,提问作者Andrei Jiácomo Zuse
相关产品推荐
相关产品推荐

