I'm trying to implement a custom tab bar widget inside a SliverAppBar. So far I've tried wrapping my CustomTabBar within a PreferredSize widget.
Here's my code:
Widget _buildBody(){
return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool boxIsScrolled) {
return <Widget>[
SliverAppBar(
leading: Container(),
backgroundColor: Colors.transparent,
expandedHeight: 200.0,
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.pin,
background: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(
"Item 1"
),
Text(
"Item 2"
),
Text(
"Item 3"
)
],
)
]),
),
bottom: PreferredSize(
preferredSize: Size.fromHeight(kToolbarHeight),
child: CustomTabWidget(
items: ['Challenge', 'My friends'],
activeColor: secondaryColor,
currentIndex: currentIndex,
backgroundColor: tabColor,
activeTextColor: Colors.white,
backgroundTextColor: Colors.white,
onTabSelect: (int index) {
onLeaderboardTabSelect(index);
},
),
),];
},
body: ListView.separated(
itemCount: 50,
itemBuilder: (context, index) {
return ListTile(
title: Text('row $index'),
);
},
separatorBuilder: (context, index) {
return Divider();
},
) // should return listview depending on the tab
);
}
CustomTabWidget
Widget build(BuildContext context) {
return Container(
height: height,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(
30.0,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildItems(context),
),
);
}
The code successfully shows the my custom tab bar widget but whenever I scroll down or tap another tab, it disappears.
I might have overlooked something within the code.
Can anyone help me?
this work on my project
class TabBarInSliverAppbar extends StatelessWidget {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
title: Text("Tabbar in SliverAppbar Example"),
pinned: true,
floating: true,
bottom: TabBar(
tabs: <Widget>[
Tab(
text: "First Tab",
),
Tab(
text: "Second Tab",
),
],
),
),
SliverToBoxAdapter(
child: TabBarView(
children: <Widget>[
Center(
child: Text("First Tab"),
),
Center(
child: Text("Second Tab"),
),
],
),
),
],
),
),
);
}
}
you can also checki this link :Flutter TabBar and SliverAppBar that hides when you scroll down
Related
Like the title, i have created a button with Navigator.push() into a new page, the page auto generates a back button on the appBar, i appreciated that autogenerated back button but i want the back button to not displace my title's positioning, is that possible?
Below is my code for the button to enter this page:
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return FeedTest();
},
),
);
This is my full code for the new page:
Widget build(BuildContext context) {
return ScrollConfiguration(
behavior: MyBehavior(),
child: SafeArea(
child: DefaultTabController(
length: 2,
child: Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.grey[850],
title: Flex(
direction: Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'images/logo2.png',
isAntiAlias: true,
scale: 1.8,
fit: BoxFit.fitHeight,
alignment: Alignment.center,
)
]),
bottom: TabBar(
indicatorColor: Colors.white,
tabs: [
Tab(
text: 'Submit Feedback',
),
Tab(
text: 'Submit Testimony',
)
],
),
),
body: TabBarView(
children: [SubmitFeedback(), SubmitTestimony()],
),
),
),
),
);
}
P.S. Title image has been blurred for privacy purposes
centerTitle: true is used to make the title center in AppBar
AppBar(
centerTitle: true,
...
)
Output:
Use centerTitle: true like below.
AppBar(
centerTitle: true, // this is all you need
//Rest Code here
)
I upload the full code like below.
Widget build(BuildContext context) {
return ScrollConfiguration(
behavior: MyBehavior(),
child: SafeArea(
child: DefaultTabController(
length: 2,
child: Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
centerTitle: true,
elevation: 0,
backgroundColor: Colors.grey[850],
title: Flex(
direction: Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'images/logo2.png',
isAntiAlias: true,
scale: 1.8,
fit: BoxFit.fitHeight,
alignment: Alignment.center,
)
]),
bottom: TabBar(
indicatorColor: Colors.white,
tabs: [
Tab(
text: 'Submit Feedback',
),
Tab(
text: 'Submit Testimony',
)
],
),
),
body: TabBarView(
children: [SubmitFeedback(), SubmitTestimony()],
),
),
),
),
);
}
I've been investigating SliverAppBar, CustomScrollView, NestedScrollView, SliverPersistentHeader, and more. I cannot find a way to build something like the Instagram user profile screen's header where only the tab bar is pinned. The main body of the screen is a TabBarView and each pane has a scrollable list.
With SliverAppBar, it is easy to add the TabBar in the bottom parameter. But I want to have an extra widget of unknown/variable height above that TabBar. The extra widget should scroll out of the way when the page is scrolled and and then the TabBar is what is pinned at the top of the screen.
All I could manage was a fixed content before the tab bar and a fixed tab bar. I cannot get the header to scroll up and stick the TabBar at the top just just below the AppBar.
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text("pabloaleko"),
),
body: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
SliverToBoxAdapter(
child: SafeArea(
child: Text("an unknown\namount of content\n goes here in the header"),
),
),
SliverToBoxAdapter(
child: TabBar(
tabs: [
Tab(child: Text('Days', style: TextStyle(color: Colors.black))),
Tab(child: Text('Months', style: TextStyle(color: Colors.black))),
],
),
),
SliverFillRemaining(
child: TabBarView(
children: [
ListView(
children: <Widget>[
ListTile(title: Text('Sunday 1')),
ListTile(title: Text('Monday 2')),
ListTile(title: Text('Tuesday 3')),
ListTile(title: Text('Wednesday 4')),
ListTile(title: Text('Thursday 5')),
ListTile(title: Text('Friday 6')),
ListTile(title: Text('Saturday 7')),
ListTile(title: Text('Sunday 8')),
ListTile(title: Text('Monday 9')),
ListTile(title: Text('Tuesday 10')),
ListTile(title: Text('Wednesday 11')),
ListTile(title: Text('Thursday 12')),
ListTile(title: Text('Friday 13')),
ListTile(title: Text('Saturday 14')),
],
),
ListView(
children: <Widget>[
ListTile(title: Text('January')),
ListTile(title: Text('February')),
ListTile(title: Text('March')),
ListTile(title: Text('April')),
ListTile(title: Text('May')),
ListTile(title: Text('June')),
ListTile(title: Text('July')),
ListTile(title: Text('August')),
ListTile(title: Text('September')),
ListTile(title: Text('October')),
ListTile(title: Text('November')),
ListTile(title: Text('December')),
],
),
],
),
),
],
),
),
);
}
}
You can achieve this behaviour using NestedScrollView with Scaffold.
As we need the widgets between the AppBar and TabBar to be dynamically built and scrolled until TabBar reaches AppBar, use the appBar property of the Scaffold to build your AppBar and use headerSliverBuilder to build other widgets of unknown heights. Use the body property of NestedScrollView to build your tab views.
This way the elements of the headerSliverBuilder would scroll away till the body reaches the bottom of the AppBar.
Might be a little confusing to understand with mere words, here is an example for you.
Code:
// InstaProfilePage
class InstaProfilePage extends StatefulWidget {
#override
_InstaProfilePageState createState() => _InstaProfilePageState();
}
class _InstaProfilePageState extends State<InstaProfilePage> {
double get randHeight => Random().nextInt(100).toDouble();
List<Widget> _randomChildren;
// Children with random heights - You can build your widgets of unknown heights here
// I'm just passing the context in case if any widgets built here needs access to context based data like Theme or MediaQuery
List<Widget> _randomHeightWidgets(BuildContext context) {
_randomChildren ??= List.generate(3, (index) {
final height = randHeight.clamp(
50.0,
MediaQuery.of(context).size.width, // simply using MediaQuery to demonstrate usage of context
);
return Container(
color: Colors.primaries[index],
height: height,
child: Text('Random Height Child ${index + 1}'),
);
});
return _randomChildren;
}
#override
Widget build(BuildContext context) {
return Scaffold(
// Persistent AppBar that never scrolls
appBar: AppBar(
title: Text('AppBar'),
elevation: 0.0,
),
body: DefaultTabController(
length: 2,
child: NestedScrollView(
// allows you to build a list of elements that would be scrolled away till the body reached the top
headerSliverBuilder: (context, _) {
return [
SliverList(
delegate: SliverChildListDelegate(
_randomHeightWidgets(context),
),
),
];
},
// You tab view goes here
body: Column(
children: <Widget>[
TabBar(
tabs: [
Tab(text: 'A'),
Tab(text: 'B'),
],
),
Expanded(
child: TabBarView(
children: [
GridView.count(
padding: EdgeInsets.zero,
crossAxisCount: 3,
children: Colors.primaries.map((color) {
return Container(color: color, height: 150.0);
}).toList(),
),
ListView(
padding: EdgeInsets.zero,
children: Colors.primaries.map((color) {
return Container(color: color, height: 150.0);
}).toList(),
)
],
),
),
],
),
),
),
);
}
}
Output:
Hope this helps!
Another solution is that you could use a pinned SliverAppBar with FlexibleSpaceBar within DefaultTabController. Sample codes:
Scaffold(
body: DefaultTabController(
length: 2,
child: NestedScrollView(
headerSliverBuilder: (context, value) {
return [
SliverAppBar(
floating: true,
pinned: true,
bottom: TabBar(
tabs: [
Tab(text: "Posts"),
Tab(text: "Likes"),
],
),
expandedHeight: 450,
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.pin,
background: Profile(), // This is where you build the profile part
),
),
];
},
body: TabBarView(
children: [
Container(
child: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return Container(
height: 40,
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: Text('List Item $index'),
);
},
),
),
Container(
child: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return Container(
height: 40,
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: Text('List Item $index'),
);
},
),
),
],
),
),
),
),
Before scrolling:
After scrolling:
I have a page with a DefaultTabController that is:
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
resizeToAvoidBottomPadding: true,
appBar: AppBar(
bottom: TabBar(
indicatorColor: Colors.red,
indicator: BoxDecoration(
color: buttonColor,
),
tabs: [Tab(text: "Login"), Tab(text: "Register", key: Key("tabRegistro"))],
),
centerTitle: true,
title: Text(appBarTitle)),
body: TabBarView(
children: [
new LoginPage(),
new RegisterPage(),
],
),
),
);
}
And for example, the LoginPage build is:
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomPadding: true,
key: _scaffoldKey,
body: SingleChildScrollView(
child: GestureDetector(
onTap: () {
FocusScope.of(context).unfocus();
},
child: Center(
child: Container(
color: backgroundColor,
child: Padding(
padding: const EdgeInsets.all(36.0),
child: Form(
key: formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
SizedBox(
height: 155.0,
child: Image.asset(
"assets/images/pet.PNG",
fit: BoxFit.contain,
),
),
SizedBox(height: 20.0),
emailField,
SizedBox(height: 10.0),
passwordField,
SizedBox(height: 20.0),
loginButon,
SizedBox(height: 20.0),
GoogleSignInButton(
darkMode: true,
text: "Login with Google",
onPressed: () {
_sigIn();
}),
],
),
)),
),
),
)));
}
But when I'm writing, the keyboard cover the Textfields and they aren't visible.
With SingleChildScrollView before it works, but now it doesn't work propertly. I have tried to put resizeToAvoidBottomPadding: true but it doesn't work. What could I do to fix this problem?
The code is Ok, the problem is at Android.manifest.xml. I'm using Android Simulator and to hide the status bar I put these line:
android:theme="#android:style/Theme.Holo.NoActionBar.Fullscreen"
If i put the original line it works. The original line is:
android:theme="#style/LaunchTheme"
I am trying to set up a SliverAppBar in a CustomScrollView using Flutter, and can't get to vertically center the title.
I already tried this solution (and this SO question is exactly what I want to do) but fortunately, it didn't work for me.
Here is my build method:
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
pinned: true,
expandedHeight: 200,
//backgroundColor: Colors.transparent,
flexibleSpace: FlexibleSpaceBar(
titlePadding: EdgeInsets.zero,
centerTitle: true,
title: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text("Should be centered", textAlign: TextAlign.center),
],
),
background: Image.asset("assets/earth.jpg", fit: BoxFit.cover),
),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.menu),
tooltip: "Menu",
onPressed: () {
// onPressed handler
},
),
],
),
SliverFixedExtentList(
itemExtent: 50,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
alignment: Alignment.center,
color: Colors.green,
child: Text("Index n°$index"),
);
},
),
)
],
),
);
}
I really don't understand what is wrong and why it isn't centered. I observed that the column is way too big when setting mainAxisSize to mainAxisSize.max.
Any idea?
Thanks in advance!
I tinkered around a bit in your code and was able to center it. So the main problem here was the expandedHeight. This height expands the SliverAppBar both upwards and downwards meaning that half of that 200 was always above the screen. Taking that into consideration, you would be trying to center the text in only the bottom half of the app bar. The simplest way was to just use Flexible to size the items relative to their container. Here's the working code:
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
pinned: true,
expandedHeight: 200,
//backgroundColor: Colors.transparent,
flexibleSpace: FlexibleSpaceBar(
titlePadding: EdgeInsets.zero,
centerTitle: true,
title: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Flexible(
flex: 3,
child: Container(),
),
Flexible(
flex: 1,
child:
Text("Should be centered", textAlign: TextAlign.center),
),
Flexible(
flex: 1,
child: Container(),
),
],
),
background: Image.asset("assets/earth.png", fit: BoxFit.cover),
),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.menu),
tooltip: "Menu",
onPressed: () {
// onPressed handler
},
),
],
),
SliverFixedExtentList(
itemExtent: 50,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
alignment: Alignment.center,
color: Colors.green,
child: Text("Index n°$index"),
);
},
),
)
],
),
);
}
A way without empty containers
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
pinned: true,
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(
titlePadding: EdgeInsets.zero,
centerTitle: true,
title: SizedBox(
height: 130,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Should be centered", textAlign: TextAlign.center),
],
),
),
background: Image.asset("assets/earth.png", fit: BoxFit.cover),
),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.menu),
tooltip: "Menu",
onPressed: () {
// onPressed handler
},
),
],
),
SliverFixedExtentList(
itemExtent: 50,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
alignment: Alignment.center,
color: Colors.green,
child: Text("Index n°$index"),
);
},
),
)
],
),
);
}
Instead of using a FlexibleSpaceBar use a different Widget.
flexibleSpace: Padding(
padding: EdgeInsets.all(4),
child: Container(),
}),
),
Kinda like what Pedro said earlier, you can instead use the flexibleSpace parameter in SliverAppBar, and then center things from there. This is what I did.
SliverAppBar(
flexibleSpace: Center(
child: Text("27 is my favorite number")
)
)
Wrapping TabBarView with SliverFillRemaining (fill remaining empty space like Expanded) gives the following error output.
flutter: A RenderPositionedBox expected a child of type RenderBox but received a child of type
flutter: RenderSliverList.
TabController tabContoller;
#override
void initState() {
tabContoller = new TabController(
vsync: this,
length: 3,
);
#override
Widget build(BuildContext context) {
return new Scaffold(
floatingActionButton:floatActionBtn(...),
bottomNavigationBar: bottomNavigationBar(...),
appBar: apBar(),
body: Stack(
children: <Widget>[
CustomScrollView(
slivers: <Widget>[
SliverAppBar(
backgroundColor: Colors.transparent,
automaticallyImplyLeading: false,
expandedHeight: 195.0,
flexibleSpace: FlexibleSpaceBar(
background: new Stack(
children: <Widget>[
...
]),
),
),
new SliverFillRemaining(
child: TabBarView(
controller: tabContoller,
children: <Widget>[
Tab(...),
Tab(...),
Tab(...)
],
),
),
],
),
],
)
Don't forget the DefaultTabController, this code is working fine:
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Container(
child: CustomScrollView(slivers: <Widget>[
SliverAppBar(),
new SliverFillRemaining(
child: TabBarView(
children: <Widget>[
Text("Tab 1"),
Text("Tab 2"),
Text("Tab 3"),
],
),
),
])),
);
}
You can use NestedScrollView like this
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
forceElevated: innerBoxIsScrolled,
automaticallyImplyLeading: false,
expandedHeight: 195.0,
flexibleSpace: FlexibleSpaceBar(
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(_, int index) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
labelColor: AppColors.black,
unselectedLabelColor: AppColors.gray,
indicatorColor: AppColors.primaryColor,
indicatorWeight: 4,
indicatorPadding: EdgeInsets.symmetric(horizontal: 16),
labelPadding: EdgeInsets.zero,
tabs: [
Text("FIRST"),
Text("SECOND"),
Text("THIRD"),
],
)
],
);
},
childCount: 1,
),
),
];
},
body: TabBarView(
children: <Widget>[
Text("FIRST TAB"),
Text("SECOND TAB"),
Text("THIRD TAB"),
],
),
),
),
);
}