How to use TabController - flutter

I just learned flutter, I was confused how to use the TabController, I had followed what was described on the official website, but an error appeared, and I don't know how to fix it.
I just want to change the title and leading from the appbar when changing tabs.
final List<ChangeTitleAndLeading> _data = [
new ChangeTitleAndLeading(title: "Home", leading: Icon(Icons.home)),
new ChangeTitleAndLeading(title: "Profile", leading: Icon(Icons.person)),
new ChangeTitleAndLeading(title: "Friends", leading: Icon(Icons.people))
];
ChangeTitleAndLeading _handler;
TabController _controller;
#override
void initState() {
super.initState();
_checkEmailVerification();
_controller = TabController(vsync: this, length: 3);
_handler = _data[0];
_controller.addListener(_handleSelected);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handleSelected() {
setState(() {
_handler = _data[_controller.index];
});
}
return MaterialApp(
theme: new ThemeData(
primarySwatch: Colors.teal,
),
home: new Scaffold(
appBar: new AppBar(
leading: Icon(Icons.home),
title: new Text("Home"),
bottom: new TabBar(
controller: _controller,
tabs: _tabs,
),
),
body: TabBarView(
controller: _controller,
children: _pages,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
print('Current Index: ${_handler.title}');
}
),
class ChangeTitleAndLeading {
final String title;
final Widget leading;
ChangeTitleAndLeading({
#required this.title,
#required this.leading
}) :
assert(title != null),
assert(leading != null);
}
Error log:
Error Log:
I/flutter (19638): No TabController for TabBarView.
I/flutter (19638): When creating a TabBarView, you must either provide an explicit TabController using the "controller"
I/flutter (19638): property or you must ensure that there is a DefaultTabController above the TabBarView.
I/flutter (19638): In this case, there was neither an explicit controller nor a default controller.
════════════════════════════════════════════════════════════════════════════════════════════════════
I/flutter (19638): Another exception was thrown: No TabController for TabBar.
And when i change this:
leading: Icon(Icons.home), to leading: _handler.leading,
and this:
title: new Text("Home"), to title: new Text(_handler.title),
always return error _handler.leading or _handler.title was null
Image

Try this solution :-
don't forget to inhert
TickerProviderStateMixin
class HomePage extends StatefulWidget {
const HomePage();
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
late TabController tabController;
#override
void initState() {
super.initState();
tabController = TabController(
initialIndex: 0,
length: 2,
vsync: this,
);
}
#override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Row(
children: [
Image.asset(
'assets/images/png/logo.png',
height: 60,
width: 60,
),
Spacer(),
Container(
width: 400,
child: TabBar(
labelColor: Color.fromRGBO(4, 2, 46, 1),
labelStyle: theme.textTheme.headline1,
indicatorColor: Color.fromRGBO(4, 2, 46, 1),
unselectedLabelColor: Colors.grey,
controller: tabController,
tabs: [
Text('الفاتورة'),
Text('دليفري'),
],
),
),
],
),
),
body: Container(
child: TabBarView(
controller: tabController,
children: [
Container(
color: Colors.red,
),
Container(
color: Colors.orange,
),
],
),
),
);
}
#override
void dispose() {
tabController.dispose();
super.dispose();
}
}

