How to implement specified custom tab indicator in Flutter?

173 Views Asked by At

New Flutter coder here, so my existing knowledge of how various components interact is limited. I've read through/attempted various tutorials (including several posted here), but am having trouble implementing with my existing code to achieve the desired result.

My existing code displays the following: current view

What I'm trying to achieve is the following: desired view

As pictured, I'd like to create a custom indicator for the top-most TabBar. Any help would be greatly appreciated. Thank you!

HomePage code:

import 'package:fallout/plans.dart';
import 'package:fallout/recipes.dart';
import 'package:flutter/material.dart';
import 'package:flutter_glow/flutter_glow.dart';

class HomePage extends StatefulWidget {
  const HomePage({super.key});
  @override
  _HomePageState createState() => _HomePageState();
}

const myColor = Color(0xFFF8EA00);

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 5,
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: Colors.black,
          title: const Center(
            child: TabBar(
              indicatorColor: Colors.black,
              indicatorWeight: 2,
              dividerColor: myColor,
              isScrollable: true,
              tabs: <Widget>[
                Tab(
                  child: GlowText(
                    blurRadius: 3,
                    'HOME',
                    style: TextStyle(color: myColor, fontSize: 18.0),
                  ),
                ),
                Tab(
                  child: GlowText(
                    blurRadius: 3,
                    'PLANS',
                    style: TextStyle(color: myColor, fontSize: 18.0),
                  ),
                ),
                Tab(
                  child: GlowText(
                    blurRadius: 3,
                    'RECIPES',
                    style: TextStyle(color: myColor, fontSize: 18.0),
                  ),
                ),
                Tab(
                  child: GlowText(
                    blurRadius: 3,
                    'CREATURES',
                    style: TextStyle(color: myColor, fontSize: 18.0),
                  ),
                ),
                Tab(
                  child: GlowText(
                    blurRadius: 3,
                    'PLANTS',
                    style: TextStyle(color: myColor, fontSize: 18.0),
                  ),
                ),
              ],
            ),
          ),
        ),
        body: TabBarView(
          children: <Widget>[
            const Text('Home'),
            PlansTabs(0xffff5722),
            RecipesTabs(0xffff5722),
            const Text('Creatures'),
            const Text('Plants'),
          ],
        ),
      ),
    );
  }
}

Plans Tab Code:

import 'package:flutter/material.dart';
import 'package:flutter_glow/flutter_glow.dart';

class PlansTabs extends StatefulWidget {
  PlansTabs(this.colorVal);
  int colorVal;

  _PlansTabsState createState() => _PlansTabsState();
}

class _PlansTabsState extends State<PlansTabs>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = new TabController(vsync: this, length: 4);
    _tabController.addListener(_handleTabSelection);
  }

  void _handleTabSelection() {
    setState(() {
      widget.colorVal = 0xffff5722;
    });
  }

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 4,
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: Colors.black,
          toolbarHeight: double.minPositive,
          bottom: TabBar(
            controller: _tabController,
            isScrollable: true,
            indicatorColor: Color(0xFFF8EA00),
            indicatorWeight: 1,
            dividerColor: Colors.black,
            tabs: const <Widget>[
              Tab(
                child: GlowText(
                  blurRadius: 3,
                  'ARMOR',
                  style: TextStyle(color: Color(0xFFF8EA00), fontSize: 18.0),
                ),
              ),
              Tab(
                child: GlowText(
                  blurRadius: 3,
                  'C.A.M.P.',
                  style: TextStyle(color: Color(0xFFF8EA00), fontSize: 18.0),
                ),
              ),
              Tab(
                child: GlowText(
                  blurRadius: 3,
                  'MODS',
                  style: TextStyle(color: Color(0xFFF8EA00), fontSize: 18.0),
                ),
              ),
              Tab(
                child: GlowText(
                  blurRadius: 3,
                  'WEAPONS',
                  style: TextStyle(color: Color(0xFFF8EA00), fontSize: 18.0),
                ),
              ),
            ],
          ),
        ),
        body: TabBarView(
          controller: _tabController,
          children: <Widget>[
            //PlansTabs(0xffff5722),
            Container(1
              height: 200.0,
              child: Center(child: Text('Armor Text')),
            ),
            Container(
              height: 200.0,
              child: Center(child: Text('C.A.M.P. Text')),
            ),
            Container(
              height: 200.0,
              child: Center(child: Text('Mods Text')),
            ),
            Container(
              height: 200.0,
              child: Center(child: Text('Weapons Text')),
            ),
          ],
        ),
      ),
    );
  }
}

