How to get height of widget in flutter using RenderBox? - flutter

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.

Related

How can I get the index of a widget in a list?

I can a little bit confuse on how should I implement the delete function.
By tapping the bottom button will add a Custom Card widget to the widget list that shows up on the top
This is the function for that
onTap: () {
setState(() {
_variationCards.add(VariationCard(
key: UniqueKey(),
variation: Variation.empty,
index: _variationCards.length,
onRemoveCard: (index) {
setState(() {
_variationCards.removeAt(index);
});
},
));
});
},
The problems of the current setup are
Lets say I delete the second Card, now that the third Card became the second Card, but the index I passed to the third Card widget is still 2 when it is actually 1 now.
I need the index in the Card widget because I need to have additional widget for the first card, but now If I delete the first card, the second card index value will not be updated.
I think I am heading to the wrong path to implement this. Any help would be much appreciated.
Thank you in advanced.
Maybe you could use "removeWhere" method. Something like:
onTap: () {
_variationCards.add(VariationCard(
key: UniqueKey(),
variation: Variation.empty,
index: _variationCards.length,
onRemoveCard: (index) {
setState(() {
_variationCards.removeWhere((card) => card.SomeId == _someId);
});
},
));
},
The leading information should rather be a list of your defined class / model instead of widgets. You want widgets to update (rebuild) according to the underlying information / state in your app. Currently your widgets are created and added to your list on gestures (like onTap) and won't update themselves - that's why the indices are "wrong" in your case.
I would suggest a combination of a class / model representing a card while making use of the ListView.builder variant:
Class representing your card:
/// Currently only holds the Variation enum value, add whatever
/// defines your cards later on
class VariationModel {
Variation variation;
VariationModel(this.variation)
}
Your widget which displays the cards in a list:
/// Need to be filled with instances of this class, maybe passed
/// down directly to this widget or grabbed from something like
/// GetIt or Provider etc.
List<VariationModel> _variations;
#override
Widget build(BuildContext context) {
...
ListView.builder(
/// +1 to include the button to add a new card at the end
itemCount: _variations.length + 1,
itemBuilder: (context, index) => index < _variations.length ?
VariationCard(
key: UniqueKey(),
variation: _variations[index].variation,
index: index,
onRemoveCard: () {
setState(() {
_variations.removeAt(index);
});
},
/// Add your widget (card) here which is used to add a new
/// new card (in this case a new VariationModel instance)
) : Card(
...
onTap: () {
setState(() {
_variations.add(VariationModel(Variation.empty));
});
},
),
),
}
Since this approach is using the ListView.builder widget which builds the children (your card widgets) dynamically and the index is properly being updated since the widgets are going to rebuild now, it should work as you intend to.
I made a little app using GetX with your requirements (I think so at least), it maybe be of help to you:
The code is here.

How to get size of a child Widget

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];
}
}

How to know if list view is scrollable programatically

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!

Flutter Markdown "Show more" button

I use MarkdownBody from flutter_markdown inside a LimitedBox. When pressing a "Show more" button the maxHeight is set to double.infinity and the full text is shown:
LimitedBox(
maxHeight:
showMoreCommentsIds
.contains(
commentId)
? double.infinity
: 100,
child: Wrap(
direction:
Axis.horizontal,
children: <Widget>[
MarkdownBody(
data: showList[index]
.comment
.comment,
)
],
),
),
But how can I find out the height of the text and only display the "Show more" button, if it is necessary?
You can find the length of the text using text.length and based on that information determine if the "Show more" button is needed. For example:
if(text.length > 60) {
_showButton();
}
You may need to do a little testing with the length of text in order to find out which length you want as the threshold. I just chose 60 as an arbitrary number.
I struggled a little bit with a similar problem because I had to know the size of a widget to apply some logic.
I learned that you can prerender the widget in an Offstage widget, determine it's size with the widget key once it is mounted.
You should wait for the rebuild, so you can force it and then you get the size with a function like this:
Future.delayed(Duration(milliseconds:100)).then(
final widgetSize = getWidgetSize(widgetKey);
// You can make logic here to remove the Offstage, variables, and free its space.
// Todo: apply the size in your code after this
)
...
Size getWidgetSize(GlobalKey key) {
final RenderBox renderBox = key.currentContext.findRenderObject();
return renderBox.size;
}
In my case, I needed it after tapping somewhere, so I used it inside a function, but maybe you will apply it directly on the initState(), inside a post frame callback.
initState(){
super.initState();
ServicesBinding.instance.addPostFrameCallback((_) async {
// here
});
}

Jumping to a new page in PageView.builder after the additon of a new page doesn't seem to work as expected [Flutter]

I have a simple layout that consists of a PageView.builder widget that contains several pages. The contents of each page are simple, they contain a Container and a Text widget inside of it.
cards is a List of type String and the Pageview.builder widget has an itemCount that's based on the length of this List. The value of the Text in each page is assigned using this List.
List<String> cards = [];
Now, whenever I add a new value to cards List, a variable newPage is used to store the last index position in the List after that element has been added.
After doing this, setState(() {}); is called so that the UI along with the PageView update to reflect the changes made in the List.
The PageView widget does reflect the changes and a new page does get added to it.
However, the problem arises when I try to jump to newly added page right after calling setState.
The error indicates that the index value that jumpToPage is trying to use is out of range in the PageView
cards.add("New card");
newPage = cards.length - 1;
setState(() {});
card_PageController.jumpToPage(newPage);
So, after trying to figure something out, I added a Timer after the setState, so that the framework get's some time to properly update the UI elements.
I'm using a small value of 50 milliseconds in the Timer function and jumping to the new page after the timer gets over.
cards.add("New card");
newPage = cards.length - 1;
setState(() {});
Timer(Duration(milliseconds: 50), () {
card_PageController.jumpToPage(newPage);
})
The addition of a Timer seems to solve the problem and there were no errors after it's addition. However, I'm not sure if this is the right way of tackling this problem.
I'd like to know as to why is this happening, as shouldn't calling jumpToPage directly after setState work without the use of a Timer?
Also, does setState infact take some time to finish updating the UI, even though it isn't async, and that due to this reason the referenced index is invalid? And could this problem have been tackled in a better way?
From the code you shared I can't detect an issue. Especially because I've reproduced what you said you wanted to achieve and it works without issue. Take a look at the code below:
class PageCards extends StatefulWidget {
#override
_PageCardsState createState() => _PageCardsState();
}
class _PageCardsState extends State<PageCards> {
PageController _pageController = PageController();
List<String> cards = [
'page 0',
'page 1',
'page 2',
];
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(
child: PageView.builder(
controller: _pageController,
itemCount: cards.length,
itemBuilder: (context, index){
return Center(
child: Text(cards[index]),
);
}
),
),
RaisedButton(
child: Text('Add page'),
onPressed: () => addCard(),
),
],
);
}
void addCard(){
setState(() {
cards.add('page ${cards.length}');
});
_pageController.jumpToPage(cards.length - 1);
}
}