I have a tab bar with a PageView inside each TabBarView. Each PageView has a ListView and when I scroll it, how can I scroll from the Scaffold's SingleChildScrollView rather than just scrolling the ListView inside the TabBarView?
Currently, the tab bar stays on the same position when I scroll the TabBarView, which looks terrible. How can I scroll individual TabBarView from SingleChildScrollView?
I tried tweaking with physics, but didn't turn out the way I wanted it to.
IMAGE :
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
int currIndex = 0;
TabController _tabController;
#override
void initState() {
super.initState();
_tabController =
TabController(vsync: this, length: 3, initialIndex: currIndex);
_tabController.addListener(() {
_handleTabSelection();
});
}
void _handleTabSelection() {
setState(() {
currIndex = _tabController.index == null ? 0 : _tabController.index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: DefaultTabController(
length: 3,
initialIndex: 0,
child: SingleChildScrollView(
child: Container(
height: MediaQuery.of(context).size.height,
child: Column(
children: [
Container(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: Container(
child: Text(
"HOME",
style: TextStyle(fontSize: 25),
)),
),
_buildTabBar(context),
],
),
),
Expanded(
child: _buildTabBarView(
context,
),
)
],
),
),
),
),
),
);
}
TabBarView _buildTabBarView(BuildContext context) {
return TabBarView(
controller: _tabController,
children: List.generate(
3,
(index) => Container(
color: Colors.red,
child: ListView.builder(
itemCount: 50,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: 60,
color: Colors.blue,
child: Center(child: Text("$index"))),
);
},
),
)));
}
Widget _buildTabBar(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8),
child: Container(
height: 45,
width: double.infinity,
decoration: buildTabBarStyle(),
child: TabBar(
controller: _tabController,
isScrollable: false,
tabs: List.generate(
3,
(index) => Center(
child: Text(
"$index",
style: TextStyle(color: Colors.black),
),
)),
),
),
);
}
BoxDecoration buildTabBarStyle() {
return BoxDecoration(
color: Color.fromARGB(255, 230, 248, 255),
border: Border.all(
width: 1,
color: Colors.black,
),
borderRadius: BorderRadius.all(Radius.circular(10)),
);
}
}
Here is a different approach to do it simply.
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
late final TabController controller;
#override
void initState() {
super.initState();
controller = TabController(length: 3, vsync: this);
}
BoxDecoration buildTabBarStyle() {
return BoxDecoration(
color: Color.fromARGB(255, 230, 248, 255),
border: Border.all(
width: 1,
color: Colors.black,
),
borderRadius: BorderRadius.all(Radius.circular(10)),
);
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverAppBar(
snap: true,
floating: true,
pinned: false,
toolbarHeight: 80,
title: Text("Home"),
// title: Search(),
centerTitle: true,
bottom: PreferredSize(
preferredSize: Size(0.0, 48.0),
child: Container(
decoration: buildTabBarStyle(),
alignment: Alignment.center,
width: double.infinity,
child: TabBar(
controller: controller,
isScrollable: true,
labelColor: Colors.green,
unselectedLabelColor: Colors.grey,
labelStyle:
TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0),
tabs: [
Tab(text: "1"),
Tab(text: "2"),
Tab(text: "3"),
],
),
),
),
),
],
body: TabBarView(
controller: controller,
children: [
...List.generate(
3,
(t) => Container(
color: Colors.red,
child: ListView.builder(
itemCount: 50,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: 60,
color: Colors.blue,
child: Center(
child: Text("tab $t index $index"),
),
),
);
},
),
),
).toList(),
],
),
),
),
);
}
}
Related
I'm making a Pomodoro app and I don't know how to implement Get and Obx if the timer is over and make that the tab bar change automatically.
This is my code:
Tab bar:
#override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).size.height,
width: double.infinity,
child: AnimatedBuilder(
animation: _countDownController.controller,
builder: (context, child) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(22),
child: Container(
color: Colors.transparent,
child: SafeArea(
child: ResponsiveWeb(
child: Column(children: [
TabBar(
controller: _tabController,
indicator: const UnderlineTabIndicator(
borderSide: BorderSide(
color: Color(0xff3B3B3B), width: 2.0),
insets: EdgeInsets.fromLTRB(
12.0, 12.0, 12.0, 12.0)),
indicatorWeight: 5,
indicatorSize: TabBarIndicatorSize.label,
labelColor: const Color(0xff3B3B3B),
labelStyle: GoogleFonts.nunito(
fontSize: 16.0,
// letterSpacing: 1,
fontWeight: FontWeight.w500),
unselectedLabelColor: const Color(0xffD7D7D7),
tabs: const [
Tab(
text: "Pomodoro",
icon: Icon(Icons.work_history_outlined,
size: 24),
),
Tab(
text: "Short break",
icon: Icon(Icons.ramen_dining_outlined,
size: 24),
),
Tab(
text: "Long break",
icon: Icon(
Icons.battery_charging_full_outlined,
size: 24),
),
]),
]),
),
),
),
),
),
),
);
},
),
);
}
}
And my timer:
This is an example from an animation which means that if the timer starts the animation starts as well
createAnimationController(TickerProvider ticker) {
currentRoundType = typeRound.pomodoro;
_changeCurrentRoundTypeString();
tickerProvider = ticker;
currentRoundSeconds.value = currentRoundType == typeRound.pomodoro
? _settingsController.secondsWork.value
: currentRoundNumber.value < _settingsController.rounds.value
? _settingsController.secondsBreak.value
: _settingsController.secondsBreakAfterRound.value;
restartTimers();
controller = AnimationController(
vsync: tickerProvider,
duration: currentDuration,
);
logger.d(controller.value);
painter = CustomTimePainter(
backgroundColor: const Color.fromARGB(0, 33, 149, 243),
color: const Color(0xffD94530),
animation: controller,
);
timerString.value =
'${(currentDuration.inHours).toString().padLeft(2, '0')}:${(currentDuration.inMinutes % 60).toString().padLeft(2, '0')}:${(currentDuration.inSeconds % 60).toString().padLeft(2, '0')}';
listRounds.value = List.generate(
_settingsController.rounds.value + 1, (index) => Rx(stateRound.undone));
}
This is another example, which means that if the timer ends the empty image is colored with red color
import 'package:flutter/material.dart';
import 'package:get/get.dart%20';
import 'package:pomodoro/3.tomatoes_interval_UI/countdown_controller.dart';
import 'package:pomodoro/3.tomatoes_interval_UI/tomato_icon.dart';
class TomatoesIcons extends StatefulWidget {
const TomatoesIcons({super.key});
#override
State<TomatoesIcons> createState() => _TomatoesIconsState();
}
class _TomatoesIconsState extends State<TomatoesIcons>
with TickerProviderStateMixin {
final CountDownController _countDownController = Get.find();
final ScrollController _horizontal = ScrollController();
#override
void initState() {
super.initState();
_countDownController.createAnimationController(this);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: AnimatedBuilder(
animation: _countDownController.controller,
builder: (context, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
color: const Color.fromARGB(255, 255, 202, 55),
height: 65,
width: MediaQuery.of(context).size.width,
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Obx(
() => Scrollbar(
controller: _horizontal,
child: SingleChildScrollView(
controller: _horizontal,
scrollDirection: Axis.horizontal,
child: Row(
children: _countDownController.listRounds
.map(
(e) => MouseRegion(
cursor: SystemMouseCursors.click,
child: TomatoIcon(e),
),
)
.toList(),
),
),
),
),
),
),
),
],
);
},
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pomodoro/3.tomatoes_interval_UI/countdown_controller.dart';
class TomatoIcon extends StatefulWidget {
final Rx<stateRound> state;
const TomatoIcon(this.state, {Key? key}) : super(key: key);
#override
State<TomatoIcon> createState() => _TomatoIconState();
}
class _TomatoIconState extends State<TomatoIcon> {
#override
Widget build(BuildContext context) {
return Obx(
() => IconButton(
onPressed: null,
icon: widget.state.value == stateRound.done
? Image.asset('assets/icons/tomatoDone.png')
: Image.asset('assets/icons/tomatoUndone.png')),
);
}
}
With these examples, I would like to create a function or " if statement" which triggers the timer is over, select automatically a tab bar.
Thanks for any help you can provide
I tried to acheive from NestedScrollView by creating a sample code. But its not exactly which I want. Please help me.
I want to achieve this ui. When scrolling appbar should dock at the top. Image top should start from status bar to the fixed height 300.
After Image there would be some dynamic content like description.
Then I have to add TabBar, which will dock just below the Appbar when scroll.
Body will scroll on top of Header Image, like a overlay.
Image Attachments
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class StickyTabbarExample extends StatefulWidget {
#override
_StickyTabbarExampleState createState() => _StickyTabbarExampleState();
}
class _StickyTabbarExampleState extends State<StickyTabbarExample>
with SingleTickerProviderStateMixin {
// TabController? _tabController;
final List<String> _tabs = <String>[
"Featured",
"Popular",
];
#override
void initState() {
super.initState();
// _tabController = TabController(vsync: this, length: _tabs.length);
}
#override
void dispose() {
// _tabController?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) => Scaffold(
body: DefaultTabController(
length: _tabs.length,
child: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) => <Widget>[
SliverAppBar(
pinned: true,
// floating: true,
toolbarHeight: 70,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(
Icons.arrow_back_ios_new_sharp,
color: Colors.white,
size: 15,
),
Icon(
Icons.shop,
color: Colors.white,
size: 15,
)
]),
backgroundColor: Colors.green,
expandedHeight: 300,
flexibleSpace: FlexibleSpaceBar(
background: Image.network(
"https://images.unsplash.com/photo-1673942393203-fe61f45b4479?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=870&q=80%20870w",
fit: BoxFit.cover,
width: double.maxFinite,
),
),
),
SliverToBoxAdapter(
child: Positioned.fill(
child: Transform.translate(
offset: Offset(0, -10.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 30,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.only(
topRight: Radius.circular(100),
topLeft: Radius.circular(100),
),
),
),
Text("Challenge description"),
],
),
),
),
),
SliverPersistentHeader(
delegate: _SliverAppBarDelegate(
TabBar(
indicator: UnderlineTabIndicator(
borderSide: BorderSide(
width: 4,
color: Color(0xFF646464),
),
insets: EdgeInsets.only(left: 0, right: 8, bottom: 4)),
isScrollable: true,
labelPadding: EdgeInsets.only(left: 0, right: 0),
tabs: [
Tab(
child: Text(
"Tab 1",
style: TextStyle(color: Colors.black),
),
),
Tab(
child: Text(
"Tab 2",
style: TextStyle(color: Colors.black),
)),
],
),
),
pinned: true,
),
],
body: TabBarView(children: [TabA(), TabB()]),
),
),
);
}
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate(this._tabBar);
final TabBar _tabBar;
#override
double get minExtent => _tabBar.preferredSize.height;
#override
double get maxExtent => _tabBar.preferredSize.height;
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) =>
Container(
color: Colors.white,
child: _tabBar,
);
#override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) => false;
}
class TabA extends StatelessWidget {
const TabA({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) => CustomScrollView(
// primary: false,
key: PageStorageKey<String>("Tab1"),
slivers: [
SliverPadding(
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 16),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: index % 2 == 0 ? Colors.green : Colors.greenAccent,
height: 80,
alignment: Alignment.center,
child: Text(
"Item $index",
style: const TextStyle(fontSize: 30),
),
),
),
// 40 list items
childCount: 40,
),
),
)
],
);
}
class TabB extends StatelessWidget {
const TabB({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) => CustomScrollView(
key: PageStorageKey<String>("Tab2"),
slivers: [
SliverPadding(
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 16),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: index % 2 == 0 ? Colors.green : Colors.greenAccent,
height: 80,
alignment: Alignment.center,
child: Text(
"Item $index",
style: const TextStyle(fontSize: 30),
),
),
),
childCount: 40,
),
),
)
],
);
}
So I was trying to build a user id page for my flutter app where you tap on a container and the containers height is increased and a different set of data is shown. On expanded I also wanted to add a scrollable tabview and that's second part of the problem.
the expected ui looks like thishttps://i.stack.imgur.com/62sro.gif.
I have tried Expanded and expansion tile, Can't quite achieve the output
Is there any other method to achieve this?
Welcome #Anand Pillai,
First add this line to your pubspec.yaml expandable: ^5.0.1
try this code
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late PageController _pageController;
final ExpandableController _controller = ExpandableController();
int activePage = 1;
int _counter = 0;
List<String> images = [
"https://images.pexels.com/photos/14686142/pexels-photo-14686142.jpeg",
"https://wallpaperaccess.com/full/2637581.jpg",
"https://uhdwallpapers.org/uploads/converted/20/01/14/the-mandalorian-5k-1920x1080_477555-mm-90.jpg"
];
List<Widget> indicators(imagesLength, currentIndex) {
return List<Widget>.generate(imagesLength, (index) {
return Container(
margin: const EdgeInsets.all(3),
width: 10,
height: 10,
decoration: BoxDecoration(
color: currentIndex == index ? Colors.white : Colors.blueGrey,
shape: BoxShape.circle),
);
});
}
AnimatedContainer slider(images, pagePosition, active) {
// double margin = active ? 10 : 20;
return AnimatedContainer(
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOutCubic,
// margin: EdgeInsets.all(margin),
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage(images[pagePosition]),
fit: BoxFit.cover,
)),
);
}
#override
void initState() {
super.initState();
_pageController = PageController();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Stack(
alignment: Alignment.center,
children: [imageSlider(), expandedWidget(context)],
),
),
],
));
}
Positioned expandedWidget(BuildContext context) {
return Positioned.fill(
bottom: _controller.expanded ? 0 : 60,
left: 0,
right: 0,
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_controller.expanded
? const SizedBox.shrink()
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: indicators(images.length, activePage)),
ExpandableNotifier(
child: AnimatedContainer(
height: _controller.expanded ? 400 : 110.0,
width: double.infinity,
alignment: Alignment.bottomCenter,
padding: const EdgeInsets.all(15.0),
margin: _controller.expanded
? EdgeInsets.zero
: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: const Color.fromARGB(255, 255, 79, 77).withOpacity(0.8),
borderRadius: _controller.expanded
? const BorderRadius.only(
topRight: Radius.circular(15),
topLeft: Radius.circular(15),
)
: BorderRadius.circular(15.0),
),
duration: const Duration(milliseconds: 500),
child: Column(
children: <Widget>[
ScrollOnExpand(
scrollOnExpand: true,
scrollOnCollapse: false,
child: ExpandablePanel(
controller: _controller
..addListener(() {
setState(() {});
}),
theme: const ExpandableThemeData(
headerAlignment: ExpandablePanelHeaderAlignment.center,
tapBodyToCollapse: true,
iconColor: Colors.white,
),
header: Padding(
padding: const EdgeInsets.all(10),
child: Text(
"ExpandablePanel",
style: Theme.of(context)
.textTheme
.bodyText1!
.copyWith(color: Colors.white),
)),
collapsed: const Text(
"loremIpsum",
style: TextStyle(color: Colors.white),
softWrap: true,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
expanded: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
for (var _ in Iterable.generate(5))
const Padding(
padding: EdgeInsets.only(bottom: 10),
child: Text(
"loremIpsum",
style: TextStyle(color: Colors.white),
softWrap: true,
overflow: TextOverflow.fade,
)),
],
),
builder: (_, collapsed, expanded) {
return Padding(
padding: const EdgeInsets.only(
left: 10, right: 10, bottom: 10),
child: Expandable(
collapsed: collapsed,
expanded: expanded,
theme: const ExpandableThemeData(crossFadePoint: 0),
),
);
},
),
),
],
),
)),
],
));
}
PageView imageSlider() {
return PageView.builder(
itemCount: images.length,
physics: _controller.expanded
? const NeverScrollableScrollPhysics()
: ScrollPhysics(),
padEnds: false,
controller: _pageController,
onPageChanged: (page) {
setState(() {
activePage = page;
});
},
itemBuilder: (context, pagePosition) {
bool active = pagePosition == activePage;
return slider(images, pagePosition, active);
});
}
}
class _MyHomePageState extends State<MyHomePage> {
double _margin = 30, _height = 100, _width = 300;
final Text _widget1 = const Text('This is my Foo');
final Text _widget2 = const Text('This is Bar');
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: GestureDetector(
// When the child is tapped, set state is called.
onTap: () {
setState(() {
_margin = _margin == 30 ? 0 : 30;
_height = _height == 100 ? 300 : 100;
_width = _width == 300 ? MediaQuery.of(context).size.width : 300;
});
},
// The custom button
child: Align(
alignment: Alignment.bottomCenter,
child: AnimatedContainer(
width: _width,
height: _height,
curve: Curves.easeInExpo,
margin: EdgeInsets.fromLTRB(_margin, 0, _margin, _margin),
duration: Duration(milliseconds: 250),
padding: const EdgeInsets.all(0),
decoration: BoxDecoration(
color: Colors.lightBlue,
borderRadius: BorderRadius.circular(8.0),
),
child: _margin == 30 ? _widget1 : _widget2,
),
),
)),
);
}
}
Simple logic is to animate the container when tapped and change the widget in it. On tap it calls setsate that sets the height, width, margin and child of the container.
This is the output that I want. I am still new in flutter so can anyone let me know if there is already a widget for this kind of switch or how should I make one ??
Also, I want the data shown below this button to change if I choose the other button but I guess that's obvious.
Thanks in advance.
You can use the TabBar widget to achieve this. I added a full example demonstrating how you can create this using the TabBar widget:
CODE
class StackOver extends StatefulWidget {
#override
_StackOverState createState() => _StackOverState();
}
class _StackOverState extends State<StackOver>
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(
appBar: AppBar(
title: Text(
'Tab bar',
),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
// give the tab bar a height [can change hheight to preferred height]
Container(
height: 45,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(
25.0,
),
),
child: TabBar(
controller: _tabController,
// give the indicator a decoration (color and border radius)
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(
25.0,
),
color: Colors.green,
),
labelColor: Colors.white,
unselectedLabelColor: Colors.black,
tabs: [
// first tab [you can add an icon using the icon property]
Tab(
text: 'Place Bid',
),
// second tab [you can add an icon using the icon property]
Tab(
text: 'Buy Now',
),
],
),
),
// tab bar view here
Expanded(
child: TabBarView(
controller: _tabController,
children: [
// first tab bar view widget
Center(
child: Text(
'Place Bid',
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.w600,
),
),
),
// second tab bar view widget
Center(
child: Text(
'Buy Now',
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
],
),
),
);
}
}
OUTPUT
Try out this you have to change some colour and font:-
import 'package:flutter/material.dart';
typedef SwitchOnChange = Function(int);
class CustomSwitch extends StatefulWidget {
SwitchOnChange onChange;
CustomSwitch({this.onChange});
#override
State<StatefulWidget> createState() {
return CustomSwitchState();
}
}
class CustomSwitchState extends State<CustomSwitch>
with TickerProviderStateMixin {
AnimationController controller;
Animation animation;
GlobalKey key = GlobalKey();
#override
void initState() {
Future.delayed(Duration(milliseconds: 100)).then((v) {
controller = AnimationController(
vsync: this, duration: Duration(milliseconds: 300));
tabWidth = key.currentContext.size.width / 2;
// var width = (media.size.width - (2 * media.padding.left)) / 2;
animation = Tween<double>(begin: 0, end: tabWidth).animate(controller);
setState(() {});
controller.addListener(() {
setState(() {});
});
});
super.initState();
}
var selectedValue = 0;
double tabWidth = 0;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
selectedValue == 0 ? this.controller.forward() : controller.reverse();
selectedValue = selectedValue == 0 ? 1 : 0;
},
child: Container(
key: key,
height: 44,
decoration: BoxDecoration(
color: Colors.grey, borderRadius: BorderRadius.circular(22)),
child: Stack(
children: <Widget>[
Row(
children: <Widget>[
Transform.translate(
offset: Offset(animation?.value ?? 0, 0),
child: Container(
height: 44,
width: tabWidth,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(22),
boxShadow: [
BoxShadow(color: Colors.grey, blurRadius: 3),
]),
),
),
],
),
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
width: tabWidth,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.directions_walk),
SizedBox(width: 5),
Text("Place Bid")
],
),
),
Container(
width: tabWidth,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.directions_walk),
SizedBox(width: 5),
Text("Buy now")
],
),
)
],
),
),
],
),
),
);
}
}
The following is my workaround, which I believe to be the best method.
import 'package:flutter/material.dart';
class SettingsScreen extends StatelessWidget {
const SettingsScreen({
super.key,
});
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Text('Settings'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(AppBar().preferredSize.height),
child: Container(
height: 50,
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 5,
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
10,
),
color: Colors.grey[200],
),
child: TabBar(
labelColor: Colors.white,
unselectedLabelColor: Colors.black,
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(
10,
),
color: Colors.pink,
),
tabs: const [
Tab(
text: 'Basic',
),
Tab(
text: 'Advanced',
)
],
),
),
),
),
),
body: const TabBarView(
children: [
Center(
child: Text(
'Basic Settings',
style: TextStyle(
fontSize: 30,
),
),
),
Center(
child: Text(
'Advanced Settings',
style: TextStyle(
fontSize: 30,
),
),
),
],
),
),
);
}
}
You can use also PageView widget.
const double borderRadius = 25.0;
class CustomSwitchState extends StatefulWidget {
#override
_CustomSwitchStateState createState() => _CustomSwitchStateState();
}
class _CustomSwitchStateState extends State<CustomSwitchState> with SingleTickerProviderStateMixin {
PageController _pageController;
int activePageIndex = 0;
#override
void dispose() {
_pageController.dispose();
super.dispose();
}
#override
void initState() {
super.initState();
_pageController = PageController();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: _menuBar(context),
),
Expanded(
flex: 2,
child: PageView(
controller: _pageController,
physics: const ClampingScrollPhysics(),
onPageChanged: (int i) {
FocusScope.of(context).requestFocus(FocusNode());
setState(() {
activePageIndex = i;
});
},
children: <Widget>[
ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: Center(child: Text("Place Bid"),),
),
ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: Center(child: Text("Buy Now"),),
),
],
),
),
],
),
),
),
));
}
Widget _menuBar(BuildContext context) {
return Container(
width: 300.0,
height: 50.0,
decoration: const BoxDecoration(
color: Color(0XFFE0E0E0),
borderRadius: BorderRadius.all(Radius.circular(borderRadius)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(borderRadius)),
onTap: _onPlaceBidButtonPress,
child: Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.symmetric(vertical: 15),
alignment: Alignment.center,
decoration: (activePageIndex == 0) ? const BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.all(Radius.circular(borderRadius)),
) : null,
child: Text(
"Place Bid",
style: (activePageIndex == 0) ? TextStyle(color: Colors.white) : TextStyle(color: Colors.black),
),
),
),
),
Expanded(
child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(borderRadius)),
onTap: _onBuyNowButtonPress,
child: Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.symmetric(vertical: 15),
alignment: Alignment.center,
decoration: (activePageIndex == 1) ? const BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.all(Radius.circular(borderRadius)),
) : null,
child: Text(
"Buy Now",
style: (activePageIndex == 1) ? TextStyle(color: Colors.white, fontWeight: FontWeight.bold) : TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
),
),
),
),
],
),
);
}
void _onPlaceBidButtonPress() {
_pageController.animateToPage(0,
duration: const Duration(milliseconds: 500), curve: Curves.decelerate);
}
void _onBuyNowButtonPress() {
_pageController.animateToPage(1,
duration: const Duration(milliseconds: 500), curve: Curves.decelerate);
}
}
OUTPUT
If you want tab layout like this you can use this
Output:
import 'package:flutter/material.dart';
import 'package:icons_helper/icons_helper.dart';
class DetailScreen extends StatefulWidget {
var body;
String title = "";
DetailScreen(this.body, this.title);
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<DetailScreen> with TickerProviderStateMixin {
late int _startingTabCount;
List<Tab> _tabs = <Tab>[];
List<Widget> _generalWidgets = <Widget>[];
late TabController _tabController;
#override
void initState() {
_startingTabCount = widget.body["related_modules"].length;
_tabs = getTabs(_startingTabCount);
_tabController = getTabController();
super.initState();
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
bottom: TabBar(
isScrollable: true,
tabs: _tabs,
controller: _tabController,
),
flexibleSpace: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.grey,
Colors.blue,
],
stops: [0.3, 1.0],
),
),
),
leading: IconButton(
icon: Icon(Icons.arrow_back_ios),
color: Colors.white,
onPressed: () {
Navigator.of(context, rootNavigator: true).pop(context);
},
),
actions: <Widget>[
IconButton(
icon: Icon(Icons.skip_previous),
color: Colors.white,
onPressed: () {
goToPreviousPage();
},
),
Container(
margin: EdgeInsets.only(right: 15),
child: IconButton(
icon: Icon(Icons.skip_next),
color: Colors.white,
onPressed: () {
goToNextPage();
},
),
)
],
),
body: Column(
children: <Widget>[
Expanded(
child: TabBarView(
physics: NeverScrollableScrollPhysics(),
controller: _tabController,
children: getWidgets(),
),
),
],
),
);
}
TabController getTabController() {
return TabController(length: _tabs.length, vsync: this)
..addListener(_updatePage);
}
Tab getTab(int widgetNumber) {
return Tab(
icon: Column(
children: [
if (widget.body["related_modules"][widgetNumber]["icon"].toString() ==
"fa-comments-o") ...[
Icon(
Icons.comment_outlined,
),
] else if (widget.body["related_modules"][widgetNumber]["icon"]
.toString() ==
"fa-map-marker") ...[
Icon(
Icons.location_on_rounded,
),
] else if (widget.body["related_modules"][widgetNumber]["icon"]
.toString() ==
"fa-address-card") ...[
Icon(
Icons.contact_page_sharp,
),
] else ...[
Icon(
getIconUsingPrefix(
name: widget.body["related_modules"][widgetNumber]["icon"]
.toString()
.substring(3),
),
)
]
],
),
text: widget.body["related_modules"][widgetNumber]["label"].toString(),
);
}
Widget getWidget(int widgetNumber) {
return Center(
child: Text("Widget nr: $widgetNumber"),
);
}
List<Tab> getTabs(int count) {
_tabs.clear();
for (int i = 0; i < count; i++) {
_tabs.add(getTab(i));
}
return _tabs;
}
List<Widget> getWidgets() {
_generalWidgets.clear();
for (int i = 0; i < _tabs.length; i++) {
_generalWidgets.add(getWidget(i));
}
return _generalWidgets;
}
void _updatePage() {
setState(() {});
}
//Tab helpers
bool isFirstPage() {
return _tabController.index == 0;
}
bool isLastPage() {
return _tabController.index == _tabController.length - 1;
}
void goToPreviousPage() {
_tabController.animateTo(_tabController.index - 1);
}
void goToNextPage() {
isLastPage()
? showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("End reached"),
content: Text("This is the last page.")))
: _tabController.animateTo(_tabController.index + 1);
}
}
I need to add a tab bar without an app bar and I got a solution from StackOverflow to use flexible space and it is working but it makes additional unwanted space in tab bar bottomHow to remove this or hide this?
My full code
import 'package:flutter/material.dart';
class TemplesListingWithTabMode extends StatefulWidget {
TemplesListingWithTabMode({Key key}) : super(key: key);
#override
_TemplesListingWithTabModeState createState() =>
_TemplesListingWithTabModeState();
}
class _TemplesListingWithTabModeState extends State<TemplesListingWithTabMode> {
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
height: MediaQuery.of(context).size.height-kToolbarHeight-kMaterialListPadding.top-kTabLabelPadding.top,
child: DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
flexibleSpace: TabBar(
indicatorColor: Colors.pink,
tabs: [
Tab(
child: Text("ALL",style: TextStyle(color: Colors.pink),),
),Tab(
child: Text("Favorites",style: TextStyle(color: Colors.pink),),
)
]),
),
body : Container(
color: Colors.grey,
child: TabBarView(
children: [
ListView.builder(
itemCount: 100,
itemBuilder: (context,index){
return Container(
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text("i am $index"),
),
),
);
}),
ListView.builder(
itemCount: 5,
itemBuilder: (context,index){
return Container(
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text("i am $index"),
),
),
);
})
]),
),
),
),
)
],
);
}
}
The solution provided by #Darshan is not solved my issue and the solution is
Wrap TabBar in SafeArea widget.
and the result is
How to remove this small bottom from appbar?
The reason is AppBar have its size + status bar size. There are multiple ways fix this. As other answer mentioned, simple way is to add SafeArea.
And note that even after you will get ugly little space under two tabs.
To solve that you can use PreferredSize (there are other ways for this also).
Code for the above screenshot:
class _TemplesListingWithTabModeState extends State<TemplesListingWithTabMode> {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: SafeArea(
child: Scaffold(
appBar: PreferredSize(
preferredSize: Size(double.infinity, 60),
child: TabBar(
indicatorColor: Colors.pink,
tabs: [
Tab(
child: Text("ALL",style: TextStyle(color: Colors.pink),),
),Tab(
child: Text("Favorites",style: TextStyle(color: Colors.pink),),
)
]),
),
body : Container(
color: Colors.grey,
child: TabBarView(
children: [
ListView.builder(
itemCount: 100,
itemBuilder: (context,index){
return Container(
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text("i am $index"),
),
),
);
}),
ListView.builder(
itemCount: 5,
itemBuilder: (context,index){
return Container(
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text("i am $index"),
),
),
);
})
]),
),
),
),
);
}
}
By default, ListView acts as if SafeArea is turned on.
Setting the padding to zero will remove that white space.
ListView(
padding: EdgeInsets.zero;
...
);
Discussion on ListView and the default SafeArea
Wrap TabBar in SafeArea widget. It adds the necessary padding to the child widget which in your case, minimizes the space you are seeing. Working code below:
child: Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
flexibleSpace: SafeArea(
child: TabBar(
indicatorColor: Colors.pink,
tabs: [
Tab(
child: Text("ALL",style: TextStyle(color: Colors.pink),),
),Tab(
child: Text("Favorites",style: TextStyle(color: Colors.pink),),
)
]),)
),
Hope this answers your question.
You can create your appbar by extending AppBar
class MyAppBar extends AppBar {
MyAppBar({PreferredSizeWidget child, Color backgroundColor})
: super(bottom: child, backgroundColor: backgroundColor);
#override
Size get preferredSize => bottom.preferredSize;
}
class TemplesListingWithTabMode extends StatefulWidget {
TemplesListingWithTabMode({Key key}) : super(key: key);
#override
_TemplesListingWithTabModeState createState() =>
_TemplesListingWithTabModeState();
}
class _TemplesListingWithTabModeState extends State<TemplesListingWithTabMode> {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: MyAppBar(
backgroundColor: Colors.white,
child: TabBar(indicatorColor: Colors.pink, tabs: [
Tab(
child: Text(
"ALL",
style: TextStyle(color: Colors.pink),
),
),
Tab(
child: Text(
"Favorites",
style: TextStyle(color: Colors.pink),
),
)
]),
),
body: Container(
color: Colors.grey,
child: TabBarView(children: [
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return Container(
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text("i am $index"),
),
),
);
}),
ListView.builder(
itemCount: 5,
itemBuilder: (context, index) {
return Container(
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text("i am $index"),
),
),
);
})
]),
),
),
);
}
}
import 'package:bubble_tab_indicator/bubble_tab_indicator.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class AppBarrTest extends StatefulWidget {
#override
_AppBarrTestState createState() => _AppBarrTestState();
}
class _AppBarrTestState extends State<AppBarrTest>with SingleTickerProviderStateMixin {
int index = 0;
TabController _controller;
#override
void initState() {
_controller = new TabController(length: 2, vsync: this);
_controller.addListener(() {
setState(() {});
});
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
flexibleSpace: fun_Appbar(),
bottom: PreferredSize(
preferredSize: Size.fromHeight(50),
child: Column(
children: <Widget>[
Card(
shape: Border.all(color: Colors.blue),
color: Colors.white,
child: fun_tabBar(20),
),
],
),
),
),
),
);
}
fun_Appbar(){
double h = MediaQuery.of(context).size.height;
return Container(
height: 50,
child: Center(
child: Text(
"Messages",
style: TextStyle(
fontSize: 22,
color: Colors.white,
letterSpacing: 2.0,
fontFamily: 'Nunito',
),
),
),
);
}
fun_tabBar(double fontSize){
return TabBar(
controller: _controller,
//indicatorWeight: 20,
indicatorSize: TabBarIndicatorSize.label,
labelPadding: EdgeInsets.only(left: 0, right: 0),
dragStartBehavior: DragStartBehavior.start,
unselectedLabelColor: Colors.black,
indicatorColor: Colors.red,
indicator: new BubbleTabIndicator(
indicatorHeight: 40.0,
indicatorColor: Color(0xFF343193),
//padding: EdgeInsets.all(20),
tabBarIndicatorSize: TabBarIndicatorSize.tab,
indicatorRadius: 30,
),
tabs: <Widget>[
Tab(
child: Container(
alignment: Alignment.center,
child: Text(
"Inbox",
style: TextStyle(
fontFamily: 'Nunito',
fontSize: fontSize,
),
),
),
),
Tab(
child: Container(
alignment: Alignment.center,
child: Text(
"Sent",
style: TextStyle(
fontFamily: 'Nunito',
fontSize: fontSize,
),
),
),
),
],
);
}
}
bubble_tab_indicator: "^0.1.4"
or Just wrap your flexible space with SizeBox and set height
flexibleSpace: SizedBox(
height: 100,
child: TabBar(
indicatorColor: Colors.pink,
tabs: [
Tab(
child: Text("ALL",style: TextStyle(color: Colors.pink),),
),Tab(
child: Text("Favorites",style: TextStyle(color: Colors.pink),),
)
]),
),