Xamarin.Forms ListView size to content?
Asked Answered
K

7

15

I have a pretty large form (adapted mainly for tablets), that has a TabbedPage nesting a ScrollView and a vertical StackPanel containing many controls.

I have few occurrences where I have a ListView that contains a few single-line items, and I need it to size to content.
I'd like to get rid of its scroll-bars, but anyway I don't want it to take up more space than what's required for its items.
Is there a way (even an ugly one) to achieve that without have to write a renderer x3 platforms?

Here's a pseudo describing my tree:

<ContentPage>
  <MasterDetailPage>
    <MasterDetailPage.Detail>
      <TabbedPage>
        <ContentPage>
          <ScrollView>
            <StackPanel>
              <!-- many controls-->
              <ListView>

When rendered, there is a huge gap coming after the ListView. How can I avoid that?

I tried messing around with the VerticalOptions and HeightRequest, non of which worked.

I'm looking for a dynamic way (preferably without inheritance) to achieve that without involving custom renderers.

Kit answered 20/6, 2017 at 13:54 Comment(6)
in other words you want to wrap the data content in the ListView with leaving any space ?Steere
I wanna put the ListView to size itself to its items' necessary height, optionally with a max height.Kit
Ok let send you the codeSteere
Check out my solutionSteere
maybe skip the list view and add the items directly to your stack?Anking
@Anking I'm using MVVM, your solution is too match of a hassle.Kit
K
13

Based on Lutaaya's answer, I made a behavior that automates this, determining and setting the row-height (Gist).

Behavior:

namespace Xamarin.Forms
{
  using System;
  using System.Linq;
  public class AutoSizeBehavior : Behavior<ListView>
  {
    ListView _ListView;
    ITemplatedItemsView<Cell> Cells => _ListView;

    protected override void OnAttachedTo(ListView bindable)
    {
      bindable.ItemAppearing += AppearanceChanged;
      bindable.ItemDisappearing += AppearanceChanged;
      _ListView = bindable;
    }

    protected override void OnDetachingFrom(ListView bindable)
    {
      bindable.ItemAppearing -= AppearanceChanged;
      bindable.ItemDisappearing -= AppearanceChanged;
      _ListView = null;
    }

    void AppearanceChanged(object sender, ItemVisibilityEventArgs e) =>
      UpdateHeight(e.Item);

    void UpdateHeight(object item)
    {
      if (_ListView.HasUnevenRows)
      {
        double height;
        if ((height = _ListView.HeightRequest) == 
            (double)VisualElement.HeightRequestProperty.DefaultValue)
          height = 0;

        height += MeasureRowHeight(item);
        SetHeight(height);
      }
      else if (_ListView.RowHeight == (int)ListView.RowHeightProperty.DefaultValue)
      {
        var height = MeasureRowHeight(item);
        _ListView.RowHeight = height;
        SetHeight(height);
      }
    }

    int MeasureRowHeight(object item)
    {
      var template = _ListView.ItemTemplate;
      var cell = (Cell)template.CreateContent();
      cell.BindingContext = item;
      var height = cell.RenderHeight;
      var mod = height % 1;
      if (mod > 0)
        height = height - mod + 1;
      return (int)height;
    }

    void SetHeight(double height)
    {
      //TODO if header or footer is string etc.
      if (_ListView.Header is VisualElement header)
        height += header.Height;
      if (_ListView.Footer is VisualElement footer)
        height += footer.Height;
      _ListView.HeightRequest = height;
    }
  }
}

Usage:

<ContentPage xmlns:xf="clr-namespace:Xamarin.Forms">
  <ListView>
    <ListView.Behaviors>
      <xf:AutoSizeBehavior />
