import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; class LineTabSwitcher extends StatefulWidget { const LineTabSwitcher({ super.key, required this.tabs, required this.onTabChange, required this.selectedTab, }); final List tabs; final void Function(int index) onTabChange; final int selectedTab; @override State createState() => _LineTabSwitcherState(); } class _LineTabSwitcherState extends State { List textWidgetKeys = []; List textWidgetSizes = []; double totalWidth = 0; final double itemPadding = 16; final double barPadding = 8; bool textWidgetsMeasured = false; @override void didUpdateWidget(covariant LineTabSwitcher oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.tabs.length != widget.tabs.length) { textWidgetKeys = List.generate(widget.tabs.length, (_) => GlobalKey()); textWidgetSizes = []; totalWidth = 0; textWidgetsMeasured = false; WidgetsBinding.instance.addPostFrameCallback((_) => measure()); } } double _calcBarLeft() { double left = 0; if (textWidgetKeys.isEmpty || textWidgetSizes.isEmpty) { return 0; } for (int i = 0; i < widget.selectedTab; i++) { left += textWidgetSizes[i].width + itemPadding; } left += barPadding; return left; } @override void initState() { super.initState(); textWidgetKeys = List.generate(widget.tabs.length, (index) => GlobalKey()); WidgetsBinding.instance.addPostFrameCallback((_) => measure()); } void measure() { if(textWidgetsMeasured) return; setState(() { textWidgetSizes = textWidgetKeys .map((k) => k.currentContext!.size) .whereType() .toList(); for(final size in textWidgetSizes) { totalWidth += size.width+itemPadding; } totalWidth += barPadding; textWidgetsMeasured = true; }); } @override Widget build(BuildContext context) { if (!textWidgetsMeasured) { WidgetsBinding.instance.addPostFrameCallback((_) => measure()); } return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ Container( height: 28, child: Row( children: widget.tabs.map((item){ final index = widget.tabs.indexOf(item); return GestureDetector( onTap: () { widget.onTabChange(index); }, child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ AnimatedDefaultTextStyle( duration: Duration(milliseconds: 150), style: DefaultTextStyle.of(context).style.copyWith( inherit: true, fontSize: 16, fontWeight: FontWeight.w500, color: widget.selectedTab == index ? Theme.of(context).colorScheme.onSurface : Theme.of(context).colorScheme.onSurfaceVariant, ), child: Padding( padding: EdgeInsets.symmetric(horizontal: itemPadding/2), child: Text( item, key: textWidgetKeys[index], ), ), ), ], ), ); }).toList() ) ), Container( width: totalWidth, height: 2, child: Stack( children: [ AnimatedPositioned( curve: Curves.easeOut, left: _calcBarLeft(), bottom: 0, duration: Duration(milliseconds: 150), child: AnimatedSize( duration: Duration(milliseconds: 150), child: Container( height: 2, width: textWidgetSizes.isEmpty ? 0 : textWidgetSizes[widget.selectedTab].width, color: Theme.of(context).colorScheme.onSurface, ), ), ), ], ), ), ], ); } }