Selective FloatingActionButton visibility in Flutter tabs - flutter

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?

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

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:

Related

how to achieve a functionality like linear loading bar which will load up as user move between various screens

I am using android studio and flutter. I want to build the screen as shown below in the image:screen Image
let's say I have 4 screens. on the first screen, the bar will load up to 25%. the user will move to next screen by clicking on continue, the linearbar will load up to 50% and so on. the user will get back to previous screens by clicking on the back button in the appbar.
I tried stepper but it doesn't serve my purpose.
You can use the widget LinearProgressIndicator(value: 0.25,) for the first screen and with value: 0.5 for the second screen etc.
If you want to change the bar value within a screen, just use StatefullWidget's setState(), or any state management approaches will do.
import 'package:flutter/material.dart';
class ProgressPage extends StatefulWidget {
const ProgressPage({super.key});
#override
State<ProgressPage> createState() => _ProgressPageState();
}
class _ProgressPageState extends State<ProgressPage> {
final _pageController = PageController();
final _pageCount = 3;
int? _currentPage;
double? _screenWidth;
double? _unit;
double? _progress;
#override
void initState() {
super.initState();
_pageController.addListener(() {
_currentPage = _pageController.page?.round();
setState(() {
_progress = (_currentPage! + 1) * _unit!;
});
});
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
_screenWidth = MediaQuery.of(context).size.width;
_unit = _screenWidth! / _pageCount;
_progress ??= _unit;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('HOZEROGOLD')),
body: Column(
children: [
Align(
alignment: Alignment.topLeft,
child: Container(
color: Colors.yellow,
height: 10,
width: _progress,
),
),
Expanded(
child: PageView(
controller: _pageController,
children: _createPage(),
),
),
],
),
);
}
List<Widget> _createPage() {
return List<Widget>.generate(
_pageCount,
(index) => Container(
color: Colors.white,
child: Center(
child: ElevatedButton(
onPressed: () => _moveNextPage(),
child: Text('NEXT $index'),
),
),
),
);
}
void _moveNextPage() {
if (_pageController.page!.round() == _pageCount-1) {
_pageController.jumpToPage(0);
} else {
_pageController.nextPage(
curve: Curves.bounceIn,
duration: const Duration(milliseconds: 100));
}
}
}
HAPPY CODING! I hope it will be of help.

flutter tabbar swippable issue

