I require to have multiple SliverAppBar, each with its own SliverList in a single view. Currently only the first SliverAppBar is responding correctly.
I have of course, done extended searching on SO and Google, but have not found a solution yet!
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Details'),
),
body: CustomScrollView(
slivers: <Widget>[
new SliverAppBar(
floating: true,
automaticallyImplyLeading: false,
title: Text('1'),
),
new SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('Text 1')),
childCount: 20,
),
),
new SliverAppBar(
automaticallyImplyLeading: false,
title: Text('2'),
floating: true,
),
new SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('Text 2')),
childCount: 20,
),
),
],
),
);
}
If you do scroll, I expect to see the title "2" floating as well, when you are scrolling the list.
This seems to be a limitation of CustomScrollView. It's possible to work around that, but it's very tricky, unless you have fixed-height items and fixed-length lists. If so, you can assume the height of your whole session (AppBar height + height of each list item).
Take a look:
class Foo extends StatefulWidget {
#override
_FooState createState() => _FooState();
}
class _FooState extends State<Foo> {
static const double listItemHeight = 50;
static const int listItemCount = 15;
static const double sessionHeight = kToolbarHeight + (listItemCount * listItemHeight);
int floatingAppBarIndex;
ScrollController controller;
#override
void initState() {
super.initState();
floatingAppBarIndex = 0;
controller = ScrollController()..addListener(onScroll);
}
void onScroll() {
double scrollOffset = controller.offset;
int sessionsScrolled = 0;
while (scrollOffset > sessionHeight) {
scrollOffset -= sessionHeight;
sessionsScrolled++;
}
if (sessionsScrolled != floatingAppBarIndex) {
setState(() {
floatingAppBarIndex = sessionsScrolled;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Details'),
),
body: CustomScrollView(
controller: controller,
slivers: <Widget>[
new SliverAppBar(
floating: floatingAppBarIndex == 0,
automaticallyImplyLeading: false,
title: Text('1'),
),
new SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return SizedBox(
height: listItemHeight,
child: ListTile(
title: Text('Text 1'),
),
);
},
childCount: listItemCount,
),
),
new SliverAppBar(
floating: floatingAppBarIndex == 1,
automaticallyImplyLeading: false,
title: Text('2'),
),
new SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return SizedBox(
height: listItemHeight,
child: ListTile(
title: Text('Text 2'),
),
);
},
childCount: listItemCount,
),
),
],
),
);
}
}
As I said, you're still able to do that in a list with variable values (item height and list length), but it would be very very tricky. If this is your case, I recommend using one of these plugins:
https://pub.dartlang.org/packages/sticky_headers
https://pub.dartlang.org/packages/flutter_sticky_header
Related
I'm trying to achieve a behavior that is similar to the SliverAppBar coupled with a TabBar, where the AppBar disappears on scroll but the TabBar stays, but in reverse, i.e. The TabBar slowly disappears but the AppBar stays visible. The TabBar (or any other bottom widget) should also reappear when scrolling up again.
I couldn't manage to achieve this behavior with the SliverAppBar, does anyone have an idea how this could be achieved?
This is how I tried, but I don't know how to reverse the behavior or the actual AppBar and the bottom widget.
import 'package:flutter/material.dart';
void main() {
runApp(const TestWidget());
}
class TestWidget extends StatelessWidget {
const TestWidget({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light(
useMaterial3: true,
),
home: Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
floating: true,
pinned: true,
snap: true,
title: const Text("My App Title"),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: SizedBox(
height: kToolbarHeight,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 25,
itemBuilder: (context, index) => FilterChip(
label: Text("Chip $index"),
onSelected: (_) {},
),
),
),
),
),
SliverList(
delegate: SliverChildListDelegate(
List.generate(
100,
(index) => ListTile(
title: Text("Item $index"),
),
),
),
)
],
),
),
);
}
}
Thanks in advance!
Class for ScrollListener
class ScrollListener extends ChangeNotifier {
double bottom = 0;
double _last = 0;
ScrollListener.initialise(ScrollController controller, [double height = 56]) {
controller.addListener(() {
final current = controller.offset;
bottom += _last - current;
if (bottom <= -height) bottom = -height;
if (bottom >= 0) bottom = 0;
_last = current;
if (bottom <= 0 && bottom >= -height) notifyListeners();
});
}
}
Use of ScrollListener to Hide/Show BottomBar:
class HomePage extends StatelessWidget {
final ScrollController _controller = ScrollController();
final double bottomNavBarHeight = 56;
late final ScrollListener _model;
HomePage({super.key}) {
_model = ScrollListener.initialise(_controller);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedBuilder(
animation: _model,
builder: (context, child) {
return Stack(
children: [
ListView.builder(
controller: _controller,
itemCount: 20,
itemBuilder: (_, i) => ListTile(title: Text('Item $i')),
),
Positioned(
left: 0,
right: 0,
bottom: _model.bottom,
child: bottomBar(),
),
],
);
},
),
);
}
Widget bottomBar() {
return SizedBox(
height: bottomNavBarHeight,
child: BottomNavigationBar(
backgroundColor: Colors.amber[800],
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
}
more reference also checkout this link
Here is the case, the first sliverAppBar pins to the top correctly when scrolling. Within customScrollView are two tabs that have its' own scroll view. Inside the first tab, is another sliverAppBar that is supposed to pin under the first one. However, it slides beneath the first sliverAppBar. Now since our complex view does not allow only one customScrollView to fix the problem, is there any other way?
See image here (Notice how first AppBar overlaps the second AppBar)
Desired Effect Before Scroll
Desired Effect After Scroll
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Material App',
home: Test(),
);
}
}
class Test extends StatelessWidget {
const Test({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final List<String> _tabs = ['Tab 1', 'Tab 2'];
return DefaultTabController(
length: _tabs.length,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverAppBar(
title: const Text('1st App Bar'),
pinned: true,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
tabs: _tabs.map((String name) => Tab(text: name)).toList(),
),
),
];
},
body: TabBarView(
children: [
CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(
title: Text('Item $index'),
);
},
childCount: 3,
),
),
),
const SliverAppBar(
toolbarHeight: 150,
pinned: true,
backgroundColor: Colors.purple,
title: Text('2nd App Bar'),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
);
},
childCount: 20,
),
),
),
],
),
Placeholder(),
],
),
),
),
);
}
}
We can use Scaffold to hold 1st appBar. And inside tab we can make 2nd appBar as pinned. Let me know if you wish any changes.
Output
class Test extends StatefulWidget {
const Test({Key? key}) : super(key: key);
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> with SingleTickerProviderStateMixin {
static const List<Tab> myTabs = <Tab>[
Tab(text: 'Tab1'),
Tab(text: 'Tab2'),
];
late TabController controller;
#override
void initState() {
super.initState();
controller = TabController(length: myTabs.length, vsync: this);
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Appbar 1"),
bottom: TabBar(
controller: controller,
tabs: myTabs,
),
),
body: TabBarView(
controller: controller,
children: [
CustomScrollView(
slivers: [
SliverAppBar(
title: Text("AppBar 2"),
pinned: true,
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(
title: Text('Item $index'),
);
},
childCount: 213,
),
),
),
],
),
Container(
color: Colors.pink,
),
],
),
);
}
}
I think u can use SliverPersistentHeader and SliverAppBar.
I have a Header that consists of:
1 - appBar (title + return button)
2 - row with a data picker)
3 - a row serving as a customized TabBar (with two Tabs)
4 - a row serving as a FilterBar (with a Button and a search field)
Then I have the "body" with a list of Cards that is scrollable.
I needed to retract/hide numbers 1-3 and keep only number 4 visible when I scroll down the list of cards. How could I build a customized Widget to help me with that?
Here is part of the code (it's too long to bring everything):
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
backgroundColor: Colors.blue,
title: 'List of Tasks',
elevation: 0.0,
pinned: true,
),
SliverPadding(
padding: EdgeInsets.all(15),
sliver: SliverToBoxAdapter(
child: DatePicker(
date: date,
onChange: (newDate) {
setState(() {});
},
),
),
),
SliverToBoxAdapter(
child: ActivitiesTabController(
pageController: _pageController,
currentTab: currentTab,
onChangedTab: (currentTab) {},
),
),
SliverToBoxAdapter(
child: Row(
children: [
SearchFilterBar(
taskId: taskId,
onChange: (floors, serviceName) {},
),
],
),
),
SliverList(
delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
return new Container(color: Colors.amberAccent, height: 150.0);
}),
),
],
));
}
I think you're referring to two app bars, where the first one can be scrolled away while the second one is persistent.
I think it would be something like this:
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(slivers: [
SliverAppBar(
title: Text('This hides'),
),
SliverAppBar(
title: Text('This is always shown'),
pinned: true,
primary: false,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(_, i) {
return ListTile(title: Text('Line $i'));
},
childCount: 150,
),
),
]),
);
}
I'm generating a NestedScrollView comprised of an AppBar + TabBar + TabBarView with a dynamic number of tabs depending on values I have stored in the app's state.
When the number of tabs to be generated is greater than 1, an error is thrown with the following message:
Controller's length property (<actual tab length>) does not match the number of tabs (1) present in TabBar's tabs property.
For some reason it seems that the TabBar's tabs length is always 1 although the same variable is used to determine the length everywhere in the code.
What am I missing here?
import 'package:flutter/material.dart';
import 'package:test_app/models/app_state_container.dart';
import 'package:test_app/utils/seeder.dart';
import 'dart:math';
import 'package:test_app/views/tabs/home/team_row.dart';
class MyLeaderboard extends StatefulWidget {
#override
_MyLeaderboardState createState() => _MyLeaderboardState();
}
class _MyLeaderboardState extends State<MyLeaderboard> {
final _tabName = "Leaderboard";
#override
Widget build(BuildContext context) {
var container = AppStateContainer.of(context);
var appState = container.state;
return DefaultTabController(
length: appState.leaderboard.groups.length,
child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
title: Text(_tabName, style: TextStyle(fontSize: 30)),
actions: <Widget>[
IconButton(
icon: Icon(Icons.refresh),
onPressed: () => setState(() {
appState.leaderboard = Seeder.generateLeaderboard();
}),
),
],
pinned: true,
floating: true,
snap: true,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
tabs: appState.leaderboard.groups.map((g) => Text(g.name)).toList(),
),
)
];
},
body: TabBarView(
children: <Widget>[
CustomScrollView(
physics: BouncingScrollPhysics(),
slivers: appState.leaderboard.groups
.map((g) => SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final int itemIndex = index ~/ 2;
if (index.isEven) {
return TeamRow(g.teams[itemIndex]);
}
return Divider(
height: 1,
);
},
childCount: max(0, g.teams.length * 2 - 1),
)))
.toList(),
),
],
),
),
);
}
}
It turns out I was not building the body of the TabBarView properly, (I was doing multiple SliverLists inside a CustomScrollView instead of multiple CustomScrollViews.
Here is the working build method of the code posted in my question:
Widget build(BuildContext context) {
var container = AppStateContainer.of(context);
var appState = container.state;
return DefaultTabController(
length: appState.leaderboard.groups.length,
child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
title: Text(_tabName, style: TextStyle(fontSize: 30)),
actions: <Widget>[
IconButton(
icon: Icon(Icons.refresh),
onPressed: () => setState(() {
appState.leaderboard = Seeder.generateLeaderboard();
}),
),
],
pinned: true,
floating: true,
snap: true,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
tabs: appState.leaderboard.groups
.map((g) => Text(g.name))
.toList(),
),
)
];
},
body: TabBarView(
children: appState.leaderboard.groups
.map((g) => CustomScrollView(
physics: BouncingScrollPhysics(),
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final int itemIndex = index ~/ 2;
if (index.isEven) {
return TeamRow(g.teams[itemIndex]);
}
return Divider(
height: 1,
);
},
childCount: max(0, g.teams.length * 2 - 1),
))
]))
.toList(),
),
),
);
}
The first time I had this error, the code was like this:
child: TabBarView(children: [],controller: _tabController,)
Then the amendment was done and the contents were given as follows:
That I Have Three Tabs
child: TabBarView(children: [
Container( ),
Container( ),
Container( ),
],controller: _tabController,)
And then the code worked systematically
I want to have a view with on top a non scrollable part like an image for example with at the bottom a tab bar that i can scroll to the top to let appear a list of item and be able to scroll inside the list of item.
For that i used a CustomScrollView, with a sliver grid in place of the image for the moment, and a sliver app bar for the tabbar and a sliverFixedExtentList for the list.
Widget build(BuildContext context) {
return new Scaffold(
body: new CustomScrollView(
slivers: <Widget>[
new SliverGrid(
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 0.58,
crossAxisCount: 1,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
color: Colors.red,
child: new Container(
color: Colors.green,
child: new Text('IMG HERE'),
)
);
},
childCount: 1,
),
),
new SliverAppBar(
title: new Text("title"),
floating: false,
pinned: true,
primary: true,
actions: <Widget>[
new IconButton(
icon: const Icon(Icons.arrow_upward),
onPressed: () {
},
),
],
bottom: new TabBar(
controller: _tabController,
isScrollable: true,
tabs: _bars,
),
),
new SliverFixedExtentList(
itemExtent: 100.0,
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.lightGreen[100 * (index % 9)],
child: new Text('list item $index'),
);
},
),
),
],
)
);
}
But i have 3 problems :
I can't figure out how to make a sliver non scrollable for the slivergrid here.
I don't know how to make the appBar be placed exactly at the botom of the screen on launch.
I have a problem with the list when the appbar reach the top the list jump some items, it seems it represents the size of the sliverGrid element.
Thanks
I've tried your code and it seems that there are some missing essential parts there. I can't see what's the code behind _tabController and _bars, so I just made my own _tabController and _bars. I've run it and this is what I've got so far:
On launch:
Browsing till the AppBar goes to the top.
So I made some changes in your code for presentation purposes:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#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 SingleTickerProviderStateMixin {
TabController _tabController;
List<Widget> _bars = [
Tab(icon: Icon(Icons.image)),
Tab(icon: Icon(Icons.image)),
];
int _selectedIndex = 0;
#override
void initState() {
super.initState();
_tabController = TabController(length: _bars.length, vsync: this);
_tabController.addListener(() {
setState(() {
_selectedIndex = _tabController.index;
});
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new CustomScrollView(
slivers: <Widget>[
new SliverGrid(
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: .69,
crossAxisCount: 1,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return SafeArea(
child: new Container(
color: Colors.green,
child: new Text('IMG HERE'),
),
);
},
childCount: 1,
),
),
new SliverAppBar(
title: new Text("title"),
floating: false,
pinned: true,
primary: true,
actions: <Widget>[
new IconButton(
icon: const Icon(Icons.arrow_upward),
onPressed: () {},
),
],
bottom: new TabBar(
controller: _tabController,
isScrollable: true,
tabs: _bars,
),
),
new SliverFixedExtentList(
itemExtent: 100.0,
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.lightGreen[100 * (index % 9)],
child: new Text('list item $index'),
);
},
),
),
],
));
}
}
Here is the output:
As you can see, I've played around with the value of childAspectRatio so that you can set the AppBar` at the bottom of the screen by default, that's how I understood your question number 2.
For question number 3, it seems that your code is working fine. I am able to properly see the ascending list item from 0 sequenced properly.
And for question number 1, I am quiet confused of how you want it to happen. You don't want the SliverGrid to be scrollable but you are expecting the AppBar to be on the top of the screen after scrolling. I guess giving more context on this part could give clarity for everyone.