You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Flutter TabBar选中标签下划线仅适配文本长度的实现问题

Flutter TabBar选中标签下划线仅适配文本长度的实现问题

我完全懂你的需求!想让TabBar里选中标签的下划线只跟着文本长度走,同时还得保证所有标签都能完整显示在屏幕上不滚动对吧?你之前用了按屏幕宽度比例分配的SizedBox来固定每个Tab的宽度,这确实会让下划线撑满整个SizedBox的宽度,和文本长度不匹配,这问题太常见了~

给你两个靠谱的解决方案,你可以按需选:


方案一:自定义Tab指示器(推荐)

这个方法不用改动你原本的Tab布局逻辑太多,只需要自定义一个Tab指示器,让它的宽度自动匹配选中Tab的文本长度,同时保留所有标签占满屏幕不滚动的效果。

步骤1:实现自定义指示器类

先写一个继承自Decoration的自定义指示器,它会自动计算Tab中文本的宽度,然后绘制对应长度的下划线:

class CustomTextUnderlineIndicator extends Decoration {
  final double thickness;
  final Color color;
  final double horizontalPadding;

  const CustomTextUnderlineIndicator({
    this.thickness = 2.0,
    this.color = Colors.blueAccent,
    this.horizontalPadding = 0.0,
  });

  @override
  BoxPainter createBoxPainter([VoidCallback? onChanged]) {
    return _CustomIndicatorPainter(
      decoration: this,
      onChanged: onChanged,
    );
  }
}

class _CustomIndicatorPainter extends BoxPainter {
  final CustomTextUnderlineIndicator decoration;

  _CustomIndicatorPainter({
    required this.decoration,
    VoidCallback? onChanged,
  }) : super(onChanged);

  @override
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    // 取出Tab里的文本组件
    final textWidget = (configuration.widget as Tab).child;
    if (textWidget is! Text) return;

    // 计算文本的实际宽度
    final textPainter = TextPainter(
      text: TextSpan(text: textWidget.data, style: textWidget.style),
      textDirection: TextDirection.ltr,
    )..layout();

    // 计算下划线的左右位置,让它对齐文本中心
    final textWidth = textPainter.width;
    final tabWidth = configuration.size!.width;
    final underlineLeft = offset.dx + (tabWidth - textWidth) / 2;
    final underlineRight = underlineLeft + textWidth;
    final underlineBottom = offset.dy + configuration.size!.height - decoration.thickness;

    // 绘制下划线
    final paint = Paint()
      ..color = decoration.color
      ..strokeWidth = decoration.thickness
      ..style = PaintingStyle.fill;

    canvas.drawRect(
      Rect.fromLTRB(
        underlineLeft + decoration.horizontalPadding,
        underlineBottom,
        underlineRight - decoration.horizontalPadding,
        underlineBottom + decoration.thickness,
      ),
      paint,
    );
  }
}

步骤2:修改你的TabBar代码

把之前给每个Tab套的固定宽度SizedBox去掉,然后在TabBar里使用我们自定义的指示器:

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  static TextStyle tabTextStyle = smallCaps13;

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 4, // 你的标签数量
      child: Scaffold(
        appBar: AppBar(
          bottom: TabBar(
            isScrollable: false, // 保证标签不滚动,占满屏幕
            indicator: const CustomTextUnderlineIndicator(
              color: Colors.blueAccent,
              thickness: 2,
              horizontalPadding: 4, // 可以给下划线加一点左右内边距,更美观
            ),
            tabs: const [
              Tab(child: Text("HOME")),
              Tab(child: Text("SHARE THE GOOD")),
              Tab(child: Text("ABOUT")),
              Tab(child: Text("CONTACT")),
            ],
          ),
        ),
        body: const TabBarView(
          children: [
            // 对应每个标签的页面内容
            Center(child: Text("Home Page")),
            Center(child: Text("Share The Good Page")),
            Center(child: Text("About Page")),
            Center(child: Text("Contact Page")),
          ],
        ),
      ),
    );
  }
}

方案二:手动控制Tab布局与下划线

如果你不想写自定义指示器,也可以用Stack给每个Tab手动添加下划线,然后通过TabController监听选中状态来控制下划线的显示:

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

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateMixin {
  late TabController _tabController;
  static TextStyle tabTextStyle = smallCaps13;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 4, vsync: this);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        bottom: PreferredSize(
          preferredSize: const Size.fromHeight(48),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround, // 让标签均匀分布在屏幕上
            children: List.generate(4, (index) {
              final tabTexts = ["HOME", "SHARE THE GOOD", "ABOUT", "CONTACT"];
              return GestureDetector(
                onTap: () => _tabController.animateTo(index),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Text(
                      tabTexts[index],
                      style: tabTextStyle.copyWith(
                        color: _tabController.index == index ? Colors.blueAccent : Colors.grey,
                      ),
                    ),
                    if (_tabController.index == index)
                      Container(
                        margin: const EdgeInsets.only(top: 8),
                        height: 2,
                        // 这里可以用TextPainter精确计算宽度,下面是简化的估算方式
                        width: tabTexts[index].length * 8,
                        color: Colors.blueAccent,
                      ),
                  ],
                ),
              );
            }),
          ),
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: const [
          Center(child: Text("Home Page")),
          Center(child: Text("Share The Good Page")),
          Center(child: Text("About Page")),
          Center(child: Text("Contact Page")),
        ],
      ),
    );
  }
}

这个方案的优点是完全自定义,自由度高,但需要自己处理Tab的点击事件和选中状态,适合需要更个性化布局的场景。


两种方案都能解决你的问题,方案一更贴合Flutter原生TabBar的使用逻辑,推荐优先尝试~

备注:内容来源于stack exchange,提问作者pl8nt

火山引擎 最新活动