i am newbie to flutter and i created a tab bar and tab bar view by adding dynamic content as following code. when click on tab in tab bar it works fine. but swiping tab bar view new value always detect on another swipe for example move to tab one tab bar view says 0. move to tab 2 tab bar view says 1.
please help me friends.
#override
void initState() {
super.initState();
setState(() {
_isLoading = true;
num = 0;
for(int i=0;i<3;i++){
_tabs.add(Tab(text: '${i + 1}'));
tabView.add(getWidget());
}
_tabController = TabController(vsync: this, length: _tabs.length, initialIndex: 0);
_tabController.addListener((){
print('index${_tabController.index}');
setState(() {
num = _tabController.index ;
});
});
Widget getWidget(){
return Builder(builder: (BuildContext context) {
return Text('${num}');
});
}
#override
Widget build(BuildContext context) {
return
DefaultTabController(
length: _tabs.length,
child:Scaffold(
appBar: AppBar(
title: Text("cart"),
bottom: TabBar(
controller: _tabController,
tabs: _tabs,
),
),
body: TabBarView(
controller: _tabController,
children:tabView,
),
});
Use AutomaticKeepAliveClientMixin
override wantKeepAlive property and return true
Instead of this:
tabView.add(getWidget());
use:
tabView.add(Tabpageview(TabData(i)));
class Tabpageview extends StatefulWidget {
final tabData;
Tabpageview(this.tabData);
#override
_TabpageviewState createState() => _TabpageviewState();
}
class _TabpageviewState extends State<Tabpageview>
with AutomaticKeepAliveClientMixin {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return Tab(child: Center(child: Text('Tab : ${widget.tabData.data}')));
}
}
class TabData {
int data;
TabData(this.data);
}
Modify your code with some smooth animation, plz modify setState() method as per your need.
final aniValue = _tabController.animation.value;
if (aniValue > 0.5 && index != 1) {
products.forEach((item) {
setState(() {
displayProducts.add(item);
});
});
} else if (aniValue <= 0.5 && index != 0) {
products.forEach((item) {
setState(() {
displayProducts.add(item);
});
});
}

How to "merge" scrolls on a TabBarView inside a PageView?

I have an app that uses a PageView on its main page. Today, I got assigned to insert a TabBarView in one of these pages. The problem is that when I scroll the between the tabs when in the last tab, scrolling to the left won't scroll the PageView.
I need a way to make the scroll of page view scroll when at the start or end of the tabbarview.
I found a question with the inverted problem: flutter PageView inside TabBarView: scrolling to next tab at the end of page
However, the method stated there is not suitable to my issue.
I made a minimal example:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) => MaterialApp(
title: 'TabBarView inside PageView',
home: MyHomePage(),
);
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final PageController _pageController = PageController();
#override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text('TabBarView inside PageView'),
),
body: PageView(
controller: _pageController,
children: <Widget>[
Container(color: Colors.red),
GreenShades(),
Container(color: Colors.yellow),
],
),
);
}
class GreenShades extends StatefulWidget {
#override
_GreenShadesState createState() => _GreenShadesState();
}
class _GreenShadesState extends State<GreenShades>
with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
this._tabController = TabController(length: 3, vsync: this);
super.initState();
}
#override
Widget build(BuildContext context) => Column(
children: <Widget>[
TabBar(
labelColor: Colors.green,
indicatorColor: Colors.green,
controller: _tabController,
tabs: <Tab>[
const Tab(text: "Dark"),
const Tab(text: "Normal"),
const Tab(text: "Light"),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: <Widget>[
Container(color: Colors.green[800]),
Container(color: Colors.green),
Container(color: Colors.green[200]),
],
),
)
],
);
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
}
Note that, in this MRE, it's possible to reach the 3rd page if you drag the TabBar, but not if you drag the TabBarView.
How may I achieve this behavior?
Edit:
As stated by #Fethi, there's a similar question:
Is it possible to swipe from an TabBarView content area to an adjacent PageView page?
However, the question was not answered satisfactorily, as the solution given does not really "blend" the scroll, although the behavior is similar to what was described. It doesn't scroll naturally.
This is possible by using the PageController.postion attribute's drag method, which internally drags the ScrollPosition of the screen. This way, user can intuitively drag the pages like drag halfway and then leave or continue fully.
The idea is inspired from the other post to use the OverScrollNotification but add rather more step to continue intuitive dragging.
Collect the DragstartDetail when user starts scrolling.
Listen for OverScrollNotification and start the draging and at the same time update the drag using the drag.update with the DragUpdateDetails from OverscrollNotification method.
On ScrollEndNotification cancel the the drag.
To keep the idea simple I am pasting only build method of the Tabs page.
A fully working example is available in this dart pad.
#override
Widget build(BuildContext context) {
// Local dragStartDetail.
DragStartDetails dragStartDetails;
// Current drag instance - should be instantiated on overscroll and updated alongside.
Drag drag;
return Column(
children: <Widget>[
TabBar(
labelColor: Colors.green,
indicatorColor: Colors.green,
controller: _tabController,
tabs: <Tab>[
const Tab(text: "Dark"),
const Tab(text: "Normal"),
const Tab(text: "Light"),
],
),
Expanded(
child: NotificationListener(
onNotification: (notification) {
if (notification is ScrollStartNotification) {
dragStartDetails = notification.dragDetails;
}
if (notification is OverscrollNotification) {
drag = _pageController.position.drag(dragStartDetails, () {});
drag.update(notification.dragDetails);
}
if (notification is ScrollEndNotification) {
drag?.cancel();
}
return true;
},
child: TabBarView(
controller: _tabController,
children: <Widget>[
Container(color: Colors.green[800]),
Container(color: Colors.green),
Container(color: Colors.green[200]),
],
),
),
),
],
);
}
Old Answer
The above might not handle some edge cases. If you need more control below code provides the same result but you can handle UserScrollNotification. I am pasting this because, it might be useful for others who would like to know which direction the use is scrolling w.r.t the Axis of the ScrollView.
if (notification is ScrollStartNotification) {
dragStartDetails = notification.dragDetails;
}
if (notification is UserScrollNotification &&
notification.direction == ScrollDirection.forward &&
!_tabController.indexIsChanging &&
dragStartDetails != null &&
_tabController.index == 0) {
_pageController.position.drag(dragStartDetails, () {});
}
// Simialrly Handle the last tab.
if (notification is UserScrollNotification &&
notification.direction == ScrollDirection.reverse &&
!_tabController.indexIsChanging &&
dragStartDetails != null &&
_tabController.index == _tabController.length - 1) {
_pageController.position.drag(dragStartDetails, () {});
}
so you want to scroll the page view to the left when you reach the end of tabs and the same goes to scrolling to the right when on the first tab, what i have been thinking about is manually swipe the page view when in those cases as follow:
index value should the index of page that comes before the tab bar page and after it.
pageController.animateToPage(index,
duration: Duration(milliseconds: 500), curve: Curves.ease);
here is a complete code of what you are looking for, hopefully this helps!
I have a different approach using Listener Widget and TabView physics as show below:
//PageView Widget
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
children: [
Widge1()
TabBarWidget(),
Widget2()
]
)
)
}
//TabBar Widget
final _physycsNotifier = ValueNotifier<bool>(false);
....
....
#override
Widget build(BuildContext context) {
return Column(
children: [
TabBar(
controller: _tabController,
//... other properties
)
Expanded(
child: Listener(
onPointerMove: (event) {
final offset = event.delta.dx;
final index = _tabController.index;
//Check if we are in the first or last page of TabView and the notifier is false
if(((offset > 0 && index == 0) || (offset < 0 && index == _categories.length - 1)) && !_physycsNotifier.value){
_physycsNotifier.value = true;
}
},
onPointerUp: (_) => _physycsNotifier.value = false;
child: ValueListenableBuilder<bool>(
valueListenable: _physycsNotifier,
builder: (_, value, __) {
return TabBarView(
controller: _tabController,
physics: value ? NeverScrollableScrollPhysics() : null,
children: List.generate(_categories.length, (index) {
return _CategoryTab(index: index);
})
);
},
),
)
)
]
)
}
this works fine if you set default physics for PageView and TabView (it means null) if you set other physisc like BouncingScrollPhsysisc there will be some bugs, but i think this is good workaround.

