TabBar详解

Flutter中提供了许多方便的UI控件,供我们进行快速的开发,本篇将对TabBar进行介绍学习。

 

简介

 

TabBar在应用中是比较常见的一个控件,通常是配合TabBarView。TabBa作导航栏,TabBarView作导航栏当前所对应的内容区。来看一下TabBar配合TabBarView使用的效果:

 

F8CE5F8885472A373E3CCF6D5ABD7600.gif

 

 

使用

 

TabBar的参数:

 

const TabBar({
    Key key,
    @required this.tabs,//子标签
    this.controller,//控制器
    this.isScrollable = false,//能否滑动,false:tab宽度则等比,true:tab宽度则包裹item
    this.indicatorColor,//指示器颜色
    this.indicatorWeight = 2.0,
    this.indicatorPadding = EdgeInsets.zero,
    this.indicator,
    this.indicatorSize,//TabBarIndicatorSize.label:indicator与文字同宽,TabBarIndicatorSize.tab:与tab同宽
    this.labelColor,//选中标签颜色
    this.labelStyle,//选中标签样式
    this.labelPadding,
    this.unselectedLabelColor,//未选中标签颜色
    this.unselectedLabelStyle,
    this.dragStartBehavior = DragStartBehavior.down,
    this.onTap,//点击事件
  })

 

TabBar中主要有两个参数:

  • tabs:tabs属性接受一个Widget数组,表示每一个Tab子菜单,我们可以自定义,也可以像示例中一样直接使用Tab 组件,它是Material组件库提供的Material风格的Tab菜单。Tab组件有三个可选参数,除了可以指定文字外,还可以指定Tab菜单图标,或者直接自定义组件样式。

 

controller:用于控制/监听Tab菜单切换,作用于TabBar和TabBarView,负责两者之间联动。TabBar与TabBarView通过index有一一对应关系,并且默认提供了DefaultTabController建立两者之间的关系,若要切换动画以及监听切换交互,可以自定义一个 Controller。

 

TabBarView的参数:

 

const TabBarView({
    Key key,
    @required this.children,
    this.controller,
    this.physics,
    this.dragStartBehavior = DragStartBehavior.start,
  });

 

这里可以看到TabBarView中也有controller参数,即上述所说通过Controller来控制二者之间联动交互。

 

这里有几个注意事项:

 

  1. 页面必须继承StatefulWidget
  2. 页面必须实现SingleTickerProviderStateMixin
  3. 页面初始化时,实例化TabController
  4. 在TabBar组件中指定controller为我们实例化的TabController
  5. 在TabBarView组件中指定controller为我们实例化的TabController

 

接下来我们看下具体在项目中是如何使用的:

 

添加Tab

List<Tab> titleTabs = = <Tab>[
      Tab(text: '首页',
          icon: new Icon(Icons.android)
      ),
      Tab(text: '社群',
          icon: new Icon(Icons.home)
      ),
      Tab(text: '财务',
          icon: new Icon(Icons.accessibility)
      ),
    ];

定义controller

TabController tabcontroller = TabController(
        length: 3,   //Tab页的个数
        vsync: this //动画效果的异步处理,默认格式
    );

完成布局

 

class HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  //Tab页的控制器,可以用来定义Tab标签和内容页的坐标
  TabController tabcontroller;
  List<Tab> titleTabs;

  //生命周期方法插入渲染树时调用,只调用一次
  @override
  void initState() {
    super.initState();
    tabcontroller = TabController(
        length: 3,   //Tab页的个数
        vsync: this //动画效果的异步处理,默认格式
    );

    titleTabs = <Tab>[
      Tab(text: '首页',
          icon: new Icon(Icons.android)
      ),
      Tab(text: '社群',
          icon: new Icon(Icons.home)
      ),
      Tab(text: '财务',
          icon: new Icon(Icons.accessibility)
      ),
    ];
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading:false,
        elevation: 1,
        title: Text("首页",
            style: TextStyle(color: Color.fromARGB(255, 51, 51, 51))),
        backgroundColor: Colors.white,
      ),
      body: Center(
        child: TabBarView(
            controller: tabcontroller,
            children: [
                Center(child:Text('view1')),
                Center(child:Text('view2')),
                Center(child:Text('view3')),
            ]),
      ),
      bottomNavigationBar: Material(
        color: Colors.white,
        child: TabBar(
          controller: tabcontroller,
          tabs: titleTabs,
          //tab被选中时的颜色,设置之后选中的时候,icon和text都会变色
          labelColor: Color.fromARGB(255, 163, 117, 136),
          //tab未被选中时的颜色,设置之后选中的时候,icon和text都会变色
          unselectedLabelColor: Colors.black,
        ),
      ),
    );
  }

  //组件即将销毁时调用
  @override
  void dispose() {
    //释放内存,节省开销
    tabcontroller.dispose();
    super.dispose();
  }
}

 

