一个自定义的TabController
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: MyTabControllerDemo(),
);
}
}
class MyTabControllerDemo extends StatefulWidget {
const MyTabControllerDemo({super.key});
@override
_MyTabControllerDemoState createState() => _MyTabControllerDemoState();
}
// 1. 混入SingleTickerProviderStateMixin
class _MyTabControllerDemoState extends State<MyTabControllerDemo>
with SingleTickerProviderStateMixin {
late TabController _tabController;
// 2.重写initState方法,用于初始化 TabController
@override
void initState() {
super.initState();
// 创建 TabController,3 个 tab
// 这里的 vsync 参数用于动画效果,参数来源于 SingleTickerProviderStateMixin
_tabController = TabController(length: 3, vsync: this);
// 监听 tab 切换
_tabController.addListener(() {
// 判断 index 是否在切换过程中
if (_tabController.indexIsChanging) {
// 在这里可以执行一些操作,比如记录切换的 tab
// 打印当前切换的 tab 索引
print("切换到 tab: ${_tabController.index}");
}
});
}
/// 3.重写dispose方法,释放 TabController
@override
void dispose() {
_tabController.dispose(); // 释放资源
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("TabController Demo"),
// 4. 创建 TabBar,并且绑定 TabController
bottom: TabBar(
// 绑定 TabController
controller: _tabController,
// 5. 创建选项卡Tabs
tabs: const [
Tab(text: "首页", icon: Icon(Icons.home)),
Tab(text: "消息", icon: Icon(Icons.message)),
Tab(text: "我的", icon: Icon(Icons.person)),
],
),
),
// 6. 创建 TabBarView,并且绑定 TabController
body: TabBarView(
// 绑定 TabController
controller: _tabController,
children: const [
Center(child: Text("这里是首页内容", style: TextStyle(fontSize: 20))),
Center(child: Text("这里是消息内容", style: TextStyle(fontSize: 20))),
Center(child: Text("这里是我的页面", style: TextStyle(fontSize: 20))),
],
),
// 7. 创建 FloatingActionButton: 悬浮按钮,用于切换 tab
// 实际上这步可以不需要,因为 TabBar 已经提供了切换功能
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.skip_next),
onPressed: () {
// 点击按钮,切换到下一个 tab
final newIndex = (_tabController.index + 1) % 3;
// 动画切换
_tabController.animateTo(newIndex);
},
),
);
}
}
SingleTickerProviderStateMixin
with SingleTickerProviderStateMixin
的作用是:让当前 State
对象成为 TickerProvider
,从而可以被 TabController
、AnimationController
等需要 vsync 的对象使用。
在 Flutter 里,动画 都需要依赖一个 时钟信号(Ticker),这个时钟会不停“滴答”发出帧信号,用来驱动动画计算。
- 如果没有 vsync,动画会一直刷新(哪怕页面不可见),浪费 CPU/GPU 资源。
- 如果提供了 vsync,Flutter 就会在当前 Widget 不可见时自动停止动画更新,避免性能浪费。
在这里SingleTickerProviderStateMixin
的主要作用是给 State
提供 一个 vsync(适合 TabController、单个 AnimationController)。
_tabController = TabController(length: 3, vsync: this);
TabController
TabController 是什么
一个 控制 Tab 页切换的类。本质上是一个 继承自 ChangeNotifier
的 AnimationController,它内部用 _index
表示当前 Tab 的索引。它可以:
- 保存当前选中的 tab 索引;
- 提供 动画效果(切换时平滑过渡);
- 提供 事件监听(监听 tab 切换)。
TabController
就是 Tab 页的大脑,负责管理当前选中页、驱动动画、协调 TabBar 和 TabBarView 的联动,并允许程序监听或控制 Tab 切换。
常用构造函数
TabController({
required int length, // tab 的数量(必须)
required TickerProvider vsync, // 动画时钟(通常传 this)
int initialIndex = 0, // 初始显示第几个 tab(默认 0)
})
length
:Tab 的数量,比如TabBar(tabs: [...])
里有几个 tab,就要传几。vsync
:动画时钟,用来避免动画在屏幕不可见时浪费资源(通常用with SingleTickerProviderStateMixin
)。initialIndex
:初始选中第几个 tab。
主要属性
属性 | 类型 | 说明 |
---|---|---|
index |
int | 当前选中的 Tab 索引 |
previousIndex |
int | 上一个 Tab 索引 |
length |
int | Tab 的总数 |
animation |
Animation? | 一个动画对象,表示当前 Tab 的切换进度 |
indexIsChanging |
bool | 是否正在切换 Tab |
主要方法
方法 | 说明 |
---|---|
animateTo(int index, {Duration duration, Curve curve}) |
以动画的方式切换到指定 Tab |
dispose() |
销毁控制器(必须在 State.dispose() 里调用) |
addListener(void Function()) |
添加监听器,监听 Tab 的变化 |
removeListener(void Function()) |
移除监听器 |
TabBar 与 Tab
TabBar
是什么
TabBar
是一个水平方向的 标签栏组件,通常用来显示多个 Tab 页签,可以和 TabController
绑定,实现 点击切换 和 滑动联动。
主要作用:
- 显示多个 选项卡(tab)。
- 提供 点击事件 切换页面。
- 和
TabBarView
配合,构成 多页面切换 的常见 UI(类似微信顶部的「聊天 / 通讯录 / 发现」)。
常用属性
属性 | 类型 | 说明 |
---|---|---|
tabs |
List<Widget> |
必填,表示每个 Tab 的内容(通常是 Tab(text: "标题") 或带 icon 的)。 |
controller |
TabController? |
控制器,决定当前选中的是哪个 Tab。若不传,会从 DefaultTabController 获取。 |
isScrollable |
bool |
是否允许滚动(默认 false,所有 Tab 平均分布)。true 时 Tab 可以横向滑动。 |
indicatorColor |
Color |
底部指示器的颜色。 |
indicatorWeight |
double |
指示器的高度。 |
indicatorPadding |
EdgeInsets |
指示器的内边距。 |
labelColor |
Color |
选中 Tab 的文字颜色。 |
unselectedLabelColor |
Color |
未选中 Tab 的文字颜色。 |
labelStyle |
TextStyle |
选中 Tab 的文字样式。 |
unselectedLabelStyle |
TextStyle |
未选中 Tab 的文字样式。 |
Tab
Tab
是 Flutter 提供的一个 widget,用于 TabBar 内的单个标签页按钮。
它本质上就是 一个显示文字或图标的 Widget,然后被 TabBar 使用,成为可点击的标签
// Tab的构造函数
const Tab({super.key, this.text, this.icon, this.iconMargin, this.height, this.child})
: assert(text != null || child != null || icon != null),
assert(text == null || child == null);
text
→ 显示文字icon
→ 显示图标iconMargin
→ 图标和文字间距child
→ 可以自定义任意 Widget
Tab(
text: "首页", // 显示文字
icon: Icon(Icons.home), // 可选,显示图标
iconMargin: EdgeInsets.only(bottom: 4), // 图标和文字间距
)
点击事件是谁处理的?
Tab
本质只是一个显示组件(文字 / 图标 / 自定义 Widget),它本身 没有 onTap 或回调,也不知道 TabController。
真正处理点击的是 TabBar。
TabBar
在构建时,会把 tabs
中的每个 Tab
包装成一个可点击区域(InkWell / GestureDetector)。
Tab
被点击时调用 _handleTap(index)
,然后通过 _controller.animateTo(index)
,通知TabController执行``TabBar
动画的刷新。
关键点:TabBar 自己处理点击事件,但是不处理动画
class TabBar extends StatefulWidget implements PreferredSizeWidget {
const TabBar({
super.key,
required this.tabs,
this.controller,
// 省略代码
this.textScaler,
this.indicatorAnimation,
}) : _isPrimary = true,
assert(indicator != null || (indicatorWeight > 0.0));
@override
State<TabBar> createState() => _TabBarState();
}
class _TabBarState extends State<TabBar> {
// 在_TabBarState中build有这么一段代码
@override
Widget build(BuildContext context) {
// 省略代码
wrappedTabs[index] = InkWell(
onTap: () {
_handleTap(index); // ← 点击事件触发
},
child: ...
);
// 省略代码
}
void _handleTap(int index) {
assert(index >= 0 && index < widget.tabs.length);
_controller!.animateTo(index); // ← 真正触发 TabController
widget.onTap?.call(index);
}
}
TabBarView
TabBarView
是一个 页面容器,内部通常是一个 PageView,每个 Tab 对应一个子页面。
作用
- 显示每个 Tab 对应的 具体内容页面。
- 与
TabBar
联动,点击 Tab 或滑动页面时,自动保持同步。
常用属性:
属性 | 类型 | 说明 |
---|---|---|
children |
List<Widget> |
必填,对应每个 Tab 的页面内容,数量要和 tabs 一一对应。 |
controller |
TabController? |
控制器,决定显示哪一个页面。 |
physics |
ScrollPhysics? |
页面滚动的物理效果,比如 NeverScrollableScrollPhysics() 禁止手动滑动。 |
dragStartBehavior |
DragStartBehavior |
手势拖拽行为,默认 DragStartBehavior.start 。 |
TabController、TabBar和TabBarView三者的关系
创建阶段
自定义的TabController
_controller = TabController(length: widget.length, vsync: this);
创建一个TabController
TabBar:添加监听器
一个负责“过程中的流畅动画同步”(animation listener),
一个负责“最终状态的确认和 UI 更新”(controller listener)。
_controller!.addListener
const TabBar({
super.key,
required this.tabs,
this.controller,
/// 省略部分代码
this.indicatorAnimation,
}) : _isPrimary = true,
assert(indicator != null || (indicatorWeight > 0.0));
@override
State<TabBar> createState() => _TabBarState();
class _TabBarState extends State<TabBar> {
// didChangeDependencies 生命周期的方法
@override
void didChangeDependencies() {
super.didChangeDependencies();
assert(debugCheckHasMaterial(context));
_updateTabController();
_initIndicatorPainter();
}
// didUpdateWidget 生命周期的方法
@override
void didUpdateWidget(TabBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
_updateTabController();
_initIndicatorPainter();
// 在 _updateTabController 方法里增加监听器
void _updateTabController() {
_controller = newController;
if (_controller != null) {
// 给controller增加监听器
_controller!.animation!.addListener(_handleTabControllerAnimationTick);
_controller!.addListener(_handleTabControllerTick); // 给controller增加监听器
_currentIndex = _controller!.index;
}
didChangeDependencies()
:initState
后、依赖变化时触发,发生在**build()
**前面
didUpdateWidget()
:父组件重建并传入新配置时,发生在**build()
**后面
animation.addListener
→ 每一帧动画变化时都会触发 _handleTabControllerAnimationTick
。
addListener
→ 只要 TabController
的 notifyListeners()
被调用(index 改变或动画结束)时,会触发 _handleTabControllerTick
。
_handleTabControllerAnimationTick
void _handleTabControllerAnimationTick() {
assert(mounted);
if (!_controller!.indexIsChanging && widget.isScrollable) {
// Sync the TabBar's scroll position with the TabBarView's PageView.
// 同步 TabBar 的滚动位置,跟随 TabBarView 的 PageView
_currentIndex = _controller!.index;
_scrollToControllerValue();
}
触发时机: controller.animation
的值变化时(比如用户用手指滑动 TabBarView 的 PageView,PageController 会同步 offset 给 TabController)。
作用:
- 如果当前不是点击触发的切换(
indexIsChanging == false
,即用户在滑动 PageView), - 且 TabBar 是 可滚动的,
- 那么根据
controller.animation.value
来移动 TabBar 的滚动位置(调用_scrollToControllerValue()
)。
效果:保证 TabBar 的标签按钮(以及指示器)在 PageView 滑动时跟随同步移动。
_handleTabControllerTick
void _handleTabControllerTick() {
if (_controller!.index != _currentIndex) {
_currentIndex = _controller!.index;
if (widget.isScrollable) {
_scrollToCurrentIndex();
}
}
setState(() {
// Rebuild the tabs after a (potentially animated) index change
// has completed.
// 触发重建,刷新 Tab 的选中状态
});
}
触发时机: controller.notifyListeners()
被调用时(比如:controller.animateTo()
动画结束后,或者立即切换 index 时)。
作用:
- 检查当前的 index 是否发生变化,如果变了就更新
_currentIndex
。 - 如果是可滚动 TabBar,则自动滚动到选中 Tab 的位置(调用
_scrollToCurrentIndex()
)。 - 调用
setState()
触发 TabBar 重建,用于更新 Tab 样式(高亮、指示器位置等)。
效果:保证 TabBar 在 index 确认切换后,UI 状态(选中 Tab、文字样式、指示器位置)和滚动位置正确更新。
TabBarView
_controller!.addListener
class TabBarView extends StatefulWidget {
/// Creates a page view with one child per tab.
///
/// The length of [children] must be the same as the [controller]'s length.
const TabBarView({
super.key,
required this.children,
this.controller,
this.physics,
this.dragStartBehavior = DragStartBehavior.start,
this.viewportFraction = 1.0,
this.clipBehavior = Clip.hardEdge,
});
@override
State<TabBarView> createState() => _TabBarViewState();
class _TabBarViewState extends State<TabBarView> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateTabController();
_currentIndex = _controller!.index;
@override
void didUpdateWidget(TabBarView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
_updateTabController();
_currentIndex = _controller!.index;
_jumpToPage(_currentIndex!);
}
// 如果发现是新控制器,就先移除旧监听、再把新监听加到 controller.animation上:
void _updateTabController() {
if (_controllerIsValid) {
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
}
_controller = newController;
if (_controller != null) {
// 添加监听器
_controller!.animation!.addListener(_handleTabControllerAnimationTick);
}
}
didChangeDependencies()
:initState
后、依赖变化时触发,发生在**build()
**前面
didUpdateWidget()
:父组件重建并传入新配置时,发生在**build()
**后面
_handleTabControllerAnimationTick
void _handleTabControllerAnimationTick() {
if (_scrollUnderwayCount > 0 || !_controller!.indexIsChanging) {
return;
} // This widget is driving the controller's animation.
if (_controller!.index != _currentIndex) {
_currentIndex = _controller!.index;
_warpToCurrentIndex();
}
}
if (_scrollUnderwayCount > 0 || !_controller!.indexIsChanging)
_scrollUnderwayCount > 0
:表示用户正在用手指拖拽 PageView,这时 PageView 自己驱动切换,就不用再响应 controller 的动画了,否则会冲突。!_controller!.indexIsChanging
:表示当前 TabController 没有处于「点击 Tab 切换」触发的动画状态(即可能是用户在拖动 PageView 时改变 animation 值)。这种情况不由 TabBarView 主动处理。如果是上面这两种情况,直接 return,不做任何事。
if (_controller!.index != _currentIndex)
当 TabController 的 index 改变,并且是通过
animateTo()
触发的动画切换时,进入逻辑。_currentIndex = _controller!.index;
:更新_currentIndex
,让 TabBarView 内部知道现在要切换到哪个页面。调用
_warpToCurrentIndex()
,这是一个内部方法,用来驱动 PageController 立即跳转到对应页面,或者带动画切换。
TabBar → TabController→TabBar&TabBarView
TabBar → TabController:点击 Tab 的过程
TabController→TabBar&TabBarView:更新页面
用户点击了某个
Tab
,触发了InkWell.onTap
:onTap: () { _handleTap(index); }
_handleTap
内部调用:_controller!.animateTo(index);
animateTo
最终走到:void _changeIndex(int value, {Duration? duration, Curve? curve}) { if (value == _index || length < 2) return; _previousIndex = index; // 记录切换前的 index _index = value; // 更新当前 index if (duration != null && duration > Duration.zero) { _indexIsChangingCount += 1; notifyListeners(); // 通知所有监听 TabController 的 widget(TabBar、TabBarView) // 让 TabBar 的下划线动画 和 TabBarView 的 PageView 切换动画开始运行。 _animationController! .animateTo( _index.toDouble(), duration: duration, curve: curve!, ) .whenCompleteOrCancel(() { _indexIsChangingCount -= 1; notifyListeners(); // 动画完成后再次通知 }); } else { _indexIsChangingCount += 1; _animationController!.value = _index.toDouble(); _indexIsChangingCount -= 1; notifyListeners(); } }
关键点:
notifyListeners()
通知所有监听TabController
的组件,包括 TabBar 和 TabBarView。TabBar
收到通知,调用_handleTabControllerTick()
,更新UI。重建后,TabBar
会根据controller.index
来决定高亮哪个 Tab,并且_indicatorPainter
根据动画值绘制下划线。TabBarView
收到通知,调用_handleTabControllerTick()
,更新UI。。重建后,TabBarView
会根据controller.index
来决定高亮哪个 页面。