I'm new guy using the flutter and when I code my app I met a problem that how could I set SliverAppBar collapsed status by default. To solve my confuse, I check the flutter's doc, unfortunately I couldn't find the solution, so I'm here.
Below is my code, if you know how to solve it, help me plz. Thanks guys.
class CollapsingBarLayoutState extends State<CollapsingBarLayout>
with TickerProviderStateMixin {
final List<ListItem> listData = [];
#override
Widget build(BuildContext context) {
for (int i = 0; i < 20; i++) {
listData.add(new ListItem("$i", Icons.cake));
}
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
expandedHeight: 200.0,
pinned: true,
centerTitle: false,
snap: true,
floating: true,
titleSpacing: 0,
backgroundColor: Colors.green,
flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
title: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _buildBriefCalendarChildren(),
),
),
background: GridPage(
children: _buildCalendarPageChildren(),
column: 7,
row: 5,
)),
),
];
},
body: Center(
child: new ListView.builder(
itemBuilder: (BuildContext context, int index) {
return new ListItemWidget(listData[index]);
},
itemCount: listData.length,
),
),
),
);
}
}
There is a good plugin https://pub.dev/packages/after_layout I used in a situation like this.
Just add a mixin and then the only method you would need to implement would look something like this:
void afterFirstLayout(BuildContext context) {
_scrollController //scroll controller of your list view under sliver
.animateTo((expandedHeight) - collapsedHeight,
duration: new Duration(milliseconds: 1),//0 dont work as duration
curve: Curves.linear)
.then((_) {
setState(() {
expandedPercentage = 0; //just my custom tracking of expansion percentage
});
});
}
Edit - better solution:
Initialise a new ScrollController(initialScrollOffset: expandedHeight - collapsedHeight); and pass it as a controller to your NestedScrollView.
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
My goal is to use some kind of Profile Page where I have a SliverAppBar at the top which has a tabBar as bottom to switch between the Lists or elements shown below the AppBar.
I found that this is archived by NestedScrollView but noticed a strange behavior when either having just a few elements and scrolling up, where it still scrolls up under the tab bar, even tho there are no new elements to show at the bottom, or when you have a lot of items and want to use some kind of persistent App Bar in one of the lists, to for example have a textfield which should be used to sort or filter the list.
I tried to use SliverOverlapAbsorber, but it didn't solve the problem. So below I just show the sample code without it:
I thought about inserting a Container that changes it's height according to the scrolling position, to 'solve' this issue, but I'd prefer the proper way to do this..
here's a gif to illustrate the problem:
https://gfycat.com/decentoccasionalhyena
https://gfycat.com/fastslightekaltadeta
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:widgettests/widgets/sliverHeader.dart';
class NestedTabbarSliverOverpaling extends StatefulWidget {
#override
_NestedTabbarSliverOverpalingState createState() => _NestedTabbarSliverOverpalingState();
}
class _NestedTabbarSliverOverpalingState extends State<NestedTabbarSliverOverpaling> with TickerProviderStateMixin {
ScrollController _scrollController;
TabController _tabController;
#override
void initState() {
super.initState();
_scrollController = new ScrollController();
_tabController = new TabController(length: 2, vsync: this);
}
#override
void dispose() {
_scrollController.dispose();
_tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
controller: _scrollController,
physics: BouncingScrollPhysics(),
dragStartBehavior: DragStartBehavior.start,
headerSliverBuilder: (context, innerBoxIsScrolled) =>
<Widget>[
SliverAppBar(
pinned: true,
expandedHeight: 100,
bottom: PreferredSize(
preferredSize: Size.fromHeight(-5),
child: TabBar(
controller: _tabController,
labelColor: Colors.white,
tabs: <Widget>[Tab(text: '1st List',), Tab(text: '2nd List',),]
),
),
),
],
body: TabBarView(
controller: _tabController,
children: <Widget>[
SafeArea(
top: false,
bottom: false,
child: _buildfirstList(context),
),
SafeArea(
top: false,
bottom: false,
child: _build2ndList(context),
),
],
),
),
);
}
Widget _buildfirstList(BuildContext context) {
final children = <Widget>[
CustomScrollView(
key: PageStorageKey<String>('1st'),
physics: ClampingScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
backgroundColor: Colors.blueGrey,
floating: true,
snap: true,
title: TextField(
showCursor: false,
),
titleSpacing: 2,
),
_List(),
//SliverFillRemaining(),
],
),
];
return Stack(
children: children,
);
}
Widget _build2ndList(BuildContext context) {
final children = <Widget>[
CustomScrollView(
key: PageStorageKey<String>('2nd'),
physics: ClampingScrollPhysics(),
slivers: <Widget>[
_List(),
//SliverFillRemaining(),
],
),
];
return Stack(
children: children,
);
}
Widget _List() {
return SliverList(
delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
return new ListTile(
title: Text('tile no. ${index}'),
);
},
childCount: 55,
),
);
}
}
I am working on app that displays products. The app has 4 blocks in a listview. Each block showcases the product image with its information, and can be scrolled horizontally to get more data.
I have following sample code
class Home extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return HomeState();
}
}
class HomeState extends State<Home> {
//------- other codes
#override
void initState() {
bloc.fetchNewProperties(0);
super.initState();
}
Widget build(BuildContext context) {
return Scaffold(
drawer: CustomDrawer(),
backgroundColor: Constants.scaffoldColor,
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
expandedHeight: 216,
floating: false,
titleSpacing: 0.0,
pinned: true,
flexibleSpace: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Constants.gradientStart, Constants.gradientEnd]),
),
child: FlexibleSpaceBar(
centerTitle: true,
background: Swiper(
itemBuilder: (BuildContext context, int index) {
return new Image.asset(
tempBannerImages[index],
fit: BoxFit.fill,
);
},
itemCount: 2,
pagination: new SwiperPagination(),
autoplay: true,
autoplayDelay: 8000,
autoplayDisableOnInteraction: true,
),
),
),
),
];
},
body: ListView(
padding: EdgeInsets.only(
left: size.getSizePx(10), right: size.getSizePx(10)),
children: <Widget>[
newProductsBlock(),
newProductsBlock(),
newProductsBlock(),
newProductsBlock(),
newProductsBlock(), //have added widget multiple times to make stuff scrollable
],
),
),
);
}
Widget newProductsBlock() {
print("---This gets called just once---");
return Column(
children: <Widget>[
Text("New Products"),
Container(
height: 230,
child: StreamBuilder(
stream: bloc.allNewProps,
builder: (context, AsyncSnapshot<ProductsModel> snapshot) {
print("--this gets called multiple times.---");
if (snapshot.hasData) {
return buildNewPropListSwipe(snapshot);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return showCirularLoadingIndicator();
},
),
)
],
);
}
}
Problem
The products are loaded successfully in these blocks. But when I scrolled all the way down and back to top. The circularloading indicator gets visible, in the first block, instead of already loaded items. When I checked i found that Streambuilder is called multiple times as I scroll down and back again up.
Can anybody help me on this, i am quite new to flutter.
Thanks
For anybody who stumble upon same issue,
i changed Parent Listview to ScrollSingleChildView and wrapped widgets into Column and it seems to get fixed now
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 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