Tab选项卡,这是一个非常常见且权重很高的一个组件,随便打开一个App,比如掘金,如下图,首页顶部就是一个Tab选项卡,这个功能可以说,几乎每个App都会存在。
在Android中,我们可以使用TabLayout+ViewPager,轻松的实现一个Tab指示器+页面滑动,而在Flutter当中呢,可以很负责任的告诉大家,也是很简单的就可以实现,主要使用到了TabBar和TabBarView,举一个特别简单的例子,如下代码所示,就是非常简单的Tab选项卡+底部页面的效果。
@override
Widget build(BuildContext context) {
List<Widget> tabs = []; //tab指示器
List<Widget> bodyList = []; //tab指示器下面的内容Widget
for (int i = 0; i < 9; i++) {
tabs.add(Tab(text: "条目$i"));
bodyList.add(Text("条目$i"));//内容可以是任意的Widget,比如列表等
}
return DefaultTabController(
// 标签数量
length: tabs.length,
child: Scaffold(
appBar: TabBar(
// 多个标签时滚动加载
isScrollable: true,
// 标签指示器的颜色
indicatorColor: Colors.red,
// 标签的颜色
labelColor: Colors.red,
// 未选中标签的颜色
unselectedLabelColor: Colors.black,
// 指示器的大小
indicatorSize: TabBarIndicatorSize.label,
// 指示器的权重,即线条高度
indicatorWeight: 4.0,
tabs: tabs),
// 标签页所对应的页面
body: TabBarView(children: bodyList)));
}
代码效果如下:
在Flutter当中实现起来是不是也是非常的简单呢,既然已经如此的简单了,为什么我们还要再封装一层呢?说白了一是为了扩展,扩展一下系统无法满足的功能,二是为了调用起来得心应手。ok,废话不多说,开始今天的概述。
今天的内容大概如下:
1、封装效果一览
2、确定封装属性和拓展属性
3、源码和具体使用
4、相关总结
一、封装效果一览
所有的效果都是基于原生而实现的,如下图所示:
二、确定封装属性和拓展属性
基本上封装的效果就如上图所示,要封装哪些属性,关于系统的属性,比如指示器的颜色,标签选中和未选中的颜色等等,都可以抛出去,让使用者选择性进行使用。
而需要的拓展的属性,就使得自定义的Tab更加的灵活,满足不同的实际的需求,比如,文本指示器,图片指示器,图文指示器等等,都可以灵活的添加一下。
具体的属性如下,大家在实际封装中,可以根据自身需要来动态的灵活的设置。
属性 |
类型 |
概述 |
tabTitleList |
List<String> |
tab指示器的标题集合,文字形式 |
tabImageList |
List<String> |
tab指示器的标题集合,图片形式 |
tabWidgetList |
List<Widget> |
tab指示器的标题集合,Widget形式 |
tabIconAndTextList |
List<TabBarBean> |
tab指示器的标题集合,左图右文形式 |
tabBodyList |
List<Widget> |
tab指示器对应的页面 |
onPageChange |
Function(int) |
页面滑动回调 |
indicatorColor |
Color |
指示器的颜色 |
labelColor |
Color |
标签的颜色 |
unselectedLabelColor |
Color |
未选中标签的颜色 |
indicatorSize |
TabBarIndicatorSize |
指示器的大小 是和文字宽度一样还是充满 |
indicatorHeight |
double |
indicatorHeight |
isScrollable |
bool |
指示器是否支持滑动 |
tabImageWidth |
double |
图片指示器的宽 仅用于图片指示器和图文指示器 |
tabImageHeight |
double |
图片指示器的高 仅用于图片指示器和图文指示器 |
tabIconAndTextMargin |
double |
左图右文指示器,icon距离文字的距离 |
tabHeight |
double |
tab高度 |
三、源码和具体使用
源码相对比较的简单,仅仅对TabBar和TabBarView做了简单的封装,支持了多种格式的Tab类型,由于需要Tab控制器,这里使用了有状态的StatefulWidget。源码整体如下:
import 'package:flutter/material.dart';
import 'package:vip_flutter/ui/widget/vip_text.dart';
///AUTHOR:AbnerMing
///DATE:2023/5/18
///INTRODUCE:TabBar组件
class VipTabBarView extends StatefulWidget {
final List<String>? tabTitleList; //tab指示器的标题集合,文字形式
final List<String>? tabImageList; //tab指示器的标题集合,图片形式
final List<Widget>? tabWidgetList; //tab指示器的标题集合,Widget形式
final List<VipTabBarBean>? tabIconAndTextList; //tab指示器的标题集合,左图右文形式
final List<Widget>? tabBodyList; //tab指示器的页面
final Function(int)? onPageChange; //页面滑动回调
final Color? indicatorColor; //指示器的颜色
final Color? labelColor; //标签的颜色
final Color? unselectedLabelColor; //未选中标签的颜色
final TabBarIndicatorSize? indicatorSize; //指示器的大小 是和文字宽度一样还是充满
final double? indicatorHeight; //指示器的高度
final bool? isScrollable; //指示器是否支持滑动
final double? tabImageWidth; //图片指示器的宽 仅用于图片指示器和图文指示器
final double? tabImageHeight; //图片指示器的高 仅用于图片指示器和图文指示器
final double? tabIconAndTextMargin; //左图右文指示器,icon距离文字的距离
final double? tabHeight; //tab高度
const VipTabBarView(
{this.tabTitleList,
this.tabImageList,
this.tabWidgetList,
this.tabIconAndTextList,
this.tabBodyList,
this.onPageChange,
this.indicatorColor = Colors.black,
this.labelColor = Colors.black,
this.unselectedLabelColor = Colors.grey,
this.indicatorSize = TabBarIndicatorSize.tab,
this.indicatorHeight = 2,
this.isScrollable = true,
this.tabImageWidth = 15,
this.tabImageHeight = 15,
this.tabIconAndTextMargin = 5,
this.tabHeight = 44,
super.key});
@override
State<VipTabBarView> createState() => _GWMTabBarViewState();
}
///左图右文的对象
class VipTabBarBean {
String title;
String icon;
VipTabBarBean(this.title, this.icon);
}
class _GWMTabBarViewState extends State<VipTabBarView>
with SingleTickerProviderStateMixin {
// 标签控制器
late TabController _tabController;
@override
void initState() {
super.initState();
// 定义控制器
_tabController = TabController(
vsync: this,
length: widget.tabBodyList != null ? widget.tabBodyList!.length : 0,
);
// 添加监听事件
_tabController.addListener(() {
//滑动的索引
if (widget.onPageChange != null && !_tabController.indexIsChanging) {
widget.onPageChange!(_tabController.index);
}
});
}
@override
void dispose() {
super.dispose();
// 杀死控制器
_tabController.dispose();
}
/*
* 指示器点击
*/
void onPage(position) {}
@override
Widget build(BuildContext context) {
List<Widget> tabList = []; //tab指示器
List<Widget> bodyList = []; //tab指示器对应的页面
//文字形式
if (widget.tabTitleList != null) {
tabList = widget.tabTitleList!
.map((e) => Tab(
text: e,
height: widget.tabHeight,
))
.toList();
}
//图片形式
if (widget.tabImageList != null) {
tabList = widget.tabImageList!.map((e) {
Widget view;
if (e.contains("http")) {
//网络图片
view = Image.network(
e,
width: widget.tabImageWidth,
height: widget.tabImageHeight,
);
} else {
view = Image.asset(
e,
width: widget.tabImageWidth,
height: widget.tabImageHeight,
);
}
return Tab(icon: view, height: widget.tabHeight);
}).toList();
}
//自定义Widget
if (widget.tabWidgetList != null) {
tabList = widget.tabWidgetList!;
}
//左图右文形式
if (widget.tabIconAndTextList != null) {
tabList = widget.tabIconAndTextList!.map((e) {
return VipText(
e.title,
leftIcon: e.icon,
height: widget.tabHeight,
leftIconWidth: widget.tabImageWidth,
leftIconHeight: widget.tabImageHeight,
iconMarginRight: widget.tabIconAndTextMargin,
);
}).toList();
}
//指示器对应的页面
if (widget.tabBodyList != null) {
bodyList = widget.tabBodyList!.map((e) => e).toList();
}
return Scaffold(
appBar: TabBar(
// 加上控制器
controller: _tabController,
tabs: tabList,
// 标签指示器的颜色
indicatorColor: widget.indicatorColor,
// 标签的颜色
labelColor: widget.labelColor,
// 未选中标签的颜色
unselectedLabelColor: widget.unselectedLabelColor,
// 指示器的大小
indicatorSize: widget.indicatorSize,
// 指示器的权重,即线条高度
indicatorWeight: widget.indicatorHeight!,
// 多个标签时滚动加载
isScrollable: widget.isScrollable!,
onTap: onPage,
),
body: TabBarView(
// 加上控制器
controller: _tabController,
children: bodyList,
),
);
}
}
简单使用
传一个标题集合和页面集合就可以轻松实现了。
@override
Widget build(BuildContext context) {
return const VipTabBarView(
tabTitleList: ["条目一", "条目二"],
tabBodyList: [
Text("第一个页面"),//可以是任意的Widget
Text("第二个页面"),//可以是任意的Widget
],
);
}
所有案例
对应第一条的封装效果,可直接复制查看效果。
import 'package:flutter/material.dart';
import '../widget/vip_tab_bar_view.dart';
import '../widget/vip_text.dart';
///AUTHOR:AbnerMing
///DATE:2023/5/20
///INTRODUCE:TabBar组件效果页面
class TabBarPage extends StatefulWidget {
const TabBarPage({super.key});
@override
State<TabBarPage> createState() => _TabBarPageState();
}
class _TabBarPageState extends State<TabBarPage> {
@override
Widget build(BuildContext context) {
var tabs = ["条目一", "条目二", "条目三", "条目四", "条目五", "条目六", "条目七", "条目八"];
var tabs2 = ["条目一", "条目二", "条目三"];
var tabImages = [
"https://www.vipandroid.cn/ming/pic/new_java.png",
"https://www.vipandroid.cn/ming/pic/new_android.png",
"https://www.vipandroid.cn/ming/pic/new_kotlin.png"
]; //图片指示器
var bodyList = tabs
.map((e) => VipText(e, backgroundColor: Colors.amberAccent))
.toList();
var bodyList2 = tabs2
.map((e) => VipText(e, backgroundColor: Colors.amberAccent))
.toList();
return Column(children: [
const VipText("多个Tab滑动",
alignment: Alignment.topLeft,
marginTop: 10,
style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(
height: 80,
child: VipTabBarView(
tabTitleList: tabs,
tabBodyList: bodyList,
onPageChange: ((position) {
//页面滑动监听
print(position);
}),
)),
const VipText("固定Tab不滑动",
alignment: Alignment.topLeft,
marginTop: 10,
style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(
height: 80,
child: VipTabBarView(
tabTitleList: tabs2,
tabBodyList: bodyList2,
isScrollable: false,
)),
const VipText("修改指示器颜色",
alignment: Alignment.topLeft,
marginTop: 10,
style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(
height: 80,
child: VipTabBarView(
tabTitleList: tabs2,
tabBodyList: bodyList2,
isScrollable: false,
labelColor: Colors.red,
unselectedLabelColor: Colors.black,
indicatorColor: Colors.red,
)),
const VipText("修改指示器大小",
alignment: Alignment.topLeft,
marginTop: 10,
style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(
height: 80,
child: VipTabBarView(
tabTitleList: tabs2,
tabBodyList: bodyList2,
isScrollable: false,
labelColor: Colors.red,
unselectedLabelColor: Colors.black,
indicatorColor: Colors.red,
indicatorSize: TabBarIndicatorSize.label,
)),
const VipText("图片指示器",
alignment: Alignment.topLeft,
marginTop: 10,
style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(
height: 80,
child: VipTabBarView(
tabImageList: tabImages,
tabBodyList: bodyList2,
isScrollable: false,
labelColor: Colors.red,
unselectedLabelColor: Colors.black,
indicatorColor: Colors.red,
indicatorSize: TabBarIndicatorSize.label,
)),
const VipText("左图右文指示器",
alignment: Alignment.topLeft,
marginTop: 10,
style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(
height: 80,
child: VipTabBarView(
tabIconAndTextList: [
VipTabBarBean(
"Java", "https://www.vipandroid.cn/ming/pic/new_java.png"),
VipTabBarBean("Android",
"https://www.vipandroid.cn/ming/pic/new_android.png"),
VipTabBarBean("Kotlin",
"https://www.vipandroid.cn/ming/pic/new_kotlin.png"),
],
tabBodyList: bodyList2,
isScrollable: false,
labelColor: Colors.red,
unselectedLabelColor: Colors.black,
indicatorColor: Colors.red,
indicatorSize: TabBarIndicatorSize.label,
))
]);
}
}
四、相关总结
在Flutter中我们使用Tab选项卡和底部的页面结合使用时,一定要考虑懒加载,否则,在有网络请求的时候,每次切换页面的时候,数据都会重新加载,这给用户体验是相当的不好,具体如何实现,大家可以去网上搜索搜索,有大把的文章概述,这里就不赘述了,好了铁子们,本篇文章就先到这里,希望可以帮助到大家。