The issue is that you are missing a tabbarcontroller
Your code should be:
return MaterialApp(
theme: new ThemeData(
primarySwatch: Colors.teal,
),
home: DefaultTabController(
length: 3,
child: new Scaffold(
appBar: new AppBar(
leading: Icon(Icons.home),
title: new Text("Home"),
bottom: new TabBar(
controller: _controller,
tabs: _tabs,
),
),
body: TabBarView(
controller: _controller,
children: _pages,
)...

Related

How to get the index of the current NavigationBar?

So in here i want to change some condition if my tabview switch to the second tab but i don't know how to get the tabbar index, already try this and that. Im hoping some solution without statefull, Im using GetX thanks.
im planning to change the extendBody: true, in my main page to false when the tab switch to the second tab i had the logic for that hopefully but the only problem is the index :(.
My tabs :
List<Tab> myTabs = [
Tab(
text: 'Following',
),
Tab(
text: 'Trending',
),
Tab(
text: 'Search',
),
];
DefaultController code :
DefaultTabController(
length: myTabs.length,
child: Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: bgColor,
// APPBAR
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
toolbarHeight: 60,
// BOTTOM
bottom: PreferredSize(
preferredSize: const Size.fromHeight(0),
child: Align(
alignment: Alignment.centerLeft,
child: TabBar(
isScrollable: true,
labelPadding: EdgeInsets.only(left: 20),
labelColor: Colors.white,
labelStyle: poppins.copyWith(
fontSize: 15,
fontWeight: bold,
),
unselectedLabelColor: Color(0xff585861),
indicatorColor: Colors.white.withOpacity(0),
indicatorSize: TabBarIndicatorSize.label,
// TABS
tabs: myTabs,
),
),
),
),
body: TabBarView(
children: [
FollowingTab(),
TrendingTab(),
search(),
],
),
),
);
Use TabBar with TabController and you can find current index while switching to next tab
#override
void initState() {
super.initState();
_controller = TabController(length: 6, vsync: this);
_controller!.addListener(() {
print(_controller!.index);
});
}
your build method be like:
#override
Widget build(BuildContext context) {
return Scaffold(
bottom:TabBar(
controller: _controller,
tabs:[
//your tabs will be here
]
),
body:TabBarView(
controller: _controller,
children: [
//your tabbarview will be here
]
),
);
}
Here i just code of Tabbar with using Getx and Stateless Widget.
CheckOut my code and if you find solution then give up me. Thanks in advance
class TabDemo extends StatelessWidget {
TabDemo({Key? key}) : super(key: key);
final DemoController demoController = Get.put(DemoController());
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.flight)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_car)),
],
),
title: const Text('Tabs Demo'),
),
body: TabBarView(
controller: demoController.controller?.value,
children: const [
Icon(Icons.flight, size: 350),
Icon(Icons.directions_transit, size: 350),
Icon(Icons.directions_car, size: 350),
],
),
),
);
}
}
You need to create controller class to define controller and initmethod
class DemoController extends GetxController with SingleGetTickerProviderMixin {
Rx<TabController>? controller;
#override
void onInit() {
// TODO: implement onInit
controller?.value = TabController(length: 6, vsync: this);
controller?.value.addListener(() {
print(controller?.value.index);
});
super.onInit();
}
}

How to fix No TabController for TabBar

I'm new in flutter and I'm trying to make a TabBar view like in the figure. But it shows "No TabController for TabBar" and When creating a TabBarView, you must either provide an explicit TabController using the "controller" property, or you must ensure that there is a DefaultTabController above the TabBarView.
In this case, there was neither an explicit controller nor a default controller.
I have tried a suggestion that mentioned in the internet but still got error. Can anyone help me how to fix it?? Thank you
class _TransactionState extends State<Transaction> with SingleTickerProviderStateMixin {
int _value = 1;
TabController controller;
#override
void initState() {
controller = new TabController(vsync: this, length: 2);
super.initState();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Transaction'),
actions: <Widget>[
DropdownButton(
dropdownColor: primary,
value: _value,
items: [
DropdownMenuItem(
child: Text(
"Daily",
style: TextStyle(color: Colors.white,
fontWeight: FontWeight.bold,
),
),
value: 1,
),
DropdownMenuItem(
child: Text("Monthly",
style: TextStyle(color: Colors.white,
fontWeight: FontWeight.bold,
),),
value: 2,
),
DropdownMenuItem(
child: Text("Yearly",
style: TextStyle(color: Colors.white,
fontWeight: FontWeight.bold,
),),
value: 3,
),
],
onChanged: (int value){
setState(() {
_value = value;
});
//Padding(padding: EdgeInsets.all(20.0));
style: new TextStyle(
color: Colors.white,
);
},
),
],
automaticallyImplyLeading: false,
backgroundColor: secondary,
bottom: new TabBar(
controller: controller,
tabs: <Widget>[
new Tab(icon: new Icon(Icons.add_shopping_cart),text: "Expense",),
new Tab(icon: new Icon(Icons.attach_money),text: "Income",),
],
),
),
body: TabBarView(
controller: controller,
children: <Widget>[
new expense.TransactionExpense(),
new income.TransactionIncome(),
],
),
);
}
}
This should solve it.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: MyAppState()
);
}
}
class MyAppState extends StatefulWidget{
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyAppState> with TickerProviderStateMixin {
TabController _controller;
final List<Tab> topTabs = <Tab>[
new Tab(text: 'Profile'),
new Tab(text: 'Match'),
new Tab(text: 'Chat'),
];
#override
void initState() {
super.initState();
_controller = TabController(vsync: this, length: 3);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('MyApp'),
bottom: TabBar(
controller: _controller,
tabs: topTabs,
),
),
body: TabBarView(
controller: _controller,
children: [
new Container(
color: Colors.lightBlueAccent,
child: Center(child: Text('Profile', style: TextStyle(color: Colors.white),),),
),
new Container(
color: Colors.purpleAccent,
child: Center(child: Text('Match', style: TextStyle(color: Colors.white),),),
),
new Container(
color: Colors.lightGreenAccent,
child: Center(child: Text('Chat', style: TextStyle(color: Colors.white),),),
)
]),
);
}
}

