flutter 封装一个 tab

flutter 封装一个 tab

flutter版本:3.35.4

demo 链接:

text 复制代码
https://github.com/yhtqw/FrontEndDemo/tree/main/flutter_demo/lib/pages/customize_tab

在flutter中使用 TabBar + TabBarView 就能很好的实现,为了使用的便捷性和通用性,所以进行一些封装。

需要看最终效果和最终的封装代码,直接拉到最底部即可~

简单的使用一下,对一些基本的结构限制做出解释。

dart 复制代码
import 'package:flutter/material.dart';

class Page19 extends StatefulWidget {
  const Page19({super.key});

  @override
  State<Page19> createState() => _Page19State();
}

class _Page19State extends State<Page19> with SingleTickerProviderStateMixin {
  // 必须,使用TabController作为桥梁连接TabBar和TabBarView
  late TabController _controller;

  @override
  void initState() {
    super.initState();

    _controller = TabController(
      length: 2,
      vsync: this,
      initialIndex: 0,
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Column(
        children: [
          const SizedBox(height: 100,),

          TabBar(
            controller: _controller,
            // tab bar选项,数量得和下面一致
            tabs: const [
              Text('1111'),
              Text('2222'),
            ],
          ),

          // 使用TabBarView时,必须指定容器的限制范围。
          // 这里直接撑满剩余空间
          Expanded(
            child: TabBarView(
              controller: _controller,
              // tab bar每个选项对应的tab页面,数量得很上面一致
              children: const [
                Text('1'),
                Text('2'),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

效果如下:

简单的场景当然也可以不使用TabController,但需要DefaultTabController包裹,这样无需手动创建控制器自动就关联上了,大致使用如下:

dart 复制代码
// ... 其他省略
class _Page19State extends State<Page19> with SingleTickerProviderStateMixin {
  @override
  Widget build(BuildContext context) {
    return const DefaultTabController(
      // 数量得和下面的一致
      length: 2,
      child: Scaffold(
        backgroundColor: Colors.white,
        body: Column(
          children: [
            SizedBox(height: 100,),

            TabBar(
              tabs: [
                Text('1111'),
                Text('2222'),
              ],
            ),

            Expanded(
              child: TabBarView(
                children: [
                  Text('1'),
                  Text('2'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

效果和上面一致。

从上面可以看出 flutter 已经实现了滑动的动画效果,我们只需要对结构样式进行封装,下面我们去体验一下其他的属性,为封装做准备。

dart 复制代码
// 其他省略...
class _Page19State extends State<Page19> with SingleTickerProviderStateMixin {
  late TabController _controller;

  @override
  void initState() {
    super.initState();

    _controller = TabController(
      length: 3,
      vsync: this,
      initialIndex: 0,
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Column(
        children: [
          const SizedBox(height: 100,),

          TabBar(
            controller: _controller,
            // 注意:选项文本颜色就通过下面设置,不通过选项自身的TextStyle来设置
            // tab bar 选项文本选中时的颜色
            labelColor: Colors.blue,
            // tab bar 选项文本未选中时的颜色
            unselectedLabelColor: Colors.black,

            // 选中指示器相关的属性
            // 设置tab bar选中指示器的颜色
            indicatorColor: Colors.blue,
            // 设置tab bar选中指示器的大小,默认和选项的文本宽度一致。
            // 这里设置的指示器宽度和选项的宽度一致
            // 这样设置后切换tab的时候蠕虫蠕动效果就没有了
            indicatorSize: TabBarIndicatorSize.tab,
            // 设置tab bar选中指示器的高度
            indicatorWeight: 10,

            // 移除tab bar选项点击时的高亮和水波纹反馈效果
            overlayColor: WidgetStateProperty.all(Colors.transparent),

            // 设置tab bar选项的内边距
            // 在indicatorSize设置为label的时候再设置这个值比较好
            // labelPadding: const EdgeInsets.symmetric(horizontal: 40),

            // 设置tab bar底部的那条分割线高度,也可以通过dividerColor设置其颜色
            dividerHeight: 0,

            tabs: ['1111','2222','3333'].map(
              (item) => Text(item,),
            ).toList(),
          ),

          Expanded(
            child: TabBarView(
              controller: _controller,
              children: ['1','2','3'].map(
                (item) =>Text(item,),
              ).toList(),
            ),
          ),
        ],
      ),
    );
  }
}

效果如下:

可以看到,上面我们使用了许多选中和选中指示器的效果,也是在tab bar个数少的时候展示的效果,接下来我们加几个再看看效果:

可以看到选项被挤压,这个时候我们就可以通过 TabBar 的 isScrollable 属性来设置是否可以滚动:

dart 复制代码
// 其他省略...
TabBar(
  // 其他省略...

  // 设置tab bar选项个数过多超过限制的宽度的时候,是否允许滚动
  isScrollable: true,
),
// 其他省略...

我们又发现了允许滚动后,第一个的排列距离容器左边有一些边距,如果想让选项在最左边开始显示,就需要设置tabAlignment属性:

dart 复制代码
// 其他省略...
TabBar(
  // 其他省略...

  // 设置tab bar选项个数过多超过限制的宽度的时候,是否允许滚动
  isScrollable: true,
  // 设置tab bar选项的显示位置
  tabAlignment: TabAlignment.start,
),
// 其他省略...

效果如下:

关于TabBar的一些常用的属性就体验得差不多了,如果我们要设置整个TabBar容器的样式,例如背景色,圆角,高度等属性时,就需要对TabBar进行包裹,使用外层包裹部件来设置样式:

dart 复制代码
// 其他省略...
Container(
  height: 60,
  padding: const EdgeInsets.symmetric(horizontal: 20),
  decoration: const BoxDecoration(
    color: Colors.grey,
    borderRadius: BorderRadius.only(
      topLeft: Radius.circular(20),
      topRight: Radius.circular(20),
    )
  ),
  child: TabBar(
    // 其他省略...
  ),
),
// 其他省略...

效果如下:

这下关于TabBar的样式就设置的差不多了,那么关于TabBarView的样式就自行通过传染的children部件自行封装实现。

我们从上面的效果中可以看到tab bar选中样式通过属性去自定义可能有些不好看,如果我们想用让指示器为整个背景,并且设置部分样式效果,我们就需要自定义TabBar的indicator属性。

可以看到,如果我们要自定义指示器,需要实现Decoration,并且如果设置了indicator属性,则会忽略indicatorColor和indicatorWeight属性。

为了实现自定义的indicator,我们需要继承Decoration去实现绘制的方法,接下来我们就去实现一个简单的圆角矩形样式(这里不深入展开绘制相关的知识,后面有时间再单独写绘制相关的知识):

dart 复制代码
import 'package:flutter/material.dart';

class CustomizeTabIndicator extends Decoration {
  const CustomizeTabIndicator({
    required this.color,
    this.radius = BorderRadius.zero,
  });

  /// 指示器颜色
  final Color color;
  /// 指示器的圆角属性
  final BorderRadius radius;

  // 需要重写绘制方法
  @override
  BoxPainter createBoxPainter([VoidCallback? onChanged]) =>
      _RoundedPainter(this, onChanged);
}

// 自定义绘制方法
class _RoundedPainter extends BoxPainter {
  _RoundedPainter(this.decoration, VoidCallback? onChanged) : super(onChanged);

  final CustomizeTabIndicator decoration;

  // 重写绘制的方法,这个方法会传给我们绘制区域的信息
  // 我们利用这些信息就可以实现自定义的绘制
  @override
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    // 获取绘制区域的大小,整个变换过程中都会更新
    double width = configuration.size!.width;
    double height = configuration.size!.height;
    // 获取绘制区域的偏移量(距离最边上的距离)
    Offset baseOffset = Offset(offset.dx, offset.dy,);

    // 设置要绘制的圆角矩形
    final RRect indicatorRRect = _buildRRect(
      baseOffset,
      width,
      height,
    );
    // 设置画笔属性
    final Paint paint = Paint()
      ..color = decoration.color
      ..style = PaintingStyle.fill;

    // 绘制圆角矩形
    canvas.drawRRect(indicatorRRect, paint);
  }

  /// 绘制圆角指示器
  RRect _buildRRect(
    Offset offset,
    double width,
    double height,
  ) {
    return RRect.fromRectAndCorners(
      // 圆角矩形的绘制中心
      Rect.fromCenter(
        center: Offset(
          offset.dx + width / 2,
          offset.dy + height / 2,
        ),
        width: width,
        height: height,
      ),
      topLeft: decoration.radius.topLeft,
      topRight: decoration.radius.topRight,
      bottomRight: decoration.radius.bottomRight,
      bottomLeft: decoration.radius.bottomLeft,
    );
  }
}

使用:

dart 复制代码
// 其他省略...
class _Page19State extends State<Page19> with SingleTickerProviderStateMixin {
  // 其他省略...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Column(
        children: [
          const SizedBox(height: 100,),

          Container(
            height: 60,
            width: double.infinity,
            padding: const EdgeInsets.symmetric(horizontal: 20),
            decoration: const BoxDecoration(
              color: Colors.grey,
              borderRadius: BorderRadius.only(
                topLeft: Radius.circular(20),
                topRight: Radius.circular(20),
              )
            ),
            child: TabBar(
              controller: _controller,
              // 注意:选项文本颜色就通过下面设置,不通过选项自身的TextStyle来设置
              // tab bar 选项文本选中时的颜色
              labelColor: Colors.blue,
              // tab bar 选项文本未选中时的颜色
              unselectedLabelColor: Colors.black,

              // 选中指示器相关的属性
              // 自定义指示器
              indicator: const CustomizeTabIndicator(
                color: Colors.amber,
              ),

              // 设置tab bar选项个数过多超过限制的宽度的时候,是否允许滚动
              isScrollable: true,
              // 设置tab bar选项的显示位置
              tabAlignment: TabAlignment.start,

              // 设置tab bar选项的内边距
              // 在indicatorSize设置为label的时候再设置这个值比较好
              labelPadding: const EdgeInsets.only(right: 30),

              // 设置tab bar底部的那条分割线高度,也可以通过dividerColor设置其颜色
              dividerHeight: 0,

              tabs: ['1111','2222','3333','4444','5555','6666','7777'].map(
                (item) => Padding(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 20,
                    vertical: 5,
                  ),
                  child: Text(item,)
                ),
              ).toList(),
            ),
          ),
          // 其他省略...
        ],
      ),
    );
  }
}

效果如下:

通过上面的一系列的使用,接下来封装就很明了了,属性主要分为:

  • tab bar相关
    • tab bar容器的属性
    • tab bar相关属性
  • tab views相关
  • 其他属性

接下来基于上述的开始封装:

dart 复制代码
import 'package:flutter/material.dart';

import 'customize_tab_indicator.dart';

/// 抽取一些默认的属性
BorderRadius ctDefaultIndicatorBorderRadius = BorderRadius.circular(10);

class CustomizeTab extends StatefulWidget {
  const CustomizeTab({
    super.key,
    this.tabBarHeight = kToolbarHeight,
    this.tabBarBackgroundColor,
    this.tabBarPadding,
    this.tabBarBorderRadius,
    required this.tabs,
    this.unselectedColor,
    this.selectedColor,
    this.tabBarOptionMargin = const EdgeInsets.only(right: 10),
    this.tabBarOptionPadding = EdgeInsets.zero,
    this.indicatorColor = Colors.blue,
    this.indicatorBorderRadius,
    required this.tabViews,
    this.initialIndex,
    this.onChangeTabIndex,
  });

  /// tab bar 容器的高度,默认为AppBar工具栏组件的高度
  final double tabBarHeight;
  /// tab bar 容器的背景颜色
  final Color? tabBarBackgroundColor;
  /// tab bar 容器的内边距
  final EdgeInsetsGeometry? tabBarPadding;
  /// tab bar 容器的圆角属性
  final BorderRadiusGeometry? tabBarBorderRadius;
  /// tab 选项
  final List<Widget> tabs;
  /// tab 选项未选中时的颜色
  final Color? unselectedColor;
  /// tab 选项选中时的颜色
  final Color? selectedColor;
  /// tab bar 选项每项的margin
  final EdgeInsetsGeometry tabBarOptionMargin;
  /// tab bar 选项每项的padding(因为大概率每项的padding是一致的,所以进行抽取)
  final EdgeInsetsGeometry tabBarOptionPadding;
  /// 指示器的颜色
  final Color indicatorColor;
  /// 指示器的圆角属性
  final BorderRadius? indicatorBorderRadius;
  /// tab 页面
  final List<Widget> tabViews;
  /// 初始化显示tab的索引
  final int? initialIndex;
  /// 当tab索引发生改变时的回调函数
  final Function(int)? onChangeTabIndex;

  @override
  State<CustomizeTab> createState() => _CustomizeTabState();
}

class _CustomizeTabState extends State<CustomizeTab> with SingleTickerProviderStateMixin {
  late TabController _controller;

  @override
  void initState() {
    super.initState();

    _controller = TabController(
      length: widget.tabs.length,
      vsync: this,
      initialIndex: widget.initialIndex ?? 0,
    )..addListener(() {
      // indexIsChanging主要作用就是标识TabController是否正处于索引切换过程中。
      // 点击切换,在执行动画期间为true,用户手势操作结束后且动画完成为false
      // 滑动切换为false
      // 使用indexIsChanging来判断当前tab变化是否完成,完成了就执行回调
      if (!_controller.indexIsChanging) {
        widget.onChangeTabIndex?.call(_controller.index);
      }
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  /// 对每个tab加内边距
  Widget _buildTab(Widget tab) {
    return Padding(
      padding: widget.tabBarOptionPadding,
      child: tab,
    );
  }

  @override
  Widget build(BuildContext context) {
    // 通过父容器的约束动态构建子部件
    return LayoutBuilder(
      builder: (_, BoxConstraints boxConstraints) => Column(
        children: [
          Container(
            width: double.infinity,
            height: widget.tabBarHeight,
            padding: widget.tabBarPadding,
            decoration: BoxDecoration(
              color: widget.tabBarBackgroundColor,
              borderRadius: widget.tabBarBorderRadius,
            ),
            child: TabBar(
              controller: _controller,
              indicator: CustomizeTabIndicator(
                color: widget.indicatorColor,
                radius: widget.indicatorBorderRadius ??
                    ctDefaultIndicatorBorderRadius,
              ),
              isScrollable: true,
              dividerHeight: 0,
              labelPadding: widget.tabBarOptionMargin,
              tabAlignment: TabAlignment.start,
              unselectedLabelColor: widget.unselectedColor,
              labelColor: widget.selectedColor,
              tabs: widget.tabs.map((tab) => _buildTab(tab)).toList(),
            ),
          ),

          // 因为TabBarView必须要约束
          // 所以定义它的高度就为外界的约束高度减去TabBar的高度
          SizedBox(
            width: double.infinity,
            height: boxConstraints.maxHeight - widget.tabBarHeight,
            child: TabBarView(
              controller: _controller,
              children: widget.tabViews,
            ),
          ),
        ],
      ),
    );
  }
}

使用:

dart 复制代码
import 'package:flutter/material.dart';

import '../widgets/page19/customize_tab.dart';

class Page19 extends StatefulWidget {
  const Page19({super.key});

  @override
  State<Page19> createState() => _Page19State();
}

class _Page19State extends State<Page19> with SingleTickerProviderStateMixin {
  Widget _buildTab(String txt) {
    return Text(
      txt,
      style: const TextStyle(
        fontSize: 14,
      ),
    );
  }

  Widget _buildTabView(String txt) {
    return Container(
      padding: const EdgeInsets.all(20),
      color: Colors.grey,
      child: Text(txt),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Column(
        children: [
          const SizedBox(height: 100,),

          Expanded(
            child: CustomizeTab(
              tabBarBackgroundColor: Colors.amber,
              tabBarPadding: const EdgeInsets.symmetric(
                horizontal: 20,
              ),
              tabBarBorderRadius: const BorderRadius.only(
                topRight: Radius.circular(20),
                topLeft: Radius.circular(20),
              ),
              unselectedColor: Colors.black,
              selectedColor: Colors.white,
              tabBarOptionPadding: const EdgeInsets.symmetric(
                horizontal: 15,
                vertical: 5,
              ),
              indicatorColor: Colors.orange,
              onChangeTabIndex: (index) {
                print('当前的索引为:$index');
              },
              tabs: ['tab1', 'tab2', 'tab3', 'tab4', 'tab5'].map(
                (item) => _buildTab(item),
              ).toList(),
              tabViews: ['1', '2', '3', '4', '5'].map(
                (item) => _buildTabView(item),
              ).toList(),
            ),
          ),
        ],
      ),
    );
  }
}

效果如下:

上面我们就完成了一个简单的tab封装,但是目前还不算很通用,例如我想要TabBar在下面,TabView在上面,这个就不能直接使用了,得更改内部代码,所以继续继续封装,允许用户自定义方向。

接下来进行TabBar位置的分析:

  1. 在顶部(top)

不用具体分析了,因为上面就是基于在顶部封装的。

  1. 在底部(bottom)

与在顶部相比,只是结构倒序一下就行了。

  1. 在左边(left)

在左边,结构从上下结构(Column)变成左右结构(Row),而且TabBar不支持直接转成上下结构,所以只有另辟蹊径。从左右结构变成上下结构旋转90度也能实现效果,基于此在做调整,让整个容器旋转90度的时候,TabBar的选项则也被旋转了90度,从视觉效果上看就是倒置的,大致效果如下:

所以我们对每个子项向反方向旋转90度即可还原显示。

对于TabView,原先的交互方式为左右滑动切换,现在变成左右结构了,那么交互方式得从左右变成上下,TabView不支持直接变成上下滑动,所以有两种方式解决:

一种是使用PageView,这个天生就支持上下滑动,但是如果使用这个,得维护PageController,同步TabBar的切换,有兴趣的可自行试一下。

另一种方式依然是旋转,我们将其旋转90度后,自然而然滑动手势从左右变成了上下,不过需要注意的是,子项依然要反方向旋转90度还原。

  1. 在右边(right)

与在左边相比,只是结构倒序一下就行了。

简单的分析了如何实现,那么接下来就开始编写:

dart 复制代码
import 'package:flutter/material.dart';

import 'customize_tab_indicator.dart';

/// tab bar 位置的枚举
enum TabBarPosition { top, bottom, left, right }

/// 抽取一些默认的属性
BorderRadius ctDefaultIndicatorBorderRadius = BorderRadius.circular(10);

class CustomizeTab extends StatefulWidget {
  const CustomizeTab({
    super.key,
    this.tabBarHeight = kToolbarHeight,
    this.tabBarBackgroundColor,
    this.tabBarPadding = EdgeInsets.zero,
    this.tabBarBorderRadius,
    required this.tabs,
    this.unselectedColor,
    this.selectedColor,
    this.tabBarOptionMargin = const EdgeInsets.only(right: 10),
    this.tabBarOptionPadding = EdgeInsets.zero,
    this.indicatorColor = Colors.blue,
    this.indicatorBorderRadius,
    required this.tabViews,
    this.initialIndex,
    this.onChangeTabIndex,
    this.position = TabBarPosition.top,
  });

  /// tab bar 容器的高度,默认为AppBar工具栏组件的高度
  final double tabBarHeight;
  /// tab bar 容器的背景颜色
  final Color? tabBarBackgroundColor;
  /// tab bar 容器的内边距
  final EdgeInsetsGeometry tabBarPadding;
  /// tab bar 容器的圆角属性
  final BorderRadiusGeometry? tabBarBorderRadius;
  /// tab 选项
  final List<Widget> tabs;
  /// tab 选项未选中时的颜色
  final Color? unselectedColor;
  /// tab 选项选中时的颜色
  final Color? selectedColor;
  /// tab bar 选项每项的margin
  final EdgeInsetsGeometry tabBarOptionMargin;
  /// tab bar 选项每项的padding(因为大概率每项的padding是一致的,所以进行抽取)
  final EdgeInsetsGeometry tabBarOptionPadding;
  /// 指示器的颜色
  final Color indicatorColor;
  /// 指示器的圆角属性
  final BorderRadius? indicatorBorderRadius;
  /// tab 页面
  final List<Widget> tabViews;
  /// 初始化显示tab的索引
  final int? initialIndex;
  /// 当tab索引发生改变时的回调函数
  final Function(int)? onChangeTabIndex;
  /// tab bar所在位置,默认为top
  final TabBarPosition position;

  @override
  State<CustomizeTab> createState() => _CustomizeTabState();
}

class _CustomizeTabState extends State<CustomizeTab> with SingleTickerProviderStateMixin {
  late TabController _controller;

  @override
  void initState() {
    super.initState();

    _controller = TabController(
      length: widget.tabs.length,
      vsync: this,
      initialIndex: widget.initialIndex ?? 0,
    )..addListener(() {
      // indexIsChanging主要作用就是标识TabController是否正处于索引切换过程中。
      // 点击切换,在执行动画期间为true,用户手势操作结束后且动画完成为false
      // 滑动切换为false
      // 使用indexIsChanging来判断当前tab变化是否完成,完成了就执行回调
      if (!_controller.indexIsChanging) {
        widget.onChangeTabIndex?.call(_controller.index);
      }
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  /// 对每个tab加内边距
  Widget _buildTabOption(Widget tab) {
    final Widget tabOption = Padding(
      padding: widget.tabBarOptionPadding,
      child: tab,
    );

    // 如果是left和right,因为外层的TabBar容器旋转了90度
    // 那Tab选项就旋转-90度还原,达到视觉的统一
    if (widget.position == TabBarPosition.left || widget.position == TabBarPosition.right) {
      return RotatedBox(
        quarterTurns: -1,
        child: tabOption,
      );
    } else {
      return tabOption;
    }
  }

  /// 构建TabBar
  Widget _buildTabBar() {
    final Widget tabBar = Container(
      width: double.infinity,
      height: widget.tabBarHeight,
      padding: widget.tabBarPadding,
      decoration: BoxDecoration(
        color: widget.tabBarBackgroundColor,
        borderRadius: widget.tabBarBorderRadius,
      ),
      child: TabBar(
        controller: _controller,
        indicator: CustomizeTabIndicator(
          color: widget.indicatorColor,
          radius: widget.indicatorBorderRadius
              ?? ctDefaultIndicatorBorderRadius,
        ),
        isScrollable: true,
        dividerHeight: 0,
        labelPadding: widget.tabBarOptionMargin,
        tabAlignment: TabAlignment.start,
        unselectedLabelColor: widget.unselectedColor,
        labelColor: widget.selectedColor,
        tabs: widget.tabs.map((tab) => _buildTabOption(tab)).toList(),
      ),
    );

    if (widget.position == TabBarPosition.left || widget.position == TabBarPosition.right) {
      // 如果是left和right,则旋转90度,
      return RotatedBox(
        quarterTurns: 1,
        child: tabBar,
      );
    } else {
      return tabBar;
    }
  }

  Widget _buildTabView(BoxConstraints boxConstraints) {
    final EdgeInsets tabBarPadding = widget.tabBarPadding.resolve(TextDirection.ltr);

    return widget.position == TabBarPosition.top || widget.position == TabBarPosition.bottom ? SizedBox(
      width: double.infinity,
      height: boxConstraints.maxHeight -
          widget.tabBarHeight -
          tabBarPadding.top -
          tabBarPadding.bottom,
      child: TabBarView(
        controller: _controller,
        children: widget.tabViews,
      ),
    ) : SizedBox(
      // 如果是left或者right,则宽高设置交换,并且将TabBarView旋转90度
      width: boxConstraints.maxWidth -
          widget.tabBarHeight -
          tabBarPadding.top -
          tabBarPadding.bottom,
      height: double.infinity,
      child: RotatedBox(
        quarterTurns: 1,
        child: TabBarView(
          controller: _controller,
          // 因为TabBarView旋转了90度,对应的Tab项要旋转-90度还原
          children: widget.tabViews.map((tabView) => RotatedBox(
            quarterTurns: -1,
            child: tabView,
          )).toList(),
        ),
      ),
    );
  }

  /// 构建tab
  Widget _buildTab(BoxConstraints boxConstraints) {
    List<Widget> children = [
      _buildTabBar(),
      _buildTabView(boxConstraints),
    ];

    // 如果是bottom和right,则渲染的结构会倒置
    if (widget.position == TabBarPosition.bottom
        || widget.position == TabBarPosition.right) {
      children = children.reversed.toList();
    }

    // 如果是top或者bottom,则是上下结构
    // 如果是left或者right,则是左右结构
    return widget.position == TabBarPosition.top || widget.position == TabBarPosition.bottom
        ? Column(
          children: children,
        )
        : Row(
          children: children,
        );
  }

  @override
  Widget build(BuildContext context) {
    // 通过父容器的约束动态构建子部件
    return LayoutBuilder(
      builder: (_, BoxConstraints boxConstraints) => _buildTab(boxConstraints),
    );
  }
}

完成编码后就开始测试使用:

dart 复制代码
import 'package:flutter/material.dart';

import '../widgets/page19/customize_tab.dart';

class Page19 extends StatefulWidget {
  const Page19({super.key});

  @override
  State<Page19> createState() => _Page19State();
}

class _Page19State extends State<Page19> with SingleTickerProviderStateMixin {
  // 用于动态切换tab bar的位置
  TabBarPosition _tabBarPosition = TabBarPosition.top;

  /// 构建tab项
  Widget _buildTab(String txt) {
    return Text(
      txt,
      style: const TextStyle(
        fontSize: 14,
      ),
    );
  }

  /// 构建tab view
  Widget _buildTabView(String txt) {
    return Container(
      padding: const EdgeInsets.all(20),
      color: Colors.grey,
      child: Column(
        children: [
          Container(
            width: double.infinity,
            height: 60,
            decoration: BoxDecoration(
              color: Colors.amber,
              borderRadius: BorderRadius.circular(20),
            ),
            alignment: Alignment.center,
            child: Text(txt),
          )
        ],
      ),
    );
  }

  /// 改变tab bar的位置
  void _onChangePosition(TabBarPosition tabBarPosition) {
    setState(() {
      _tabBarPosition = tabBarPosition;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Column(
        children: [
          const SizedBox(height: 100,),

          Expanded(
            child: CustomizeTab(
              tabBarHeight: 100,
              tabBarBackgroundColor: Colors.amber,
              tabBarPadding: const EdgeInsets.symmetric(
                horizontal: 20,
              ),
              unselectedColor: Colors.black,
              selectedColor: Colors.white,
              tabBarOptionPadding: const EdgeInsets.symmetric(
                horizontal: 15,
                vertical: 5,
              ),
              indicatorColor: Colors.orange,
              position: _tabBarPosition,
              onChangeTabIndex: (index) {
                print('当前的索引为:$index');
              },
              tabs: ['tab1', 'tab2', 'tab3', 'tab4', 'tab5'].map(
                (item) => _buildTab(item),
              ).toList(),
              tabViews: ['1', '2', '3', '4', '5'].map(
                (item) => _buildTabView(item),
              ).toList(),
            ),
          ),

          Container(
            height: 200,
            color: Colors.black,
            padding: const EdgeInsets.symmetric(horizontal: 20),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                ElevatedButton(
                  onPressed: () => _onChangePosition(TabBarPosition.left),
                  child: const Text('左'),
                ),
                ElevatedButton(
                  onPressed: () => _onChangePosition(TabBarPosition.right),
                  child: const Text('右'),
                ),
                ElevatedButton(
                  onPressed: () => _onChangePosition(TabBarPosition.top),
                  child: const Text('上'),
                ),
                ElevatedButton(
                  onPressed: () => _onChangePosition(TabBarPosition.bottom),
                  child: const Text('下'),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

效果如下:

至此我们就简单封装了一个tab,极大的方便了使用。

感兴趣的也可以关注我的微信公众号【前端学习小营地】,不定时会分享一些小功能~

文章到此就结束了,感谢阅读,拜拜~

相关推荐
AiFlutter3 小时前
Flutter实现手电筒亮度修改
flutter
食品一少年5 小时前
【Day7-10】开源鸿蒙之Flutter 的自定义组件封装(1)
flutter·开源·harmonyos
勇气要爆发6 小时前
【第五阶段—高级特性和架构】第六章:自定义Widget开发指南
flutter
白茶三许9 小时前
【2025】Flutter 卡片组件封装与分页功能实现:实战指南
flutter·开源·openharmony
Bervin121389 小时前
Flutter Android环境的搭建
android·flutter
fouryears_2341717 小时前
现代 Android 后台应用读取剪贴板最佳实践
android·前端·flutter·dart
等你等了那么久18 小时前
Flutter国际化语言轻松搞定
flutter·dart
神经蛙39711 天前
settings.gradle' line: 22 * What went wrong: Plugin [id: 'org.jetbrains.kotlin.a
flutter
stringwu1 天前
一个bug 引发的Dart 与 Java WeakReference 对比探讨
flutter