I was trying to list dropdown like widget, but luckily found the expansion panel list widget to get my desired UX to feel.
So, I am using ExpansionPanelList in my flutter app, but don't require the default elevation/border-shadow it comes with.
I have no idea how to remove it, so as to make it look part of the body rather than an elevated container.
Currently looking like this:
Following is my code:
class _PracticetestComp extends State<Practicetest> {
var listofpracticetest;
List<Item> _data = [
Item(
headerValue: 'Previous Question Papers',
expandedValue: '',
)
];
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xffF8FDF7),
appBar: AppBar(
backgroundColor: Color(0xffF8FDF7), // status bar color
brightness: Brightness.light,
elevation: 0.0,
leading: Container(
margin: EdgeInsets.only(left: 17),
child: RawMaterialButton(
onPressed: () {
Navigator.pushNamed(context, '/');
},
child: new Icon(
Icons.keyboard_backspace,
color: Colors.red[900],
size: 25.0,
),
shape: new CircleBorder(),
elevation: 4.0,
fillColor: Colors.white,
padding: const EdgeInsets.all(5.0),
),
),
),
body: Container(
// height: 200,
margin: EdgeInsets.only(top: 40),
child: ListView(
shrinkWrap: true,
scrollDirection: Axis.vertical,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
margin: EdgeInsets.only(top: 30),
child: Theme(
data: Theme.of(context)
.copyWith(cardColor: Color(0xffF8FDF7)),
child: _buildPanelPreviousPapers()))
],
)
],
),
));
}
Widget _buildPanelPreviousPapers() {
return ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
_data[index].isExpanded = !isExpanded;
});
},
children: _data.map<ExpansionPanel>((Item item) {
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: Text(item.headerValue),
);
},
body: Container(
child: ListTile(
leading: Text(
'Alegbra',
style:
TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
),
),
),
),
isExpanded: item.isExpanded,
);
}).toList(),
);
}
}
// stores ExpansionPanel state information
class Item {
Item({
this.expandedValue,
this.headerValue,
this.isExpanded = false,
});
String expandedValue;
String headerValue;
bool isExpanded;
}
wrap your entire expansion widget child inside Material widget and change the elevation based on expansion child is expanded or not with method
Material(
elevation: isSelected ? 4 : 0,
child: ExpansionTile(
onExpansionChanged:(value){
isSelected=value;
setState(){};
},
title: getExpantionTitle(context),
children: getChildrentList(),
),
),
),
in case if you don't like divider in ExpansionTile tile do some thing like this
final theme = Theme.of(context).copyWith(dividerColor:
Colors.transparent);
//use as a child
child:Theme(data: theme, child: ExpansionTile(...));
Just add this line:
ExpansionPanelList(
elevation: 0, // this line
expansionCallback: ...
First, it isn't recommended to not use elevation for ExpansionPanelList according to Material design spec.
However, if you really want to do that, there are 2 solutions for you, either you create your own custom ExpansionPanelList, or get ready to add couple of lines to the source file. I'm providing you the latter solution.
Open expansion_panel.dart file, go to the build() method of _ExpansionPanelListState and make following changes
return MergeableMaterial(
hasDividers: true,
children: items,
elevation: 0, // 1st add this line
);
Now open mergeable_material.dart file, navigate to _paintShadows method of _RenderMergeableMaterialListBody class and make following changes:
void _paintShadows(Canvas canvas, Rect rect) {
// 2nd add this line
if (boxShadows == null) return;
for (final BoxShadow boxShadow in boxShadows) {
final Paint paint = boxShadow.toPaint();
canvas.drawRRect(kMaterialEdges[MaterialType.card].toRRect(rect), paint);
}
}
Screenshot:
Unfortunately ExpansionPanelList elevation hardcoded, but you can make same widget with ExpansionTile, check this dartpad example.
https://dartpad.dev/0412a5ed17e28af4a46f053ef0f7a5c2
I would wrap it in a ClipRect.
Widget _buildPanelPreviousPapers() {
final panel = ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
_data[index].isExpanded = !isExpanded;
});
},
children: _data.map<ExpansionPanel>((Item item) {
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: Text(item.headerValue),
);
},
body: Container(
child: ListTile(
leading: Text(
'Alegbra',
style:
TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
),
),
),
),
isExpanded: item.isExpanded,
);
}).toList(),
);
return ClipRect(child: panel);
}
To alter the default card background color add a Theme override:
return ClipRect(
child: Theme(
data: Theme.of(context).copyWith(cardColor: Colors.pink),
child: child,
),
);
I was able to set elevation in the constructor (default value is 2), perhaps this is a recent API change:
https://api.flutter.dev/flutter/material/ExpansionPanelList/ExpansionPanelList.html
Related
I have a listview that gets properties from a map like a picture and how many clicks. I am wondering how I can change color so I can display a green checkbox icon on a specific Card. As is now I am only able to change the color on all the checkboxes on all the cards at once. I guess I would like to be able to select just the tapped Card so that its checkbox changes to green. This is the most relevant code:
Main:
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: ListView.builder(
itemCount: widget._passoverCol.keys.length,
itemBuilder: (BuildContext context, int index)
return Container(
height: 100,
padding: const EdgeInsets.all(8.0),
child: Stack(
children: <Widget>[
myCard(
fileName: widget._passoverCol.keys.elementAt(index),
displayName: widget._passoverCol.values
.elementAt(index)
.displayName,
tapsCount: widget._passoverCol.values
.elementAt(index)
.tapsCount,
color: cardColor,
onTap: () {
setState(() {
cardColor = Colors.green;
});
},
),
],
));
}),
),
);
}
}
Card:
class myCard extends StatefulWidget {
const myCard({
required this.tapsCount,
required this.fileName,
required this.displayName,
this.onTap,
this.color,
});
final int? tapsCount;
final String? fileName;
final String? displayName;
final Color? color;
final Function()? onTap;
#override
_myCardState createState() => _myCardState();
}
class _myCardState extends State<myCard> {
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.onTap!,
child: Card(
child: Row(
children: <Widget>[
Expanded(
child: Image.asset(
//getImageFile()
widget.fileName!,
height: 100.0,
width: 100.0,
)),
Padding(
padding: const EdgeInsets.only(left: 32.0),
child: Text(widget.displayName!),
),
SizedBox(
width: 15.0,
),
Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Text(
widget.tapsCount!.toString(),
),
),
Icon(FontAwesomeIcons.check, color: widget.color!),
SizedBox(
width: 200.0,
),
],
),
),
);
}
}
you can use the list of white colors for a specific card; otherwise set a color property in _passoverCol with the default value white color.
List<Color> colors = [Colors.white,Colors.white,Colors.white,Colors.white,Colors.white];
color: colors[index],
setState(() {
colors[index] = Colors.green;
});
},
// set a color property in _passoverCol class and change it like this
setState(() {
widget._passoverCol.values
.elementAt(index)
.color = Colors.green;
});
},
Thank you guys a lot! I dicided to go for #BloodLoss answer! I had to mod a little though. I converted the map keys to a list. And then I used that variable in onTap as you can see in my code otherwise It was very much as the solution:
Main:
var keys = widget._passoverCol.keys.toList();
return Container(
height: 100,
padding: const EdgeInsets.all(8.0),
child: Stack(
children: <Widget>[
foodCard(
fileName: widget._passoverCol.keys.elementAt(index),
displayName: widget._passoverCol.values
.elementAt(index)
.displayName,
tapsCount: widget._passoverCol.values
.elementAt(index)
.tapsCount,
color: selectColorKey!.contains(keys[index])
? Colors.green
: Colors.white,
onTap: () {
setState(() {
selectColorKey!.add(keys[index]);
});
},
Create Array to store your tap keys.
List<String> selectColorKey = [];
Add keys to array
selectColorKey.add(widget._passoverCol.keys.elementAt(index));
If you want to remove from the array when tapping again or click the remove button
selectColorKey.removeWhere((e) => e == widget._passoverCol.keys.elementAt(index));
Checking logic
if(selectColorKey.contains(widget._passoverCol.keys.elementAt(index))) {
// set color to green
} else {
// set default color
}
Your Full Code
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: ListView.builder(
itemCount: widget._passoverCol.keys.length,
itemBuilder: (BuildContext context, int index)
return Container(
height: 100,
padding: const EdgeInsets.all(8.0),
child: Stack(
children: <Widget>[
myCard(
fileName: widget._passoverCol.keys.elementAt(index),
displayName: widget._passoverCol.values
.elementAt(index)
.displayName,
tapsCount: widget._passoverCol.values
.elementAt(index)
.tapsCount,
color:selectColorKey.contains(widget._passoverCol.keys.elementAt(index))? Colors.green:Colors.white,
onTap: () {
setState(() {selectColorKey.add(widget._passoverCol.keys.elementAt(index));
});
},
),
],
));
}),
),
);
}
}
As the question suggests I have an ExpansionPanelList, one ExpansionPanel (the last one or the 7th one) should have 2 additional buttons, but how can I add them just in this one last panel & not in all the others as well?
This is the code of my whole Expansion panel, as Im not sure where you have to add the behaviour, but guessing in the body of the ExpansionPanel (close to line 40):
class ExpansionList extends StatefulWidget {
final Info info;
const ExpansionList({
Key key,
this.info,
}) : super(key: key);
#override
_ExpansionListState createState() => _ExpansionListState();
}
class _ExpansionListState extends State<ExpansionList> {
Widget _buildListPanel() {
return Container(
child: Theme(
data: Theme.of(context).copyWith(
cardColor: Color(0xffDDBEA9),
),
child: ExpansionPanelList(
dividerColor: Colors.transparent,
elevation: 0,
expansionCallback: (int index, bool isExpanded) {
setState(() {
infos[index].isExpanded = !isExpanded;
});
},
children: infos.map<ExpansionPanel>((Info info) {
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: !isExpanded
? Text(
info.headerValue,
) //code if above statement is true
: Text(
info.headerValue,
textScaleFactor: 1.3,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
);
},
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: BoxDecoration(
color: Color(0xffFFE8D6),
borderRadius: BorderRadius.circular(25)),
child: Column(
children: [
ListView.separated(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsets.only(left: 40.0,),
itemCount: info.expandedValueData.length,
itemBuilder: (context, index) {
return CheckboxListTile(
title: Text(info.expandedValueData[index].title,
style: TextStyle(
decoration: info.expandedValueData[index]
.completed
? TextDecoration.lineThrough
: null)),
value: info.expandedValueData[index].completed,
onChanged: (value) {
setState(() {
// Here you toggle the checked item state
infos.firstWhere(
(currentInfo) => info == currentInfo)
..expandedValueData[index].completed =
value;
});
});
},
separatorBuilder: (BuildContext context, int index) {
return SizedBox(
height: 20,
);
},
),
Row(children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.16),
Text("Abschnitt bis zum Neustart löschen"),
SizedBox(
width: MediaQuery.of(context).size.width * 0.11),
IconButton(
icon: Icon(Icons.delete),
onPressed: () {
setState(() {
infos.removeWhere(
(currentInfo) => info == currentInfo);
});
},
)
]),
],
),
),
),
isExpanded: info.isExpanded);
}).toList(),
),
),
);
}
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Container(
child: _buildListPanel(),
),
);
}
}
Thanks for suggestions!
Hi Just add a field (if you already do not have one) in the info object that will allow you to change the widget that is inflated based on that field.
For example
...
children: infos.map<ExpansionPanel>((Info info) {
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return info.type == TYPE_A ? TypeAWidgetHeader(info) : TypeBWidgetHeader(info);
body: info.type == TYPE_A ? TypeAWidgetBody(info) : TypeBWidgetBody(info);
...
in flutter expansion panel, there is a icon on it by default
i want to remove the icon from expansion panel
how i'm gonna do this?
here is my code
ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {},
children: [
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Stack(children: [
ListTile(//and the rest of code...
the only way to do it is by editing the ExpansionPanel source code.
I added a new property called hasIcon and set it by default to true
(to make sure it will not break the code).
ExpansionPanel(
hasIcon: false, // <------
canTapOnHeader: true,
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: Text(
'Title',
),
);
},
body: Container(), // <------
isExpanded: false, // <------
),
and here is how you edit the source code:
Press CTRL + click on ExpansionPanel widget,
then search for
this.isExpanded = false,
and add below it
this.isExpanded = false,
this.hasIcon = true,
then search for
final bool isExpanded;
and add below it
final bool isExpanded;
final bool hasIcon;
finally, search for
Widget header = Row(
children: <Widget>[
Expanded(
child: AnimatedContainer(
duration: widget.animationDuration,
curve: Curves.fastOutSlowIn,
margin: _isChildExpanded(index) ? widget.expandedHeaderPadding : EdgeInsets.zero,
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: _kPanelHeaderCollapsedHeight),
child: headerWidget,
),
),
),
expandIconContainer,
],
);
and replace it
Widget header = Row(
children: <Widget>[
Expanded(
child: AnimatedContainer(
duration: widget.animationDuration,
curve: Curves.fastOutSlowIn,
margin: _isChildExpanded(index) ? widget.expandedHeaderPadding : EdgeInsets.zero,
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: _kPanelHeaderCollapsedHeight),
child: headerWidget,
),
),
),
Container(
child: child.hasIcon? expandIconContainer:null,
),
],
);
This works,
Just add SizedBox.shrink() to the trailing properties of ExpansionTile
ExpansionTile(
trailing: SizedBox.shrink(),
title: Text(
"Title",
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold
),
),
children: <Widget>[
ExpansionTile(
title: Text(
'Sub title',
),
children: <Widget>[
ListTile(
title: Text('data'),
)
],
),
ListTile(
title: Text(
'data'
),
)
],
),
You can add this file to your code and use it in place of ExpansionPanelList.
Or You could edit the source code of ExpansionPanelList directly on your version of flutter (not recommended)
If you are using ExpantionTile inside the panel, you can provide trailing widget to it which will replace the arrow.
There is an ongoing effort in adding this feature to ExpansionPanel, but that is moment, it is not supported.
You can extend this and customize or other libraries which provide this feature. (The one mentioned by #bensal)
I had a similar issue with ExpandablePanel, I created an ExpandableThemeData and set the hasIcon property to false.
var themeData = ExpandableThemeData(hasIcon: false);
ExpandablePanel(
theme: themeDate,
... rest of the code...
)
I am using Cards in Flutter and want Progress Indicator at the left bottom position for 2 seconds while Tap on the card so that another page load successfully.
Does anyone know how to add?
Container(
height: 130,
child: Card(
child: Row(
children: <Widget>[
Expanded(
child: ListTile(
title: Text(
'My card Location',
style: TextStyle(
fontSize: 15, fontWeight: FontWeight.w700),
),
leading: Icon(Icons.setting),
// color: Colors.blueAccent, size: mediumIconSize),
trailing: Icon(Icons.keyboard_arrow_right),
selected: true,
onTap: () async {
// I try this one but not working
// Flushbar(
//
// showProgressIndicator: true,
// duration: Duration(seconds: 2),
// );
getDetails().then((myCardlocations) {
Navigator
.of(context)
.pushNamed('/myCardlocations',
arguments: ObjectLocations(locations, 'myCardlocations'));
}
);
}
),
),
],
),
),
),
You can do something like this using Stack and CircularProgressIndicator..
class _MyWidgetState extends State<MyWidget> {
bool isLoading = false;
#override
Widget build(BuildContext context) {
return Container(
height: 130,
child: Stack(
children: [
Container(
height: 130,
child: Card(
child: Row(
children: <Widget>[
Expanded(
child: ListTile(
title: Text(
'My card Location',
style: TextStyle(
fontSize: 15, fontWeight: FontWeight.w700),
),
leading: Icon(Icons.settings),
// color: Colors.blueAccent, size: mediumIconSize),
trailing: Icon(Icons.keyboard_arrow_right),
selected: true,
onTap: () async {
setState(() {
isLoading = true;
});
getDetails().then((myCardLocations) {
setState(() {
isLoading = false;
});
// navigation code here
});
},
),
),
],
),
),
),
Align(
alignment: Alignment.bottomLeft,
child: isLoading
? Padding(
padding: EdgeInsets.fromLTRB(15,0,0,15),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(),
),
)
: SizedBox(),
),
],
),
);
}
}
Edit:
Looks like I misunderstood the question a bit. Specifically, the place where to show the progress indicator. Anyways, if you get the idea, you can put the indicator at a different place as per your requirement.
There are certain things, which I would like to mention before I give the actual answer.
Read about Flutter.delayed constructor, very useful thing to make some thing wait for a while and do the operation by providing Duration. Whatever you want to do after that duration, it will implement in the callback function
Future.delayed(Duration(seconds: your_time, (){
//it will perform this operation after that much of seconds
}));
You can always show/hide a Widget using bool value, and make changes accordingly
Use a column and Add the LinearProgressIndicator at the end of the Widget. Show/hide it based up on the data
Also, use MediaQuery to give out the height. It is more efficient way of giving the dimensions according to all phone size. Like match-parent in Android Studio. Do the math accordingly, I have shown in the code also
Column(
children: [
Row(),
bool val ? LinearProgressIndicator() : Container() // Container() is nothing but an empty widget which shows nothing
]
)
Some heads up: I have not used getData, since it is not defined properly but you can call it the in function which I will show you in the code, that is pageTransit(). Follow the comments and you are good to go
class _MyHomePageState extends State<MyHomePage> {
// this takes care of the show/hide of your progress indicator
bool _showProgress = false;
// this takes care of the operation
void pageTransit(){
// first show when the ListTile is clicked
setState(() => _showProgress = true);
Future.delayed(Duration(seconds: 2), (){
// hide it after 2 seconds
setState(() => _showProgress = false);
// do the page trnasition here
//getDetails().then((myCardlocations) {
//Navigator.of(context).pushNamed('/myCardlocations',
//arguments: ObjectLocations(locations, 'myCardlocations'));
//}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
height: MediaQuery.of(context).size.height * 0.1,
child: Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// use your items here, based upon the bool value show hide your
// progress indicator
Row(
children: <Widget>[
Expanded(
child: ListTile(
title: Text(
'My card Location',
style: TextStyle(
fontSize: 15, fontWeight: FontWeight.w700),
),
leading: Icon(Icons.settings),
// color: Colors.blueAccent, size: mediumIconSize),
trailing: Icon(Icons.keyboard_arrow_right),
selected: true,
onTap: () => pageTransit()
)
)
]
),
// show/hide in the card
_showProgress ? LinearProgressIndicator() : Container()
]
)
)
)
);
}
}
Result
Look at the ProgressIndicator, it remains there for 2 seconds, and then goes away
1. You need to define a GlobalKey for the Scaffold so that you can use a SnackBar (you can define the GloablKey in your page's State).
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
2. You need to set the key for the Scaffold.
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
...
3. You need to wrap the Card with a GestureDetector and set the onTap function to call showLoading which shows a SnackBar on the bottom of the screen. Call your getDetails function in the showLoading. Full code (except the define key step):
void _showLoading() {
_scaffoldKey.currentState.showSnackBar(new SnackBar(
duration: new Duration(seconds: 2),
content: new Row(
children: <Widget>[
new CircularProgressIndicator(),
new Text("Loading...")
],
),
));
// call to your getDetails and its steps should be here
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: Text("My app"),
),
body: Center(
child: GestureDetector(
child: Card(
child: Row(children: <Widget>[
Expanded(
child: ListTile(
title: Text(
'My card Location',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700),
),
leading: Icon(Icons.settings),
// color: Colors.blueAccent, size: mediumIconSize),
trailing: Icon(Icons.keyboard_arrow_right),
selected: true,
)),
])),
onTap: () => _showLoading(),
)),
);
}
}
Note: you can also style the SnackBar.
Result:
This is a repository to create a minimal reproducible example.
I want SliverAppBar hidden when ScrollablePositionedList.builder is Scrolled. This is the relevant piece of code I am including here.
NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverAppBar(
backgroundColor: Colors.blue,
expandedHeight: 112,
snap: true,
pinned: false,
floating: true,
forceElevated: true,
actions: <Widget>[
IconButton(
icon: Icon(Icons.event),
)
],
flexibleSpace: SafeArea(
child: Column(
children: <Widget>[
Container(
height: kToolbarHeight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Title',
style: Theme.of(context)
.textTheme
.title
.copyWith(
fontSize: 16, color: Colors.white),
),
SizedBox(
height: 2,
),
Text(
'Date',
style: Theme.of(context)
.textTheme
.caption
.copyWith(
fontSize: 10, color: Colors.white),
),
SizedBox(
height: 2,
),
Text(
'Another Text',
style: Theme.of(context)
.textTheme
.subtitle
.copyWith(
fontSize: 14, color: Colors.white),
),
],
),
),
Expanded(
child: Container(
height: kToolbarHeight,
width: MediaQuery.of(context).size.width,
color: Colors.white,
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(
'Prev',
),
Text(
'Next',
)
],
),
),
)
],
),
),
)
],
body: ScrollablePositionedList.builder(
physics: ScrollPhysics(),
itemPositionsListener: itemPositionListener,
itemScrollController: _itemScrollController,
initialScrollIndex: 0,
itemCount: 500,
itemBuilder: (BuildContext ctxt, int index) {
return Container(
margin: EdgeInsets.all(16)
,
child: Text('$index'));
})),
I tried two approaches so far none of them working properly,
Approach 1
I added physics: ScrollPhysics(), to ScrollablePositionedList.builder
Output:
Appraoch 2
I added physics: NeverScrollableScrollPhysics(), to ScrollablePositionedList.builder
SliverAppBar hides this time but now I can not scroll to the very end of ScrollablePositionedList.builder I have 500 items on my list but it scrolls up to only 14th item, see the output. Also, it lags too much on scroll
Output:
Thanks in advance.
Answering question myself
This problem has no solution for it. I have created an issue here
It looks like ScrollablePositionedList with SliverAppBar cannot work until Flutter Team does not add shrinkwrap property to ScrollablePositionedList.
Feature request to add shrinkwrap is created here
It works for me
//create list of global keys
List<GlobalKey> _formKeys = [];
//assign keys from your list
for(int i=0 ;i< syourlist.length;i++){
final key = GlobalKey();
_formKeys.add(key);
}
//in list view give key as below
key:_formKeys[index]
//on button click
Scrollable.ensureVisible(_formKeys[index].currentContext);
Here is a basic workaround:
Use the ItemsPositionsListener to listen for the current item the list has scrolled to.
Then create boolean values to check the scroll-direction and amount.
These conditions control an AnimatedContainer controlling the height of a custom header.
This is placed as a child in a Column with the header in a Flexible widget so the scrollablelist correctly takes up the space before and after animation.
Although this is very basic and does not use the NestedScrollView, it keeps use of the ScrollablePositionedList, and achieves a similar effect with a header that slides in and out, based on the set scroll conditions.
Providing in case helps anyone else, until the underlying issue is fixed...:)
import 'package:flutter/material.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class ScrollAllWords extends StatefulWidget {
const ScrollAllWords({
Key? key,
required this.list,
}) : super(key: key);
final List<String> list;
#override
State<ScrollAllWords> createState() => _ScrollAllWordsState();
}
class _ScrollAllWordsState extends State<ScrollAllWords> {
/// use this listener to control the header position.
final _itemPositionsListener = ItemPositionsListener.create();
///Can also use the ItemScrollController to animate through the list (code omitted)
final _itemScrollController = ItemScrollController();
/// Gets the current index the list has scrolled to.
int _currentIndex = 0;
/// Compares against current index to determine the scroll direction.
int _shadowIndex = 0;
bool _reverseScrolling = false;
bool _showHeader = true;
#override
void initState() {
/// Set up the listener.
_itemPositionsListener.itemPositions.addListener(() {
checkScroll();
});
super.initState();
}
void checkScroll() {
/// Gets the current index of the scroll.
_currentIndex =
_itemPositionsListener.itemPositions.value
.elementAt(0)
.index;
/// Checks the scroll direction.
if (_currentIndex > _shadowIndex) {
_reverseScrolling = false;
_shadowIndex = _currentIndex;
}
if (_currentIndex < _shadowIndex) {
_reverseScrolling = true;
_shadowIndex = _currentIndex;
}
/// Checks whether to show or hide the scroller (e.g. show when scrolled passed 15 items and not reversing).
if (!_reverseScrolling && _currentIndex > 15) {
_showHeader = false;
} else {
_showHeader = true;
}
setState(() {});
}
#override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 120),
height: _showHeader ? 200 : 0,
curve: Curves.easeOutCubic,
child: Container(
color: Colors.red,
height: size.height * 0.20,
),
),
Flexible(
child: ScrollablePositionedList.builder(
itemScrollController: _itemScrollController,
itemPositionsListener: _itemPositionsListener,
itemCount: widget.list.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(widget.list[index]),
);
},
),
),
],
);
}
}