Can I use flutter's TabBar for filtering a list of cards?

I have created a TabBar inside my AppBar with the code below. I'm curious if I can use this TabBar as a filtering mechanism like the image below. I feel like there might already be a widget available for this, but I can't find any evidence of that. If I do have to use the TabBar, how would I go about toggling each option and filtering based on this list of toggles.
Current code:
import 'package:flutter/material.dart';
class ExploreDetail extends StatefulWidget {
static const routeName = 'explore_detail';
#override
_ExploreDetailState createState() => _ExploreDetailState();
}
class _ExploreDetailState extends State<ExploreDetail>
with SingleTickerProviderStateMixin {
TabController _tabController;
List<Widget> tabs = [
Tab(
text: 'All',
),
Tab(
text: 'Experience Consulting',
),
Tab(
text: 'Front Office Transformation',
),
];
#override
void initState() {
// TODO: implement initState
super.initState();
// Create TabController for getting the index of current tab
_tabController = TabController(
length: tabs.length,
initialIndex: 0,
vsync: this,
);
}
#override
Widget build(BuildContext context) {
final Map category = ModalRoute.of(context).settings.arguments;
return Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(kTextTabBarHeight + kToolbarHeight),
child: AppBar(
title: Text(
category['title'],
style: TextStyle(color: Colors.black),
),
backgroundColor: Colors.white,
iconTheme: IconThemeData(
color: Colors.black,
),
bottom: PreferredSize(
preferredSize: Size.fromHeight(kTextTabBarHeight),
child: Align(
alignment: Alignment.centerLeft,
child: TabBar(
tabs: tabs,
controller: _tabController,
indicatorColor: Colors.transparent,
labelColor: Colors.blue,
isScrollable: true,
unselectedLabelColor: Colors.grey,
),
),
),
),
),
body: Center(
child: Text('list of cards will go here'),
),
);
}
}
You can handle the tab selections by adding an addListener method to your tabController in your initState(). And then you can filter your data by the selected tab option.
It will look like:
#override
void initState() {
// TODO: implement initState
super.initState();
// Create TabController for getting the index of current tab
_tabController = TabController(
length: tabs.length,
initialIndex: 0,
vsync: this,
);
// Here is the addListener!
_tabController.addListener(_handleTabSelection);
}
And then:
void _handleTabSelection() {
if (_tabController.indexIsChanging) {
switch (_tabController.index) {
case 0:
filterData('all');
break;
case 1:
filterData('experienceConsulting');
break;
case 2:
filterData('frontOfficeTransformation');
break;
}
}
}

Flutter - Change the animation of TabBarView