Kit answered 5/7, 2017 at 3:57 Comment(8)
Hey Great answer.It works for reducing the size of the listview but it is unable to make it bigger again.Hauser
@Shimmy Above code doesn't work for me. Cell.Renderheight always returns fixed value i.e. 40 for me irrespective of the cell content. Any idea why so?Wohlert
@Wohlert I don't remember but what are you filling the cell with? Are you sure the layout inside the cell is in the correct size?Kit
This doesn't works if row height of each cell is dynamic. Do you any way to do this for dynamic cell height?Marquis
@Shimmy, this solution did not work for me also. Rather, it made the list view scroll so big and I cannot even reach the end to see other remaining elements (Labels and Buttons) in the page.Zeculon
I also get a cell height of 40, which is then applied onto the whole list - the listview height is 40 and displays only one element. The list view gets an ItemTemplate, which only consists of a Label. If I use a fixed value like in Lutaaya's solution it is working (but only for a certain type of layout of course).Revers
I'm using Xamarin.Forms v5.0.0.2244.Nickolas
@Wohlert your original list needs to have hasUnevenRows=true for this to work tight or ull get 40Fcc
S
6

Ok Assume your ListView is Populated with NewsFeeds, lets use an ObservableCollection to contain our data to populate a ListView as Below :

XAML Code :

<ListView x:Name="newslist"/>

C# Code

ObservableCollection <News> trends = new ObservableCollection<News>();

Then you assign the trends List to the ListView :

newslist.ItemSource = trends;

Then , we have make some Logic on the ListView and the data , So that the ListView Wraps the data , as the data increases the ListView also increases and viceversa :

int i = trends.Count;
int heightRowList = 90;
i = (i * heightRowList);
newslist.HeightRequest = i;

Therefore the complete code is :

ObservableCollection <News> trends = new ObservableCollection<News>();
newslist.ItemSource = trends;
int i = trends.Count;
int heightRowList = 90;
i = (i * heightRowList);
newslist.HeightRequest = i;

Hope it Helps .

Steere answered 20/6, 2017 at 14:24 Comment(3)
This solution will work if you can set your rowheight to fixed 90. Similar question: #44501696Tipstaff
Is there a way to have the row height calculated dynamically? How about uneven rows?Kit
You have to calculate the row height in the cell if you set it for uneven rowsAnking
B
4

I could make a event handler that takes into account the on the changing size of the ListView cells. Here's it:

    <ListView ItemsSource="{Binding Items}"
         VerticalOptions="Start"
         HasUnevenRows="true"
         CachingStrategy="RecycleElement"
         SelectionMode="None" 
         SizeChanged="ListView_OnSizeChanged">
               <ListView.ItemTemplate>
                    <DataTemplate>
                        <ViewCell >
                           <Frame Padding="10,0" SizeChanged="VisualElement_OnSizeChanged">

Frame can be changed by Grid, StackLayout, etc. xaml.cs:

        static readonly Dictionary<ListView, Dictionary<VisualElement, int>> _listViewHeightDictionary = new Dictionary<ListView, Dictionary<VisualElement, int>>();

    private void VisualElement_OnSizeChanged(object sender, EventArgs e)
    {
        var frame = (VisualElement) sender;
        var listView = (ListView)frame.Parent.Parent;
        var height = (int) frame.Measure(1000, 1000, MeasureFlags.IncludeMargins).Minimum.Height;
        if (!_listViewHeightDictionary.ContainsKey(listView))
        {
            _listViewHeightDictionary[listView] = new Dictionary<VisualElement, int>();
        }
        if (!_listViewHeightDictionary[listView].TryGetValue(frame, out var oldHeight) || oldHeight != height)
        {
            _listViewHeightDictionary[listView][frame] = height;
            var fullHeight = _listViewHeightDictionary[listView].Values.Sum();
            if ((int) listView.HeightRequest != fullHeight 
                && listView.ItemsSource.Cast<object>().Count() == _listViewHeightDictionary[listView].Count
                )
            {
                listView.HeightRequest = fullHeight;
                listView.Layout(new Rectangle(listView.X, listView.Y, listView.Width, fullHeight));
            }
        }
    }

    private void ListView_OnSizeChanged(object sender, EventArgs e)
    {
        var listView = (ListView)sender;
        if (listView.ItemsSource == null || listView.ItemsSource.Cast<object>().Count() == 0)
        {
            listView.HeightRequest = 0;
        }
    }

