Animate tab in Tab Bar when scroll/slide tabs - flutter

I'm using flutter_animator 3.2.0 to implement animation when tapped on Tab Bar. Everything is doing fine but when scroll/slide to next Tab. The animation wouldn't work and I've tried with tab controller listener as well as listening to tabcontroller animation, but it wouldn't trigger the immediate playing of animation like when the TabBar is selected.
Edit:
https://imgur.com/a/qNNEQ2g as shown in here, when I tap on the Tab Bar, it animates nicely, while when I scroll/slide to another Tab, it doesn't work properly & instead it animates also the previous Index's tab. I would like to achieve the same way the animation plays (on tap) when scroll/slide to another Tab.
import 'dart:ui' as ui;
import 'package:flutter_animator/flutter_animator.dart';
class Playground extends StatefulWidget {
#override
_PlaygroundState createState() => _PlaygroundState();
}
class _PlaygroundState extends State<Playground> with TickerProviderStateMixin {
late TabController _tabController;
late final List<GlobalKey<AnimatorWidgetState>> _animKeys;
#override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this)
..animation!.addListener(_animationStatus);
_animKeys = List.generate(
4,
(index) => GlobalKey<AnimatorWidgetState>(),
);
}
#override
void dispose() {
_animKeys.forEach((key) {
if (key.currentState != null) {
key.currentState!.dispose();
}
});
super.dispose();
}
void _animationStatus() {
if (_tabController.animation!.status == AnimationStatus.forward) {
print('Going forward');
_animKeys[_tabController.index].currentState!.forward();
}
}
#override
Widget build(BuildContext context) {
List<Widget> _tabs = List.generate(4, (index) {
return ClipRRect(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: 100,
maxHeight: 55,
),
child: Tada(
preferences: AnimationPreferences(
duration: Duration(milliseconds: 1000),
autoPlay: AnimationPlayStates.None,
),
key: _animKeys[index],
child: CustomPaint(
painter: TabPainter(
animation: _tabController.animation!,
index: index,
),
child: Tab(
child: Text(
'Tab $index',
style: TextStyle(color: Colors.black),
),
),
),
),
),
);
});
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.blue,
elevation: 0,
title: Text(
'Tab Bar',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
),
),
centerTitle: true,
bottom: TabBar(
onTap: (index) => _animKeys[index].currentState!.forward(),
controller: _tabController,
labelPadding: EdgeInsets.only(top: 5.0, bottom: 2.0),
indicatorColor: Colors.transparent,
tabs: _tabs,
),
),
body: TabBarView(
controller: _tabController,
children: List.generate(
4,
(index) => FittedBox(
child: Text('Tab $index'),
)),
),
);
}
}
class TabPainter extends CustomPainter {
final Animation<double> animation;
final int index;
final tabPaint = Paint();
TabPainter({
required this.animation,
required this.index,
});
#override
void paint(ui.Canvas canvas, ui.Size size) {
if ((animation.value - index).abs() < 1) {
final rect = Offset.zero & size;
canvas.clipRect(rect);
canvas.translate(size.width * (animation.value - index), 0);
final tabRect =
Alignment.bottomCenter.inscribe(Size(size.width, 3), rect);
canvas.drawRect(tabRect, tabPaint..color = Colors.black);
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

Related

How do I create curved bottom navigation bar in flutter

In my ongoing project, I need a curved bottom navigation bar. I have tried with curved_navigation_bar package. The result I got that's matched 80% of my requirements. The problem I have facing I can't make the curved position transparent.
This is the picture what I'm getting
This is the picture that I need
Here I've attached two pictures, First picture is what I tried myself I indicate the curved position that I need to make transparent and want to see the bottom list view like the second attached picture. Can anyone help me to reach to requirements.
my code:
import 'package:flutter/material.dart';
import 'package:curved_navigation_bar/curved_navigation_bar.dart';
void main() => runApp(const App());
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: BottomNavBar(),
);
}
}
class BottomNavBar extends StatefulWidget {
const BottomNavBar({Key? key}) : super(key: key);
#override
_BottomNavBarState createState() => _BottomNavBarState();
}
class _BottomNavBarState extends State<BottomNavBar> {
int _page = 0;
final GlobalKey<CurvedNavigationBarState> _bottomNavigationKey = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: CurvedNavigationBar(
key: _bottomNavigationKey,
index: 0,
height: 60.0,
items: <Widget>[
bottomItem(
title: "Wish List", index: 0, icon: Icons.favorite_border),
bottomItem(title: "Home", index: 1, icon: Icons.home),
bottomItem(title: "My Cart", index: 2, icon: Icons.shopping_cart),
],
color: Colors.black,
buttonBackgroundColor: Colors.white,
backgroundColor: Colors.blue,
animationCurve: Curves.easeInOut,
animationDuration: const Duration(milliseconds: 600),
onTap: (index) {
setState(() {
_page = index;
});
},
letIndexChange: (index) => true,
),
body: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return Container(
height: 150,
color: Colors.primaries[index % Colors.primaries.length],
child: FittedBox(
child: Text(index.toString()),
),
);
}));
}
Widget bottomItem(
{required int index, required String title, required IconData icon}) {
if (index == _page) {
return Icon(
icon,
size: 26,
color: Colors.black,
);
} else {
return Padding(
padding: const EdgeInsets.only(top: 6.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 22,
color: Colors.white,
),
const SizedBox(height: 5),
Text(
title,
style: const TextStyle(color: Colors.white),
)
],
),
);
}
}
}
Make these two changes to your code:
Change the background of your CurvedNavigationBar
backgroundColor: Colors.transparent
Add this to Your Scaffold
extendBody: true,
Does backgroundColor: Colors.transparent fit your usecase?
I have used curved navigation bar. You can make background transparent but there is no option to change curved angles and space between that animation area. You can try fluid_bottom_nav_bar package.

