4

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

0

2 Answers 2

10

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

Sign up to request clarification or add additional context in comments.

Comments

5

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

1 Comment

Thank you @mr-vd, could you please recheck the question, I updated it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.