When Frame is displaying (ListView.ItemTemplate is applying), size of frame changing. We take it's actual height via Measure() method and put it into Dictionary, which knows about current ListView and holds Frame's height. When last Frame is shown, we sum all heights. If there's no items, ListView_OnSizeChanged() sets listView.HeightRequest to 0.

Bespoke answered 29/3, 2019 at 7:4 Comment(0)
A
3

I may be grossly oversimplifying things here, but just adding HasUnevenRows="True" to the ListView worked for me.

Adjure answered 21/7, 2018 at 7:0 Comment(1)
Doesn't work when there are things after the ListView. At least it didn't when I wrote my answer. Be sure I've tried that before I even posted my question.Kit
D
2

Especially when You have small item counts and have no necessity to scrolling, then simply use the bindableLayout. https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/layouts/bindable-layouts this always renders it exactly to size! if you have complex items, with interactions then these work perfectly fine.

Diner answered 2/2, 2022 at 15:19 Comment(1)
lol, that was what i was looking for waaaay to long. this works especially good as the parent is already scrollable.Fcc
N
0

I know this is an old thread, but so far I couldn't find a better solution - more than two years later :-(

I'm using Xamarin.Forms v5.0.0.2244. Implemented and applied the behavior as suggested by Shimmy in "Usage:" above, however, AppearanceChanged is executed for each listviewitem, rather than the listview itself. Thus, when height is measured in UpdateHeight, it measures the height of a single item, and sets the height of the listview to that value - which is not correct. That behavior seems resonable as it is attached to ItemAppearing and ItemDisappering events.

However, following Shimmy's idea, here's an improved (but far from perfect) implementation. The idea is process item appearances, measure those items and calculate their sum, updating parent listview height as we go.

For me, the initial height calculation doesn't work correctly, as if some of the items wouldn't appear, and thus are not counted. I hope someone can improve on it, or point to a better solution:

internal class ListViewAutoShrinkBehavior : Behavior<ListView>
{
    ListView _listView;
    double _height = 0;

    protected override void OnAttachedTo(ListView listview)
    {
        listview.ItemAppearing += OnItemAppearing;
        listview.ItemDisappearing += OnItemDisappearing;
        _listView = listview;
    }

    protected override void OnDetachingFrom(ListView listview)
    {
        listview.ItemAppearing -= OnItemAppearing;
        listview.ItemDisappearing -= OnItemDisappearing;
        _listView = null;
    }

    void OnItemAppearing(object sender, ItemVisibilityEventArgs e)
    {
        _height += MeasureRowHeight(e.Item);
        SetHeight(_height);
    }

    void OnItemDisappearing(object sender, ItemVisibilityEventArgs e)
    {
        _height -= MeasureRowHeight(e.Item);
        SetHeight(_height);
    }

    int MeasureRowHeight(object item)
    {
        var template = _listView.ItemTemplate;
        var cell = (Cell)template.CreateContent();
        cell.BindingContext = item;
        double height = cell.RenderHeight;
        return (int)height;
    }

    void SetHeight(double height)
    {
        //TODO if header or footer is string etc.
        if (_listView.Header is VisualElement header)
            height += header.Height;
        if (_listView.Footer is VisualElement footer)
            height += footer.Height;
        _listView.HeightRequest = height;
    }
}
Nickolas answered 27/12, 2021 at 23:29 Comment(0)
C
0

Most answers here have the right idea but don't seem to work great for ViewCell elements. Here's the most efficient solution I could think of:

public class AutoSizeBehavior : Behavior<ListView>
{
    ListView _listView;
    VisualElement _parent;

    ITemplatedItemsList<Cell> _itemsList;
    Dictionary<int, double> _cells = new Dictionary<int, double>();
    Dictionary<VisualElement, int> _elMap = new Dictionary<VisualElement, int>();

    double _parentHeight;
    double _contentHeight;

