flutter:TabController、TabBar、Tab和TabBarView

一个自定义的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,从而可以被 TabControllerAnimationController 等需要 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 的索引。它可以:

  1. 保存当前选中的 tab 索引
  2. 提供 动画效果(切换时平滑过渡);
  3. 提供 事件监听(监听 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

TabFlutter 提供的一个 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 → 只要 TabControllernotifyListeners() 被调用(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 时)。

作用:

  1. 检查当前的 index 是否发生变化,如果变了就更新 _currentIndex
  2. 如果是可滚动 TabBar,则自动滚动到选中 Tab 的位置(调用 _scrollToCurrentIndex())。
  3. 调用 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:更新页面

  1. 用户点击了某个 Tab,触发了 InkWell.onTap

    onTap: () {
      _handleTap(index);
    }
    
  2. _handleTap 内部调用:

    _controller!.animateTo(index);
    
  3. 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 的组件,包括 TabBarTabBarView

  4. TabBar 收到通知,调用 _handleTabControllerTick(),更新UI。重建后,TabBar 会根据 controller.index 来决定高亮哪个 Tab,并且 _indicatorPainter 根据动画值绘制下划线。

    TabBarView收到通知,调用 _handleTabControllerTick(),更新UI。。重建后,TabBarView 会根据 controller.index 来决定高亮哪个 页面。

×

喜欢就点赞,疼爱就打赏