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,
),
),
]),
);
}
Related
Is there a way to show widgets (for example a SliverToBoxAdapter or a normal Container) underneath a SliverChildBuilderDelegate? I have a condition that turns on the SliverList and there can only be one parent widget for a condition, what would I need to wrap it in?
Code currently looks like:
lass Testaaa extends StatelessWidget {
final bool hasData;
const Testaaa({Key key, this.hasData}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: DrawerSettings(),
body: Container(
child: CustomScrollView(
slivers: <Widget>[
AppBarSliver(),
hasData
? SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int i) {
return ListTile(
title: Text('cool'),
);
},
),
) // I would like to put another Container here that
// scrolls with the bottom, underneath the SliverList
: Container(
child: Text('no data'),
),
],
),
),
);
}
seems like a work for the spread operator
CustomScrollView(
slivers: <Widget>[
AppBarSliver(),
...hasData
? <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int i) {
return ListTile(
title: Text('cool'),
);
},
),
),
const SliverToBoxAdapter(
child: SizedBox(
child: Text('End of the list'),
),
),
]
: <Widget>[
const SliverToBoxAdapter(
child: SizedBox(
child: Text('no data'),
),
)
]
],
)
This way you're telling if hasData is true add all this widgets in the brackets (SliverList and SliverToBoxAdapter with Text 'End of the list') if false add all this other widgets (which is only a SliverToBoxAdapter)
I'm working on an app in Flutter and want to have the following design of my SliverAppBar with the rounded corners to the bottom in my App. How can I do this when I use slivers?
Design rounded corner
I found the design on Dribbble from a YouTuber who created the app as a tutorial but without using a PageView with Slivers
This is a simplified version of the code I'm using:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: PageView(
children: [
CustomScrollView(
slivers: <Widget>[
SliverAppBar(
backgroundColor: Colors.red,
expandedHeight: 250.0,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Card(
child: ListTile(
title: Text('something'),
),
);
},
childCount: 10,
))
],
),
],
),
),
);
}
}
use SliverPadding like this:
DefaultTabController(
child:NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return [
SliverPadding(
padding: new EdgeInsets.only(top: 5.0),
sliver: new SliverList(
delegate: new SliverChildListDelegate(
[YOR_WIDGET()]),
),
);
]
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
I show image from network, which shrink on scrolling. I want to show whole image without paddings or crops. But if I comment line with expandedHeight - there is no image - only appbar with its height. Is there any widget, which can change its size according to size of uploaded image?
CustomScrollView(
controller: controller,
key: listKey,
slivers: <Widget>[
SliverAppBar(
// expandedHeight: 200.0,
flexibleSpace: FlexibleSpaceBar(
background: getHeroWidget(
_conference.dbId,
FadeInImage.assetNetwork(
placeholder: conf_img_placeholder,
image: _conference.info.image,
fit: BoxFit.cover,
)),
title: Text(conference_title),
centerTitle: true,
),
pinned: true,
),
Finally I created custom SliverAppBar. Another issue appears - width of status bar which have to be taken into consideration on calculation.
import 'dart:ui' as ui;
class _HeaderBar extends StatefulWidget {
#override
State<StatefulWidget> createState() => _HeaderState();
}
class _HeaderState extends State<_HeaderBar> {
#override
Widget build(BuildContext context) {
Image image = Image.network(...);
Completer<ui.Image> completer = new Completer<ui.Image>();
image.image.resolve(ImageConfiguration()).addListener((ImageInfo info, bool _) {
completer.complete(info.image);
});
final double statusBarHeight = MediaQuery.of(context).padding.top;
return FutureBuilder(
future: completer.future,
builder: (context, AsyncSnapshot<ui.Image> snapshot) {
return SliverAppBar(
expandedHeight: snapshot.hasData
? MediaQuery.of(context).size.width / snapshot.data.width.toDouble() * snapshot.data.height.toDouble() -
statusBarHeight
: 0.0,
...
I was looking for this problem, I find a solution with NestedScrollView and a plugin SliverStickyHeader
Here is how I did this,
Scaffold(
body: SafeArea(
child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverStickyHeader(
sticky: false,
header: Container(
color: Theme.of(context).primaryColor,
child: Column(
children: [
Text(
'Header',
style: Theme.of(context).textTheme.headline6,
),
Text(
'Header',
style: Theme.of(context).textTheme.headline5,
),
Text(
'Header',
style: Theme.of(context).textTheme.headline4,
),
Text(
'Header',
style: Theme.of(context).textTheme.headline3,
),
],
),
),
)
];
},
body: Column(
children: [
AppBar(
title: Text('My List items'),
),
Expanded(
child: ListView.builder(itemBuilder: (context, i) {
return ListTile(
leading: CircleAvatar(child: Text('$i')),
title: Text('Appbar with dynamic height'),
);
}),
),
],
),
),
),
);
and it is fulfilling my requirement, Plugin also support fully Sliver functionality, but I use it with as header only. I hope it will be helpful.