这里注意在组件销毁的时候记得释放tabcontroller。

 

原理

 

我们通过分析Tabcontroller是如何协调TabBar和TabBarView联动这条线来深入的学习一下TabBar。

 

首先看一下TabBar内部实现:

 

@override
  Widget build(BuildContext context) {
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
    if (_controller.length == 0) {
      return Container(
        height: _kTabHeight + widget.indicatorWeight,
      );
    }

    final TabBarTheme tabBarTheme = TabBarTheme.of(context);

    final List<Widget> wrappedTabs = List<Widget>(widget.tabs.length);
    for (int i = 0; i < widget.tabs.length; i += 1) {
      wrappedTabs[i] = Center(
        heightFactor: 1.0,
        child: Padding(
          padding: widget.labelPadding ?? tabBarTheme.labelPadding ?? kTabLabelPadding,
          child: KeyedSubtree(
            key: _tabKeys[i],
            child: widget.tabs[i],
          ),
        ),
      );

    }

    if (_controller != null) {
      final int previousIndex = _controller.previousIndex;

      if (_controller.indexIsChanging) {
        final Animation<double> animation = _ChangeAnimation(_controller);
        wrappedTabs[_currentIndex] = _buildStyledTab(wrappedTabs[_currentIndex], true, animation);
        wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation);
      } else {
        final int tabIndex = _currentIndex;
        final Animation<double> centerAnimation = _DragAnimation(_controller, tabIndex);
        wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation);
        if (_currentIndex > 0) {
          final int tabIndex = _currentIndex - 1;
          final Animation<double> previousAnimation = ReverseAnimation(_DragAnimation(_controller, tabIndex));
          wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, previousAnimation);
        }
        if (_currentIndex < widget.tabs.length - 1) {
          final int tabIndex = _currentIndex + 1;
          final Animation<double> nextAnimation = ReverseAnimation(_DragAnimation(_controller, tabIndex));
          wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, nextAnimation);
        }
      }
    }
    
    final int tabCount = widget.tabs.length;
    for (int index = 0; index < tabCount; index += 1) {
      wrappedTabs[index] = InkWell(
        onTap: () { _handleTap(index); },
        child: Padding(
          padding: EdgeInsets.only(bottom: widget.indicatorWeight),
          child: Stack(
            children: <Widget>[
              wrappedTabs[index],
              Semantics(
                selected: index == _currentIndex,
                label: localizations.tabLabel(tabIndex: index + 1, tabCount: tabCount),
              ),
            ],
          ),
        ),
      );
      if (!widget.isScrollable)
        wrappedTabs[index] = Expanded(child: wrappedTabs[index]);
    }

    Widget tabBar = CustomPaint(
      painter: _indicatorPainter,
      child: _TabStyle(
        animation: kAlwaysDismissedAnimation,
        selected: false,
        labelColor: widget.labelColor,
        unselectedLabelColor: widget.unselectedLabelColor,
        labelStyle: widget.labelStyle,
        unselectedLabelStyle: widget.unselectedLabelStyle,
        child: _TabLabelBar(
          onPerformLayout: _saveTabOffsets,
          children: wrappedTabs,
        ),
      ),
    );

    if (widget.isScrollable) {
      _scrollController ??= _TabBarScrollController(this);
      tabBar = SingleChildScrollView(
        dragStartBehavior: widget.dragStartBehavior,
        scrollDirection: Axis.horizontal,
        controller: _scrollController,
        child: tabBar,
      );
    }

    return tabBar;
  }

 

