flutter implement sticky headers and the snap to item effect - dart

For the last few days, I've been reading through flutter framework documentation and especially the sliver part but I'm not quite sure where to start.
I'm trying to implement the sticky headers and snap effect.
Might the RenderSliverList be a good start? Do I need to re-layout things? Do I need to do additional drawing? And if so where?
Any help on where to start would be a huge help, thanks in advance!
Edit: I think I understood the layout part now, but I just can't find where the painting is supposed to happen.
Edit 2: For clarification, this is the desired "sticky header effect":
How can I make sticky headers in RecyclerView? (Without external lib)
and this is the "snap" effect:
https://rubensousa.github.io/2016/08/recyclerviewsnap

For the "sticky header effect" I ran into this problem myself, so I created this package to manage sticky headers with slivers: https://github.com/letsar/flutter_sticky_header
To use it you have to create one SliverStickyHeader per section in a CustomScrollView.
One section can be wrote like this:
new SliverStickyHeader(
header: new Container(
height: 60.0,
color: Colors.lightBlue,
padding: EdgeInsets.symmetric(horizontal: 16.0),
alignment: Alignment.centerLeft,
child: new Text(
'Header #0',
style: const TextStyle(color: Colors.white),
),
),
sliver: new SliverList(
delegate: new SliverChildBuilderDelegate(
(context, i) => new ListTile(
leading: new CircleAvatar(
child: new Text('0'),
),
title: new Text('List tile #$i'),
),
childCount: 4,
),
),
);
If you want, the entire source code for the above demo is here: https://github.com/letsar/flutter_sticky_header/blob/master/example/lib/main.dart
I hope this will help you.

It's dead simple :
Use a CustomScrollView and give it as child both a SliverList and a SliverAppBar. You may replace the SliverList with a SliverGrid if you need to.
Then, depending on the effect you want to achieve, there are a few properties you may set on SliverAppBar:
snap
expandedHeight (+ flexibleSpace)
floating
pinned
In the end, you may have something similar to :
new CustomScrollView(
slivers: <Widget>[
new SliverAppBar(
title: new Text("Title"),
snap: true,
floating: true,
),
new SliverFixedExtentList(
itemExtent: 50.0,
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: new Text('list item $index'),
);
},
),
),
],
)
Even better, you can concatenate different scroll behaviour inside a single CustomScrollView.
Which means you can potentially have a grid followed by a list just by adding a SliverGrid as a child to your scrollView.
I know I know, flutter is awesome.

I managed to do the stickyheader effect on Flutter for an iOS app using the following code - credit goes to this piece of code written here from where I drew my inspiration (https://github.com/flutter/flutter/blob/master/examples/flutter_gallery/lib/demo/animation/home.dart#L112):
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate({
#required this.collapsedHeight,
#required this.expandedHeight,}
);
final double expandedHeight;
final double collapsedHeight;
#override double get minExtent => collapsedHeight;
#override double get maxExtent => math.max(expandedHeight, minExtent);
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Container(color: Colors.red,
child: new Padding(
padding: const EdgeInsets.only(
left: 8.0, top: 8.0, bottom: 8.0, right: 8.0),
child: new Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
new Text("Time"), new Text("Price"), new Text("Hotness")
],
),
)
);
}
#override
bool shouldRebuild(#checked _SliverAppBarDelegate oldDelegate) {
return expandedHeight != oldDelegate.expandedHeight
|| collapsedHeight != oldDelegate.collapsedHeight;
}
}
To make it sticky, add the _SliverAppBarDelegate to the silvers widget list:
new SliverPersistentHeader(delegate: new _SliverAppBarDelegate(collapsedHeight: 36.0, expandedHeight: 36.0), pinned: true, ),
I'm not really sure how to make the _SliverAppBarDelegate wrap the content though, I had to provide it with a size of 36 logical pixels to get it to work. If anyone know how it could just wrap content, please drop a comment to the answer below.

I solved this problem, try sticky_and_expandable_list.
Features
Support build an expandable ListView, which can expand/collapse section or create sticky section header.
Use it with CustomScrollView、SliverAppBar.
Listen the scroll offset of current sticky header, current sticky header index and switching header index.
Only use one list widget, so it supports large data and a small memory usage.
More section customization support, you can return a new section widget by sectionBuilder, to customize background,expand/collapse animation, section layout, and so on.
Support add divider.

