I want to do when I click on each chip, it will change the content of my body but I am using tab controller which I wrap with chip widget.
I also want to decorate my chip when it is selected and unselected. I try to declare my text for each widget using list array but I am stuck. Can someone help me. This is what I have been done so far
class YearTab extends StatefulWidget {
const YearTab({
Key? key,
}) : super(key: key);
#override
State<YearTab> createState() => _YearTabState();
}
class _YearTabState extends State<YearTab>
with SingleTickerProviderStateMixin {
late TabController _controller;
bool _selectedTab = false;
List<Widget> list = const [
Chip(label: Text('This year')),
Chip(label: Text('2021')),
Chip(label: Text('2020')),
Chip(label: Text('2019')),
Chip(label: Text('2018')),
];
#override
void initState() {
super.initState();
_controller = TabController(length: list.length, vsync: this);
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Container(
color: AppColor.white,
width: MediaQuery.of(context).size.width,
child: TabBar(
// unselectedLabelColor: Colors.yellow,
// labelColor: Colors.red,
physics: const BouncingScrollPhysics(),
indicator: BoxDecoration(
border: Border.all(color: Colors.red),
borderRadius: BorderRadius.circular(10),
color: const Color.fromARGB(255, 176, 208, 255)
// color: !widget.selected
// ? Color.fromARGB(255, 176, 208, 255)
// : Colors.transparent
),
controller: _controller,
onTap: (index) {},
isScrollable: true,
tabs: list,
)),
SizedBox(
height: MediaQuery.of(context).size.height * 2.2,
child: TabBarView(
controller: _controller,
children: const [
//Content for Demografi Pengguna
Content1(),
Content2(),
Content3(),
Content4(),
Content5(),
],
),
)
],
);
}
}
Instead of Tabview ,you can use this :
dependencies:
toggle_switch: ^2.0.1
Example:
ToggleSwitch(
minWidth: 90.0,
initialLabelIndex: 1,
cornerRadius: 20.0,
activeFgColor: Colors.white,
inactiveBgColor: Colors.grey,
inactiveFgColor: Colors.white,
totalSwitches: 2,
labels: ['Tab1', 'Tab2'],
icons: [FontAwesomeIcons.mars, FontAwesomeIcons.venus],
activeBgColors: [[Colors.blue],[Colors.pink]],
onToggle: (index) {
print('switched to: $index');
//change the view as per index
},
),
When you request a chip widget, it is called in initState() the first time you call the widget. You can initialize the array from there and get the data. Alternatively, you can call the widget by observing the array using the Stream structure using getX or Provider.
i changed a few things
class YearTab extends StatefulWidget {
const YearTab({
Key? key,
}) : super(key: key);
#override
State<YearTab> createState() => _YearTabState();
}
class _YearTabState extends State<YearTab> with SingleTickerProviderStateMixin {
List<Widget> tabs = const [
Chip(label: Text('This year')),
Chip(label: Text('2021')),
Chip(label: Text('2020')),
Chip(label: Text('2019')),
Chip(label: Text('2018')),
];
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: tabs.length,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
indicatorColor: Theme.of(context).colorScheme.secondary,
tabs: tabs,
),
),
body: TabBarView(
children: [
//Content for Demografi Pengguna
Container(
color: Colors.blueAccent,
),
Container(
color: Colors.red,
),
Container(
color: Colors.green,
),
Container(
color: Colors.yellow,
),
Container(
color: Colors.grey,
),
],
),
),
);
}
}
Related
I want something like this.
Im able to implement something similar with SwitchTab,but not able to use gradient color for selected tab.
Please help
SwitchTab(
text: const [ "Personal",
"Group",
],
selectedTextColor: Colors.black,
unselectedTextColor:Colors.white,
shape: SwitchTabShape.rounded,
thumbColor: Colors.white,
backgroundColour:
Color.fromARGB(255, 31, 89, 169),
onValueChanged: (index) {
setState(() {
selected = index;
});
},
),
You can use a combination of a normal TabBar, BoxDecoration and LinearGradient for this:
Code Example:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
MyWidgetState createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin {
late TabController _tabController;
#override
void initState() {
_tabController = TabController(length: 2, vsync: this);
super.initState();
}
#override
void dispose() {
super.dispose();
_tabController.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Stack Overflow Example'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Container(
height: 45,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(
25.0,
),
),
child: TabBar(
controller: _tabController,
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(
25.0,
),
color: Colors.green,
gradient: LinearGradient(
begin: Alignment.topRight,
end: Alignment.bottomLeft,
colors: [
Colors.pink,
Colors.deepPurple,
],
),
),
labelColor: Colors.white,
unselectedLabelColor: Colors.black,
tabs: [
const Tab(text: 'Personal'),
const Tab(text: 'Group'),
],
),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
Center(child: Text('Personal Content')),
Center(child: Text('Group Content')),
],
),
),
],
),
),
);
}
}
I want to make a custom tab widget and set a list of this widgets to a tab bar. But tab bar can't take all space and some space will remain. I try to wrap it with PreferredSize but it doesn't work .
The tab bar (ScrollTabBar) :
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(0.0, 0.75),
),
],
borderRadius: BorderRadius.circular(widget.borderRadiusT),
color: Colors.red,
),
height: widget.tabHeight,
child: PreferredSize(
preferredSize: Size.fromHeight(widget.tabHeight),
child: TabBar(
indicator: UnderlineTabIndicator(
borderSide: BorderSide(
color: widget.indicatorColor,
width: widget.indicatorWheight,
),
insets: EdgeInsets.symmetric(
horizontal: widget.horizontalPadding,
),
),
indicatorWeight: widget.indicatorWheight,
indicatorColor: widget.indicatorColor,
labelPadding: EdgeInsets.only(
bottom: 0,
right: widget.horizontalPadding,
left: widget.horizontalPadding,
),
labelColor: widget.activeTextColor,
unselectedLabelColor: widget.diactiveTextColor,
controller: widget.tabController,
tabs: widget.tabList,
isScrollable: true,
),
),
),
Expanded(
child: TabBarView(
controller: widget.tabController,
children: [
for (var builder in widget.screenList) builder.call(context)
],
),
),
],
),
);
}
tabList is list of FTabComp :
FTabComp(
String title,
Key key,
ScrollTabBar parent, {
bool haveDivider = true,
}) {
return Tab(
key: key,
child: Container(
color: Colors.blue,
width: parent.tabLength,
child: Stack(
clipBehavior: Clip.none,
children: [
Align(
alignment: Alignment.center,
child: Text(title),
),
haveDivider
? Positioned.fill(
left: parent.tabLength * - 1.5,
child: SizedBox(
height: double.maxFinite,
child: VerticalDivider(
color: parent.outerBackgroundColor,
thickness: parent.dviderWidth,
),
),
)
: Center()
,
],
),
),
);
}
Container are red . Tabs are blue , if you solve this , I will say thank you.
Image
To make the TabBar use the maximum width of the screen you should remove the isScrollable property or set it to false. This way the TabBar is going to fill the entire width of the screen and resize each tab accordingly. From the docs, each tab gets an equal share of the available space). Also, the tabLength should be removed as it doesn't make sense anymore.
Now, the position of VerticalDivider should be calculated. It should take into account the screen width, the width of the tab, and also the padding between tabs. And it should be places in a Stack with the TabBar to make it the same height as the TabBar itself.
The algorithm to calculate the tab width can be something like this:
double width = MediaQuery.of(context).size.width;
double tabLength = (width -
horizontalPadding * (tabCount + 1) -
horizontalPadding * (tabCount - 1)) /
tabCount;
Below are the screenshots of the final result. Take a look also on the live demo on DartPad (Wait a little to run):
Small
Medium
Large
Take a look at the changed code below:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
late TabController tabController;
late TabController tabController2;
#override
void initState() {
// TODO: implement initState
super.initState();
tabController = TabController(length: 4, vsync: this);
tabController2 = TabController(length: 3, vsync: this);
}
#override
Widget build(BuildContext context) {
double horizontalPadding = 8;
return Scaffold(
body: ScrollTabBar(
tabController: tabController,
horizontalPadding: horizontalPadding,
tabList: const [
FTabComp(
title: 'secKey',
key: ValueKey('tab1'),
),
FTabComp(
title: 'firstKey',
key: ValueKey('tab2'),
),
FTabComp(
title: 'otherKey',
key: ValueKey('tab3'),
),
FTabComp(
title: 'anotherKey',
key: ValueKey('tab4'),
),
],
screenList: [
(context) => const Text('Tab 1'),
(context) => const Text('Tab 2'),
(context) => const Text('Tab 3'),
(context) => const Text('Tab 4'),
],
),
);
}
}
class ScrollTabBar extends StatefulWidget {
final double borderRadiusT;
final double tabHeight;
final Color indicatorColor;
final Color activeTextColor;
final Color diactiveTextColor;
final double indicatorWheight;
final double horizontalPadding;
final Color outerBackgroundColor;
final double dviderWidth;
final TabController? tabController;
final List<Widget> tabList;
final List<Widget Function(BuildContext)> screenList;
final bool haveDivider;
const ScrollTabBar({
Key? key,
this.borderRadiusT = 4,
this.tabHeight = 48,
this.indicatorColor = Colors.blue,
this.activeTextColor = Colors.black,
this.diactiveTextColor = Colors.white38,
this.indicatorWheight = 8,
this.horizontalPadding = 8,
required this.tabController,
required this.tabList,
required this.screenList,
this.outerBackgroundColor = Colors.black,
this.dviderWidth = 4,
this.haveDivider = true,
}) : super(key: key);
#override
State<ScrollTabBar> createState() => _ScrollTabBarState();
}
class _ScrollTabBarState extends State<ScrollTabBar> {
#override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
double tabLength = (width -
widget.horizontalPadding * (widget.tabList.length + 1) -
widget.horizontalPadding * (widget.tabList.length - 1)) /
widget.tabList.length;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Container(
decoration: BoxDecoration(
boxShadow: const [
BoxShadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(0.0, 0.75),
),
],
borderRadius: BorderRadius.circular(widget.borderRadiusT),
color: Colors.red,
),
height: widget.tabHeight,
child: Stack(
children: [
TabBar(
indicator: UnderlineTabIndicator(
borderSide: BorderSide(
color: widget.indicatorColor,
width: widget.indicatorWheight,
),
insets: EdgeInsets.symmetric(
horizontal: widget.horizontalPadding,
),
),
indicatorWeight: widget.indicatorWheight,
indicatorColor: widget.indicatorColor,
labelPadding: EdgeInsets.only(
bottom: 0,
right: widget.horizontalPadding,
left: widget.horizontalPadding,
),
labelColor: widget.activeTextColor,
unselectedLabelColor: widget.diactiveTextColor,
controller: widget.tabController,
tabs: widget.tabList,
),
for (int i = 1; i < widget.tabList.length; i++)
Positioned.fill(
left: (widget.horizontalPadding + tabLength + widget.horizontalPadding) * i - (widget.dviderWidth / 2),
right: width,
child: SizedBox(
height: widget.tabHeight,
child: VerticalDivider(
color: widget.outerBackgroundColor,
thickness: widget.dviderWidth,
),
),
),
],
),
),
Expanded(
child: TabBarView(
controller: widget.tabController,
children: [
for (var builder in widget.screenList) builder.call(context)
],
),
),
],
),
);
}
}
class FTabComp extends StatelessWidget {
final String title;
const FTabComp({
Key? key,
required this.title,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Tab(
key: key,
child: Container(
color: Colors.blue,
child: Stack(
clipBehavior: Clip.none,
children: [
Align(
alignment: Alignment.center,
child: Text(title),
),
],
),
),
);
}
}
Inside the Stack, you use Positioned.fill. try removing it
I'm trying to nest a tabview in a Scrollview, and can't find a good way to accomplish the task.
A diagram is included below:
The desired functionality is to have a normal scrollable page, where one of the slivers is a tab view with different sized (and dynamically resizing) tabs.
Unfortunately, despite looking at several resources and the flutter docs, I haven't come across any good solutions.
Here is what I have tried:
SingleChildScrollView with a column child, with the TabBarView wrapped in an IntrinsicHeight widget (Unbound constraints)
CustomScrollView variations, with the TabBarView wrapped in a SliverFillRemaining and the header and footer each wrapped with a SliverToBoxAdapter. In all cases, the content is forced to expand to the full size of the viewport (as if using a SliverFillViewport Sliver with a viewport fraction of 1.0) if smaller, or a nested scroll/overflow is created within the space if larger (see below)
If the children of the TabBarView are scrollable widgets, the sliver with the tab bar is given a height equal to the ViewPort (1.0) and any leftover space is empty.
If the children are not scrollable, they are force-expanded to fit if smaller, or give an overflow error if larger.
NestedScrollView comes closest but still suffers the ill effects of the previous implementation (see below for code example)
Various other unorthodox approaches (such as removing the TabBarView and trying to use an AnimatedSwitcher in conjunction with a listener on the TabBar to animate between the "tabs" but this wasn't swipable and the animation janked and the switched widgets overlapped)
The thus-far "best" implementation's code is given below, but it is not ideal.
Does anyone know of any way(s) to accomplish this?
Thank you in advance.
// best (more "Least-bad") solution code
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo',
routes: {
'root': (context) => const Scaffold(
body: ExamplePage(),
),
},
initialRoute: 'root',
);
}
}
class ExamplePage extends StatefulWidget {
const ExamplePage({
Key? key,
}) : super(key: key);
#override
State<ExamplePage> createState() => _ExamplePageState();
}
class _ExamplePageState extends State<ExamplePage>
with TickerProviderStateMixin {
late TabController tabController;
#override
void initState() {
super.initState();
tabController = TabController(length: 2, vsync: this);
tabController.addListener(() {
setState(() {});
});
}
#override
void dispose() {
tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) => Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: Colors.grey[100],
appBar: AppBar(),
body: NestedScrollView(
floatHeaderSlivers: false,
physics: const AlwaysScrollableScrollPhysics(),
headerSliverBuilder: (BuildContext context, bool value) => [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
bottom: 24.0,
top: 32.0,
),
child: Column(
children: [
// TODO: Add scan tab thing
Container(
height: 94.0,
width: double.infinity,
color: Colors.blueGrey,
alignment: Alignment.center,
child: Text('A widget with information'),
),
const SizedBox(height: 24.0),
GenaricTabBar(
controller: tabController,
tabStrings: const [
'Tab 1',
'Tab 2',
],
),
],
),
),
),
],
body: CustomScrollView(
slivers: [
SliverFillRemaining(
child: TabBarView(
physics: const AlwaysScrollableScrollPhysics(),
controller: tabController,
children: [
// Packaging Parts
SingleChildScrollView(
child: Container(
height: 200,
color: Colors.black,
),
),
// Symbols
SingleChildScrollView(
child: Column(
children: [
Container(
color: Colors.red,
height: 200.0,
),
Container(
color: Colors.orange,
height: 200.0,
),
Container(
color: Colors.amber,
height: 200.0,
),
Container(
color: Colors.green,
height: 200.0,
),
Container(
color: Colors.blue,
height: 200.0,
),
Container(
color: Colors.purple,
height: 200.0,
),
],
),
),
],
),
),
SliverToBoxAdapter(
child: ElevatedButton(
child: Text('Button'),
onPressed: () => print('pressed'),
),
),
],
),
),
);
}
class GenaricTabBar extends StatelessWidget {
final TabController? controller;
final List<String> tabStrings;
const GenaricTabBar({
Key? key,
this.controller,
required this.tabStrings,
}) : super(key: key);
#override
Widget build(BuildContext context) => Container(
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(8.0),
),
padding: const EdgeInsets.all(4.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// if want tab-bar, uncomment
TabBar(
controller: controller,
indicator: ShapeDecoration.fromBoxDecoration(
BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
color: Colors.white,
),
),
tabs: tabStrings
.map((String s) => _GenaricTab(tabString: s))
.toList(),
),
],
),
);
}
class _GenaricTab extends StatelessWidget {
final String tabString;
const _GenaricTab({
Key? key,
required this.tabString,
}) : super(key: key);
#override
Widget build(BuildContext context) => Container(
child: Text(
tabString,
style: const TextStyle(
color: Colors.black,
),
),
height: 32.0,
alignment: Alignment.center,
);
}
The above works in Dartpad (dartpad.dev) and doesn't require any external libraries
Ideally, there is a better answer out there somewhere. BUT, until it arrives, this is how I got around the issue:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo',
// darkTheme: Themes.darkTheme,
// Language support
// Routes will keep track of all of the possible places to go.
routes: {
'root': (context) => const Scaffold(
body: ExamplePage(),
),
},
initialRoute: 'root', // See below.
);
}
}
class ExamplePage extends StatefulWidget {
const ExamplePage({
Key? key,
}) : super(key: key);
#override
State<ExamplePage> createState() => _ExamplePageState();
}
class _ExamplePageState extends State<ExamplePage>
with TickerProviderStateMixin {
late TabController tabController;
late PageController scrollController;
late int _pageIndex;
#override
void initState() {
super.initState();
_pageIndex = 0;
tabController = TabController(length: 2, vsync: this);
scrollController = PageController();
tabController.addListener(() {
if (_pageIndex != tabController.index) {
animateToPage(tabController.index);
}
});
}
void animateToPage([int? target]) {
if (target == null || target == _pageIndex) return;
scrollController.animateToPage(
target,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
setState(() {
_pageIndex = target;
});
}
void animateTabSelector([int? target]) {
if (target == null || target == tabController.index) return;
tabController.animateTo(
target,
duration: const Duration(
milliseconds: 100,
),
);
}
#override
void dispose() {
tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) => Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: Colors.grey[100],
appBar: AppBar(),
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
bottom: 24.0,
top: 32.0,
),
child: Column(
children: [
// TODO: Add scan tab thing
Container(
height: 94.0,
width: double.infinity,
color: Colors.blueGrey,
alignment: Alignment.center,
child: Text('A widget with information'),
),
const SizedBox(height: 24.0),
GenaricTabBar(
controller: tabController,
tabStrings: const [
'Tab 1',
'Tab 2',
],
),
],
),
),
),
SliverToBoxAdapter(
child: Container(
height: 200,
color: Colors.black,
),
),
SliverToBoxAdapter(
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
// if page more than 50% to other page, animate tab controller
double diff = notification.metrics.extentBefore -
notification.metrics.extentAfter;
if (diff.abs() < 50 && !tabController.indexIsChanging) {
animateTabSelector(diff >= 0 ? 1 : 0);
}
if (notification.metrics.atEdge) {
if (notification.metrics.extentBefore == 0.0) {
// Page 0 (1)
if (_pageIndex != 0) {
setState(() {
_pageIndex = 0;
});
animateTabSelector(_pageIndex);
}
} else if (notification.metrics.extentAfter == 0.0) {
// Page 1 (2)
if (_pageIndex != 1) {
setState(() {
_pageIndex = 1;
});
animateTabSelector(_pageIndex);
}
}
}
return false;
},
child: SingleChildScrollView(
controller: scrollController,
scrollDirection: Axis.horizontal,
physics: const PageScrollPhysics(),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Parts
SizedBox(
width: MediaQuery.of(context).size.width,
child: Container(
color: Colors.teal,
height: 50,
),
),
// 2. Symbols
SizedBox(
width: MediaQuery.of(context).size.width,
child: Container(
color: Colors.orange,
height: 10000,
),
),
],
),
),
),
),
SliverToBoxAdapter(
child: Column(
children: [
Container(
color: Colors.red,
height: 200.0,
),
Container(
color: Colors.orange,
height: 200.0,
),
Container(
color: Colors.amber,
height: 200.0,
),
Container(
color: Colors.green,
height: 200.0,
),
Container(
color: Colors.blue,
height: 200.0,
),
Container(
color: Colors.purple,
height: 200.0,
),
],
),
),
],
),
);
}
class GenaricTabBar extends StatelessWidget {
final TabController? controller;
final List<String> tabStrings;
const GenaricTabBar({
Key? key,
this.controller,
required this.tabStrings,
}) : super(key: key);
#override
Widget build(BuildContext context) => Container(
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(8.0),
),
padding: const EdgeInsets.all(4.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// if want tab-bar, uncomment
TabBar(
controller: controller,
indicator: ShapeDecoration.fromBoxDecoration(
BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
color: Colors.white,
),
),
tabs: tabStrings
.map((String s) => _GenaricTab(tabString: s))
.toList(),
),
],
),
);
}
class _GenaricTab extends StatelessWidget {
final String tabString;
const _GenaricTab({
Key? key,
required this.tabString,
}) : super(key: key);
#override
Widget build(BuildContext context) => Container(
child: Text(
tabString,
style: const TextStyle(
color: Colors.black,
),
),
height: 32.0,
alignment: Alignment.center,
);
}
(Dartpad ready)
The basic idea is to not use a Tabview at all and instead use a horizontal scroll view nested in our scrollable area.
By using page physics for the horizontal scroll and using a PageController instead of a normal ScrollController, we can achieve a a scroll effect between the two widgets in the horizontal area that snap to whichever page is correct.
By using a notification listener, we can listen for changes in the scrollview and update the tab view accordingly.
LIMITATIONS:
The above code assumes only two tabs, so would require more thought to optimize for more tabs, particularly in the NotificationListener function.
This also may not be performant for large tabs since both tabs are being built, even if one is out of view.
Finally, the vertical height of each tab is the same; so a tab that is much larger will cause the other tab to have a lot of empty vertical space.
Hope this helps anyone in a similar boat, and am open to suggestions to improve.
Following is a custom tab I've created:
class Tabs extends StatefulWidget {
final tabs;
final views;
Tabs({this.tabs, this.views});
#override
_TabsState createState() => _TabsState();
}
class _TabsState extends State<Tabs> with SingleTickerProviderStateMixin {
late TabController _tabController;
#override
void initState() {
_tabController = TabController(length: widget.tabs.length, vsync: this);
super.initState();
}
#override
void dispose() {
super.dispose();
_tabController.dispose();
}
#override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return SingleChildScrollView(
physics: BouncingScrollPhysics(),
child: Container(
height: size.height,
child: Column(
children: [
Container(
height: 45,
decoration: BoxDecoration(
border: Border(
bottom:
BorderSide(color: Colors.grey, width: 3),
),
),
child: TabBar(
isScrollable: true,
controller: _tabController,
indicatorPadding: EdgeInsets.only(bottom: -2.5),
indicator: UnderlineTabIndicator(
borderSide: BorderSide(
width: 4,
color: Colors.blue,
),
),
labelColor: Colors.blue,
unselectedLabelColor: Colors.black,
tabs: [
...widget.tabs.map(
(e) => Tab(
child: Container(
child: Text(
e,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
),
)
],
),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [...widget.views],
),
),
],
),
),
);
}
}
But as soon as I use it inside my app screen, I'm getting overflow issues:
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: "Home"
elevation: 0,
toolbarHeight: 36,
),
drawer: DrawerNavigation(),
body: SingleChildScrollView(
physics: BouncingScrollPhysics(),
child: Column(
children: [
SizedBox(
height: 100,
),
Tabs(
tabs: ['tab1', 'tab2'],
views: [
Tab1(),
Tab2(),
],
),
],
),
),
);
}
}
The error I'm getting from is from single child scroll view.
I've tried removing single child scroll view from the tabs, but then the custom tabs wont' work.
just remove single child scroll view from custom Tab like this,
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class Tabs extends StatefulWidget {
final tabs;
final views;
Tabs({this.tabs, this.views});
#override
_TabsState createState() => _TabsState();
}
class _TabsState extends State<Tabs> with SingleTickerProviderStateMixin {
late TabController _tabController;
#override
void initState() {
_tabController = TabController(length: widget.tabs.length, vsync: this);
super.initState();
}
#override
void dispose() {
super.dispose();
_tabController.dispose();
}
#override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Container(
height: size.height,
child: Column(
children: [
Container(
height: 45,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.grey, width: 3),
),
),
child: TabBar(
isScrollable: true,
controller: _tabController,
indicatorPadding: EdgeInsets.only(bottom: -2.5),
indicator: UnderlineTabIndicator(
borderSide: BorderSide(
width: 4,
color: Colors.blue,
),
),
labelColor: Colors.blue,
unselectedLabelColor: Colors.black,
tabs: [
...widget.tabs.map(
(e) => Tab(
child: Container(
child: Text(
e,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
),
)
],
),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [...widget.views],
),
),
],
),
);
}
}
and convert stateless widget (Home) into stateful widget and it will work fine like this,
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
SizedBox(
height: 100,
),
Tabs(
tabs: ["tab1", "tab2"],
views: [okay(), okay()],
)
],
),
),
);
}
}
I create Tab Bar for my project. It includes two tabs and these each tabs are represent two pages.
Here is the code
import 'package:flutter/material.dart';
class TabView extends StatefulWidget {
#override
_TabViewState createState() => _TabViewState();
}
class _TabViewState extends State<TabView> with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
_tabController = TabController(length: 2, vsync: this);
super.initState();
}
#override
void dispose() {
super.dispose();
_tabController.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade300,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Container(
height: 45,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(
16.0,
),
),
child: TabBar(
controller: _tabController,
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(
16.0,
),
color: Colors.grey.shade900,
),
labelColor: Colors.white,
unselectedLabelColor: Colors.grey.shade900,
tabs: [
Tab(
text: 'One',
),
Tab(
text: 'Two',
),
],
),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
Center(
child: Text(
'Page One',
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.w600,
),
),
),
Center(
child: Text(
'Page Two',
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
],
),
),
),
);
}
}
Here is output
I want to use the tab bar for a single page to change a widget state.
Example
I want use the tab bar to change the color of the container in page one from red to blue and I don't want to switch to page two
How can I do it?
TabBar is not quite suitable for this purpose, although it can be adapted. I suggest you to use CupertinoSegmentedControl from cupertino package. Here is docs, and here is code example:
enum _Tab { one, two }
class MyWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
_Tab _selectedTab = _Tab.one;
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 16),
CupertinoSegmentedControl<_Tab>(
selectedColor: Colors.black,
borderColor: Colors.black,
pressedColor: Colors.grey,
children: {
_Tab.one: Text('One'),
_Tab.two: Text('Two'),
},
onValueChanged: (value) {
setState(() {
_selectedTab = value;
});
},
groupValue: _selectedTab,
),
SizedBox(height: 64),
Builder(
builder: (context) {
switch (_selectedTab) {
case _Tab.one:
return Center(
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
);
case _Tab.two:
return Center(
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
);
}
},
),
],
);
}
}
Also take a look at CupertinoSlidingSegmentedControl.
for your requirement don't use TabBarView at all, directly use container, change it's color value as per selectedtab index
class Tabscreenstate extends State<Tabscreen> with TickerProviderStateMixin {
int selectedTabIndex = 0;
TabController tabController;
#override
void initState() {
tabController = TabController(length: 2, vsync: this);
tabController.addListener(() {
setState(() {
selectedTabIndex = tabController.index;
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Column(
children: [
tabbar(),
Container(color:selectedTabIndex == 0 ? Colors.red : Colors.green),
],
);
}
Widget tabbar() => TabBar(
controller: tabController,
onTap: (value) {
setState(() {
selectedTabIndex = value;
});
},
tabs: [
Text("tab one"),
Text("tab two"),
],
);
}