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();
Related
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
I'm making an app which shows place detail when clicking a place on Google Maps. I can open a container at bottom of the page but I want to do that when the user swipe this container to up with animation, the container will cover the whole page and I will get extra information about that place and show the user.
How can I do that with Flutter?
You can try to use the package here:
https://pub.dev/packages/sliding_up_panel
However, if this does not fullfill you wish, try this instead: Wrap you Widget with a GestureDetector and an AnimatedController:
double containerHeight = 0;
GestureDetector(
onVerticalDragEnd: (dragUpdateDetails) {
setState(){
containerHeight = //device height or use MediaQuery.of(context).size.height//
}
},
child: AnimatedContainer(
duration: Duration(milliseconds: //how long should it take//),
height: containerHeight,
child: //whatever you want//
)
),
AnimatedController automatically animates between changes of properties. However, you might want to try changing onVertivalDragEnd to something else like onVerticalDragUpdate to fullfill your wish completely. If you want to have that behaviour for the full screen, wrap your first return Widget with the GestureDetector.
I guess DragableScrollableSheet fits your need. There is a good explanation here in the documentation.
You can use DraggableScrollableSheet
Document and Video tutorial available here:
https://api.flutter.dev/flutter/widgets/DraggableScrollableSheet-class.html
I have this listview.builder which is supposed to show some orders from an array of Order objects based on their status.
It looks kinda like this:
The List works just fine until, for some reason, when I scroll down and the viewport can only display the order with index (the one in the listview builder function) 5 and then press another category like "New", setState() is called and the whole thing rebuilds, but the builder's index starts at 5 and the listview.builder doesn't build anything from 0 - 4. I've printed the index in the builder and caught this bug, but I still don't understand why this is happening.
This is what my listview.builder code looks like:
ListView.builder(
controller: _scrollController,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
print("INDEX: $index");
return _showOrder(index);
},
itemCount: orders.length,
),
This is the code for _showOrder():
Widget _showOrder(int i) {
String _currOrderStatus = orders[i].orderStatus;
/// _selectedOrderStatus is just a String which changes depending on the selected category of orders.
/// e.g. "New" or "Past"
return _currOrderStatus == _selectedOrderStatus
? ShowMyOrderWidget()
: SizedBox(
/// AND FOR SOME REALLY WEIRD REASON, THIS FIXES THE PROBLEM
/// It works with any height, as long as it's not 0, but if I have a lot of them, then the
/// layout gets spaced out and messy. With such a low number, this is highly unlikely, but
/// still seems like a stupid fix.
/// Why does this work? Why is this happening? Is there a better way to fix it?
height: 0.000000001,
);
}
And I just call setState() in the onPressed() function of those buttons on top of the screen.
Changing the items inside the ListView doesn't reset scroll position.
Since you're already assigning a ScrollController to your ListView, try calling "jumpTo(0.0)" to reset it's scroll position before calling setState().
_scrollController.jumpTo(0.0);
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.
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.