As per documentation, you can place a StickyHeader or StickyHeaderBuilder inside any scrollable content.
The documentation page provides only an example how to apply a StickyHeader with a ListView, ok what about the other widgets such as SingleChildScrollView or CustomScrollView ?
In this example I will provide a very simple StickyHeader with a SingleChildScrollView and sure you can put it inside any SingleChildScrollView you want:
return Container(
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("My title"),
StickyHeader(
header: Container(
child: // Put here whatever widget you like as the sticky widget
),
content: Column(
children: [
Container(
child: // Put here whatever widget you like as scrolling content (Column, Text, ListView, etc...)
),
],
),
),
],
),
),
);

without implementing yours using sliver, you can achieve this using flutter community awesome plugin.
https://pub.dev/packages/sticky_headers

Related

Bottom padding after ListView.Builder

I have a Listview inside a bottom sheet .. like this:
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return Directionality(
textDirection: globals.getDirection(),
child: Container(
margin: EdgeInsets.only(
right: 10.0,
left: 10.0),
height: MediaQuery.of(context).size.height / 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Flexible(
child: TextField()
),
Expanded(
child: ListView.builder(
shrinkWrap: false,
itemCount: nationalities.length,
itemBuilder: (BuildContext context, int index) {
return new Padding(
padding: EdgeInsets.only(top: 10.0),
child: GestureDetector(
onTap: () {
setState(() {
_data.nationality = nationalities[index];
});
Navigator.pop(context);
},
child: Text(
nationalities[index],
textAlign: TextAlign.center,
),
));
},
),
),
),
);
}
);
The problem that i have huge white space at the bottom i don't now from where it's coming .. as you can see from the screenshot:
How to remove this white space and make the listview expand in it instead?
Using an Expanded widget makes a child of a Row, Column, or Flex expand to fill the available space in the main axis (e.g., horizontally for a Row or vertically for a Column). If multiple children are expanded, the available space is divided among them according to the flex factor.
That's maybe your problem. Removing it may help you.
Next thing ...
Using a Flexible widget gives a child of a Row, Column, or Flex the flexibility to expand to fill the available space in the main axis (e.g., horizontally for a Row or vertically for a Column), but, unlike Expanded, Flexible does not require the child to fill the available space.
Next Thing:
By default, ListView will automatically pad the list's scrollable extremities to avoid partial obstructions indicated by MediaQuery's padding. To avoid this behavior, override with a zero padding property.
ListView(
padding: EdgeInsets.zero,
...);
Second Option
MediaQuery.removePadding(
context: context,
removeTop: true,
child: ListView(...),
)
Sorry for Exrta explanation ;
This is a bug that's caused by having a Flexible widget and an Expanded widget in the same Row/Column, https://github.com/flutter/flutter/issues/20575
You can fix it by removing the Flexible widget that's wrapping your TextField.
I faced the same issue when using ListView.builder() in ModalBottomSheet. I have to use Column instead of:
sortList() {
var itemList = <Widget>[
Text('1'),
Text('2'),
];
// for loop to add more items here
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: itemList);
}
Removing mainAxisSize: MainAxisSize.min will reproduce the issue.

How expand text and container according text size?

