I am copying my question from here as it is the same question but for flutter
How do you know if your ListView has enough number of items so that it
can scroll?
For instance, If I have 5 items on my ListView all of it will be
displayed on a single screen. But if I have 7 or more, my ListView
begins to scroll. How do I know if my List can scroll
programmatically?
Thank you
I am adding the code I tried, in which I test if the controller is attached, to be able to get the position. I couldn't get the position because the controller is not attached until you actually scroll
Widget build(BuildContext context) {
_afterBuild();
ListView.builder(
controller: controller,
// ...
)
}
Future<void> _afterBuild () async {
if (controller.hasClients) {
print("controller.hasClients");
// here I would be able to get the position
} else {
print("controller.has no Clients");
}
}
Edit: For anyone coming here: The controller was not being attached because I had a condition under which to build the ListView
So I combined the comments with the accepted answer (which is actually the answer for the question) and solved it like this (with some pseudocode):
Widget build(BuildContext context) {
if (not loaded results from api) {
return Something()
} else {
Future(_afterBuild);
return ListView.builder(
controller: controller,
// ...
)
}
}
Future<void> _afterBuild () async {
if (controller.hasClients) {
if(controller.position.maxScrollExtent > 0){
print('it can be scrolled');
}
} else {
print("controller has no client");
}
}
Actually it's quite easy to do in Flutter. You should have a ScrollController attached to your ListView and then you can check the maxScrollExtent. If it's bigger than zero then your ListView can be scrolled. It also works for any scrolling view which uses ScrollController.
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if(controller.position.maxScrollExtent > 0){
print('it can be scrolled');
}
});
}
Step 1 - Assign a GlobalKey
GlobalKey myKey= GlobalKey();
Step 2 - Assign key to ListView
ListView(
key: myKey,
...
)
Step 3 - In your function that checks if the ListView is scrollable, use the following code-
final RenderBox renderBoxRed = myKey.currentContext.findRenderObject();
final height = renderBoxRed.size.height; // find height of ListView
if (height > MediaQuery.of(context).size.height) { // checking if listview height is greater than page height
print("ListView is SCROLLABLE !!!");
}
I like where most of the rest of the answers are going, but they aren't getting the data the most succinctly or reliably. What you want to do is, yes, attach a ScrollController, and then look for the .position on that (which may throw if there's no single attached position). Then, ask the position for extentAfter and extentBefore, and you'll discover how much virtual view (in pixels) there is after and before the current visible portion. If those are 0, no scrolling. No need to figure out the size of the screen or the containers. Flutter already knows all that when it laid everything out!
Related
ok i decided to use
ScrollablePositionedList.builder
https://pub.dev/packages/scrollable_positioned_list
instead of ListView.builderfor more controlling of the index navigation ..
we know that ScrollablePositionedList.builder use ItemScrollController() instead of ScrollController()
using ScrollController() we can do the following to detect if we reach the top list or bottom
ScrollController scrollController= ScrollController();
scrollController.addListener(() {
if(scrollController.position.atEdge){
if (scrollController.position.pixels == 0) {
///we arrive to the end
}
}else{
///we arrive to the top
}
}
});
but how could i do the same with using ScrollablePositionedList.builder that use ItemScrollController()
so i tried to do this
ItemScrollController scrollController = ItemScrollController();
final listener = ItemPositionsListener.create();
listener.itemPositions.addListener(() {
listener.itemPositions.value // here all what i get .. i cannot see any
position.atEdge or pixels like normal ScrollController
});
really does it not support the detection of top list or bottom or something went wrong with me ?
thanks
You can check
if the value of itemPositionsListener.itemPositions.value.first.index is 0
to say it is on top. And to check
if the value of ${itemPositionsListener.itemPositions.value.last.index} = list.length - 1
to say it reaches the bottom.
itemPositionsListener.itemPositions.addListener(() {
print(
'====current first ${itemPositionsListener.itemPositions.value.first.index}====');
print(
'====current last ${itemPositionsListener.itemPositions.value.last.index}====');
});
I try to render the child of a listitem (somewhere up) to a different place (widget) in the tree;
In the approach below, BlendMask is the "target" widget that checks for and paints "source" widgets that got themself an GlobalKey which is stored in blendKeys.
This works somewhat. And I'm not quite sure if I might fight the framework, or just missing some points...
The problems are two:
The minor one: This approach doesn't play nice with the debugger. It compiles and runs fine but every hot-reload (on save f.e.) throws an "can't findRenderObject() of inactive element". Maybe I miss some debug flag?
the real problem, that brought me here questioning the idea en gros: As mentioned, the Source-Widget is somewhere in the subtree of the child of a Scrollable (from a ListView.build f.e.): How can I update the Òffset for the srcChild.paint() when the list is scrolled? - without accessing the lists scrolController?! I tried listening via WidgetsBindingObservers didChangeMetrics on the state of the Source widget, but as feared no update on scroll. Maybe a strategically set RepaintBounderyis all it needs? *hope* :D
Anyway, every tip much appreciated. Btw the is an extend of this question which itself extends this...
class BlendMask extends SingleChildRenderObjectWidget {
[...]
#override
RenderObject createRenderObject(context) {
return RenderBlendMask();
}
}
class RenderBlendMask extends RenderProxyBox {
[...]
#override
void paint(PaintingContext context, offset) { <-- the target where we want to render a widget
[...] from somewhere else in the tree!
for (GlobalKey key in blendKeys) {
if (key.currentContext != null) {
RenderObject? srcChild <-- the source we want to render in this sibling widget!
= key.currentContext!.findRenderObject();
if (srcChild != null) {
Matrix4 mOffset = srcChild.getTransformTo(null);
context.pushTransform(true, offset, mOffset, (context, offset) {
srcChild.paint(context, offset);
});
}
}
}
}
} //RenderBlendMask
I have a special requirement to allow for a header widget, usually containing static content to appear at the top of a scroll view. The scroll view should overlap the header widget so that a clip shape can be used for effect. I've achieved this effect by using a stack view with the header widget as the first item in the stack, the scroll view as the top element. The scroll view contains a column with the first child being an empty container of the desired height (the height of the header widget minus the amount of overlap). This achieves the desired effect when passing in a known height as a hard-coded parameter. (NOTE: I tried to accomplish this using a Sliver List, but wasn't able to achieve the desired overlap to meet product requirements.)
The header widget contains an image which is loaded via an API response. The height of the images can vary so I need to determine this at runtime and adjust the UI accordingly. I didn't think this would be a difficult task but so far, I've not found a way to do this. The following two images show the desired effect, the image on the right shows the correct behavior on scrolling up. Note that the scroll view must overlap the header image by the same amount as the radius of the clip.
This generates the list. _getComponents provides child widgets for a column contained in the SingleChildScrollView:
List<Widget> _getComponents(List<Section> sections, BuildContext context, double? height) {
List<Widget> widgetList = [
Container(height: 225) // need to obtain correct height here
];
for (int i = 1; i < sections.length; i++) {
Section section = sections[i];
if (section.model != null) {
switch (section.code) {
case mediaTypeCode:
if (section.model.runtimeType == MediaModel) {
widgetList.add(HeaderComponent(section: section));
}
break;
case articleTypeCode:
if (section.model.runtimeType == ArticleSectionModel) {
widgetList.add(TitleContentHeader(
model: section.model as ArticleSectionModel));
}
break;
}
}
}
return widgetList;
}
Then in my view widget, the stack is built as follows:
return Container(
color: Colors.white,
child: Stack(
children: [
_getHeader(sections),
SingleChildScrollView(
child: Column(
children: _getComponents(sections!, context, null),
),
),
],
),
);
I need to be able to get the height of the header returned in _getHeader(sections) and pass it to the _getComponents function so that I can determine the correct starting position for the scroll view.
Can anyone offer any insight into this?
Or, can you suggest a plugin that would allow the behavior show in the images above? As mentioned above, Sliver List did not produce the desired effect.
Thanks!
You can get size of a widget using RenderBox :
import 'package:flutter/material.dart';
class WidgetPosition {
getSizes(GlobalKey key) {
final RenderBox renderBoxRed = key.currentContext.findRenderObject();
final sizeRed = renderBoxRed.size;
// print("SIZE: $sizeRed");
return [sizeRed.width, sizeRed.height];
}
getPositions(GlobalKey key) {
final RenderBox renderBoxRed = key.currentContext.findRenderObject();
final positionRed = renderBoxRed.localToGlobal(Offset.zero);
// print("POSITION: $positionRed ");
return [positionRed.dx, positionRed.dy];
}
}
Totally new to flutter, less than 10h on it.
I have a PageView handing 3 pages, it scrolls horizontally through them perfectly.
But I would like to change the "allowed swipe" area. I don't want to let the user change the pages scrolling from any position of the page, but instead, for example, just let him scroll the pages if he swipes on the AppBar component for example.
I saw the PageView has the applyTo method, I'm just lost about how to give to it the ID(keys) or the appBar to see if this will work.
Is there a way of achieving this "only scroll if the user swipes on the component X"?
Edit 1
Solution suggested by Alberto Miola works like a charm, here is my code (I had to implement from PreferredSizeWidget since is required to modify the AppBar).
class GestureDetectorForAppBar extends StatelessWidget implements PreferredSizeWidget {
final double height;
final AppBar appbar;
final GestureDragUpdateCallback onPanUpdate;
GestureDetectorForAppBar({
Key key,
this.appbar,
this.onPanUpdate,
#required this.height,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return GestureDetector(child: this.appbar, onPanUpdate: this.onPanUpdate,);
}
#override
Size get preferredSize => Size.fromHeight(height);
}
First of all, you need to "block" the swipe gesture on the PageView itself using NeverScrollableScrollPhysics:
PageView(
physics: const NeverScrollableScrollPhysics(),
children: [...],
controller: ...
);
Note the usage of a const constructor. In this way you won't be able to move among pages with your finger. In Flutter you have widgets and not components.
Give a look at the GestureDetector widget which can be used to listen on swipes and change the currently visible content of your PageView. It can be used to detect swipes:
GestureDetector(
onPanUpdate: (data) {
if (data.delta.dx > 0) {
// right swipe
}
if (data.delta.dx < 0) {
// right swipe
}
}
);
In order, I suggest you to first read about NeverScrollableScrollPhysics() in the official documentation. It's used to "block" scrolling behaviors. Then, use GestureDetector() to wrap the widget you want to be used as a "scroll director" (the one that actually scrolls the pages).
Inside onPanUpdate you'll deal with the animateToPage method of the PageController to change the currently visible page.
I've written the following function in which I pass GlobalKey that I assigned to a widget, and I'm storing the returned height in an array called 'heightsList'. But for some reason I'm getting a wrong height, which is 65( which is same for all widgets, I tried for dropdown, checkbox and radio).
However, I get the correct height(269 for checkbox and 77 for dropdown) when I perform a hot reload(ie, the function gets called again).
void getSizes(GlobalKey key, int index) async {
if(key.currentContext!=null) {
RenderBox renderBox = key.currentContext.findRenderObject();
heightsList[index] = renderBox.size.height;
}
}
Here is an example of the widget
(NOTE: CustomCheckBox is hardly any different from checkbox) -
Container(
key: keys[index],
child: CustomCheckBox(
item: item,
count: i,
onChanged: (dynamic response) {
responseDetails = response;
},
formItems: data['data']['FORM'][0]['sections']['sections'][index]['definition']
)
)
I have no idea where that 65 is coming from. I'm just trying to get proper height of the widget. So if there is anything that I'm doing wrong, or there is any other way around, please let me know.
Any help is much appreciated.