I've tried referencing the following solutions, but can't seem to integrate it properly with my code:

2

There are 2 best solutions below

0
Ashikul Islam Sawan On

I think it's not possible to achieve the desired style within Flutter built-in TabBar widget. You've to make your own Custom TabBar widget. I've tried many things before to style tabbar. Hope you'll get a hint from this.

class CustomTabBar extends StatelessWidget implements PreferredSizeWidget {
  final List<String> tabTitles;
  final TabController controller;

  const CustomTabBar(
      {super.key, required this.tabTitles, required this.controller});

  @override
  Size get preferredSize => const Size.fromHeight(52);

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: preferredSize.height,
      child: TabBar(
        indicatorSize: TabBarIndicatorSize.label,
        indicatorPadding: const EdgeInsets.only(right: -24, left: -24),
        indicator: const BoxDecoration(
          border: Border(
            top: BorderSide(color: Color(0xFFF8EA00), width: 2),
            right: BorderSide(color: Color(0xFFF8EA00), width: 2),
            left: BorderSide(color: Color(0xFFF8EA00), width: 2),
            bottom: BorderSide(color: Colors.black, width: 4),
          ),
        ),
        // indicatorColor: Colors.black,
        controller: controller,
        tabs: [
          for (int i = 0; i < tabTitles.length; i++)
            Tab(
              child: Text(
                tabTitles[i],
                maxLines: null,
                overflow: TextOverflow.ellipsis,
                textAlign: TextAlign.center,
                style: const TextStyle(color: Colors.white),
              ),
            ),
        ],
      ),
    );
  }
}

Outfup

0
Hydra On

demo:

enter image description here

code:

import 'package:flutter/material.dart';
import 'package:flutter_glow/flutter_glow.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});
  @override
  State<HomePage> createState() => _HomePageState();
}

const myColor = Color(0xFFF8EA00);

class _HomePageState extends State<HomePage> {
  final tabs = [
    'HOME',
    'PLANS',
    'RECIPES',
    'CREATURES',
    'PLANTS',
  ];

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 5,
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: Colors.black,
          title: Center(
            child: Builder(
              builder: (context) {
                final ctrl = DefaultTabController.of(context);
                return TabBar(
                  indicatorColor: Colors.transparent,
                  dividerColor: Colors.transparent,
                  isScrollable: true,
                  tabs: tabs.map((tabText) {
                    final tabWidget = Tab(
                      child: GlowText(
                        tabText,
                        blurRadius: 3,
                        style: const TextStyle(
                          color: Color(0xFFF8EA00),
                          fontSize: 18.0,
                        ),
                      ),
                    );
                    if (tabText == 'HOME') {
                      return CustomPaint(
                        painter: TabsPainter(controller: ctrl),
                        child: tabWidget,
                      );
                    }
                    return tabWidget;
                  }).toList(),
                );
              },
            ),
          ),
        ),
        body: const TabBarView(
          children: <Widget>[
            Text('Home'),
            Text('Plans'),
            Text('Recipes'),
            Text('Creatures'),
            Text('Plants'),
          ],
        ),
      ),
    );
  }
}

class TabsPainter extends CustomPainter {
  final TabController controller;

  TabsPainter({
    required this.controller,
  }) : super(repaint: controller);

  final List<double> widths = [89, 98, 111, 141, 108];

  @override
  void paint(Canvas canvas, Size size) {
    final animation = controller.animation!;
    final paint = Paint()..color = myColor;
    paint.style = PaintingStyle.stroke;
    paint.strokeWidth = 1.5;
    final dx = sumUntil(animation.value);
    final dx2 = sumUntil(animation.value + 1);
    final path = Path()..moveTo(-15, 47);
    path.relativeLineTo(dx, 0);
    path.relativeLineTo(0, -23);
    path.relativeLineTo(8, 0);
    path.moveTo(dx2 - 27, 24);
    path.relativeLineTo(8, 0);
    path.relativeLineTo(0, 23);
    path.lineTo(sumUntil(5) - 20, 47);
    canvas.drawPath(path, paint);
  }

  double sumUntil(double animation) {
    double distance = 0;
    final index = animation.floor();
    for (int i = 0; i < index; i++) {
      distance += widths[i];
    }
    if (index < widths.length) {
      final leftover = animation - index;
      distance += leftover * widths[index];
    }
    return distance;
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}