Xamarin.Forms ListView如何自适应内容大小?

15

我有一个相当大的表单(主要适用于平板电脑),其中包含一个嵌套了ScrollView和一个垂直的StackPanelTabbedPage,其中包含许多控件。

我有几个ListView的出现,它包含一些单行项目,并且我需要其大小自适应内容。
我想摆脱它的滚动条,但无论如何,我都不希望它占用比其项目所需的更多空间。
有没有办法(即使是丑陋的方法)在不必编写三个平台的渲染器的情况下实现这一点?

以下是描述我的树状结构的伪代码:

<ContentPage>
  <MasterDetailPage>
    <MasterDetailPage.Detail>
      <TabbedPage>
        <ContentPage>
          <ScrollView>
            <StackPanel>
              <!-- many controls-->
              <ListView>
渲染后,ListView 后面会出现巨大的间隙。我该如何避免这种情况?
我尝试过调整 VerticalOptionsHeightRequest,但都没有起作用。
我正在寻找一种动态方式(最好不需要继承)来实现此目标,而不涉及自定义渲染器。

你想在不留下任何空白的情况下将数据内容包装在ListView中吗? - Lutaaya Huzaifah Idris
我想让 ListView 自适应其所需的高度,可选地设置最大高度。 - Shimmy Weitzhandler
好的,让我把代码发送给你。 - Lutaaya Huzaifah Idris
请查看我的解决方案。 - Lutaaya Huzaifah Idris
也许可以跳过列表视图,直接将项目添加到您的堆栈中? - snowCrabs
@snowCrabs,我正在使用MVVM,你的解决方案太麻烦了。 - Shimmy Weitzhandler
7个回答

13

根据Lutaaya的答案,我编写了一个行为,自动确定和设置行高(Gist)。

行为:

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

使用方法:

<ContentPage xmlns:xf="clr-namespace:Xamarin.Forms">
  <ListView>
    <ListView.Behaviors>
      <xf:AutoSizeBehavior />

1
以上代码对我来说不起作用。Cell.Renderheight始终返回固定值,即使单元格内容发生变化也是40。你知道为什么吗? - user2185592
3
如果每个单元格的行高是动态的,这种方法就不起作用了。你知道有什么方法可以应用于动态单元格高度吗? - Arun Gupta
1
@Shimmy,这个解决方案对我也没有用。相反,它使列表视图滚动得很大,我甚至无法到达页面底部查看其他剩余元素(标签和按钮)。 - Sagar S. Kadookkunnan
我正在使用Xamarin.Forms v5.0.0.2244。 - balintn
@user2185592,这个操作需要在你的原始列表中设置hasUnevenRows=true才能正常工作,否则你会得到40。 - innom
显示剩余3条评论

6

假设您的ListView已经填充了新闻Feed,让我们使用一个ObservableCollection来包含我们的数据,以便填充ListView如下:

XAML代码:

<ListView x:Name="newslist"/>

C# 代码

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

然后将趋势列表分配给 ListView:
newslist.ItemSource = trends;

然后,我们对ListView和数据进行了一些逻辑处理,使得ListView包裹着数据,随着数据的增加,ListView也会增加,反之亦然。
int i = trends.Count;
int heightRowList = 90;
i = (i * heightRowList);
newslist.HeightRequest = i;

因此完整代码是:

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

希望这能帮到您。

如果您可以将行高固定为90,则此解决方案将起作用。类似问题:https://dev59.com/cKLia4cB1Zd3GeqPkYRo#44638010 - Yuri S
1
有办法动态计算行高吗?那不规则行呢? - Shimmy Weitzhandler
如果您将单元格设置为不均匀行,则必须计算行高。 - snowCrabs

4

我可以创建一个事件处理程序,考虑到ListView单元格的大小变化。以下是代码:

    <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">

通过使用Grid、StackLayout等控件,可以改变框架。 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;
        }
    }

当 Frame 显示时(ListView.ItemTemplate 应用时),Frame 的大小会发生变化。 我们通过 Measure() 方法获取其实际高度,并将其放入字典中,该字典知道当前 ListView 并保存 Frame 的高度。当显示最后一个 Frame 时,我们将所有高度相加。 如果没有项目,则 ListView_OnSizeChanged() 将 listView.HeightRequest 设置为 0。

3

我可能过于简化了这个问题,但是只需将 HasUnevenRows="True" 添加到 ListView 中即可解决该问题。


3
如果ListView后面还有其他内容,它就无法正常工作。至少在我回答这个问题时是这样的。请确保在发布问题之前我已经尝试过这种情况。 - Shimmy Weitzhandler

2

1
哈哈,这正是我寻找已久的东西。这个特别适用于父级已经可滚动的情况下。 - innom

0

这里的大多数答案都有正确的想法,但似乎对于 ViewCell 元素并不起作用。以下是我能想到的最有效的解决方案:

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

这个 AutoSizeBehavior 的实现能够很好地调整大小并自我清理。


0

我知道这是一个旧的帖子,但到目前为止我还没有找到更好的解决方案 - 两年后 :-(

我正在使用Xamarin.Forms v5.0.0.2244。 按照Shimmy在上面“用法:”中建议的方式实现并应用了该行为,但是AppearanceChanged对于每个listviewitem而不是listview本身执行。因此,在UpdateHeight中测量高度时,它会测量单个项目的高度,并将列表视图的高度设置为该值-这是不正确的。 由于它附加到ItemAppearing和ItemDisappering事件,因此该行为似乎是合理的。

然而,根据Shimmy的想法,这里有一个改进(但远非完美)的实现。这个想法是处理项目出现,测量这些项目并计算它们的总和,随着我们的操作更新父列表视图的高度。

对我来说,初始高度计算不起作用,就好像某些项目不会出现,因此不会计数。 我希望有人能够改进它,或者指出更好的解决方案:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接