这里主要看下onTap()点击事件,它执行了_handleTap(index);来看下_handleTap方法:

 

 void _handleTap(int index) {
    assert(index >= 0 && index < widget.tabs.length);
    _controller.animateTo(index);
    if (widget.onTap != null) {
      widget.onTap(index);
    }
  }

 

该方法实现比较简单,可以看到controller的身影,它执行了animateTo方法,继续看animateTo实现:

 

void animateTo(int value, { Duration duration = kTabScrollDuration, Curve curve = Curves.ease }) {
    _changeIndex(value, duration: duration, curve: curve);
  }

 

这里可以看到controller是通过_changeIndex方法来进行对TabBar及TabBarView的控制:

 

void _changeIndex(int value, { Duration duration, Curve curve }) {
    if (value == _index || length < 2)
      return;
    _previousIndex = index;
    _index = value;
    if (duration != null) {
      _indexIsChangingCount += 1;
      notifyListeners(); // Because the value of indexIsChanging may have changed.
      _animationController
        .animateTo(_index.toDouble(), duration: duration, curve: curve)
        .whenCompleteOrCancel(() {
          _indexIsChangingCount -= 1;
          notifyListeners();
        });
    } else {
      _indexIsChangingCount += 1;
      _animationController.value = _index.toDouble();
      _indexIsChangingCount -= 1;
      notifyListeners();
    }
  }

 

更新index,并通知TabBar及TabBarView注册的listener进行页面切换,流程还是比较简单清晰的。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
AxcAE_TabBar language Build Status MIT License Platform CocoaPods Axc AxcAE_TabBar 简介: AxcAE_TabBar,以开放为封装核心的TabBar组件,尽量将属性、API等参数全部开放给使用者去自定义,能够方便快速使用的一个TabBar选项卡组件 框架支持 最低支持:iOS 8.0 IDE:Xcode 9.0 及以上版本 (由于适配iPhone X使用iOS11api,所以请使用Xcode 9.0及以上版本) 使用/安装 第一种:手动 1.找到包含: AxcAE_TabBar.h.m、 AxcAE_TabBarBadge.h.m、 AxcAE_TabBarItem.h.m、 AxcAE_TabBarDefine.h 的AxcAE_TabBar文件夹; 2.直接把AxcAE_TabBar文件夹拖入到您的工程中; 3.导入 "AxcAE_TabBar.h" 第二种:Cocoapods 1.在Podfile 中添加 pod 'AxcAE_TabBar' 2.执行 pod setup 3.执行 pod install 或 pod update 4.导入 #import <AxcAE_TabBar/AxcAE_TabBar.h> 功能介绍 支持横竖屏 (已适配iPhone X) 支持自定义Item背景图 支持自定义Item图标的自定义渲染颜色(tingColor,某种情况再不需要两套TabBar图标啦) 支持自定义Item的触发动画(预设有弹簧、放大缩小、渐变) 支持自定义Item的内部布局 支持自定义Item的内部组件(组件全开放指针,可以直接外部操作属性,如创建之初就能定义个别Item的字体等) 支持自定义Item的内部组件大小 支持自定义Item的内部组件的相关属性(点语法可能会有点长,比如item.iconImageView.ContentMode = ...) 支持自定义TabBar的背景图 支持自定义TabBar的背景图的模糊毛玻璃遮罩 支持自定义TabBar上Item相对在各自单元格内的排布方式以及对齐方式 支持自定义TabBar上Item小气泡(徽标)的左中右排布 支持自定义TabBar的凸起按钮 支持自定义TabBar的凸起按钮触发事件,包括能切换视图 支持自定义TabBar的凸起按钮的位置,只要你想,凸起按钮也可以不一定在中间 支持自定义TabBar的多重凸起按钮,如果遇到奇葩多个凸起按钮的需求,别慌 支持自定义TabBar的多重复合凸起按钮,有圆有方怎么办,循环遍历特殊对待(还能再奇葩么) 支持自定义TabBar的Item自定义大小等 支持TabBar中控制器可获取对应Item的方式 支持TabBar与系统TabBar隐藏的同步 支持TabBar在Push的时候与系统同步Hidden的效果(因为父视图就是系统的TabBar) 支持并不依赖其他三方库,适配由自行计算Frame GitHub:https://github.com/axclogo/AxcAE_TabBar
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值