likes
comments
collection
share

Flutter控件之Tab选项卡封装

作者站长头像
站长
· 阅读数 14

我正在参加「掘金·启航计划」

Tab选项卡,这是一个非常常见且权重很高的一个组件,随便打开一个App,比如掘金,如下图,首页顶部就是一个Tab选项卡,这个功能可以说,几乎每个App都会存在。

Flutter控件之Tab选项卡封装

在Android中,我们可以使用TabLayout+ViewPager,轻松的实现一个Tab指示器+页面滑动,而在Flutter当中呢,可以很负责任的告诉大家,也是很简单的就可以实现,主要使用到了TabBar和TabBarView,举一个特别简单的例子,如下代码所示,就是非常简单的Tab选项卡+底部页面的效果。

@override
  Widget build(BuildContext context) {
    List<Widget> tabs = []; //tab指示器
    List<Widget> bodyList = []; //tab指示器下面的内容Widget
    for (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控件之Tab选项卡封装

在Flutter当中实现起来是不是也是非常的简单呢,既然已经如此的简单了,为什么我们还要再封装一层呢?说白了一是为了扩展,扩展一下系统无法满足的功能,二是为了调用起来得心应手。ok,废话不多说,开始今天的概述。

今天的内容大概如下:

1、封装效果一览

2、确定封装属性和拓展属性

3、源码和具体使用

4、相关总结

一、封装效果一览

所有的效果都是基于原生而实现的,如下图所示:

Flutter控件之Tab选项卡封装

二、确定封装属性和拓展属性

基本上封装的效果就如上图所示,要封装哪些属性,关于系统的属性,比如指示器的颜色,标签选中和未选中的颜色等等,都可以抛出去,让使用者选择性进行使用。

而需要的拓展的属性,就使得自定义的Tab更加的灵活,满足不同的实际的需求,比如,文本指示器,图片指示器,图文指示器等等,都可以灵活的添加一下。

具体的属性如下,大家在实际封装中,可以根据自身需要来动态的灵活的设置。

属性类型概述
tabTitleListList<String>tab指示器的标题集合,文字形式
tabImageListList<String>tab指示器的标题集合,图片形式
tabWidgetListList<Widget>tab指示器的标题集合,Widget形式
tabIconAndTextListList<TabBarBean>tab指示器的标题集合,左图右文形式
tabBodyListList<Widget>tab指示器对应的页面
onPageChangeFunction(int)页面滑动回调
indicatorColorColor指示器的颜色
labelColorColor标签的颜色
unselectedLabelColorColor未选中标签的颜色
indicatorSizeTabBarIndicatorSize指示器的大小 是和文字宽度一样还是充满
indicatorHeightdoubleindicatorHeight
isScrollablebool指示器是否支持滑动
tabImageWidthdouble图片指示器的宽 仅用于图片指示器和图文指示器
tabImageHeightdouble图片指示器的高 仅用于图片指示器和图文指示器
tabIconAndTextMargindouble左图右文指示器,icon距离文字的距离
tabHeightdoubletab高度

三、源码和具体使用

源码相对比较的简单,仅仅对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});

  @override
  State<VipTabBarView> createState() => _GWMTabBarViewState();
}

///左图右文的对象
class VipTabBarBean {
  String title;
  String icon;

  VipTabBarBean(this.title, this.icon);
}

class _GWMTabBarViewState extends State<VipTabBarView>
    with SingleTickerProviderStateMixin {
  // 标签控制器
  late TabController _tabController;

  @override
  void 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);
      }
    });
  }

  @override
  void dispose() {
    super.dispose();
    // 杀死控制器
    _tabController.dispose();
  }

  /*
   * 指示器点击
   */
  void onPage(position) {}

  @override
  Widget 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();
    }
    //自定义Widget
    if (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,
      ),
    );
  }
}

简单使用

传一个标题集合和页面集合就可以轻松实现了。

  @override
  Widget build(BuildContext context) {
    return const VipTabBarView(
      tabTitleList:  ["条目一", "条目二"],
      tabBodyList: [
        Text("第一个页面"),//可以是任意的Widget
        Text("第二个页面"),//可以是任意的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});

  @override
  State<TabBarPage> createState() => _TabBarPageState();
}

class _TabBarPageState extends State<TabBarPage> {
  @override
  Widget 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选项卡和底部的页面结合使用时,一定要考虑懒加载,否则,在有网络请求的时候,每次切换页面的时候,数据都会重新加载,这给用户体验是相当的不好,具体如何实现,大家可以去网上搜索搜索,有大把的文章概述,这里就不赘述了,好了铁子们,本篇文章就先到这里,希望可以帮助到大家。