Flutter Customizing TabBar Indicator

6,272

Solution 1

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

Solution 2

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),
          ],
        )
      ),
    );
  }
}
Share:
6,272
Jihad Naji
Author by

Jihad Naji

Updated on December 24, 2022

Comments

  • Jihad Naji
    Jihad Naji over 1 year

    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(...),
          ),
        );
      }
    }
    
    
  • Jihad Naji
    Jihad Naji over 3 years
    Thank you @mr-vd, could you please recheck the question, I updated it.