Flutter中如何跟随用户滚动实现类似iOS设置的搜索栏动画
实现iOS设置风格的滚动显示/隐藏搜索栏效果
核心思路
放弃Sliver的默认预设行为,改用ScrollController监听滚动方向,结合AnimationController实现精准的平滑显示/隐藏动画:
- 初始状态下搜索栏完全隐藏
- 监听滚动偏移量变化,通过对比前后偏移值判断滚动方向
- 向上滚动时触发动画隐藏搜索栏;仅在列表顶部向下滚动时触发动画显示搜索栏
完整代码实现
import 'package:flutter/material.dart'; class ScrollSearchBar extends StatefulWidget { const ScrollSearchBar({super.key}); @override State<ScrollSearchBar> createState() => _ScrollSearchBarState(); } class _ScrollSearchBarState extends State<ScrollSearchBar> with SingleTickerProviderStateMixin { final ScrollController _scrollController = ScrollController(); late AnimationController _animationController; double _previousOffset = 0; // 搜索栏固定高度,可根据需求调整 static const _searchBarHeight = 56.0; @override void initState() { super.initState(); // 初始化动画控制器,时长贴近iOS原生动画速度 _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 200), value: 0, // 初始状态为隐藏 ); // 添加滚动监听 _scrollController.addListener(_onScroll); } void _onScroll() { final currentOffset = _scrollController.offset; // 判断滚动方向 if (currentOffset > _previousOffset) { // 向上滚动:若搜索栏未在隐藏状态,触发反向动画 if (_animationController.status != AnimationStatus.reverse) { _animationController.reverse(); } } else { // 向下滚动:仅当列表处于顶部时,触发正向动画显示搜索栏 if (currentOffset <= 0 && _animationController.status != AnimationStatus.forward) { _animationController.forward(); } } _previousOffset = currentOffset; } @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: [ // 主列表 ListView.builder( controller: _scrollController, // 根据动画值动态调整列表顶部padding,避免被搜索栏遮挡 padding: EdgeInsets.only(top: _animationController.value * _searchBarHeight), itemCount: 50, itemBuilder: (context, index) { return ListTile(title: Text('设置项 $index')); }, ), // 动态显示的搜索栏 AnimatedBuilder( animation: _animationController, builder: (context, child) { return Transform.translate( // 通过Y轴偏移控制显示:值为0时完全显示,-56时完全隐藏 offset: Offset(0, -_searchBarHeight * (1 - _animationController.value)), child: Container( height: _searchBarHeight, color: Theme.of(context).scaffoldBackgroundColor, child: const TextField( decoration: InputDecoration( hintText: '搜索设置', prefixIcon: Icon(Icons.search), border: InputBorder.none, contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 16), ), ), ), ); }, ), ], ), ); } @override void dispose() { _scrollController.dispose(); _animationController.dispose(); super.dispose(); } }
关键细节说明
- 滚动方向判断:通过对比当前偏移量与上一次偏移量,精准区分向上/向下滚动动作
- 触发条件限制:仅当列表处于顶部(
currentOffset <= 0)时,向下滚动才显示搜索栏,避免中间滚动误触发 - 动画流畅性:使用
Transform.translate实现偏移动画,比修改容器高度更贴近iOS原生的平滑效果;200ms的动画时长匹配系统动画速度 - 列表适配:列表顶部padding随动画值动态调整,确保搜索栏显示时不会遮挡列表内容
为什么Slivers不适用?
SliverAppBar的floating/pinned属性是预设行为,无法实现「仅在顶部向下滚动显示,任意向上滚动隐藏」的自定义逻辑,因此需要手动监听滚动事件来实现完全匹配需求的效果。
内容的提问来源于stack exchange,提问作者Stark
相关产品推荐
相关产品推荐

