Flutter控件之Tab选项卡封装

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

Flutter控件之Tab选项卡封装_flutter选项卡

在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 tab_02

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


今天的内容大概如下:


1、封装效果一览

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

3、源码和具体使用

4、相关总结


一、封装效果一览

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


Flutter控件之Tab选项卡封装_flutter选项卡_03

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

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

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

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

属性

类型

概述

tabTitleList

List<String>

tab指示器的标题集合,文字形式

tabImageList

List<String>

tab指示器的标题集合,图片形式

tabWidgetList

List<Widget>

tab指示器的标题集合,Widget形式

tabIconAndTextList

List<TabBarBean>

tab指示器的标题集合,左图右文形式

tabBodyList

List<Widget>

tab指示器对应的页面

onPageChange

Function(int)

页面滑动回调

indicatorColor

Color

指示器的颜色

labelColor

Color

标签的颜色

unselectedLabelColor

Color

未选中标签的颜色

indicatorSize

TabBarIndicatorSize

指示器的大小 是和文字宽度一样还是充满

indicatorHeight

double

indicatorHeight

isScrollable

bool

指示器是否支持滑动

tabImageWidth

double

图片指示器的宽 仅用于图片指示器和图文指示器

tabImageHeight

double

图片指示器的高 仅用于图片指示器和图文指示器

tabIconAndTextMargin

double

左图右文指示器,icon距离文字的距离

tabHeight

double

tab高度

三、源码和具体使用

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




上一篇:Java对象与XML互相转换(xstream)


下一篇:Unity项目性能优化列表