I'm trying to create a card with a text within a container but I would like to show only a part of the text and when the user click on "show more", show the rest. I saw a Widget to construct text like this here, but I need expand the card container either and I don't know how to do that because I need to know how many lines the text have to expand with the correctly size. Exists a way to calculate the size according the number of lines or characters?
I tried to create the card as follows, where the DescriptionText is the Widget on the link and specify a minHeight in the Container in the hope of expanding the container along with the text but did not work.
Widget _showAnswerCard(Answer answer, User user) {
return Card(
elevation: 3.0,
color: Theme.of(context).backgroundColor,
child: Container(
constraints: BoxConstraints(minHeight: 90),
padding: EdgeInsets.all(10.0),
child: Flex(
direction: Axis.horizontal,
children: <Widget>[
Expanded(flex: 1, child: _showUserAvatar(answer)),
Expanded(flex: 3, child: _showAnswerDetails(answer, user)),
],
),
));
}
Widget _showAnswerDetails(Answer answer, User user) {
return Flex(
direction: Axis.vertical,
children: <Widget>[
Expanded(
flex: 3,
child: DescriptionTextWidget(text: answer.content),
),
Expanded(
flex: 1,
child: _showAnswerOptions(),
)
],
);
}
I'll really appreciate if someone could help me with that.
Just use Wrap widget to wrap your Card widget.
Based on your link for suggested answer. I did change to use Wrap widget.
Jus do copy/paste below code and check.
import 'package:flutter/material.dart';
class ProductDetailPage extends StatelessWidget {
final String description =
"Flutter is Google’s mobile UI framework for crafting high-quality native interfaces on iOS and Android in record time. Flutter works with existing code, is used by developers and organizations around the world, and is free and open source.";
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: const Text("Demo App"),
),
body: new Container(
child: new DescriptionTextWidget(text: description),
),
);
}
}
class DescriptionTextWidget extends StatefulWidget {
final String text;
DescriptionTextWidget({#required this.text});
#override
_DescriptionTextWidgetState createState() =>
new _DescriptionTextWidgetState();
}
class _DescriptionTextWidgetState extends State<DescriptionTextWidget> {
bool flag = true;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Wrap(
children: <Widget>[
Card(
margin: EdgeInsets.all(8),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
child: Column(
children: <Widget>[
Container(
child: Text(
widget.text,
overflow: flag ? TextOverflow.ellipsis : null,
style: TextStyle(
fontSize: 15,
),
),
),
InkWell(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Text(
flag ? "show more" : "show less",
style: new TextStyle(color: Colors.blue),
),
],
),
onTap: () {
setState(() {
flag = !flag;
});
},
),
],
)),
),
],
);
}
}
Result:
The solution I can think of is to use two labels, one for displaying only one line of text and one for displaying all the text. When the button is clicked, the two labels are alternately displayed in an animated manner. There is no computer at the moment, it is not convenient to verify, I hope to give you some help in the implementation of the program.

Flutter Expansion Tile -- Header Color Change, and Trailing Animated Arrow Color Change