    protected override void OnAttachedTo(ListView bindable)
    {
        bindable.ItemAppearing += HandleItemAppearing;

        _listView = bindable;
        _itemsList = ((ITemplatedItemsView<Cell>)_listView).TemplatedItems;

        if (_listView.Parent is VisualElement parent)
        {
            AttachToParent(parent);
        }
        else
        {
            _listView.PropertyChanged += HandleListViewPropertyChanged;
        }

        for (int i = 0; i < _itemsList.Count; i++)
            UpdateItemHeight(i);
    }

    protected override void OnDetachingFrom(ListView bindable)
    {
        _listView.ItemAppearing -= HandleItemAppearing;
        _listView.PropertyChanged -= HandleListViewPropertyChanged;

        for (int i = 0; i < _itemsList.Count; i++)
        {
            var cell = _itemsList[i];
            cell.PropertyChanged -= HandleCellPropertyChanged;
        }

        foreach (var pair in _elMap)
        {
            var el = pair.Key;
            el.SizeChanged -= HandleCellSizeChanged;
        }

        if (_parent != null)
            _parent.SizeChanged -= HandleParentSizeChanged;

        _elMap.Clear();
        _cells.Clear();

        _cells = null;
        _itemsList = null;
        _listView = null;
        _parent = null;
        _parentHeight = _contentHeight = -1;
    }

    #region Handlers
    private void HandleItemAppearing(object sender, ItemVisibilityEventArgs e)
    {
        UpdateItemHeight(e.ItemIndex);
    }

    private void HandleListViewPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(ListView.Parent))
        {
            if (_listView.Parent is VisualElement parent)
            {
                AttachToParent(parent);
                _listView.PropertyChanged -= HandleListViewPropertyChanged;
            }
        }
    }

    private void HandleParentSizeChanged(object sender, EventArgs e)
    {
        if (_parent == null) return;

        _parentHeight = _parent.Height;
        AdjustListHeight();
    }

    private void HandleCellPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (!(sender is Cell cell)) return;

        if (e.PropertyName == nameof(Cell.RenderHeight))
        {
            int index = _itemsList.IndexOf(cell);

            if (index < 0)
                return;

            UpdateItemHeight(index);
        }
    }

    private void HandleCellSizeChanged(object sender, EventArgs e)
    {
        if (!(sender is VisualElement el)) return;

        if (_elMap.TryGetValue(el, out int index))
            UpdateItemHeight(index);
    }
    #endregion

    private void AttachToParent(VisualElement parent)
    {
        _parent = parent;
        _parentHeight = _parent.Height;
        _parent.SizeChanged += HandleParentSizeChanged;
    }

    private void UpdateItemHeight(int index)
    {
        Cell cell = _itemsList[index];
        double newHeight;

        if (!_cells.TryGetValue(index, out double oldHeight))
        {
            oldHeight = 0.0;
            _cells[index] = oldHeight;

            if (cell is ViewCell viewCell)
            {
                _elMap[viewCell.View] = index;
                viewCell.View.SizeChanged += HandleCellSizeChanged;
            }
            else
            {
                cell.PropertyChanged += HandleCellPropertyChanged;
            }
        }

        if (cell is ViewCell vCell)
        {
            newHeight = vCell.View.Height;
        }
        else
        {
            newHeight = cell.RenderHeight;
        }

        if (newHeight >= 0)
        {
            double delta = newHeight - oldHeight;
            if (delta == 0) return;

            _cells[index] = newHeight;

            _contentHeight += delta;
            AdjustListHeight();
        }
    }

    private void AdjustListHeight()
    {
        if (_contentHeight > 0 && _contentHeight < _parentHeight)
        {
            _listView.HeightRequest = _contentHeight;
        }
        else if (_parentHeight >= 0)
        {
            if (_listView.HeightRequest != _parentHeight)
                _listView.HeightRequest = _parentHeight;
        }
    }
}

This implementation of AutoSizeBehavior resizes quite well and cleans up after itself.

Chasten answered 11/4, 2022 at 1:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.