Tab选项卡,这是一个非常常见且权重很高的一个组件,随便打开一个App,比如CSDN,如下图,首页顶部就是一个Tab选项卡,这个功能可以说,几乎每个App都会存在。
在Android中,我们可以使用TabLayout+ViewPager,轻松的实现一个Tab指示器+页面滑动,而在Flutter当中呢,可以很负责任的告诉大家,也是很简单的就可以实现,主要使用到了TabBar和TabBarView,举一个特别简单的例子,如下代码所示,就是非常简单的Tab选项卡+底部页面的效果。
@overrideWidget build(BuildContext context) {List<Widget> tabs = []; //tab指示器List<Widget> bodyList = []; //tab指示器下面的内容Widgetfor (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});@overrideState<VipTabBarView> createState() => _GWMTabBarViewState();
}///左图右文的对象
class VipTabBarBean {String title;String icon;VipTabBarBean(this.title, this.icon);
}class _GWMTabBarViewState extends State<VipTabBarView>with SingleTickerProviderStateMixin {// 标签控制器late TabController _tabController;@overridevoid 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);}});}@overridevoid dispose() {super.dispose();// 杀死控制器_tabController.dispose();}/** 指示器点击*/void onPage(position) {}@overrideWidget 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();}//自定义Widgetif (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,),);}
}
简单使用
传一个标题集合和页面集合就可以轻松实现了。
@overrideWidget build(BuildContext context) {return const VipTabBarView(tabTitleList: ["条目一", "条目二"],tabBodyList: [Text("第一个页面"),//可以是任意的WidgetText("第二个页面"),//可以是任意的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});@overrideState<TabBarPage> createState() => _TabBarPageState();
}class _TabBarPageState extends State<TabBarPage> {@overrideWidget 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选项卡和底部的页面结合使用时,一定要考虑懒加载,否则,在有网络请求的时候,每次切换页面的时候,数据都会重新加载,这给用户体验是相当的不好,具体如何实现,大家可以去网上搜索搜索,有大把的文章概述,这里就不赘述了,好了铁子们,本篇文章就先到这里,希望可以帮助到大家。