flutter结合NestedScrollView+TabBar实现嵌套滚动
仅学习参考
scoll_page_tab.dart实现底层嵌套滚动
import 'package:flutter/material.dart';
import 'package:flutter_player/custom_refreshing.dart';class ScollPageTab extends StatefulWidget {final Future<void> Function()? onRefresh;final Future<void> Function()? onLoadMore;final bool isLoadingMore;final bool hasMore;final Widget? title;final List<Widget> tabs;final Widget? headerWidget; // 新增:用于 NestedScrollView 的头部 Sliversfinal List<Widget> tabsBody;final Function? onTopChanged; //监听是否滚动到顶部const ScollPageTab({super.key,this.title,required this.onRefresh,this.onLoadMore,required this.isLoadingMore,required this.hasMore,required this.tabs,required this.tabsBody,this.headerWidget,this.onTopChanged,}) : assert(tabs.length == tabsBody.length,"tabs与tabsBody长度必须一致",); // 增加断言,确保tabs和tabsBody长度一致;@overrideScollPageTabState createState() => ScollPageTabState();
}class ScollPageTabState extends State<ScollPageTab>with TickerProviderStateMixin {bool isTop = true;late TabController _tabController;final ScrollController _scrollController = ScrollController();final GlobalKey<CustomRefreshingState> _refreshKey =GlobalKey<CustomRefreshingState>();@overridevoid initState() {super.initState();_tabController = TabController(length: 2, vsync: this);_scrollController.addListener(_handleScroll);}@overridevoid dispose() {_tabController.dispose();_scrollController.removeListener(_handleScroll);_scrollController.dispose();super.dispose();}@overrideWidget build(BuildContext context) {return Scaffold(// 嵌套滚动视图body: Stack(children: [// 主要内容MediaQuery.removePadding(context: context,removeTop: true,child: _buildContent(context),),// 自定义刷新指示器CustomRefreshing(key: _refreshKey, onRefresh: widget.onRefresh!),],),);}/// 滚动监听处理void _handleScroll() {// 滚动到距离底部100像素以内,且不在加载中,且有更多数据if (_scrollController.position.pixels >=_scrollController.position.maxScrollExtent - 100 &&!widget.isLoadingMore &&widget.hasMore) {_triggerLoadMore();}// 滚动距离顶部50像素以内,且不在顶部状态if (_scrollController.offset > 50) {setState(() {isTop = false;});if (widget.onTopChanged != null) {widget.onTopChanged!(false);}} else if (_scrollController.offset <= 50) {setState(() {isTop = true;});if (widget.onTopChanged != null) {widget.onTopChanged!(true);}}}/// 触发加载更多Future<void> _triggerLoadMore() async {if (widget.isLoadingMore || widget.onLoadMore == null) return;try {await widget.onLoadMore!();} catch (e) {// 错误处理if (WidgetsBinding.instance.lifecycleState != null) {// 只在组件仍挂载时更新状态if (mounted) {// 可以在这里添加错误处理逻辑}}}}/// 构建主要内容Widget _buildContent(BuildContext context) {// 构建滚动内容final scrollContent = NestedScrollView(controller: _scrollController, // 绑定控制器physics: const AlwaysScrollableScrollPhysics(), // 关键修复:确保总是可滚动// 配置顶部可滚动头部headerSliverBuilder: (context, innerBoxIsScrolled) {print("innerBoxIsScrolled: $innerBoxIsScrolled");return [// 可折叠的导航栏// 处理重叠区域的关键组件SliverAppBar(collapsedHeight: 60,// 展开时的最大高度expandedHeight: 600,titleSpacing: 0,// 下拉时显示黑色标题,顶部时不显示title: widget.title,// 是否固定在顶部(折叠后不消失)pinned: true,// 是否在滚动到顶部时浮动显示floating: true,// 展开区域的内容(可放图片、搜索栏等)flexibleSpace: FlexibleSpaceBar(background: widget.headerWidget ?? Container(),collapseMode: CollapseMode.pin,),// 底部添加TabBarbottom: PreferredSize(preferredSize: Size.fromHeight(50),child: Container(color: Colors.white,height: 52,child: TabBar(controller: _tabController, tabs: widget.tabs),),),),];},// 内容区域(TabBar对应的页面)body: TabBarView(controller: _tabController, children: widget.tabsBody),);// 如果有下拉刷新功能,用RefreshIndicator包裹if (widget.onRefresh != null) {return RefreshIndicator(onRefresh: widget.onRefresh!,// 关键修复:设置触发距离displacement: 40,child: NotificationListener<ScrollNotification>(onNotification: (notification) {final state = _refreshKey.currentState;print("state${state}");if (state != null) {return state.handleScrollNotification(notification);}return false;},child: scrollContent,),);}return scrollContent;}void changeRefreshing() {_refreshKey.currentState?.changeRefreshing();}
}
custom_refreshing.dart实现自定义刷新,因嵌套滚动自带的RefreshIndicator组件无法使用,只能自己监听滚动实现
import 'package:flutter/material.dart';class CustomRefreshing extends StatefulWidget {final Future<void> Function() onRefresh;const CustomRefreshing({Key? key, required this.onRefresh}) : super(key: key);@overrideCustomRefreshingState createState() => CustomRefreshingState();
}class CustomRefreshingState extends State<CustomRefreshing>with SingleTickerProviderStateMixin {double _refreshIndicatorOffset = 0.0;bool _isRefreshing = false;bool _showRefreshIndicator = false;AnimationController? _refreshAnimationController;@overridevoid initState() {super.initState();// 初始化刷新动画控制器_refreshAnimationController = AnimationController(vsync: this,duration: const Duration(milliseconds: 300),);}@overridedispose() {_refreshAnimationController?.dispose();super.dispose();}@overrideWidget build(BuildContext context) {return _buildCustomRefreshIndicator();}/// 构建自定义刷新指示器Widget _buildCustomRefreshIndicator() {print("_refreshIndicatorOffset${_refreshIndicatorOffset}");// 如果不显示刷新指示器,返回空容器if (!_showRefreshIndicator && _refreshIndicatorOffset == 0) {return Container();}return Positioned(top: 0,left: 0,right: 0,child: Transform.translate(offset: Offset(0,_refreshIndicatorOffset > 60 ? 60 : _refreshIndicatorOffset,),// offset: Offset(0, _refreshIndicatorOffset - 60), // 60是刷新指示器的高度child: Container(height: 60,color: Colors.transparent,child: Center(child: _isRefreshing? _buildRefreshingIndicator(): _buildPullToRefreshIndicator(),),),),);}/// 构建下拉刷新指示器Widget _buildPullToRefreshIndicator() {return Container(padding: const EdgeInsets.all(8),decoration: BoxDecoration(color: Colors.white,borderRadius: BorderRadius.circular(30),boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1),blurRadius: 8,offset: const Offset(0, 2),),],),child: Row(mainAxisSize: MainAxisSize.min,children: [// 根据下拉距离显示不同的图标_refreshIndicatorOffset > 80? const Icon(Icons.refresh, color: Colors.blue): Transform.rotate(angle: _refreshIndicatorOffset / 80 * 2 * 3.14159,child: const Icon(Icons.arrow_downward, color: Colors.blue),),_refreshIndicatorOffset > 80 ? const SizedBox(width: 8) : SizedBox(),Text(_refreshIndicatorOffset > 80 ? "释放刷新" : "",style: TextStyle(color: _refreshIndicatorOffset > 80 ? Colors.blue : Colors.blue,),),],),);}/// 构建刷新中的指示器Widget _buildRefreshingIndicator() {return Container(padding: const EdgeInsets.all(8),decoration: BoxDecoration(color: Colors.white,borderRadius: BorderRadius.circular(30),boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1),blurRadius: 8,offset: const Offset(0, 2),),],),child: Row(mainAxisSize: MainAxisSize.min,children: [// 旋转的加载图标RotationTransition(turns: Tween(begin: 0.0,end: 1.0,).animate(_refreshAnimationController!),child: const Icon(Icons.refresh, color: Colors.blue),),const SizedBox(width: 8),const Text("正在刷新...", style: TextStyle(color: Colors.blue)),],),);}/// 处理滚动通知bool handleScrollNotification(ScrollNotification notification) {// 只处理与拖动相关的通知if (notification is ScrollStartNotification &¬ification.dragDetails != null) {// 滚动开始,重置状态} else if (notification is OverscrollNotification &¬ification.dragDetails != null) {// 只有在顶部且向下拉时才显示刷新指示器if (notification.dragDetails!.delta.dy > 0) {WidgetsBinding.instance.addPostFrameCallback((_) {setState(() {_refreshIndicatorOffset += notification.dragDetails!.delta.dy;});});return true; // 阻止进一步滚动}} else if (notification is ScrollEndNotification) {// 滚动结束,检查是否需要触发刷新if (_refreshIndicatorOffset > 80) {WidgetsBinding.instance.addPostFrameCallback((_) {setState(() {_showRefreshIndicator = true;});});_startRefreshing();} else {WidgetsBinding.instance.addPostFrameCallback((_) {setState(() {_refreshIndicatorOffset = 0;_showRefreshIndicator = false;});});}}return false;}/// 开始刷新void _startRefreshing() async {setState(() {_isRefreshing = true;});// 启动刷新动画_refreshAnimationController?.repeat();// 执行刷新操作if (widget.onRefresh != null) {await widget.onRefresh!();}// 刷新完成_stopRefreshing();}/// 停止刷新void _stopRefreshing() {_refreshAnimationController?.stop();setState(() {_isRefreshing = false;_refreshIndicatorOffset = 0;_showRefreshIndicator = false;});}void changeRefreshing() {_isRefreshing = !_isRefreshing;}
}
text.dart最外层组件调用示例:
import 'package:flutter/material.dart';
import 'package:flutter_player/scoll_page_tab.dart';void main() {runApp(const TextOne());
}class TextOne extends StatefulWidget {const TextOne({Key? key}) : super(key: key);@override_TextOneState createState() => _TextOneState();
}class _TextOneState extends State<TextOne> {final scollPageTabKey = GlobalKey<ScollPageTabState>();bool topStatus = true;@overridevoid dispose() {super.dispose();}@overrideWidget build(BuildContext context) {// bool topStatus = scollPageTabKey.currentState?.isTop ?? true;print("topStatus: $topStatus");return MaterialApp(title: 'TextOne',theme: ThemeData(primarySwatch: Colors.blue),home: ScollPageTab(key: scollPageTabKey,title: Container(color: topStatus ? Colors.transparent : Colors.red,width: double.infinity,// height: 56,child: Padding(padding: EdgeInsetsGeometry.only(top: 30, left: 20, bottom: 10),child: Text("标题",style: TextStyle(color: Colors.black, fontSize: 20),),),),onRefresh: () async {print("下拉刷新");await Future.delayed(const Duration(seconds: 1));scollPageTabKey.currentState?.changeRefreshing();},isLoadingMore: false,hasMore: true,onTopChanged: (isTop) {// print("回调 isTop: $isTop");setState(() {topStatus = isTop;});},tabs: const [Tab(text: "列表"),Tab(text: "网格"),],headerWidget: Column(children: [SizedBox(height: 250,width: double.infinity,child: Image.network("https://easypp.eruit.cn:10443/data/images/20250422/1745287498479033673.png",fit: BoxFit.cover,),),],),tabsBody: [// 页面1:长列表ListView.builder(// 关键:避免内容区域自己处理滚动,交给NestedScrollView统一管理physics: NeverScrollableScrollPhysics(),// 让列表占满剩余空间shrinkWrap: true,itemCount: 50,itemBuilder: (context, index) =>ListTile(title: Text("页面1 - 列表项 ${index + 1}")),),// 页面2:网格列表GridView.count(physics: NeverScrollableScrollPhysics(),shrinkWrap: true,crossAxisCount: 2,children: List.generate(50,(index) =>Card(child: Center(child: Text("页面2 - 项 ${index + 1}"))),),),],),);}
}