How can I customize the Tab indicator in the Flutter TabBar to achieve the target result below?
CURRENT STATE
TARGET
How can I do the following:
This is my code
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
var categoryTabs = <Tab>[...];
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: categoryTabs.length,
child: Scaffold(
appBar: AppBar(
title: Text('My App'),
centerTitle: true,
bottom: PreferredSize(
preferredSize: Size(100, 70),
child: Column(
children: [
TabBar(
indicatorSize: TabBarIndicatorSize.tab,
indicatorColor: Colors.transparent,
labelColor: colorPrimaryDark,
isScrollable: true,
unselectedLabelColor: Colors.white,
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(50),
color: Colors.white,
),
tabs: categoryTabs,
),
SizedBox(height: 10)
],
),
),
),
body: SafeArea(...),
),
);
}
}
You need to use AnimatedBuilder
to listen to the tab controller animation when tab controller animate from one index to another. Using this approach you can customize the transition animation of the TabBar
items. For example: Change your TabBar
implementation to the following
TabBar(
tabs: tabs
.asMap().entries
.map((entry) => AnimatedBuilder(
animation: _tabController.animation,
builder: (ctx, snapshot) {
final forward = _tabController.offset > 0;
final backward = _tabController.offset < 0;
int _fromIndex;
int _toIndex;
double progress;
// This value is true during the [animateTo] animation that's triggered when the user taps a [TabBar] tab.
// It is false when [offset] is changing as a consequence of the user dragging the [TabBarView].
if (_tabController.indexIsChanging) {
_fromIndex = _tabController.previousIndex;
_toIndex = _tabController.index;
_cachedFromIdx = _tabController.previousIndex;
_cachedToIdx = _tabController.index;
progress = (_tabController.animation.value - _fromIndex).abs() / (_toIndex - _fromIndex).abs();
} else {
if (_cachedFromIdx == _tabController.previousIndex && _cachedToIdx == _tabController.index) {
// When user tap on a tab bar and the animation is completed, it will execute this block
// This block will not be called when user draging the TabBarView
_fromIndex = _cachedFromIdx;
_toIndex = _cachedToIdx;
progress = 1;
_cachedToIdx = null;
_cachedFromIdx = null;
} else {
_cachedToIdx = null;
_cachedFromIdx = null;
_fromIndex = _tabController.index;
_toIndex = forward
? _fromIndex + 1
: backward
? _fromIndex - 1
: _fromIndex;
progress = (_tabController.animation.value - _fromIndex).abs();
}
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: entry.key == _fromIndex
? Color.lerp(Colors.white, Colors.red.shade900, progress)
: entry.key == _toIndex
? Color.lerp(Colors.red.shade900, Colors.white, progress)
: Color.lerp(Colors.red.shade900, Colors.red.shade900, progress),
borderRadius: BorderRadius.circular(200),
),
child: Text(
entry.value.toUpperCase(),
style: TextStyle(
fontSize: 10,
letterSpacing: 0.4,
fontWeight: FontWeight.w700,
),
),
);
},
))
.toList(),
controller: _tabController,
isScrollable: true,
indicatorSize: TabBarIndicatorSize.label,
indicatorWeight: 0,
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(100),
),
physics: const ClampingScrollPhysics(),
unselectedLabelColor: Colors.white,
labelColor: Colors.red,
labelPadding: EdgeInsets.only(left: 12),
),
Notice in the example above, I use 2 local variables to keep track of current and previous indices. This is specifically to handle cases when user tabs on the TabBar
tab to animate between one index to another. Put log command in AnimatedBuilder
's builder method to better understand of how it works.
int _cachedFromIdx;
int _cachedToIdx;
Here's the result
Check this code
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
backgroundColor: Colors.red,
bottom: PreferredSize(
preferredSize: Size(100,20),
child: Column(
children: [
TabBar(
// indicatorSize: TabBarIndicatorSize.tab,
indicatorColor: Colors.transparent,
labelColor: Colors.white,
unselectedLabelColor: Colors.black,
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(50),
color: Colors.redAccent),
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.report_problem)),
Tab(icon: Icon(Icons.report_problem)),
],
),
SizedBox(height: 10,)
],
),
),
),
body: TabBarView(
children: [
Icon(Icons.directions_car),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
],
)
),
);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With