文章目录
说明
前提:假设你已初步了解了flutter和dart语言,并且知道怎么创建一个简单的项目;
学习本文后,你将掌握:
- tab组件的用法;
- 组件的封装;
区分
tab页在上面的是TabBarView
组件,形如下图:
tab页在底部的是BottomNavigationBar
组件,形如下图:
TabBarView组件
TabBarView 是 Material 组件库中提供了 Tab 布局组件,通常和 TabBar 配合使用。
注:下面的示例源于:https://book.flutterchina.club/chapter6/tabview.html
TabBarView
TabBarView 封装了 PageView,它的构造方法很简单
TabBarView({
Key? key,
required this.children, // tab 页
this.controller, // TabController
this.physics,
this.dragStartBehavior = DragStartBehavior.start,
})
TabController 用于监听和控制 TabBarView 的页面切换,通常和 TabBar 联动。如果没有指定,则会在组件树中向上查找并使用最近的一个 DefaultTabController 。
TabBar
TabBar 为 TabBarView 的导航标题,如图6-20所示:
TabBar 有很多配置参数,通过这些参数我们可以定义 TabBar 的样式,很多属性都是在配置 indicator 和 label,拿上图来举例,Label 是每个Tab 的文本,indicator 指 “历史” 下面的白色下划线。
const TabBar({
Key? key,
required this.tabs, // 具体的 Tabs,需要我们创建
this.controller,
this.isScrollable = false, // 是否可以滑动
this.padding,
this.indicatorColor,// 指示器颜色,默认是高度为2的一条下划线
this.automaticIndicatorColorAdjustment = true,
this.indicatorWeight = 2.0,// 指示器高度
this.indicatorPadding = EdgeInsets.zero, //指示器padding
this.indicator, // 指示器
this.indicatorSize, // 指示器长度,有两个可选值,一个tab的长度,一个是label长度
this.labelColor,
this.labelStyle,
this.labelPadding,
this.unselectedLabelColor,
this.unselectedLabelStyle,
this.mouseCursor,
this.onTap,
...
})
TabBar 通常位于 AppBar 的底部,它也可以接收一个 TabController ,如果需要和 TabBarView 联动, TabBar 和 TabBarView 使用同一个 TabController 即可,注意,联动时 TabBar 和 TabBarView 的孩子数量需要一致。如果没有指定 controller
,则会在组件树中向上查找并使用最近的一个 DefaultTabController 。另外我们需要创建需要的 tab 并通过 tabs 传给 TabBar, tab 可以是任何 Widget,不过Material 组件库中已经实现了一个 Tab 组件,我们一般都会直接使用它:
const Tab({
Key? key,
this.text, //文本
this.icon, // 图标
this.iconMargin = const EdgeInsets.only(bottom: 10.0),
this.height,
this.child, // 自定义 widget
})
注意,text 和 child 是互斥的,不能同时制定。
实例
下面我们看一个例子:
class TabViewRoute1 extends StatefulWidget {
@override
_TabViewRoute1State createState() => _TabViewRoute1State();
}
class _TabViewRoute1State extends State<TabViewRoute1>
with SingleTickerProviderStateMixin {
late TabController _tabController;
List tabs = ["新闻", "历史", "图片"];
@override
void initState() {
super.initState();
_tabController = TabController(length: tabs.length, vsync: this);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("App Name"),
bottom: TabBar(
controller: _tabController,
tabs: tabs.map((e) => Tab(text: e)).toList(),
),
),
body: TabBarView( //构建
controller: _tabController,
children: tabs.map((e) {
return KeepAliveWrapper(
child: Container(
alignment: Alignment.center,
child: Text(e, textScaleFactor: 5),
),
);
}).toList(),
),
);
}
@override
void dispose() {
// 释放资源
_tabController.dispose();
super.dispose();
}
}
运行后效果如图6-21所示:
滑动页面时顶部的 Tab 也会跟着动,点击顶部 Tab 时页面也会跟着切换。为了实现 TabBar 和 TabBarView 的联动,我们显式创建了一个 TabController,由于 TabController 又需要一个 TickerProvider (vsync 参数), 我们又混入了 SingleTickerProviderStateMixin;由于 TabController 中会执行动画,持有一些资源,所以我们在页面销毁时必须得释放资源(dispose)。综上,我们发现创建 TabController 的过程还是比较复杂,实战中,如果需要 TabBar 和 TabBarView 联动,通常会创建一个 DefaultTabController 作为它们共同的父级组件,这样它们在执行时就会从组件树向上查找,都会使用我们指定的这个 DefaultTabController。我们修改后的实现如下:
class TabViewRoute2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
List tabs = ["新闻", "历史", "图片"];
return DefaultTabController(
length: tabs.length,
child: Scaffold(
appBar: AppBar(
title: Text("App Name"),
bottom: TabBar(
tabs: tabs.map((e) => Tab(text: e)).toList(),
),
),
body: TabBarView( //构建
children: tabs.map((e) {
return KeepAliveWrapper(
child: Container(
alignment: Alignment.center,
child: Text(e, textScaleFactor: 5),
),
);
}).toList(),
),
),
);
}
}
可以看到我们无需去手动管理 Controller 的生命周期,也不需要提供 SingleTickerProviderStateMixin,同时也没有其他的状态需要管理,也就不需要用 StatefulWidget 了,这样简单很多。
需求升级
上述的代码示例,仅能把tabbar设置在顶部,而且是写在appBar
中的,这就限制了它的写法及样式布局;如果我要是把tabbar写在中间位置呢,该怎么办?
写在中间的tabbar组件
其实,上文页提到了DefaultTabController
的关键字,用该组件,可以灵活地设置tabbar的位置。
首先要明确tabbar组件的要素
- 由
TabBar
和TabBarView
组成; - 明确指定tabs的个数,即
length
属性; - 指定
DefaultTabController
或关联TabController
封装组件
我把封装的组件写在下面,以供参考:
import 'package:flutter/material.dart';
// 自由布局的tabBar,上面或下面还有其他组件
class FreeTabBar extends StatefulWidget {
final List tabs;
final List<Widget> children;
final Widget? topWidget;
final Widget? bottomWidget;
final double maxHeight;
const FreeTabBar({
super.key,
this.topWidget, // 可选的顶部组件
this.bottomWidget, // 可选的底部组件
this.maxHeight = 300.0, // 可选的最大高度
required this.tabs,
required this.children,
});
@override
State<FreeTabBar> createState() => _FreeTabBarState();
}
class _FreeTabBarState extends State<FreeTabBar> {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: widget.tabs.length,
child: Column(
children: [
if(widget.topWidget != null)
widget.topWidget as Widget,
TabBar(
tabs: widget.tabs.map((e) => Tab(text: e)).toList(),
),
ConstrainedBox(
constraints: BoxConstraints(maxHeight: widget.maxHeight), // 设置一个最大高度
child: TabBarView(
children: widget.children.map((cell) {
return SingleChildScrollView(
child: cell,
);
}).toList(),
),
),
/// 以下为其他写法
// Expanded
// Expanded(
// child: TabBarView(
// children: widget.children.map((cell) => cell).toList(),
// ),
// ),
// SizedBox(
// height: 300,
// child: TabBarView(
// children: widget.children.map((cell) {
// return SingleChildScrollView(
// child: cell,
// );
// }).toList(),
// ),
// ),
// Flexible(
// child: TabBarView(
// children: widget.children.map((cell) => cell).toList(),
// ),
// ),
if(widget.bottomWidget != null)
widget.bottomWidget as Widget,
],
),
);
}
}
组件说明
上面的组件用了好几种写法来包裹TabBarView
,因为TabBarView
组件需要有明确的高度;
上面的Expanded
、ConstrainedBox
、SizedBox
、Flexible
这几种写法,都限定了其高度
- 显式设置高度值height;
- 尽可能大的占据垂直空间
友情提醒:不设置高度会报错
上述组件也示例了封装组件的参数、参数的类型、参数的默认值,大家封装组件是可参考该格式;
组件用法示例
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import '/utils/platform_check.dart';
import '/components/custom/tabBarFree.dart';
import '/model/mine/poetry.dart';
import 'package:intl/intl.dart';
// 我的 - 随机诗词 - iTab诗词源
class ITabPoetryPage extends StatefulWidget {
const ITabPoetryPage({super.key});
@override
State<ITabPoetryPage> createState() => _ITabPoetryPageState();
}
class _ITabPoetryPageState extends State<ITabPoetryPage> {
PoetryResponse? myResult;
// 如果是web端,则不设置请求头,否则设置
final dio = Dio(BaseOptions(
headers: {
if(!PlatformCheck.isWeb)
'Content-Type': 'application/json',
if(!PlatformCheck.isWeb)
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'
}
));
@override
void initState() {
super.initState();
debugPrint('init初始化,是否为web端:${PlatformCheck.isWeb}');
getHttp();
}
Future<void> getHttp() async {
try {
// const apiUrl = 'https://v1.jinrishici.com/all.json';
const apiUrl = 'https://api.codelife.cc/todayShici?lang=cn';
final response = await dio.get(apiUrl);
debugPrint('接口返回值: $response');
setState(() {
myResult = PoetryResponse.fromJson(response.data);
});
} catch (e) {
debugPrint('报错啦啦啦啦:$e');
}
}
Widget paddingWidget(Widget child) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: child,
);
}
// 构建内容
Widget buildText(String? data) {
return data != null ? paddingWidget(Text(data)) : const Text('暂无数据');
}
// 刷新按钮
Widget refreshBtn() {
return OutlinedButton(
onPressed: getHttp,
child: const Text('今日诗词刷新')
);
}
// 格式化时间
String formatDateTime(String? dateTimeString) {
if (dateTimeString == null) return '暂无时间数据';
try {
final dateTime = DateTime.parse(dateTimeString);
final formatter = DateFormat('yyyy-MM-dd HH:mm:ss');
return formatter.format(dateTime);
} catch (e) {
debugPrint('日期格式化错误: $e');
return '日期格式化错误';
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: FreeTabBar(
topWidget: myResult != null
? Column(
children: [
refreshBtn(),
const SizedBox(height: 10,),
// 标题
Text(myResult!.data.title),
// 朝代 - 作者
Text('${myResult!.data.dynasty} · ${myResult!.data.author}',),
// 内容
Text(myResult!.data.content),
],
) : refreshBtn(),
bottomWidget: Column(
children: [
// 创建时间
Text(formatDateTime(myResult?.data.createTime)),
],
),
tabs: const ['译文', '注释', '引言', '评语', '引言'],
children: [
// 译文
buildText(myResult?.data.translate),
// 注释
buildText(myResult?.data.annotation),
// 引言
buildText(myResult?.data.preface),
// 评语
buildText(myResult?.data.reviews),
// 引语
buildText(myResult?.data.quotes),
],
),
);
}
}
上述代码可直接看FreeTabBar
组件内部,其余可忽略;
由于组件外层还有一层其他组件,所以整体效果如下(红框内为主要内容)
常规的tabbar封装
import 'package:flutter/material.dart';
import '/utils/setting.dart';
class CustomTabBar extends StatefulWidget {
final List tabs;
final List<Widget> children;
const CustomTabBar({
super.key,
required this.tabs,
required this.children,
});
@override
State<CustomTabBar> createState() => _CustomTabBarState();
}
class _CustomTabBarState extends State<CustomTabBar> with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: widget.tabs.length, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TabBar(
indicatorColor: DefaultColor.success,
labelColor: DefaultColor.success,
controller: _tabController,
tabs: widget.tabs.map((e) => Tab(text: e)).toList()
),
Expanded(
child: TabBarView(
controller: _tabController,
children: widget.children.map((cell) {
return SingleChildScrollView(
child: cell,
);
}).toList(),
),
),
],
);
}
}
常规用法
可直接看 CustomTabBar
组件内的代码
import 'package:flutter/material.dart';
import '/components/page/common.dart';
import '/components/custom/tabBar.dart';
import './api1.dart';
import './api2.dart';
// 我的 - 随机诗词
class PoetryPage extends StatelessWidget {
final String title;
const PoetryPage({
super.key,
required this.title,
});
@override
Widget build(BuildContext context) {
return CommonPage(
title: title,
isShowLine: true,
child: const CustomTabBar(
tabs: ['iTab诗词', '今日诗词'],
children: [
ITabPoetryPage(),
JRSCPage(),
],
),
);
}
}
效果图如下: