Is there a way to get rid of the spacing between expansion panels when expanded in an expansion panel list in Flutter? - flutter

I am running into a problem where I am wanting to remove the space between expansion panels of an expansion panel list when the panels are expanded.
Images of unwanted behavior, these images are taken from flutter documentation:
List when not expanded, which is fine:
List when expanded:
- You can see the gap between the sections. This is what I do not want for my app.
Any tips are appreciated.

Try Modifying The Actual Implementations
If you have some prior programming experience, this should make sense...
Find these files:
expansion_panel.dart
expansion_title.dart
expand_icon.dart
mergeable_material.dart
These files should be located in the External Library > Dart Packages > Flutter > src > material. Note that the material folder may be expressed as "src.material".
Android Studio should allow you to hover over the ExpansionPanel Widget and right-click it. After right-clicking it you should see a list of shortcuts, one being "Go to". Click on the "Go to" option which should bring up more options, one of which being "Implementation(s)". This should bring you to where you need. According to my version, double clicking the widget name so the entire widget's name is highlighted should allow for a keyboard shortcut Ctrl+Alt+B.
The key file you must go into is "mergeable_material.dart". In "mergeable.dart", go to the MaterialGap constructor:
const MaterialGap({
#required LocalKey key,
this.size = 16.0,
//This is the size of the gap between ExpansionTiles WHEN EXPANDED
}) : assert(key != null),
super(key);
This size variable controls the gap between the Expansion Tiles ONLY when EXPANDED. Setting "this.size = 0.0" should remove the gap.
If you want to know why this is, long story short, when the ExpansionPanels list of widgets property is being defined (which is in expansion_panel.dart) a "MaterialGap" is being added between the children. We modified the material gap to be 0.0 thus effectively removing the gap.
Other things you can do:
Change the Trailing Icon in the Header.
Go to expand.dart file. Go to the bottom of the file where the build method should be. In the return statement, replace the icon button with 'whatever' you want (within reason).
#override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
assert(debugCheckHasMaterialLocalizations(context));
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final String onTapHint = widget.isExpanded ? localizations.expandedIconTapHint :
localizations.collapsedIconTapHint;
return Semantics(
onTapHint: widget.onPressed == null ? null : onTapHint,
child: Container() //Replaced the Icon Button here to remove it
);
}
Keep in mind that without a button, you must make canTapOnHeader: true, or you won't
be able to expand the panel. You can make sure you don't forget to do this by going
to the ExpansionPanel constructor and changing "this.canTapOnHeader = false;" to
"this.canTapOnHeader = true;
ExpansionPanel({
#required this.headerBuilder,
#required this.body,
this.isExpanded = false,
this.canTapOnHeader = true, //Changed this to true from false
}) : assert(headerBuilder != null),
assert(body != null),
assert(isExpanded != null),
assert(canTapOnHeader != null);
Remove the shadow and/or dividers
Go to the bottom of the expansion_panel.dart file. This is where the build method should be. At the bottom of the build method should be the return statement, which returns MergeableMaterial. You can do the following:
return MergeableMaterial(
hasDividers: false, //Change the boolean value of this
children: items,
elevation: 0,
//Add this line to remove shadow from elevation, REQUIRES ANOTHER STEP
);
If add the "elevation: 0" property you MUST go back to mergeable_material.dart and add the following line of code:
void _paintShadows(Canvas canvas, Rect rect) {
if (boxShadows == null) return; //ADD THIS LINE OF CODE
for (BoxShadow boxShadow in boxShadows) {
final Paint paint = boxShadow.toPaint();
// TODO(dragostis): Right now, we are only interpolating the border radii
// of the visible Material slices, not the shadows; they are not getting
// interpolated and always have the same rounded radii. Once shadow
// performance is better, shadows should be redrawn every single time the
// slices' radii get interpolated and use those radii not the defaults.
canvas.drawRRect(kMaterialEdges[MaterialType.card].toRRect(rect), paint);
}
}
I think you get the idea, You can do more to remove padding and whatnot by messing around with constants.
Concerns
Note: I'm relatively new to flutter, (2 months last summer and 2 months now- I'm a college student) in fact. I don't know how modifying these files may impact other widgets but I haven't noticed any issues by doing this.
This is also my first post so take it with a grain of salt. Modifying Material implementations may break some rules or conventions that I would refer to the documentation for.