I have used the Expansion Tile to generate a Expansion List View.
I'm facing some customization issues in Expansion Tile Header.
Below is my code.
ExpansionTile(
title: Container(
child: Text(
getCategory(i),
style: TextStyle(
color: Colors.white,
),
),
color: Colors.black
),
children: <Widget>[
new Container(
height: 60.0,
margin: const EdgeInsets.only(top:10.0, left: 10.0, right:10.0, bottom: 10.0),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: new BorderRadius.all( Radius.circular(5.0) ),
),
),
new Container(
height: 60.0,
margin: const EdgeInsets.only(top:10.0, left: 10.0, right:10.0, bottom: 0.0),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: new BorderRadius.all( Radius.circular(5.0) ),
),
)
],
backgroundColor: Colors.white,
)
I'm getting below result.
What I'm expecting to have is below.
If anyone know a workaround to customize the header color, please advice.
If you check the source code of the ExpansionTitle , you will notice that the header item is a ListTile , so you can't change the background because it hasn't a parent with a color property.
I modified a little the class to support what you need.
Add this file to your project: https://gist.github.com/diegoveloper/02424eebd4d6e06ae649b31e4ebcf53c
And import like this way to avoid conflicts because the same name.
import 'package:nameofyourapp/custom_expansion_tile.dart' as custom;
I put an alias 'custom' but you can change for any alias.
Usage:
custom.ExpansionTile(
headerBackgroundColor: Colors.black,
iconColor: Colors.white,
title: Container(
...
Remember, Flutter has a lot of Widgets out of the box, but If any of them don't fit what you need, you'll have to check the source code and create your own widget.
In my opinion, the more preferable way is wrap It with a new Theme, so It could work as expected:
Theme(
data: Theme.of(context).copyWith(accentColor: ColorPalette.fontColor, unselectedWidgetColor: ColorPalette.fontColor..withOpacity(0.8)),
child: ListView(
children: <Widget>[
ExpansionTile(
title: Text("Padding"),
children: <Widget>[
Text("Left"),
Text("Top"),
Text("Right"),
Text("Bottom"),
],
)
],
),
)
Instead of copy a full class to customize, check the source code, you could find more Theme attribute to override.
_borderColorTween
..end = theme.dividerColor;
_headerColorTween
..begin = theme.textTheme.subhead.color
..end = theme.accentColor;
_iconColorTween
..begin = theme.unselectedWidgetColor
..end = theme.accentColor;
_backgroundColorTween
..end = widget.backgroundColor;
If you want a more animatable widget or something, I would recommend diegoveloper's answer. Otherwise, just wrap It with Theme, so you won't need to maintain the Component, and get native flutter component.
A much easier way than all of those suggested is to wrap your ExpansionTile in a ListTileTheme.
Once you do this, you can change the backgroundColor to whatever you'd like. In my case, I've done the same thing with the ListTiles inside of the ExpansionTile so that the header can have one color and the children can have another color.
return ListTileTheme(
tileColor: Colors.grey.shade300,
child: ExpansionTile(
leading: Padding(
padding: const EdgeInsets.only(top:4.0),
child: Text('Outer Tile'),
),
title: null,
children: [
Slidable(
actionPane: SlidableDrawerActionPane(),
child: ListTileTheme(
tileColor: Colors.white,
child: ListTile(
title: Text('Inner Tile'),
subtitle: Text('subtitle'),
leading: FlutterLogo(),
),
),
)
],
),
);
I think this is easier than digging through the docs to find which Theme elements correspond to individual ListTile parameters.
I think better this solution more than custom list tile .
Widget customTheme(Widget child, BuildContext context) => Theme(
data: Theme.of(context).copyWith(
dividerColor: Colors.transparent,
dividerTheme: DividerThemeData(
color: Theme.of(context).colorScheme.background)),
child: child,
);
Code Gist
It seems the ExpansionTile was changed and you can now directly configure lots of colors. As an example, for the arrows:
return ExpansionTile(
// sets the color of the arrow when collapsed
collapsedIconColor: Colors.red,
// sets the color of the arrow when expanded
iconColor: Colors.green,
);
For changing trailing arrow icon color you can wrap expansion tile with Theme and make theme light and set accet color and primary color to dark and it will work.
return Theme(
data: ThemeData.light()
.copyWith(accentColor: darkAccent, primaryColor: darkAccent),
child: ExpansionTile(
title: Text(
data['number'],
style:
TextStyle(color: darkAccent, fontWeight: FontWeight.bold),
),
Another option which should work is wrapping both the entire expansion tile and each individual container in a Card() widget as they have a color property. The downside is that the background for your expansion will be colored as well as the title, but if you set a color for the child cards, they will not be affected by the parent an so they could be individually colored as you like.
For trouble with the arrow color: notice it's an ExpandIcon, defined as a local Widget expandIconContainer in _ExpansionPanelListState.build().
When your ExpansionPanel uses canTapOnHeader: true, you get ExpandIcon(onPressed: null), so the color used for the underlying IconButton is determined by Theme#disabledColor.
I use this great little plugin, much more flexible than the Flutter Material. You can change the background and borders and have custom icons for open and close states. https://pub.dev/packages/configurable_expansion_tile

Flutter: Banner Ad Overlapping The Main Screen

I am doing a Flutter app and managed to show the AdMob banner ad, however the ad overlaps the bottom of my app's main screen:
By following this article, I managed to make the app screen's bottom properly displayed, but the persistentFooterButtons is sacrificed, which I think is not an ideal solution.
I am thinking about putting the Scaffold object and a fixed height area into a column that is contained by a Center object, something similar to the following:
#override
Widget build(BuildContext context) {
return new Center(
child: new Column (
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new Expanded (
child: _getScaffold(),
),
new Expanded (
child: new Container(height: 50.0,)
)
],
),
);
}
But in this way I get the exception "A RenderFlex overflowed by 228 pixels on the bottom":
Anybody can show me how to build such layout? I want every component of my scaffold properly displayed, with a fixed height dummy footer that is ok to be overlapped by the Admob's banner ad.
Any help is much welcome.
Jimmy
Also we can add some trick like bottomNavigationBar under the Scaffold
bottomNavigationBar: Container(
height: 50.0,
color: Colors.white,
),
This will take floating button up.
Finally I got it:
#override
Widget build(BuildContext context) {
return new Center(
child: new Column (
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Expanded (
child: _getScaffold(),
),
new Container(height: 50.0,
child: new Placeholder(color:Colors.blue))
],
),
);
}
The trick is Expanded here is for the Scaffold only, but for the dummy footer just a fixed height Container is required. Now I can display everything available from the Scaffold object.
Layout building of Flutter sometimes really confuses me...
If I understand your question well, I think you want to have your ad shown from the bottom while using a FAB. I think using a Stack widget here is a good solution, I created this example in a rush but should be enough to show you what I mean:
class AdBar extends StatefulWidget {
#override
_AdBarState createState() => new _AdBarState();
}
class _AdBarState extends State<AdBar> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(),
body: new ListView(
children: new List.generate(50, (int index) {
return new Text("widgets$index");
}),
),
persistentFooterButtons:
<Widget>[
new Stack(
children: <Widget>[
new Container (
color: Colors.transparent,
child: new Material(
color: Colors.cyanAccent,
child: new InkWell(
onTap: () {
},
child: new Container(
//color: Colors.cyanAccent,
width: MediaQuery
.of(context)
.size
.width * 0.90,
height: MediaQuery
.of(context)
.size
.height * 0.25,
),
),),),
new Positioned(
right: 0.0,
child: new FloatingActionButton(
onPressed: () {}, child: new Icon(Icons.fastfood)))
],
)
]
);
}
}