I implemented a basic TabBar and TabBarView with a DefaultTabController, see code below.
class MyApp2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: BOTTOM_TABS,
child: Scaffold(
appBar: AppBar(title: const Text('Bottom App Bar')),
body: _tabBarView(),
bottomNavigationBar: _bottomTabBar(),
),
);
}
_tabBarView() {
return TabBarView(
physics: NeverScrollableScrollPhysics(),
children: [
Container(
color: Colors.blue,
),
Container(
color: Colors.orange,
),
Container(
color: Colors.lightGreen,
),
Container(
color: Colors.red,
),
],
);
}
_bottomTabBar() {
return TabBar(
tabs: [
Tab(
icon: new Icon(Icons.home),
),
Tab(
icon: new Icon(Icons.public),
),
Tab(
icon: new Icon(Icons.group),
),
Tab(
icon: new Icon(Icons.person),
)
],
);
}
}
Works great! Now what I want to do is change the animation between the two tabs from the default animation. But I can't find an easy way to do that.
After a bit of research it seems like I need to use a custom TabController and somehow use its animateTo method. To me that seems like a pretty big change just to change the animation. What I wonder is if that is the correct way or if I am missing some easier way to just change the default animation between the tabviews?
If someone could give me some good resources to point me in the right direction I'd greatly appreciate it.
This is not hard, just use TabController (to do so you need to use SingleTickerProviderStateMixin ) and AnimatedBuilder.
class MyApp2 extends StatefulWidget {
#override
_MyApp2State createState() => _MyApp2State();
}
class _MyApp2State extends State<MyApp2> with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
_tabController = TabController(length: 4, vsync: this);
super.initState();
}
_tabBarView() {
return AnimatedBuilder(
animation: _tabController.animation,
builder: (BuildContext context, snapshot) {
return Transform.rotate(
angle: _tabController.animation.value * pi,
child: [
Container(
color: Colors.blue,
),
Container(
color: Colors.orange,
),
Container(
color: Colors.lightGreen,
),
Container(
color: Colors.red,
),
][_tabController.animation.value.round()],
);
},
);
}
_bottomTabBar() {
return TabBar(
controller: _tabController,
labelColor: Colors.black,
tabs: [
Tab(
icon: new Icon(Icons.home),
),
Tab(
icon: new Icon(Icons.public),
),
Tab(
icon: new Icon(Icons.group),
),
Tab(
icon: new Icon(Icons.person),
)
],
);
}
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(title: const Text('Bottom App Bar')),
body: _tabBarView(),
bottomNavigationBar: _bottomTabBar(),
),
);
}
}
Screenshot (Null safe):
Code:
If you want fine-grained control, you can make use of the AnimationController.
class _MyPageState extends State<MyPage> with TickerProviderStateMixin {
late final TabController _tabController;
late final AnimationController _controller;
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 400),
value: 1,
);
_tabController = TabController(
length: 3,
vsync: this,
)..addListener(() {
if (_tabController.indexIsChanging) {
setState(() => _controller.forward(from: 0.5));
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ScaleTransition(
scale: _controller,
child: [
Container(color: Colors.red),
Container(color: Colors.green),
Container(color: Colors.blue),
][_tabController.index],
),
bottomNavigationBar: TabBar(
controller: _tabController,
tabs: [
Tab(child: Text('Red')),
Tab(child: Text('Green')),
Tab(child: Text('Blue')),
],
),
);
}
}
I don't know if you want to completely change the animation.
But if you just need some customization, did you try to use a TabController instead of a DefaultTabController ?
You just need to pass the tabController as an arg to the TabBar & TabBarView.
To customize the animation with the tabController, you should specify an Animation for the tabController and also specify the curve and duration with the animateTo function of the tabController.
https://api.flutter.dev/flutter/material/TabController/animateTo.html
https://api.flutter.dev/flutter/material/TabController-class.html
Disable animation between flutter tabs by setting animation duration to zero like this
tabController = TabController(
animationDuration: Duration.zero,
length: 4, vsync: this, initialIndex: 0);
Thank me later.

Flutter - Different floating action button in TabBar