Instead of changing the source code, you're better off making a copy of the expansion_panel.dart and using this. For the space between the items to disappear, you must comment out on lines 486 and 487.
if (_isChildExpanded(index) && index != 0 && !_isChildExpanded(index - 1))
items.add(MaterialGap(key: _SaltedKey<BuildContext, int>(context, index * 2 - 1)));
And on lines 558 and 559.
if (_isChildExpanded(index) && index != widget.children.length - 1)
items.add(MaterialGap(key: _SaltedKey<BuildContext, int>(context, index * 2 + 1)));
Another issue with this component which you might want to fix, is with the canTapOnHeader property. Setting it to true allows you to tap the card and expand, but you're stuck with a bunch of dead space on the right side of your card. To fix this, add a check to only show expandIconContainer (line 526) as follows:
if (!child.canTapOnHeader) expandIconContainer,

The ExpansionPanelList has an elevation property, which is causing the panels to appear separated. If you don't set the elevation, it is automatically set to a value of 2. To remove the space between expanded panels, you can set the elevation to 0.
However, when you do this, you may run into other graphical issues. For example, the divider doesn't appear for an expanded panel. Without the space between panels, it makes it had to see where one panel starts and the next ends. Not a tough fix to custom code your own dividers in, but thought it was worth mentioning.

Related

I want to make my Icons Automatically go to next Row in flutter if previous row gets completrly filled with icons

