Search Unity

Resolved How do I get the height of an item inside the ListView?

Discussion in 'UI Toolkit' started by Chuck_S, Mar 15, 2021.

  1. Chuck_S

    Chuck_S

    Joined:
    Dec 11, 2018
    Posts:
    15
    I understand that the ListView doesn't support multiple heights yet, and that's fine for me. I've got a ton of items (1k+) that I'd like to view, and they're all drawing a uniform interface. I'd like to try to genericize the code as much as possible, though, especially if I later decide to update the UI for the items I'm drawing. To that end, I'd like to avoid hard-coding the itemHeight if at all possible.

    Is it possible? I tried getting the current dimensions of the UI Element in the MakeItem function, but those values are all coming back as NaN. Once the ListView is created, the itemsSource is the actual list of things, not the list of UI Elements, right?

    I was thinking I'd be able to create the ListView, then get the height, then set the itemHeight to that value, but I don't know how to access one of the drawn items after the fact and the values are NaN when they're created.

    Code (CSharp):
    1. int nItems = 100;
    2. var testItems= new List<TestUIItem>(nItems);
    3. for(int i=0; i<nItems; i++)
    4. {
    5.     testItems.Add(new TestUIItem(i));
    6. }
    7. int placeholderHeight = 16;
    8. var testTemplate = Resources.Load<VisualTreeAsset>("Uxml/Test/TestUIItemView");
    9. Func<VisualElement> makeItem = () => testTemplate.CloneTree();
    10. Action<VisualElement, int> bindItem = (e, i) => e.Q<TestUIItemViewModel>("test-item").SetItem(testItems[i]);
    11. ListView listView = new ListView(testItems, placeholderHeight, makeItem, bindItem);
    12. // Get a child UIElement or VisualElement?
    13. // Get the height of that child?
    14. listView.itemHeight = childElementHeight;
    Maybe I'm not using the right code for getting the height of a VisualElement, or I'm getting it at the wrong time or something. If I use `CloneTree()` and get an instance of the UI then try to call `testItem.resolvedStyle.minHeight.value`, I get zero, which is what I also get for `testItem.resolvedStyle.maxHeight.value`. If I try `testItem.resolvedStyle.height` I get NaN. If I try `testItem.style.height` I get zero.

    Again, this isn't really the end of the world, but I'm fixating on this issue at the moment (procrastinating on other stuff, I'm sure), so any help would be appreciated.
     
  2. griendeau_unity

    griendeau_unity

    Unity Technologies

    Joined:
    Aug 25, 2020
    Posts:
    248
    Hey!

    You don't necessarily have to explicitly assign the itemHeight. If you don't, the list view will use the resolved item height of your items, so you should get the correct height of your templates. You can use the constructor without the placeholderHeight variable.

    That being said, to still answer the rest of the question, you're getting NaN and zero because the item needs to be in the hierarchy in order to have its layout resolved. Therefore you would need to add your template item to the panel and register to the GeometryChangedEvent in order to get an accurate height.

    And to get an item in the list, you could probably use a query with the uss class name, but again you would have to wait a bit until items are added to the ListView, by listening to the GeometryChangedEvent.
    This will will query the first item in the list:
    listView.Q(className: ListView.itemUssClassName)
     
    Last edited: Mar 15, 2021
    Chuck_S likes this.
  3. Chuck_S

    Chuck_S

    Joined:
    Dec 11, 2018
    Posts:
    15
    @unity_griendeau Ah perfect, thanks! I read the documentation for ListView.itemHeight and it stated it needs to be set. I thought I had read the same somewhere on the forums here, too, but I can't seem to find it.

    The IntelliSense/autocomplete for the ListView constructor just gives an empty constructor and another one that requires the itemHeight, but I tried again with just an empty constructor, then later specifying the .makeItem, .bindItem, and .itemsSource (in that order) and it worked perfect - exactly like you said.

    Thanks so much for the help!
     
  4. griendeau_unity

    griendeau_unity

    Unity Technologies

    Joined:
    Aug 25, 2020
    Posts:
    248
    Looking back at this, I realize, there's a default height of 30, and I probably tested with items of this height when I suggested that, because I'm trying this again with different items and it doesn't work. What are you seeing? Are your items of height 30 as well?

    I believe the best way then would be to resolve an item by adding it and waiting for its layout to be computed like I mentioned above.

    In any case, we are currently working on dynamic height support. So stay tuned!
     
    Chuck_S likes this.
  5. Chuck_S

    Chuck_S

    Joined:
    Dec 11, 2018
    Posts:
    15
    @unity_griendeau Okay excellent, so it wasn't just me haha. I tried it the way you suggested, using the empty constructor and specifying the makeItem, bindItem, and itemsSource after. It seemed like it was working okay with labels, like you mentioned, but when I converted to using other items that are larger it failed to work, and like you mentioned the default height was just 30 regardless of what I tried.

    The thing I found too was that the itemHeight was 30 in the callback, done the way you mentioned. I added an GeometryChangedEvent callback in the MakeItem function that went to a private method I called "ResizeItems" that got the resolvedStyle height, wrote that to Debug.Log, and then set the listView's itemHeight to that value. It was always 30.

    So then I tried creating a template item just directly in the frame, using the VisualTreeAsset's .CloneTree() and adding it to this, then registering the same GeometryChangedEvent. I modified that function to delete the item after the function gets called, thinking that I'd (1) create the item, (2) when it gets drawn I'd get height, then (3) after getting the height I'd delete the item. I figured it'd happen so fast that nobody would see it, but then I was getting crazy values for the resolvedStyle height; like 316 or something.

    What I wound up doing is a hack I'm sure, but it worked - In the UI Document, I added an instance of the UXML I wanted to use with the ListView. It's on its own and I called it "prototype-instance". In the function where I'm initializing the ListView, I query for the prototype instance, and I set its style.display = DisplayStyle.Flex, showing the prototype. Then I get the resolvedStyle's height, then I set the style.display = DisplayStyle.None, which hides the prototype.

    Because this runs in the constructor function it winds up being hidden all the time in the UIBuilder, which is great - it lets me lay out the rest of the UI the way I want. It happens all in one call, so it never visually appears in the document.

    I can't really dynamically feed it stuff; I've got to know when I'm making the UI Document what I'm going to put into the ListView, but that's not the end of the world for me at the moment. Dynamic height sizing would be great of course but this is working for me for now. Thanks for following up, though!
     
  6. dyamanoha_

    dyamanoha_

    Joined:
    Mar 17, 2013
    Posts:
    87
    Just wanted to mention that I ran into this exact same issue.

    Code (CSharp):
    1.         this.stationsListElement.makeItem = () =>
    2.         {
    3.             VisualElement ve = stationView.CloneTree();
    4.             ve.RegisterCallback<GeometryChangedEvent>(OnStationViewSized);
    5.             return ve;
    6.         };
    7.  
    8. ...
    9.  
    10.     private void OnStationViewSized(GeometryChangedEvent ge)
    11.     {
    12.         Debug.Log(((VisualElement)ge.target).resolvedStyle.height);
    13.     }
    just prints 30. The elements are closer to 100 pixels in height.

    This seems like a pretty significant flaw in the class design, sizing should all be handled internally. Looking forward to seeing this get cleaned up.

    I'm early enough in my project where I don't need to add a hack like Chuck_s's last comment and I'm just going to hardcode the height for now.
     
  7. griendeau_unity

    griendeau_unity

    Unity Technologies

    Joined:
    Aug 25, 2020
    Posts:
    248
    The dynamic height support I mentioned in my previous comment is now part of the ListView class in 2021.2 and higher. So if you set
    listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
    , the ListView will wait for the item to layout and use that height to place the items. You can also have different heights and it will work.
    The virtualizationMethod is still FixedHeight by default. In that mode, you need to specify the height of one item with the
    fixedItemHeight
    property. The class is designed like that to optimize item virtualization and layout for simple list views.
     
    Wilhelm_LAS, Nayrutes and Aldeminor like this.
  8. dyamanoha_

    dyamanoha_

    Joined:
    Mar 17, 2013
    Posts:
    87
    > The class is designed like that to optimize item virtualization and layout for simple list views.

    Interesting, I guess the ship is sailed but it seems like designing the interface for simplicity first with extra configuration for optimization would be preferable.

    I'm currently doing some feature work on 2021.1.21, but I'll upgrade as soon as I'm done to check it out
     
  9. dyamanoha_

    dyamanoha_

    Joined:
    Mar 17, 2013
    Posts:
    87
    Finally got around to doing the upgrade (2021.2.0f1). Item sizing seems to work with dynamic layouts.

    However, changes to the item source are inconsistently updating the view now. I've tried both `RefreshItems()` and `Rebuild()`. Neither really works, sometimes `Rebuild()` does, but often it shows the 'list is empty' text.

    Anyway, unless there's a known issue there or you know what might be causing that I'll open a new thread.
     
  10. martinpa_unity

    martinpa_unity

    Unity Technologies

    Joined:
    Oct 18, 2017
    Posts:
    480
    Hey!
    RefreshItems()
    should just recall the bind operation for the current source, so that it doesn't recreate the virtualized elements.

    So if the item source changed and the items themselves are not of the same type, the
    Rebuild()
    method should be used instead. It should release all the virtualized elements and re-create them.

    If that doesn't work for you, can you file an issue with a repro project using "Help \> Report a bug..."?
    Thanks
     
  11. dyamanoha_

    dyamanoha_

    Joined:
    Mar 17, 2013
    Posts:
    87
    martinpa_unity likes this.