I'm trying to get a different floatting button in a TabBar in flutter. But I will try a lot of option, but I don't know how.
Sorry, I add more details:
I want to do a app with a TabBar, like this flutter example.
If you see this is a tabBarDemo application, I can change between tabs,
but I don't know how to change the floating button between tabs. Thanks
Like this gif: https://i.stack.imgur.com/bxtN4.gif
class TabBarDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_bike)),
],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
Icon(Icons.directions_car),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
],
),
floatingActionButton: FloatingActionButton.extended
(onPressed: null,
icon: Icon(Icons.add, color: Colors.white,),
label: new Text('FLOATING TO CHANGE'),
),
floatingActionButtonLocation:FloatingActionButtonLocation.centerFloat,
),
),
);
}
}
A Minimal Example of what you want:
class TabsDemo extends StatefulWidget {
#override
_TabsDemoState createState() => _TabsDemoState();
}
class _TabsDemoState extends State<TabsDemo>
with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this, initialIndex: 0);
_tabController.addListener(_handleTabIndex);
}
#override
void dispose() {
_tabController.removeListener(_handleTabIndex);
_tabController.dispose();
super.dispose();
}
void _handleTabIndex() {
setState(() {});
}
#override
Widget build(BuildContext context) {
return SafeArea(
top: false,
child: Scaffold(
appBar: AppBar(
title: Text('Demo'),
bottom: TabBar(
controller: _tabController,
tabs: [
Tab(
text: "Tab1",
),
Tab(
text: "Tab2",
),
],
),
), // floatingActionButton: _buildFloatingActionButton(context),
body: TabBarView(controller: _tabController, children: [
Center(
child: Container(
child: Text('Tab 1'),
),
),
Center(
child: Container(
child: Text('Tab 2'),
),
),
]),
floatingActionButton: _bottomButtons(),
),
);
}
Widget _bottomButtons() {
return _tabController.index == 0
? FloatingActionButton(
shape: StadiumBorder(),
onPressed: null,
backgroundColor: Colors.redAccent,
child: Icon(
Icons.message,
size: 20.0,
))
: FloatingActionButton(
shape: StadiumBorder(),
onPressed: null,
backgroundColor: Colors.redAccent,
child: Icon(
Icons.edit,
size: 20.0,
),
);
}
}
you can achieve this by TabController
Declaration: TabController _tabController;
Initialization: in initState()
_tabController = TabController(length: 2, vsync: this, initialIndex: 0);
_tabController.addListener(_handleTabChange);
and just pass setState((){}) in method _handleTabChange to reflect ontime like
_handleTabChange(){
setState((){});
}
Now Bind or Inject in both of widget TabBar and TabBarView in their controller property.
TabBarView(
controller: _tabController,
children: [
Widget(),
Widget()
],
),
TabBar(
controller: _tabController,
tabs:[
Tab(...),
Tab(...),
]
)
Now place your different FAB button to different Tabs by according to _tabController index
floatingActionButton: _tabController.index == 0
? FloatingActionButton(
backgroundColor: Colors.blue,
onPressed: () {},
)
: FloatingActionButton(
backgroundColor: Colors.red,
onPressed: () {},
),
Keep coding ;)
Check this
import 'package:flutter/material.dart';
class Lista extends StatefulWidget {
#override
_ListaState createState() => _ListaState();
}
class _ListaState extends State<Lista> {
int indexTab=0;
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
initialIndex: 0,
child: Scaffold(
appBar: AppBar (
title: Text("Test"),
bottom: TabBar(
onTap: (index){
setState(() {
indexTab = index;
});
},
tabs: <Widget>[
Tab(icon: Icon(Icons.calendar_today)),
Tab(icon: Icon(Icons.whatshot)),
],
),
),
floatingActionButton: indexTab==0? FloatingActionButton (
onPressed: () {},
child: Icon(Icons.add),
):FloatingActionButton (
onPressed: () {},
child: Text('test'),
),
body: TabBarView(
children: <Widget>[
Text('1'),
Text('2'),
],
)
),
);
}
}
I found that the accepted answer was not providing a good enough solution for me. The problem is that animation feels laggy and untimely.
The main point of change is listening to Animation of TabController instead of TabController state.
There is my approach to create a more or less reusable solution:
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:
Based on this answer from Ilia Kurtov, here's a reusable component for tab-dependent FABs.
Implementation
import 'package:flutter/material.dart';
typedef FabBuilder = Widget? Function(int tabIndex);
typedef TransformBuilder = Widget Function(
BuildContext context, Widget child, double t);
The basic idea is to transform the animation from the tab controller to an index and distance (TabFocus class) using a custom subclass of Animatable.
/// Represent a tab index with a distance metric.
class TabFocus {
/// Distance to the tab
///
/// from 0.0 (on tab) to 1.0 (half way to next or previous tab)
final double distance;
/// Index of the tab that closest to the current `t`.
final int index;
const TabFocus._({required this.distance, required this.index});
/// Get the tab focus at a tab position
factory TabFocus.at(double t) {
final index = t.round();
final t0 = index.toDouble();
final distance = (t - t0).abs() * 2;
return TabFocus._(distance: distance, index: index);
}
}
/// Subclass of [Animatable] that transforms a `double t` tab position into a [TabFocus].
class TabFocusAnimatable extends Animatable<TabFocus> {
#override
TabFocus transform(double t) => TabFocus.at(t);
const TabFocusAnimatable();
}
When we create our widget, we turn this Animatable<TabFocus> into an Animation<TabFocus> by attaching it to the TabController.animation
/// A tab-dependent FAB based on <https://stackoverflow.com/a/71123870/4087068>
class TabbedFab extends StatefulWidget {
TabbedFab(
{Key? key,
required TabController tabController,
FabBuilder? builder,
Animatable<TabFocus> focusAnimatable = const TabFocusAnimatable(),
TransformBuilder? transformBuilder})
: this._(
key: key,
builder: builder,
tabController: tabController,
fabAnimation: focusAnimatable.animate(tabController.animation!));
const TabbedFab._(
{Key? key,
required this.tabController,
required this.fabAnimation,
this.transform = _defaultTransform,
this.builder})
: super(key: key);
final TransformBuilder transform;
final Animation<TabFocus> fabAnimation;
final TabController tabController;
final FabBuilder? builder;
#override
State<TabbedFab> createState() => _TabbedFabState();
}
We also define a default transformation, that just scales a widget (the FAB) based on a t from 0.0 to 1.0.
/// By default, scale the current floating action button, so that it is full
/// size when the tab is selected
Widget _defaultTransform(BuildContext context, Widget child, double t) {
return Transform.scale(scale: t, child: child);
}
In the widget state class, we listen to the fabAnimation and call setState only when the index changes.
class _TabbedFabState extends State<TabbedFab> {
int currentTab = 0;
_onTabAnimation() {
final animationIndex = widget.fabAnimation.value.index;
if (animationIndex != currentTab) {
setState(() {
currentTab = animationIndex;
});
}
}
#override
void dispose() {
widget.fabAnimation.removeListener(_onTabAnimation);
super.dispose();
}
#override
void initState() {
currentTab = widget.tabController.index;
widget.fabAnimation.addListener(_onTabAnimation);
super.initState();
}
/* build method, see below */
}
Finally, we use an AnimatedBuilder with our transform to scale the widget while the animation is running.
#override
Widget build(BuildContext context) {
// Creating fab for current index
final fab = widget.builder?.call(currentTab);
// If no fab created return nothing
if (fab == null) {
return const SizedBox.shrink();
}
return AnimatedBuilder(
animation: widget.fabAnimation,
builder: (context, child) {
// fall back to 0.0 if the animation rolled over, but we're still calling the old builder
final t = (currentTab == widget.fabAnimation.value.index)
? 1.0 - widget.fabAnimation.value.distance
: 0.0;
return widget.transform(context, child!, t);
},
child: fab,
);
}
Usage
To use it, add it to your Scaffold like so:
Widget? _createFab(index) {
return (index == 0)
? FloatingActionButton(
onPressed: () => print("Click!"),
child: const Icon(Icons.add),
)
: null;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: _tabController,
tabs: const <Tab>[
Tab(text: "Tab 1"),
Tab(text: "Tab 2")
],
)),
body: TabBarView(
controller: _tabController,
children: const <Widget>[
Center(child: Text("Tab 1")),
Center(child: Text("Tab 2")),
]),
floatingActionButton:
TabbedFab(tabController: _tabController, builder: _createFab));
}
you can use this code :
floatingActionButton: new Container(
height: 140.0,
child: new Stack(
children: <Widget>[
Align(
alignment: Alignment.bottomRight,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Container(
height: 60.0,
child: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
),
new Container(
height: 20.0,
), // a space
Container(
height: 60.0,
child: new FloatingActionButton(
onPressed: _decremenrCounter,
backgroundColor: Colors.red,
tooltip: 'Increment',
child: new Icon(Icons.remove),
),
),
],
),
)
],
),
)
screenshot :
here is all the code if you want it : main.dart
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
void _decremenrCounter() {
setState(() {
_counter--;
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: new Container(
height: 140.0,
child: new Stack(
children: <Widget>[
Align(
alignment: Alignment.bottomRight,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Container(
height: 60.0,
child: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
),
new Container(
height: 20.0,
), // a space
Container(
height: 60.0,
child: new FloatingActionButton(
onPressed: _decremenrCounter,
backgroundColor: Colors.red,
tooltip: 'Increment',
child: new Icon(Icons.remove),
),
),
],
),
)
],
),
) // This trailing comma makes auto-formatting nicer for build methods.
);
}
}