I am building a quiz app using Dart flutter in that some questions will get displayed and users have to click on true or false buttons depending upon question is right or wrong. To tell users that they are right or wrong icon (✓ or ✘) will get displayed in bottom row every time they choose answer by clicking on either of the button. But in flutter if row gets completely filled i got error
value: Not in inclusive range. So i want that icon should automatically go to next row.
Heres the short code which i tried:
List scorekeeper = [];
void score(bool userpickedanswer) {
setState((){
if (userpickedanswer == correctanswer) {
scorekeeper.add(Icon(Icons.check, color: Colors.green,));
}
else {
scorekeeper.add(Icon(Icons.close, color: Colors.red,));
}
}
Row {
childeren : scorekeeper
You can use the Wrap widget instead of the Row widget and when the line of icon fills to the end the Wrap widget makes another line automatically
Use the code like this:
Wrap(
children: scorekeeper,
),
You can use a wrap instead of a row. It prevents overflow by displaying the items depending on the available space:
https://api.flutter.dev/flutter/widgets/Wrap-class.html

Detect user created widgets in flutter widget tree

I've been working on a problem today to detect certain widgets in the widget tree so I've been playing around with context.visitChildElements and element. visitChildren. I can see all the widgets in the tree, but there's just too many.
I looked at the way the flutter widget inspector does it and they have some internal expectations that won't exist within other users code bases. The example, I have a scaffold with a body Center and a child Material button. Passing the context to my function below prints out about 200+ widgets with those three scattered in between. I would like to only print out those three, or at least elliminate all widgets created by Flutter automatically and not created by the code the user supplied.
List<WidgetInfo> getElements(BuildContext context) {
var widgetsOfInterest = <WidgetInfo>[];
widgetsOfInterest.clear();
int indentation = 0;
void visitor(Element element) {
indentation++;
Key? key = element.widget.key;
String className = element.widget.runtimeType.toString();
while (element.findRenderObject() is! RenderBox) {}
RenderBox box = element.findRenderObject() as RenderBox;
var offset = box.getTransformTo(null).getTranslation();
final indent = ' ' * indentation;
// Here I want to check if this is a widget we created and print its name and offset
if (debugIsLocalCreationLocation(element)) print('$className $offset');
if ((MaterialButton).toString() == className) {
widgetsOfInterest.add(WidgetInfo(
indentation: indentation,
size: box.size,
paintBounds: box.paintBounds.shift(
Offset(offset.x, offset.y),
),
key: key,
className: className,
));
}
element.visitChildren(visitor);
}
context.visitChildElements(visitor);
return widgetsOfInterest;
}
If anyone have any insights or experience with the Flutter widget tree that could point me in the right direction I would appreciate that.
it's obviously seems not the best solution here(and will increase unnecessary code) but this might work.
you can create a custom widget key that have some prefix inside of it and use it in every component you want it to be detected
for example
//1
Appbar(key: FSKey())
//2
Center(key:FSKey("awesome_widget"))
internally if you have access to those key while you iterate through elements you can detect those widgets using the prefix you set.
actuall key values
//1
"fskey_1273zj72ek628"
//2
"fskey_awesome_widget"
again this might not be a very optimal solution but iit gives you some control of what parts of the tree you want it to be detected and eventually if there is no other way.. this will work.

Is there a way to automatically wrap widgets within a row

Sorry if this is a dup, I see a lot of questions regarding text wrapping, but not generic widget wrapping in a row.
I have a dynamic row of buttons within a row. I'd like to wrap the buttons to create a second row if the list becomes longer than the width of the screen. Is it possible to do this automatically, or do I need to manually detect and create the correct number of rows.
If the latter, does anybody have pointers to measuring a widget's width vs screen width? Right now I'm doing something like:
...
return Row(
children: List.generate(count*2 + 1, (i) {
if (i %2 == 0) {
return Spacer();
}
return RaisedButton(child: Text((i / 2).round().toString(), onPressed: (){...});
});
Row is useful when you need to organize child widgets in a single line. Wrap with default direction is an alternative to Row that moves child widgets to the next line if there is no enough space

How to force ScrollController to recalculate position.maxExtents?

I'm trying to create a desktop-style scrollbar, that changes it's size based on the size of the content. My scrollbar shares a ScrollController with a list, and relies on the position.maxExtents to know how large the content area is.
The issue is that when I change the number of rows, the maxExtents will not update, until a scrollEvent is initiated.
I've worked around it with code like this, moving 1px up, and 1px down over 100ms:
widget.controller.jumpTo(controller.position.pixels + 1);
Future.microtask(() => widget.controller.animateTo(controller.position.pixels - 1, duration: 100.milliseconds, curve: Curves.linear));
Which works pretty quite well when the list can scroll. However, when the list is < the height of the view, it can't scroll, and these calls have no effect, and I'm stuck with a stale maxExtents.
How can I just tell the list: "Hey, list, recalculate your children!"?
You can delay your code to when the controller has been updated using the following.
WidgetsBinding.instance.addPostFrameCallback((_) {
...your code which requires controller's max extents...
});
Aside from the issues you've mentioned, animating the scroll position will cause Flutter to draw frames unnecessarily.
Borrowing from #ltk and #Rakaton, I think there's a simpler and more efficient way to do this:
// Run this code whenever the layout changes (i.e. any time you setState
// to update the list content)
WidgetsBinding.instance?.addPostFrameCallback((_) {
widget.scrollController.position.notifyListeners();
});
You may also want to wrap your list component in a SizeChangedLayoutNotifier to detect size changes from things like window resize:
NotificationListener<SizeChangedLayoutNotification>(
onNotification: (notification) {
WidgetsBinding.instance?.addPostFrameCallback((_) {
scrollController.position.notifyListeners();
});
return true;
},
child: SizeChangedLayoutNotifier(
child: ListView(
// ...
),
),
);
I think you should consider to use force pixels, but it's a protected method so it's gonna give you a warning. And I don't know about the performance or another stuffs about it.
widget.controller.position.forcePixels(controller.position.pixels + 1);
or combination of correct pixels and notifylistener.
widget.controller.position.correctPixels(controller.position.pixels + 1);
widget.controller.position.notifyListeners();

How to use CustomMultiChildLayout & CustomSingleChildLayout in Flutter

Can someone with experience in using CustomSingleChildLayout and CustomMultiChildLayout classes be able to explain in detail (with examples) in how to use them.
I am new to Flutter and am trying to understand how to use these. However, the documentation is horrible and is not clear. I tried to scour the internet for examples, but there aren't any other documentation.
I would be eternally grateful if you could help.
Thank you!
First of all, I want to say that I am glad to help you with this as I can understand your struggles - there are benefits to figuring it out by yourself as well (the documentation is amazing).
What CustomSingleChildLayout does will be obvious after I explained CustomMultiChildLayout to you.
CustomMultiChildLayout
The point of this widget is allowing you to layout the children you pass to this widget in a single function, i.e. their positions and sizes can depend on each other, which is something you cannot achieve using e.g. the prebuilt Stack widget.
CustomMultiChildLayout(
children: [
// Widgets you want to layout in a customized manner
],
)
Now, there are two more steps you need to take before you can start laying out your children:
Every child you pass to children needs to be a LayoutId and you pass the widget you actually want to show as a child to that LayoutId. The id will uniquely identify your widgets, making them accessible when laying them out:
CustomMultiChildLayout(
children: [
LayoutId(
id: 1, // The id can be anything, i.e. any Object, also an enum value.
child: Text('Widget one'), // This is the widget you actually want to show.
),
LayoutId(
id: 2, // You will need to refer to that id when laying out your children.
child: Text('Widget two'),
),
],
)
You need to create a MultiChildLayoutDelegate subclass that handles the layout part. The documentation here seems to be very elaborate.
class YourLayoutDelegate extends MultiChildLayoutDelegate {
// You can pass any parameters to this class because you will instantiate your delegate
// in the build function where you place your CustomMultiChildLayout.
// I will use an Offset for this simple example.
YourLayoutDelegate({this.position});
final Offset position;
}
Now, all the setup is done and you can start implementing the actual layout. There are three methods you can use for that:
hasChild, which lets you check whether a particular id (remember LayoutId?) was passed to the children, i.e. if a child of that id is present.
layoutChild, which you need to call for every id, every child, provided exactly once and it will give you the Size of that child.
positionChild, which allows you to change the position from Offset(0, 0) to any offset you specify.
I feel like the concept should be pretty clear now, which is why I will illustrate how to implement a delegate for the example CustomMultiChildLayout:
class YourLayoutDelegate extends MultiChildLayoutDelegate {
YourLayoutDelegate({this.position});
final Offset position;
#override
void performLayout(Size size) {
// `size` is the size of the `CustomMultiChildLayout` itself.
Size leadingSize = Size.zero; // If there is no widget with id `1`, the size will remain at zero.
// Remember that `1` here can be any **id** - you specify them using LayoutId.
if (hasChild(1)) {
leadingSize = layoutChild(
1, // The id once again.
BoxConstraints.loose(size), // This just says that the child cannot be bigger than the whole layout.
);
// No need to position this child if we want to have it at Offset(0, 0).
}
if (hasChild(2)) {
final secondSize = layoutChild(
2,
BoxConstraints(
// This is exactly the same as above, but this can be anything you specify.
// BoxConstraints.loose is a shortcut to this.
maxWidth: size.width,
maxHeight: size.height,
),
);
positionChild(
2,
Offset(
leadingSize.width, // This will place child 2 to the right of child 1.
size.height / 2 - secondSize.height / 2, // Centers the second child vertically.
),
);
}
}
}
Two other examples are the one from the documentation (check preparation step 2) and a real world example I wrote some time back for the feature_discovery package: MultiChildLayoutDelegate implementation and CustomMultiChildLayout in the build method.
The last step is overriding the shouldRelayout method, which simple controls whether performLayout should be called again at any given point in time by comparing to an old delegate, (optionally you can also override getSize) and adding the delegate to your CustomMultiChildLayout:
class YourLayoutDelegate extends MultiChildLayoutDelegate {
YourLayoutDelegate({this.position});
final Offset position;
#override
void performLayout(Size size) {
// ... (layout code from above)
}
#override
bool shouldRelayout(YourLayoutDelegate oldDelegate) {
return oldDelegate.position != position;
}
}
CustomMultiChildLayout(
delegate: YourLayoutDelegate(position: Offset.zero),
children: [
// ... (your children wrapped in LayoutId's)
],
)
Considerations
I used 1 and 2 as the ids in this example, but using an enum is probably the best way to handle the ids if you have specific ids.
You can pass a Listenable to super (e.g. super(relayout: animation)) if you want to animate the layout process or trigger it based on a listenable in general.
CustomSingleChildLayout
The documentation explains what I described above really well and here you will also see why I said that CustomSingleChildLayout will be very obvious after understanding how CustomMultiChildLayout works:
CustomMultiChildLayout is appropriate when there are complex relationships between the size and positioning of a multiple widgets. To control the layout of a single child, CustomSingleChildLayout is more appropriate.
This also means that using CustomSingleChildLayout follows the same principles I described above, but without any ids because there is only a single child.
You need to use a SingleChildLayoutDelegate instead, which has different methods for implementing the layout (they all have default behavior, so they are technically all optional to override):
getConstraintsForChild, which is equivalent to the constraints I passed to layoutChild above.
getPositionForChild, which is equivalent to positionChild above.
Everything else is exactly the same (remember that you do not need LayoutId and only have a single child instead of children).
MultiChildRenderObjectWidget
This is what CustomMultiChildLayout is built on.
Using this requires even deeper knowledge about Flutter and is again a bit more complicated, but it is the better option if you want more customization because it is even lower level. This has one major advantage over CustomMultiChildLayout (generally, there is more control):
CustomMultiChildLayout cannot size itself based on its children (see issue regarding better documentation for the reasoning).
I will not explain how to use MultiChildRenderObjectWidget here for obvious reasons, but if you are interested, you can check out my submission to the Flutter Clock challenge after January 20, 2020, in which I use MultiChildRenderObjectWidget extensively - you can also read an article about this, which should explain a bit of how all of it works.
For now you can remember that MultiChildRenderObjectWidget is what makes CustomMultiChildLayout possible and using it directly will give you some nice benefits like not having to use LayoutId and instead being able to access the RenderObject's parent data directly.
Fun fact
I wrote all the code in plain text (in the StackOverflow text field), so if there are errors, please point them out to me and I will fix them.