I'm trying to make an event page for an app where user can view events that have a banner image and some other useful information. I really like the idea of implementing a SliverAppBar with the banner, so that the user can scroll to see more information. For this I seem to need a CustomScrollView with a SliverAppBar and FlexibleSpaceBar.
All tutorials I have seen online assume that the rest of the screen should be a list of sorts, but I rather want something like a Column widget. A Column has unbounded height, however, which causes overflow errors in the CustomScrollView. I could wrap it in a Container with specified height, but the contents of the body are of variable size, so that is not ideal. Is there a way to have a SliverAppBar and a Column work side by side?
I want something along the lines of this:
class ActivityPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(slivers: [
SliverAppBar(
flexibleSpace: FlexibleSpaceBar(
background: Image(someImage),
),
expandedHeight: Image,
floating: false,
pinned: true,
snap: false,
),
Column(
children: [
someChildren,
]
),
)
]),
),
);
}
It should be possible, because it seems to me a somewhat common pattern, but I have looked around a lot and I can only find examples where the body consists of lists...
For anyone having the same struggle: here's the solution I just found:
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
backgroundColor: this.color,
flexibleSpace: FlexibleSpaceBar(
background: YourImage(),
),
)
];
},
body: Container(
child: Builder(builder: (context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
WidgetOne(),
WidgetTwo()
]);
})),
),
)),
);
}
Use SliverList and SliverChildListDelegate instead of a Column.
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(
background: Container(color: Colors.green),
),
),
SliverList(
delegate: SliverChildListDelegate([
Container(color: Colors.yellow, height: 400),
Container(color: Colors.red, height: 800),
]),
),
],
),
);
}
Use ListView instead of Column. ListView has dynamic size
For me the best way is to use SliverToBoxAdapter. Just wrap your Column in a Container and then wrap this Container in a SliverToBoxAdapter and it should work fine.
Related
I have a fairly complicated screen I am trying to implement in Flutter.
It's a scrollview with a parallax background and...kind of a collapsing toolbar.
I know I have to probably use a NestedScrollView and SliverAppBar(?), but not sure where to start on implementing. I think a picture would best show what I am trying to accomplish:
The list starts below a Container. As you scroll the list, the Container shrinks to a smaller Container and is pinned to the top. Does that make sense? Any help would be greatly appreciated!
This is using SliverAppBar with expandedHeight. I will encourage checking this video.
#override
Widget build(BuildContext context) {
return Scaffold(
body: LayoutBuilder(
builder: (context, constraints) => CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,
expandedHeight: constraints.maxHeight * .3,
flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
title: Text("Title"),
background: Container(
color: Colors.pink,
),
),
),
SliverToBoxAdapter(
child: Container(
height: constraints.maxHeight * 4,
color: Colors.deepOrange,
),
)
],
),
),
);
}
I want to get the whole height of CustomScrollView widget. So I made a below code but it's not working.
#override
void initState(){
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => getSizeAndPosition());
}
getSizeAndPosition() {
RenderBox _customScrollBox =
_customScrollKey.currentContext.findRenderObject();
_customScrollSize = _customScrollBox.size;
_customScrollPosition = _customScrollBox.localToGlobal(Offset.zero);
print(_customScrollSize.height);
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _customScrollKey,
appBar: _appbar(),
body: CustomScrollView(
controller: _controller,
slivers: [
SliverList(
delegate: SliverChildListDelegate([
_titleSection(),
_thumnail(),
SizedBox(
height: 40,
),
])),
],
),
);
}
The height obtained by this code does not take into account the height of the list in the customScrollview. I mean, _customScrollSize.height and MediaQuery.of(context).size.width are the same.
I want this function
_controller.addListener(() {
setState(() {
if (_controller.offset < 0) {
scrollHeight = 0;
} else {
scrollHeight = _controller.offset;
}
});
});
Container(
width: size.width * (scrollHeight / _customScrollSize.height),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1)),
With the above code, '_customScrollSize.height' does not reflect the overall height and therefore the function is not implemented properly. Is there any good way to use it in this situation?
CustomScrollView
A ScrollView that creates custom scroll effects using slivers.
A CustomScrollView lets you supply slivers directly to create various scrolling effects, such as lists, grids, and expanding headers. For example, to create a scroll view that contains an expanding app bar followed by a list and a grid, use a list of three slivers: SliverAppBar, SliverList, and SliverGrid.
In your case do remove the appBar and use a sliverAppBar withing the custome scrollview , and you can use sliverfillRemaining widget for your other children
example
CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
pinned: true,
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
title: Text('Demo'),
),
),
SliverList(
delegate: SliverChildListDelegate([
_titleSection(),
_thumnail(),
SizedBox(
height: 40,
),
])),
...
#override
Widget build(BuildContext context) {
super.build(context);
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
),
child: Scaffold(
key: _scaffoldKeyProfilePage,
body: DefaultTabController(
length: 2,
child:RefreshIndicator(
onRefresh: _onRefresh,
child: NestedScrollView(
headerSliverBuilder: (context, _) {
return [
SliverList(
delegate: SliverChildListDelegate(
[ BuildMainProfile(
....//
),
Padding(
...//another design
),
];
},
// You tab view goes here
body: Column(
children: <Widget>[
TabBar(
tabs: [
Tab(text: 'A'),
Tab(text: 'B'),
],
),
Expanded(
child: TabBarView(
children: [
BuildPost(,
),
BuildWings()
],
),
),
],
),
),),
),
}
Refresh Indicator not working with NestedScrollView, is there any way to implement RefreshIndiactor?
I tried adding an empty ListView in Stack, Refresh indicator start showing but because of that my NestedScrollView doesnt scroll.
Did you try setting NestedScrollView widgets physics to AlwaysScrollableScrollPhysics()?
Please see the documentation for RefreshIndicator.
Refresh indicator does not show up
The RefreshIndicator will appear if its scrollable descendant can be
overscrolled, i.e. if the scrollable's content is bigger than its
viewport. To ensure that the RefreshIndicator will always appear, even
if the scrollable's content fits within its viewport, set the
scrollable's Scrollable.physics property to
AlwaysScrollableScrollPhysics:
ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: ... )
A RefreshIndicator can only be used with a vertical scroll view.
Please try to wrap 'body' of the NestedScrollView with RefreshIndicator widget if the headers won't change when refreshed but only the content change happens for 'body'.
I have a stateful widget with tabs at the top and has a list below it. The list is subdivided into different categories. Each category is listed into each tab. What I want to do is when an item is pressed in the tab, I want its corresponding category in the list to scroll to view.
Here is the relevant code for reference:
SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarTabDelegate(
child: Container(
child: TabBar(
isScrollable: true,
labelColor: Colors.black,
tabs: createTabList(),
controller: tabBarController,
),
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Padding(
padding: const EdgeInsets.only(top: kSpacerRegular),
child: RestaurantMenuCard(menuCard: menuCards[index]),
);
},
childCount: menuCards.length,
),
),
I searched all day looking for a solution but could not find one. I also tried this package but does not seem to work in my case.
Here is my 'widget tree' for more context:
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(),
SliverPersistentHeader(),
SliverList(),
],
),
);
}
A screen capture for more context
I solved the issue with the help of this answer. I replaced SliverList with SliverToBoxAdapter. The child is the list of my RestaurantMenuCard in a Column. Thus, the resulting widget tree is:
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(),
SliverPersistentHeader(),
SliverToBoxAdapter(
Column(
RestaurantMenuCard(),
RestaurantMenuCard(),
...
),
),
],
),
);
}
The magic happens when I added a global key to each instance of the RestaurantMenuCard as discussed in the linked answer.
Then finally, I trigger the Scrollable.ensureVisible(context) in onTap function of my TabBar.
I am using this example from flutter official docs. Here is the minimal code:
List<String> _tabs = ["Tab1", "Tab2"];
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: _tabs.length,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
// 1/2 remove this widget and only use SliverAppBar
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
child: SliverAppBar(
title: const Text('Books'),
pinned: true,
expandedHeight: 150.0,
bottom: TabBar(tabs: _tabs.map((name) => Tab(text: name)).toList()),
),
),
];
},
body: TabBarView(
children: _tabs.map((String name) {
return Builder(
builder: (BuildContext context) {
return CustomScrollView(
key: PageStorageKey<String>(name),
slivers: <Widget>[
// 2/2 remove this widget
SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(_, i) => ListTile(title: Text('Item $i')),
childCount: 30,
),
)
],
);
},
);
}).toList(),
),
),
),
);
}
Problem:
As you can see if I remove SliverOverlapAbsorber along with SliverOverlapInjector in above code, I don't see any change in the output, what's the use of having it? Docs say
SliverOverlapInjector: This widget takes the overlapping behavior of the SliverAppBar, and redirects it to the SliverOverlapInjector below. If it is missing, then it is possible for the nested "inner" scroll view below to end up under the SliverAppBar even when the inner scroll view thinks it has not been scrolled. This is not necessary if the "headerSliverBuilder" only builds widgets that do not overlap the next sliver.
Can anyone explain what does it mean and what's the use of having SliverOverlapInjector and SliverOverlapInjector
I use it in NestedScrollView
NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar()
],
body: ...
)
Without it everything in your body will go under appbar.
Check out https://api.flutter.dev/flutter/widgets/NestedScrollView-class.html
It's said there
SliverOverlapAbsorber(
// This widget takes the overlapping behavior of the SliverAppBar,
// and redirects it to the SliverOverlapInjector below. If it is
// missing, then it is possible for the nested "inner" scroll view
// below to end up under the SliverAppBar even when the inner
// scroll view thinks it has not been scrolled.
// This is not necessary if the "headerSliverBuilder" only builds
// widgets that do not overlap the next sliver.
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),