Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter Customizing TabBar Indicator

How can I customize the Tab indicator in the Flutter TabBar to achieve the target result below?

CURRENT STATE Current TabBar State

TARGET Target TabBar result

How can I do the following:

  1. Change unselected tabs as in the target picture
  2. Adjust the ripple borders to fit the content and have rounded edges.

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(...),
      ),
    );
  }
}

like image 308
Jihad Naji Avatar asked Sep 08 '25 11:09

Jihad Naji


2 Answers

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

enter image description here

like image 100
hientrq Avatar answered Sep 10 '25 01:09

hientrq


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),
          ],
        )
      ),
    );
  }
}
like image 24
Mr vd Avatar answered Sep 09 '25 23:09

Mr vd