how to navigate different pages by curved navigation bar in flutter?

i can't able to use curved navigation bar in flutter, when i slide screen so buttons of curved navigation bar are also moving but when i tap on buttons of curved navigation bar nothing happens . i think onTap() didn't work properly. how to navigate pages when i tap buttons?
here is the code of my program=>
static final String id = 'profile_page';
#override
_PagesState createState() => _PagesState();
}
class _PagesState extends State<Pages> {
PageController _pageController;
int _Page=0;
#override
void initState() {
super.initState();
_pageController = PageController();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: _pageController,
children: <Widget>[
Search(),
Trending(),
Friends(),
Profile(),
],
onPageChanged: (int index) {
setState(() {
_pageController.jumpToPage(index);
});
}
),
bottomNavigationBar: CurvedNavigationBar(
animationCurve: Curves.easeInOutBack,
index:3,
items: <Widget>[
Icon(Icons.search, size: 30, color: Colors.white, ),
Icon(Icons.trending_up, size: 30, color: Colors.white),
Icon(Icons.group, size: 30, color: Colors.white),
Icon(Icons.person, size: 30, color: Colors.white)
],
color: Colors.blueAccent,
backgroundColor: Colors.white,
height: 60.0,
onTap: (int index) {
setState(() {
_pageController.jumpToPage(index);
});
},
),
);
}
}
I use it this way:
Create a list and a variable that holds current Page no. Here 3 classes can be stateless or statefull widgets.
final List<Widget> _tabItems = [Class1(), Class2(), Class3()];
int _activePage = 0;
Define your body like this:
body: _tabItems[_activePage], //Customise it as you want.
Then in your onTap:
onTap: (index) {
setState(() {
_activePage = index;
});
},
Hope it helps! Happy Coding:)
If you change the page of the PageView, you are telling the CurvedNavigationBar to change its page. But when you change the page of the CurvedNavigationBar you aren't telling the PageView to change its page.
You need to add a PageController to the PageView, like this:
final _pageController = PageController();
PageView(
controller: _pageController,
...
Then you should be able to do this:
_pageController.jumpToPage(index);
But make sure when you tell one to change the page of the other, the other doesn't tell again the first one to change its page, because it will be an infinite loop.
Simply replace
_pageController = PageController(); with
final _pageController = PageController();
and remove _pageController = PageController(); in the void initState() method.
No use of int _Page=0;
You will be fine.

Flutter tabcontroller index does not respond to changes in the tabbarview

Flutter tabcontroller detects the change in the tabbar but does not know the change in the tabbarview.
Listener causes the text of the floatingactionbutton to change, but there is no response when the tabbarview changes.
class TabPageState extends State<TabPage> with SingleTickerProviderStateMixin {
TabController _controller;
int _currentIndex = 0;
#override
void initState() {
super.initState();
_controller = TabController(vsync: this, length: 2);
_controller.addListener(_handleTabSelection);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Tab'),
bottom: TabBar(
controller: _controller,
tabs: <Widget>[
Tab(icon: Icon(Icons.laptop_mac),),
Tab(icon: Icon(Icons.desktop_mac),),
],
),
),
body: TabBarView(
controller: _controller,
children: <Widget>[
Center(child: Text('laptop'),),
Center(child: Text('desctop'),),
],
),
floatingActionButton: FloatingActionButton(
onPressed: (){},
child: Text('$_currentIndex'),
),
);
}
_handleTabSelection() {
if (_controller.indexIsChanging) {
setState(() {
_currentIndex = _controller.index;
});
}
}
}
just remove the condition :
if (_controller.indexIsChanging) {
Because every time you start changing from previousIndex to the currentIndex, you rebuild the widget and your _controller.index is the same as your initial index.
This should work :
_handleTabSelection() {
setState(() {
_currentIndex = _controller.index;
});
}
Doc Says:
indexIsChanging : True while we're animating from [previousIndex] to [index] as a
consequence of calling [animateTo]. 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 (and "flinging") the [TabBarView].
bool get indexIsChanging => _indexIsChangingCount != 0;
int _indexIsChangingCount = 0;
Code:
TabController _controller;
int _selectedIndex = 0;
List<Widget> list = [
Tab(icon: Icon(Icons.card_travel)),
Tab(icon: Icon(Icons.add_shopping_cart)),
];
#override
void initState() {
// TODO: implement initState
super.initState();
// Create TabController for getting the index of current tab
_controller = TabController(length: list.length, vsync: this);
_controller.addListener(() {
setState(() {
_selectedIndex = _controller.index;
});
print("Selected Index: " + _controller.index.toString());
});
}
Sample: https://github.com/jitsm555/Flutter-Problems/tree/master/tab_bar_tricks
Output: