kilokahn
kilokahn

Reputation: 1211

Selective FloatingActionButton visibility in Flutter tabs

I am trying to make an app where a route has a tabbed layout with 5 tabs. In two of these tabs, I need to place a FAB to load a new screen.

However, by default (Using DefaultTabController), this is an all or nothing choice as there is no way to get the Tab index with this controller.

However, I followed this SO question and this one and added a manual TabController. However, now when the Tabs load, I don't see the FAB unless I click on an element in the Tab and navigate back to the tab.

Also, the FAB does not disappear when I swipe to a tab where there shouldn't be a FAB.

My code is as follows:

  TabController controller;

  @override
  void initState(){
    super.initState();
    controller = new TabController(vsync: this, length: 5);
  }


  @override
  void dispose(){
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(title: new Text("My Clinic"), backgroundColor: Colors.blue,
            bottom: new TabBar(
                controller: controller,
                tabs: <Tab>[
                  new Tab(icon: new Icon(Icons.report)),
                  new Tab(icon: new Icon(Icons.person)),
                  new Tab(icon: new Icon(Icons.assistant)),
                  new Tab(icon: new Icon(Icons.calendar_today)),
                  new Tab(icon: new Icon(Icons.settings))
                ]
            )
        ),

        body: new Container(
                color: Colors.blue,
                child : new TabBarView(
                  controller: controller,
                  children: <Widget>[
                    clinicInfo(doctor),
                    doctorInfo(),
                    assistantInfo(),
                    clinicSchedule(),
                    clinicOperations()
            ]
          ),
        ),
      floatingActionButton: _bottomButtons(controller.index),
    );
  }

Here _bottomButtons is as follows:

Widget _bottomButtons(int index ) {
    switch(index) {
      case 0: // dashboard
        return null;
        break;
      case 1: // doctors
        return FloatingActionButton(
          onPressed: null,
          backgroundColor: Colors.redAccent,
          child: Icon(
            Icons.edit,
            size: 20.0,
          ),
        );
        break;
      case 2: // assistants
        return FloatingActionButton(
          onPressed: null,
          backgroundColor: Colors.redAccent,
          child: Icon(
            Icons.edit,
            size: 20.0,
          ),
        );
        break;
      case 3: // sessions
        return null;
        break;
      case 4: // settings
        return null;
        break;
    }
  }

As we can see, the FAB is only supposed to be visible on Tabs 1 and 2. What am I overlooking/doing wrong here?

Upvotes: 0

Views: 883

Answers (2)

Ilia Kurtov
Ilia Kurtov

Reputation: 1090

With this approach, you can create beautifully animated fabs for selected tabs:


class MultipleHidableFabs extends StatefulWidget {
  @override
  State<MultipleHidableFabs> createState() => _MultipleHidableFabsState();
}

class _MultipleHidableFabsState extends State<MultipleHidableFabs>
    with SingleTickerProviderStateMixin {
  // Index of initially opened tab
  static const initialIndex = 0;

  // Number of tabs
  static const tabsCount = 3;

  // List with current scales for each tab's fab
  // Initialize with 1.0 for initial opened tab, 0.0 for others
  final tabScales =
      List.generate(tabsCount, (index) => index == initialIndex ? 1.0 : 0.0);

  late TabController tabController;

  @override
  void initState() {
    super.initState();
    tabController = TabController(
      length: tabsCount,
      initialIndex: initialIndex,
      vsync: this,
    );

    // Adding listener to animation gives us opportunity to track changes more
    // frequently compared to listener of TabController itself
    tabController.animation!.addListener(() {
      setState(() {
        // Current animation value. It ranges from 0 to (tabsCount - 1)
        final animationValue = tabController.animation!.value;
        // Simple rounding gives us understanding of what tab is showing
        final currentTabIndex = animationValue.round();
        // currentOffset equals 0 when tabs are not swiped
        // currentOffset ranges from -0.5 to 0.5
        final currentOffset = currentTabIndex - animationValue;
        for (int i = 0; i < tabsCount; i++) {
          if (i == currentTabIndex) {
            // For current tab bringing currentOffset to range from 0.0 to 1.0
            tabScales[i] = (0.5 - currentOffset.abs()) / 0.5;
          } else {
            // For other tabs setting scale to 0.0
            tabScales[i] = 0.0;
          }
        }
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        bottom: TabBar(
          controller: tabController,
          tabs: [
            Tab(icon: Icon(Icons.one_k)),
            Tab(icon: Icon(Icons.two_k)),
            Tab(icon: Icon(Icons.three_k)),
          ],
        ),
      ),
      body: SafeArea(
        child: TabBarView(
          controller: tabController,
          children: [Icon(Icons.one_k), Icon(Icons.two_k), Icon(Icons.three_k)],
        ),
      ),
      floatingActionButton: createScaledFab(),
    );
  }

  Widget? createScaledFab() {
    // Searching for index of a tab with not 0.0 scale
    final indexOfCurrentFab = tabScales.indexWhere((fabScale) => fabScale != 0);
    // If there are no fabs with non-zero opacity return nothing
    if (indexOfCurrentFab == -1) {
      return null;
    }
    // Creating fab for current index
    final fab = createFab(indexOfCurrentFab);
    // If no fab created return nothing
    if (fab == null) {
      return null;
    }
    final currentFabScale = tabScales[indexOfCurrentFab];
    // Scale created fab with
    // You can use different Widgets to create different effects of switching
    // fabs. E.g. you can use Opacity widget or Transform.translate to create
    // custom animation effects
    return Transform.scale(scale: currentFabScale, child: fab);
  }

  // Create fab for provided index
  // You can skip creating fab for any indexes you want
  Widget? createFab(final int index) {
    if (index == 0) {
      return FloatingActionButton(
        onPressed: () => print("On first fab clicked"),
        child: Icon(Icons.one_k),
      );
    }
    // Not created fab for 1 index deliberately
    if (index == 2) {
      return FloatingActionButton(
        onPressed: () => print("On third fab clicked"),
        child: Icon(Icons.three_k),
      );
    }
  }
}

Advantages of this approach:

  • Synchronized animation between swiping and showing fabs
  • Tapping on tabs also animates in a right manner
  • Ability to easily skip creating fabs for selected indexes

See an example in action:

Example

Upvotes: 2

lazos
lazos

Reputation: 1075

Are you sure you change the state?
Maybe you need:

  TabController controller;

  @override
  void initState() {
    super.initState();
    controller = new TabController(vsync: this, length: 5);
    controller.addListener(updateIndex);
  }

  @override
  void dispose() {
    controller.removeListener(updateIndex);
    controller.dispose();
    super.dispose();
  }

  void updateIndex() {
    setState(() {});
  }

Upvotes: 2

Related Questions