Flutter sliver app bar with tab bar and grid view

I want to use sliver app bar with tab bar and grid view.
I tried this code:
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxScrolled) {
return <Widget>[
SliverAppBar(
backgroundColor: Colors.white,
centerTitle: true,
pinned: true,
snap: false,
floating: true,
title: Image(
image: AssetImage('assets/images/appbar_logo.png'),
width: 152,
height: 42,
),
bottom: PreferredSize(
preferredSize: Size.fromHeight(48),
child: TabBar(
isScrollable: true,
automaticIndicatorColorAdjustment: false,
indicatorSize: TabBarIndicatorSize.tab,
tabs: STR_TAB_TITLE_LIST.map((e) => Container(
padding: EdgeInsets.only(left: 4, right: 4),
child: Tab(text: e),
),
).toList(),
controller: _tabController,
),
),
),
];
},
body: TabBarView(
children: []..addAll(STR_TAB_TITLE_LIST.map((e) {
if (e == 'myWork') {
return MyWorkPage(e);
} else if (e == 'character') {
return CharactersPage(onCharacterPageItemSelected, e);
}
return TabPage(e);
})),
controller: _tabController,
),
),
),
);
}
And pages:
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.topCenter,
child: GridView.builder(
shrinkWrap: true,
padding: EdgeInsets.all(4.0),
physics: BouncingScrollPhysics(),
itemCount: SAMPLE_CARD_LIST.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
return ItemCard(SAMPLE_CARD_LIST[index]);
},
),
);
}
The problems I faced are two
It does not save page's scroll position
like this: https://github.com/flutter/flutter/issues/40740
it cuts top of page when swipe to next tab
like this: Flutter TabBar and SliverAppBar that hides when you scroll down
I tried all suggestions above links but it did not worked
How can I fix this?
Use of code:
You can make your custom appbar. I think this code will help you.
appBar: CustomTab(
onDone: (tabNo) {
},
),)
Where we have used:
import 'package:filepath/md2indicator.dart';
class CustomTab extends StatefulWidget implements PreferredSizeWidget {
final void Function(int) onDone;
CustomTab({
Key? key,
required this.onDone,
}) : super(key: key);
#override
State<CustomTab> createState() => _CustomTabState();
#override
// TODO: implement preferredSize
final Size preferredSize = const Size.fromHeight(kToolbarHeight);
}
class _CustomTabState extends State<CustomTab>
with SingleTickerProviderStateMixin {
late TabController tabcontroller;
// final GlobalKey<ScaffoldState> _key = GlobalKey<ScaffoldState>();
#override
void initState() {
// TODO: implement initState
super.initState();
tabcontroller = TabController(length: 9, vsync: this);
tabcontroller.addListener(() {
setState(() {
widget.onDone(tabcontroller.index);
tabcontroller.animateTo(tabcontroller.index);
});
});
}
#override
void dispose() {
tabcontroller.dispose();
// TODO: implement dispose
super.dispose();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: TabBar(
controller: tabcontroller,
labelStyle: TextStyle(
fontWeight: FontWeight.w700,
),
indicatorSize: TabBarIndicatorSize.label,
labelColor: Colors.green,
unselectedLabelColor: Colors.black,
isScrollable: true,
indicator: MD2Indicator(
indicatorHeight: 3,
indicatorColor: Colors.green.shade700,
indicatorSize: MD2IndicatorSize.full,
),
tabs: [
Tab(
text: 'Trending',
),
Tab(
text: 'Sports',
),
Tab(
text: 'Economy',
),
Tab(
text: 'Fashion',
),
Tab(
text: 'Entertainment',
),
Tab(
text: 'Technology',
),
Tab(
text: 'POLITICS',
),
Tab(
text: 'Viral',
),
Tab(
text: 'Videos',
)
],
),
);
}
}
And custom tab bar md2_tab_indicator style can be designed as:
import 'package:flutter/widgets.dart';
enum MD2IndicatorSize {
tiny,
normal,
full,
}
class MD2Indicator extends Decoration {
final double indicatorHeight;
final Color indicatorColor;
final MD2IndicatorSize indicatorSize;
const MD2Indicator({
required this.indicatorHeight,
required this.indicatorColor,
required this.indicatorSize,
});
#override
_MD2Painter createBoxPainter([VoidCallback? onChanged]) {
return _MD2Painter(this, onChanged);
}
}
class _MD2Painter extends BoxPainter {
final MD2Indicator decoration;
_MD2Painter(this.decoration, VoidCallback? onChanged) : super(onChanged);
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
assert(configuration.size != null);
Rect rect;
if (decoration.indicatorSize == MD2IndicatorSize.full) {
rect = Offset(
offset.dx,
configuration.size!.height - decoration.indicatorHeight,
) &
Size(configuration.size!.width, decoration.indicatorHeight);
} else if (decoration.indicatorSize == MD2IndicatorSize.tiny) {
rect = Offset(
offset.dx + configuration.size!.width / 2 - 8,
configuration.size!.height - decoration.indicatorHeight,
) &
Size(16, decoration.indicatorHeight);
} else {
rect = Offset(
offset.dx + 6,
configuration.size!.height - decoration.indicatorHeight,
) &
Size(configuration.size!.width - 12, decoration.indicatorHeight);
}
final Paint paint = Paint()
..color = decoration.indicatorColor
..style = PaintingStyle.fill;
canvas.drawRRect(
RRect.fromRectAndCorners(
rect,
topRight: Radius.circular(8),
topLeft: Radius.circular(8),
),
paint,
);
}
}```

Flutter: persistent bottom navigation bar rebuilds ALL pages when navigating between pages

I am using the persistent_bottom_nav_bar package and have implemented its custom navigation bar (basically just customized from the example in their Readme page). Reproducible code below.
The issue: when you navigate with the bottom navigation, ALL the pages rebuild on every tap. Quite draining on the app's performance! This seems to be a Flutter issue in general and solutions are given by using e.g. an IndexedStack when the full code is written by oneself instead of using a package, which I have done.
Is there any way to fix this issue when using the persistent_bottom_nav_bar package and specifically with the custom code that I have used?
My code (simplified so that anyone can just copy & run it):
main.dart
import 'package:flutter/material.dart';
import 'package:persistent_bottom_nav_bar/persistent-tab-view.dart';
import 'page1.dart';
import 'page2.dart';
import 'page3.dart';
import 'page4.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo',
home: HomeScaffold(),
);
}
}
class HomeScaffold extends StatefulWidget {
#override
_HomeScaffoldState createState() => _HomeScaffoldState();
}
class _HomeScaffoldState extends State<HomeScaffold> {
PersistentTabController _controller;
#override
void initState() {
super.initState();
_controller = PersistentTabController(initialIndex: 0);
}
List<Widget> _buildScreens() {
return [
Page1(),
Page2(),
Page3(),
Page4(),
];
}
List<PersistentBottomNavBarItem> _navBarsItems() {
return [
_buildBottomNavBarItem('Page 1', Icons.home),
_buildBottomNavBarItem('Page 2', Icons.search),
_buildBottomNavBarItem('Page 3', Icons.message),
_buildBottomNavBarItem('Page 4', Icons.settings),
];
}
#override
Widget build(BuildContext context) {
return PersistentTabView.custom(
context,
controller: _controller,
screens: _buildScreens(),
confineInSafeArea: true,
itemCount: 4,
handleAndroidBackButtonPress: true,
stateManagement: true,
screenTransitionAnimation: ScreenTransitionAnimation(
animateTabTransition: true,
curve: Curves.ease,
duration: Duration(milliseconds: 200),
),
customWidget: CustomNavBarWidget(
items: _navBarsItems(),
onItemSelected: (index) {
setState(() {
_controller.index = index;
});
},
selectedIndex: _controller.index,
),
// ),
);
}
}
class CustomNavBarWidget extends StatelessWidget {
final int selectedIndex;
final List<PersistentBottomNavBarItem> items;
final ValueChanged<int> onItemSelected;
CustomNavBarWidget({
Key key,
this.selectedIndex,
#required this.items,
this.onItemSelected,
});
Widget _buildItem(PersistentBottomNavBarItem item, bool isSelected) {
return Container(
alignment: Alignment.center,
height: kBottomNavigationBarHeight,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Flexible(
child: IconTheme(
data: IconThemeData(
size: 26.0,
color: isSelected
? (item.activeColorSecondary == null
? item.activeColorPrimary
: item.activeColorSecondary)
: item.inactiveColorPrimary == null
? item.activeColorPrimary
: item.inactiveColorPrimary),
child: isSelected ? item.icon : item.inactiveIcon ?? item.icon,
),
),
Padding(
padding: const EdgeInsets.only(top: 5.0),
child: Material(
type: MaterialType.transparency,
child: FittedBox(
child: Text(
item.title,
style: TextStyle(
color: isSelected
? (item.activeColorSecondary == null
? item.activeColorPrimary
: item.activeColorSecondary)
: item.inactiveColorPrimary,
fontWeight: FontWeight.w400,
fontSize: 12.0),
)),
),
)
],
),
);
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Container(
width: double.infinity,
height: kBottomNavigationBarHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: items.map((item) {
int index = items.indexOf(item);
return Flexible(
child: GestureDetector(
onTap: () {
this.onItemSelected(index);
},
child: _buildItem(item, selectedIndex == index),
),
);
}).toList(),
),
),
);
}
}
PersistentBottomNavBarItem _buildBottomNavBarItem(String title, IconData icon) {
return PersistentBottomNavBarItem(
icon: Icon(icon),
title: title,
activeColorPrimary: Colors.indigo,
inactiveColorPrimary: Colors.grey,
);
}
try AutomaticKeepAliveClientMixin, this won't refresh page while change tab:
1.
class PageState extends State<Page> with AutomaticKeepAliveClientMixin {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);

Flutter Listview swipe to change TabBar index

I have implemented TabBar without TabBarView. I am using a single ListView as body since the layout after selecting a tab is same for all tabs.
What I want to achieve is, change the tab while swiping left / right in the listview. How can I do this?
TabBar
TabBar(
indicatorWeight: 3,
indicatorSize: TabBarIndicatorSize.label,
onTap: (index) {
categoryId = newsProvider.categories[index].id;
page = 1;
fetchPosts(newsProvider);
},
isScrollable: true,
tabs: [
for (Category category in newsProvider.categories)
Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
child: Text(
category.name,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
),
],
),
body
ListView.builder(
padding: EdgeInsets.only(bottom: 60),
physics: BouncingScrollPhysics(),
controller: _scrollController,
itemCount: newsProvider.posts.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) =>
HabaruDetails(newsProvider.posts[index]),
));
},
child: Container(
height: 200,
margin: EdgeInsets.only(left: 10, right: 10, top: 10),
child: ClipRRect(
borderRadius: BorderRadius.circular(7),
child: Stack(
children: [
Positioned.fill(
child: Hero(
tag: newsProvider.posts[index].id,
child: FadeInImage.memoryNetwork(
placeholder: kTransparentImage,
fit: BoxFit.cover,
image: newsProvider
.posts[index].betterFeaturedImage.mediumLarge),
),
),
Container(
height: 200,
decoration: BoxDecoration(
color: Colors.white,
gradient: LinearGradient(
begin: FractionalOffset.topCenter,
end: FractionalOffset.bottomCenter,
colors: [
Colors.black.withOpacity(0.0),
Colors.black.withOpacity(0.95),
],
stops: [
0.0,
1.0
])),
),
Positioned.fill(
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 30),
child: Text(
newsProvider.posts[index].title.rendered,
textAlign: TextAlign.center,
textDirection: TextDirection.rtl,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
height: 1.7,
),
),
),
),
),
],
),
),
),
);
},
)
I think you still must use a TabBarView, but you can generate its children dynamically based on the categories list like below.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [...],
),
),
body: TabBarView(
children: newsProvider.categories.map(
(e) => ListView.builder(...).toList(),
),
),
),
),
);
}
From what I can tell, I think you're trying to create a tab bar that moves when the user scrolls down on the listview? If so, I have created an example of a class that uses a selection of tabs you can implement as a TabBar for your title in your AppBar whose controller is set to the TabBarController I have created above the build method. The listview is then set to the controller of the scrollcontroller which listens to the state of the tabbarcontroller.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
TabController _tabController;
ScrollController _scrollController;
void _scrollListener() {
var index = (_scrollController.offset / 70).round();
if(index >= choices.length){
index = choices.lastIndexOf(choices.last);
}
if(index <= 0){
index = 0;
}
if (index == choices.length)
index = choices.length;
if(mounted){
setState(() {
_tabController.animateTo(index, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
});
}
}
#override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: choices.length);
_scrollController = ScrollController();
_scrollController.addListener(_scrollListener);
}
#override
void dispose() {
super.dispose();
_tabController.dispose();
_scrollController.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: TabBar(
labelColor: Colors.black,
unselectedLabelColor: Colors.black45,
labelStyle: TextStyle(fontWeight: FontWeight.w700, fontFamily: 'Valera'),
controller: _tabController,
isScrollable: true,
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(50),
color: Colors.transparent),
onTap: (int index){
setState(() {
_scrollController.animateTo(index*80.0, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
});
},
tabs: choices.map((Choice choice) {
return Padding(
padding: const EdgeInsets.only(top: 20.0),
child: Tab(
text: choice.title,
),
);
}).toList(),
),
),
body: Container(
child: ListView.builder(
controller: _scrollController,
itemBuilder: (context, index){
return ListTile(
);
}),
),
);
}
}
class Choice {
const Choice({this.title});
final String title;
}
const List<Choice> choices = const <Choice>[
const Choice(title: 'First Tab'),
const Choice(title: 'Second Tab'),
const Choice(title: 'Third Tab'),
const Choice(title: 'Fourth Tab'),
const Choice(title: 'Fifth Tab'),
const Choice(title: 'Sixth Tab'),
];
class ChoiceCard extends StatelessWidget {
const ChoiceCard({Key key, this.choice}) : super(key: key);
final Choice choice;
#override
Widget build(BuildContext context) {
return Text(choice.title, style: TextStyle(
color: Colors.black,
),
);
}
}
You can do this by TabController and Listener
First you can define a TabController. It need to be in StatefulWidget.
TabController _tabController;
#override
void initState() {
super.initState();
_tabController = TabController(length: tabLenght, vsync: this);
}
TabBar(
...
controller: _tabController,
...
)
Then you can add Listener class to the ListView. Save the clicked position and swiping distance in onPointerDown,onPointerMove,onPointerUp and
onPointerCancel (same as onPointerUp).
Change the tab by control the offset and index. Notice offset is in range (-1.0,1.0). You should animate by yourself if it reach the next index.
double startX;
double sensitivity = 0.01;
...
child: Listener(
onPointerDown: (event) {
startX = event.position.dx;
},
onPointerMove: (event) {
double newX = event.position.dx;
double offset = ((startX - newX) * sensitivity).clamp(-1.0, 1.0);
if (!_tabController.indexIsChanging)
_tabController.offset = offset;
if (offset == 1.0 &&
_tabController.index < _tabController.length - 1) {
_tabController.animateTo(_tabController.index + 1);
startX = newX;
}
if (offset == -1.0 && _tabController.index > 0) {
_tabController.animateTo(_tabController.index - 1);
startX = newX;
}
},
onPointerUp: (_) {
if (_tabController.offset > 0.5 &&
_tabController.index < _tabController.length - 1)
_tabController.animateTo(_tabController.index + 1);
else if (_tabController.offset < -0.5 && _tabController.index > 0)
_tabController.animateTo(_tabController.index - 1);
else {
if (!_tabController.indexIsChanging)
_tabController.offset = 0;
}
},
onPointerCancel: (_) {
if (_tabController.offset > 0.5 &&
_tabController.index < _tabController.length - 1)
_tabController.animateTo(_tabController.index + 1);
else if (_tabController.offset < -0.5 && _tabController.index > 0)
_tabController.animateTo(_tabController.index - 1);
else {
if (!_tabController.indexIsChanging)
_tabController.offset = 0;
}
},
child: ListView.builder(
...
I think you want the drag effect like TabBarView? You can also check the source code inside TabBarView.

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.
);
}
}