One expansion panel was expanded, and when the other expansion panel was expanded, the previous expanded one was set to be closed.
For example, when panel 1 is expanded and panel 2 is clicked, panel 1 is closed and panel 2 is expanded.
By the way, I tried to close panel 1 after expanding panel 1, but it didn't close.
How should I solve this?
class _RecordState extends State<Record> {
#override
void initState() {
super.initState();
}
int? _activeMeterIndex;
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
child: new ListView.builder(
itemCount: 2,
itemBuilder: (BuildContext context, int i) {
return Card(
margin:
const EdgeInsets.fromLTRB(10.0, 15.0, 10.0, 0.0),
child: new ExpansionPanelList(
expansionCallback: (int index, bool status) {
setState(() {
_activeMeterIndex = (_activeMeterIndex == i ? _activeMeterIndex : i);
});
},
children: [
new ExpansionPanel(
isExpanded: _activeMeterIndex == i,
headerBuilder: (BuildContext context,
bool isExpanded) =>
new Container(
padding:
const EdgeInsets.only(left: 15.0),
alignment: Alignment.centerLeft,
child: new Text(
'list-$i',
)),
body: new Container(child: new Text('content-$i'),),
),
],
),
);
}),
);
}
you got the issue for the isExpanded: _activeMeterIndex == i, you must be used a unique index for each expansion tile and The callback gets called whenever one of the expand/collapse buttons is pressed. The arguments passed to the callback are the index of the pressed panel and whether the panel is currently expanded or not.
List<bool> _isExpand = [false, false,];
new ExpansionPanel(
isExpanded: _isExpand[i],
headerBuilder: (BuildContext context,
bool isExpanded) =>
new Container(
padding:
const EdgeInsets.only(left: 15.0),
alignment: Alignment.centerLeft,
child: new Text(
'list-$i',
)),
body: new Container(child: new Text('content-$i'),),
),
// callback
expansionCallback: (int index, bool status) {
setState(() {
_isExpand[i] = !status;
});
},
Related
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);
...
Suppose, I have a list List1 = [A,E,M,X] which I can show using listview.builder horizontally. I have some other lists such as List2 = [a,b,c,d], List3 = [e,f,g,h] etc which I can show under the listview of List1 on a similar way.
What I want is whenever a user presses A from List1, the below listview will automatically show the list List2 = [a,b,c,d]. When a user presses B from List1, the below list will automatically show the list List3 = [e,f,g,h]. I used GestureDetector to detect the press in List1 which works fine. And, all these has to happen in one screen.
Here is my home screen where I'm calling those 2 listviews,
Scaffold(
body: ListView(
padding: EdgeInsets.only(top: 50, left: 20, right: 20),
children: <Widget>[
HomeTopInfo(),
FoodCategory(),
SubFoodCategory(),
SizedBox(
height: 20.0,
),
This is the first list
class FoodCategory extends StatefulWidget {
#override
_FoodCategoryState createState() => _FoodCategoryState();
}
class _FoodCategoryState extends State<FoodCategory> {
final List<Category> _catagories = categories;
int _selectedIndex = 0;
_onSelected(int index) {
setState(() => _selectedIndex = index);
}
#override
Widget build(BuildContext context) {
return Container(
height: 80.0,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _catagories.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () {
_onSelected(index);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
child: Column(
children: <Widget>[
Text(
_catagories[index].title,
style: _selectedIndex != null && _selectedIndex == index
? TextStyle(
color: kTextColor,
fontWeight: FontWeight.bold,
)
: TextStyle(fontSize: 12),
),
if (_selectedIndex != null && _selectedIndex == index)
Container(
margin: EdgeInsets.symmetric(vertical: 5),
height: 3,
width: 22,
decoration: BoxDecoration(
color: kPrimaryColor,
borderRadius: BorderRadius.circular(10),
),
),
],
),
),
);
},
),
);
}
}
This is the second list which I'm showing under the first list
class SubFoodCategory extends StatelessWidget {
final String category;
SubFoodCategory({Key key, this.category}) : super(key: key);
final List<SubCategory> _subCategories = subCategories;
#override
Widget build(BuildContext context) {
print(category);
return Container(
height: 80.0,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _subCategories.length,
itemBuilder: (BuildContext context, int index) {
return FoodCard(
subCategoryName: _subCategories[index].subCategoryName,
imagePath: _subCategories[index].imagePath,
numberOfItems: _subCategories[index].numberOfItems,
);
},
),
);
}
}
I have seperate model class for both the list and used dummy data to populate all this. Which I'm doing right now is showing to list diffrently.
What I want is whenever I press a list item from the first list, I want to show a similar type of list under the first list.
I want to transform my item that it is bigger than the listview itself. (intention for focused navigation)
My List:
Container(
height: 100,
child: ListView.builder(
itemBuilder: (context, index) => HomeItem(title: '$index'),
scrollDirection: Axis.horizontal,
),
),
My Item:
class HomeItem extends StatelessWidget {
final String title;
final bool expand;
const HomeItem({
#required this.title,
this.expand = false,
});
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: ThemeDimens.padding8),
child: Transform.scale(
scale: expand ? 1.5 : 1,
child: AnimatedContainer(
width: 50,
height: 100,
color: expand ? ThemeColors.accent : ThemeColors.primary,
duration: ThemeDurations.shortAnimationDuration(),
child: Center(
child: Text(title),
),
),
),
);
}
}
Current behaviour
Expected behaviour
If you try to use OverflowBox or Transform, content of an item will still clip and won't be drawn outside of its bounding box. But it's possible to use Overlay to draw an element on top of list and position it on a specific list item, though it's a bit complicated.
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
final elements = List.generate(12, (i) => i);
int selectedIndex;
OverlayEntry overlayEntry;
List<LayerLink> layerLinks;
#override
void initState() {
super.initState();
// Creating a layer link for each list cell
layerLinks = List.generate(elements.length, (i) => LayerLink());
}
void createOverlayEntry(int i, BuildContext context) {
// Removing an overlay entry, if there was one
overlayEntry?.remove();
final renderBox = context.findRenderObject() as RenderBox;
final size = renderBox.size;
final offset = renderBox.localToGlobal(Offset.zero);
// Creating a new overlay entry linked to specific list element
overlayEntry = OverlayEntry(
builder: (context) => Positioned(
left: 0,
top: 0,
child: CompositedTransformFollower(
link: layerLinks[i],
showWhenUnlinked: false,
offset: Offset(-20, 0),
child: Material(
color: Colors.yellow,
child: InkWell(
onTap: () {
setState(() {
selectedIndex = null;
});
overlayEntry?.remove();
overlayEntry = null;
},
child: Container(
alignment: Alignment.center,
width: 70,
height: elementHeight,
child: Text('$i')
),
)
),
)
)
);
// Inserting an entry
Overlay.of(context).insert(overlayEntry);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
height: elementHeight,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: elements.length,
itemBuilder: (c, i) {
return CompositedTransformTarget(
link: layerLinks[i],
child: Material(
color: Colors.red,
child: InkWell(
onTap: () {
setState(() {
selectedIndex = i;
});
createOverlayEntry(i, context);
},
child: Container(
alignment: Alignment.center,
width: 30,
child: Text('${elements[i]}'),
),
),
),
);
},
separatorBuilder: (c, i) {
return Container(width: 10, height: 10);
},
),
),
);
}
}
I'm trying to initialize a SingleChildScrollView to start at a certain position with a custom ScrollController. I thought I could use initialScrollOffset and set an initial value in the init method. But somehow when the SingleChildScrollView renders, it only jumps to initialOffset at first build, then when I navigate to another instance of this Widget it doesn't jump to the initialOffset position.
I don't know why, and if I'm lucky maybe one of you have the answer.
Here's my code:
class Artiklar extends StatefulWidget {
final String path;
final double arguments;
Artiklar({
this.path,
this.arguments,
});
#override
_ArtiklarState createState() => _ArtiklarState(arguments: arguments);
}
class _ArtiklarState extends State<Artiklar> {
final double arguments;
_ArtiklarState({this.arguments});
ScrollController _scrollController;
double scrollPosition;
#override
void initState() {
super.initState();
double initialOffset = arguments != null ? arguments : 22.2;
_scrollController = ScrollController(initialScrollOffset: initialOffset);
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
final bool isAdmin = Provider.of<bool>(context) ?? false;
var pathElements = widget.path.split('/');
String tag;
if (pathElements.length == 2) {
tag = null;
} else if (pathElements.length == 3) {
tag = pathElements[2];
} else {
tag = null;
}
return StreamBuilder<List<ArtikelData>>(
stream: DatabaseService(tag: tag).artiklarByDate,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
return GlobalScaffold(
body: SingleChildScrollView(
child: Container(
child: Center(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
GradientHeading(text: "Artiklar", large: true),
isAdmin
? NormalButton(
text: "Skapa ny artikel",
onPressed: () {
Navigator.pushNamed(
context, createNewArtikelRoute);
},
)
: Container(),
SizedBox(height: 10),
SingleChildScrollView(
controller: _scrollController,
scrollDirection: Axis.horizontal,
child: TagList(path: tag),
),
SizedBox(height: 10),
LatestArtiklar(
snapshot: snapshot,
totalPosts: snapshot.data.length,
numberOfPosts: 10,
),
],
),
),
),
),
),
);
} else if (!snapshot.hasData) {
return GlobalScaffold(
body: SingleChildScrollView(
child: Container(
child: Center(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
GradientHeading(text: "Artiklar", large: true),
SizedBox(height: 10),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: TagList(path: tag),
),
SizedBox(height: 10),
LatestArtiklar(hasNoPosts: true)
],
),
),
),
),
),
);
} else {
return GlobalScaffold(
body: Center(child: CircularProgressIndicator()),
);
}
},
);
}
}
That's because that widget is already built on the tree and thus, initState won't be called again for that widget.
You can override the didUpdateWidget method that will trigger each time that widget is rebuilt and make it jump on there, for example.
#override
void didUpdateWidget(Widget old){
super.didUpdateWidget(old);
_scrollController.jumpTo(initialOffset);
}
keepScrollOffset: false
If this property is set to false, the scroll offset is never saved and initialScrollOffset is always used to initialize the scroll offset.
I have a bottom nav bar that uses this code to switch widgets:
void _onItemTapped(int index) {
setState(() {
selectedIndexGlobal = index;
print(index);
});
}
And shows the needed widgets in body:
body: SafeArea(
child: new LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
Container con = Container(
child: _widgetOptions.elementAt(selectedIndexGlobal)
);
return con;
}
))
I also have a GridView with static children list loaded from Firebase, and tried to apply a GestureDetector to it, to change selectedIndex and update screen state:
static getExpenseItems(AsyncSnapshot<QuerySnapshot> snapshot, BuildContext context) {
try {
snapshot.data.documents.sort((a, b) =>
a.data["order"].compareTo(b.data["order"]));
} on NoSuchMethodError {}
return snapshot.data.documents
.map((doc) =>
new GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
catName = doc["catName"];
setState(() {
selectedIndexGlobal = 4;
});
},
child: new Container(
child: Container(
padding: EdgeInsets.only(top: 15, left: 10, bottom: 15),
decoration: new BoxDecoration(
border: new Border.all(color: Colors.grey[200],
width: 0.8)),
child: Align(
alignment: Alignment.centerLeft,
child: Text(doc["name"],
style: TextStyle(fontSize: categoriesFont),
textAlign: TextAlign.center),
)
)
)
)
).toList();
}
But setState can't be called from a static method, as well as GridView.count doesn't let me use non-static widgets as child. What should I do then to update state on click?
This line will help you get the instance of the Widget State object you need.
All you need access to is the current context object:
_HomeState stateObject = context.findAncestorStateOfType<_HomeState>();
stateObject.setState(() {
_tabIndex = index;
});
For this to work, ensure that the context you use to access the State Object is from a child widget of the State object you are trying to find.