Reputation: 1211
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
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:
See an example in action:
Upvotes: 2
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