在 Flutter 开发中,创建具有吸顶 Tabs 的 PageView 效果可以极大地提升用户界面的交互性和用户体验。今天,我们就通过一段具体的代码来深入了解如何实现这一功能。效果预览如下:
我们从整体上看这段代码,它定义了一个名为CeilingTabsPageView的有状态组件。这个组件的作用就是构建出一个带有吸顶 Tabs 的页面,用户可以通过滑动 PageView 在不同的页面内容间切换。
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:flutter/material.dart';
代码开头引入了两个重要的库。
package:flutter/material.dart:Flutter 的核心 UI库,它提供了各种构建用户界面的基本组件和工具,比如我们后续会用到的Container、Row、Text等。
package:extended_nested_scroll_view/extended_nested_scroll_view.dart:为我们实现吸顶效果提供了关键支持,ExtendedNestedScrollView这个特殊的组件就来自于它。
class CeilingTabsPageView extends StatefulWidget {
const CeilingTabsPageView({Key? key}) : super(key: key);
State<CeilingTabsPageView> createState() => CeilingTabsPageViewState();
}
这里定义了CeilingTabsPageView组件,它是一个有状态的组件。有状态组件意味着它在运行过程中可以根据用户操作或者其他事件改变自身状态。而createState方法返回了CeilingTabsPageViewState实例,这个实例负责管理组件的状态和构建具体的 UI。
class CeilingTabsPageViewState extends State<CeilingTabsPageView> {
/// 控制器
late PageController _pageController;
int pageIndex = 0;
/// 字体样式
TextStyle myTextStyle = const TextStyle(
color: Colors.white, fontWeight: FontWeight.w600, fontSize: 20);
/// 生命周期
void initState() {
super.initState();
_pageController = PageController(initialPage: pageIndex);
}
/// 页面滑动回调
void handlePageChange(int index) {
setState(() {
pageIndex = index;
});
}
/// Tabs点击
void handleTabClick(int index) {
setState(() {
pageIndex = index;
_pageController.jumpToPage(index); // 直接跳转至指定页面
});
}
Widget build(BuildContext context) {
/// 最大宽度
double maxW = MediaQuery.of(context).size.width;
/// 最大高度
double maxH = MediaQuery.of(context).size.height;
return SizedBox(
width: maxW,
height: maxH,
child: ExtendedNestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [bannerWidget(maxW), tabsWidget(maxW)]))
];
},
// 需要固定吸顶的高度
pinnedHeaderSliverHeightBuilder: () {
return 40;
},
onlyOneScrollInBody: true,
body: SizedBox(
width: maxW,
height: maxH,
child: pageViewWidget(maxW, maxH),
)),
);
}
bannerWidget
Widget bannerWidget(double maxW) {
return Container(
width: maxW,
height: 200,
alignment: Alignment.center,
color: Colors.red.shade300,
child: Text('Banner', style: myTextStyle));
}
这个方法构建了一个Container作为 Banner。它的宽度为屏幕宽度maxW,高度为 200,背景颜色为浅红色(Colors.red.shade300),并且在容器中心显示了 “Banner” 字样,使用之前定义好的myTextStyle字体样式。
tabsWidget
Widget tabsWidget(double maxW) {
return Container(
width: maxW,
height: 40,
color: Colors.blue.shade400,
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () {
handleTabClick(0);
},
child: Container(
alignment: Alignment.center,
child: Text('Tab 1', style: myTextStyle),
),
),
),
Expanded(
child: GestureDetector(
onTap: () {
handleTabClick(1);
},
child: Container(
alignment: Alignment.center,
child: Text('Tab 2', style: myTextStyle),
),
),
)
],
),
);
}
tabsWidget构建了 Tabs 部分。同样是一个宽度为屏幕宽度maxW、高度为 40 的Container,背景颜色为浅蓝色(Colors.blue.shade400)。在这个容器内部,通过Row布局将空间分为两部分,每部分都包含一个Expanded包裹的Container,分别显示 “Tab 1” 和 “Tab 2”,同样使用myTextStyle字体样式。Expanded组件的作用是让两个 Tab 平分容器的宽度。并且添加了GestureDetector来处理点击事件。
pageViewWidget
Widget pageViewWidget(double maxW, double maxH) {
return SingleChildScrollView(
primary: true,
physics: const BouncingScrollPhysics(),
child: SizedBox(
width: maxW,
height: maxH,
child: PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
pageIndex = index;
});
},
children: [
Container(
width: maxW,
height: 1000,
color: Colors.amberAccent,
alignment: Alignment.topCenter,
child: Text('Page1', style: myTextStyle)),
Container(
width: maxW,
height: 1000,
color: Colors.deepPurpleAccent,
alignment: Alignment.topCenter,
child: Text('Page2', style: myTextStyle))
],
),
));
}
pageViewWidget构建了 PageView。它被包裹在SingleChildScrollView中,设置primary为true表示这是主要的滚动视图,physics设置为BouncingScrollPhysics以实现类似于 iOS 的弹性滚动效果。在SizedBox内部是一个PageView,包含两个页面,每个页面都是一个宽度为屏幕宽度maxW、高度为 1000 的Container,分别显示 “Page1” 和 “Page2”,背景颜色也各不相同,同样使用myTextStyle字体样式。并且把控制器绑定上,添加了onPageChanged回调事件。
const CeilingTabsPageView()
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:flutter/material.dart';
/// 吸顶Tabs的PageView
class CeilingTabsPageView extends StatefulWidget {
const CeilingTabsPageView({Key? key}) : super(key: key);
State<CeilingTabsPageView> createState() => CeilingTabsPageViewState();
}
class CeilingTabsPageViewState extends State<CeilingTabsPageView> {
late PageController _pageController;
int pageIndex = 0;
/// 字体样式
TextStyle myTextStyle = const TextStyle(
color: Colors.white, fontWeight: FontWeight.w600, fontSize: 20);
/// 生命周期
void initState() {
super.initState();
_pageController = PageController(initialPage: pageIndex);
}
/// 页面滑动回调
void handlePageChange(int index) {
setState(() {
pageIndex = index;
});
}
/// Tabs点击
void handleTabClick(int index) {
setState(() {
pageIndex = index;
_pageController.jumpToPage(index); // 直接跳转至指定页面
});
}
/// 构建UI
Widget build(BuildContext context) {
/// 最大宽度
double maxW = MediaQuery.of(context).size.width;
/// 最大高度
double maxH = MediaQuery.of(context).size.height;
return SizedBox(
width: maxW,
height: maxH,
child: ExtendedNestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [bannerWidget(maxW), tabsWidget(maxW)]))
];
},
// 需要固定吸顶的高度
pinnedHeaderSliverHeightBuilder: () {
return 40;
},
onlyOneScrollInBody: true,
body: SizedBox(
width: maxW,
height: maxH,
child: pageViewWidget(maxW, maxH),
)),
);
}
/// Banner部件
Widget bannerWidget(double maxW) {
return Container(
width: maxW,
height: 200,
alignment: Alignment.center,
color: Colors.red.shade300,
child: Text('Banner', style: myTextStyle));
}
/// Tabs部件
Widget tabsWidget(double maxW) {
return Container(
width: maxW,
height: 40,
color: Colors.blue.shade400,
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () {
handleTabClick(0);
},
child: Container(
alignment: Alignment.center,
child: Text('Tab 1', style: myTextStyle),
),
),
),
Expanded(
child: GestureDetector(
onTap: () {
handleTabClick(1);
},
child: Container(
alignment: Alignment.center,
child: Text('Tab 2', style: myTextStyle),
),
),
)
],
),
);
}
/// pageView部件
Widget pageViewWidget(double maxW, double maxH) {
return SingleChildScrollView(
primary: true,
physics: const BouncingScrollPhysics(),
child: SizedBox(
width: maxW,
height: maxH,
child: PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
pageIndex = index;
});
},
children: [
Container(
width: maxW,
height: 1000,
color: Colors.amberAccent,
alignment: Alignment.topCenter,
child: Text('Page1', style: myTextStyle)),
Container(
width: maxW,
height: 1000,
color: Colors.deepPurpleAccent,
alignment: Alignment.topCenter,
child: Text('Page2', style: myTextStyle))
],
),
));
}
}
通过这段代码,我们成功地在 Flutter 中实现了一个具有吸顶 Tabs 的 PageView 效果。从引入必要的库,到定义组件和管理状态,再到构建具体的 UI 部件,每一步都紧密配合。ExtendedNestedScrollView组件的使用是实现吸顶效果的核心,而各个部件的合理布局和样式设置则让整个页面看起来更加美观和易于交互。希望这篇文章能帮助你理解并在自己的 Flutter 项目中运用类似的功能。
本次分享就到这儿啦,我是鹏多多,如果您看了觉得有帮助,欢迎评论,关注,点赞,转发,我们下次见~
往期文章
个人主页