Flutter - The screen is not scrolling

I inserted 6 cards, however it is not possible to scroll the screen.
According to the image below, a red stripe appears in the footer, and the screen does not scroll.
What is missing to be able to scroll the screen?
main.dart
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: "Myapp",
home: new HomePage(),
);
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) => new Scaffold(
appBar: new AppBar(
backgroundColor: new Color(0xFF26C6DA),
),
body: new Column(
children: <Widget>[
new Card(
child: new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const ListTile(
leading: const Icon(Icons.album),
title: const Text('The Enchanted Nightingale'),
subtitle: const Text('Music by Julie Gable. Lyrics by Sidney Stein.'),
),
],
),
),
...
...
...
],
)
);
}
Columns don't scroll. Try replacing your outer Column with a ListView. You may need to put shrinkWrap: true on it.
To make a column scrollable, simply wrap it in a SingleChildScrollView.
This might do the trick, worked like charm for me:
shrinkWrap: true, physics: ClampingScrollPhysics(),
I needed placing SingleChildScrollView inside Column. The SingleChildScrollView also needed Column as a child, but the scroll wasn't working for me in that case. The solution was to wrap the SingleChildScrollView with Expanded. So here's how it looked:
Column(
children: <Widget>[
MyFirstWidget(),
Expanded(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
// Scrollable content.
],
),
),
),
],
),
you have to put it on ListView.builder
ListView.builder(
itemBuilder: (BuildContext context, int index){
final item = yourItemLists[index];
new Card(
child: new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const ListTile(
leading: const Icon(Icons.album),
title: const Text('The Enchanted Nightingale'),
subtitle: const Text('Music by Julie Gable. Lyrics by Sidney Stein.'),
),
],
),
);
},
itemCount: yourItemLists.length,
);
A column in a column make the layout impossible to calculate without setting height.
The second column is useless since it contains only one element, try to put the ListTile directly as the body of the Card.
You should use ListView.builder in place of the inner column (as I suggested above that columns are not scrollable).
Set shrinkWrap: true, and physics: ClampingScrollPhysics() inside ListView.builder. Just using shrinkWrap: true didn't solve my problem. But setting physics to ClampingScrollPhysics() started to make it scroll.
There are generally two ways to make a screen scrollable. One is by using Column, and the other is by using ListView.
Use Column (wrapped in SingleChildScrollView):
SingleChildScrollView( // Don't forget this.
child: Column(
children: [
Text('First'),
//... other children
Text('Last'),
],
),
)
Use ListView:
ListView(
children: [
Text('First'),
//... other children
Text('Last'),
],
)
This approach is simpler to use, but you lose features like crossAxisAlignment. For this case, you can wrap your children widget inside Align and set alignment property.
You can also Wrap the parent Column in a Container and then wrap the Container with the SingleChildscrollView widget. (This solved my issue).
Just Add physics: ClampingScrollPhysics(),

Resources