.NET MAUI ListView在iOS中实现可展开行的方法
.NET MAUI iOS ListView 自适应行高优化方案
核心问题根源
iOS原生UITableView处理动态行高时,默认会缓存行高,单元格内容变化后不会主动重新计算行高。MAUI中HasUnevenRows对iOS的适配存在局限性,直接调用ForceUpdateSize()会触发整个ListView重绘,导致性能下降。
优化方案详细落地(对应Stack Overflow方案的深入实现)
1. 自定义ListView控件
创建带行高更新命令的自定义ListView,用来通知渲染器精准更新指定行高度:
public class AutoSizeListView : ListView { public static readonly BindableProperty UpdateRowHeightCommandProperty = BindableProperty.Create(nameof(UpdateRowHeightCommand), typeof(ICommand), typeof(AutoSizeListView)); public ICommand UpdateRowHeightCommand { get => (ICommand)GetValue(UpdateRowHeightCommandProperty); set => SetValue(UpdateRowHeightCommandProperty, value); } public void RequestRowHeightUpdate(int rowIndex) { UpdateRowHeightCommand?.Execute(rowIndex); } }
2. 实现iOS自定义渲染器
在iOS项目中编写渲染器,利用原生UITableView的API仅刷新指定行,避免全局重绘:
[assembly: ExportRenderer(typeof(AutoSizeListView), typeof(AutoSizeListViewRenderer))] namespace YourAppName.iOS.Renderers { public class AutoSizeListViewRenderer : ListViewRenderer { protected override void OnElementChanged(ElementChangedEventArgs<ListView> e) { base.OnElementChanged(e); if (Control != null && e.NewElement is AutoSizeListView autoSizeListView) { // 开启原生自动行高计算 Control.RowHeight = UITableView.AutomaticDimension; // 设置合理预估行高,减少布局计算开销 Control.EstimatedRowHeight = 100; // 绑定行高更新命令,仅刷新指定行 autoSizeListView.UpdateRowHeightCommand = new Command<int>(index => { if (Control?.DataSource != null) { var indexPath = NSIndexPath.FromRowSection(index, 0); Control.ReloadRows(new[] { indexPath }, UITableViewRowAnimation.Automatic); } }); } } } }
3. ViewModel与XAML布局适配
- ViewModel层:给问答项添加展开状态属性,绑定行索引和更新命令:
public class QaItemViewModel : INotifyPropertyChanged { private bool _isExpanded; public bool IsExpanded { get => _isExpanded; set { _isExpanded = value; OnPropertyChanged(); ExpandCommand?.Execute(this); } } public string Question { get; set; } public string Answer { get; set; } public int RowIndex { get; set; } public ICommand ExpandCommand { get; set; } public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } public class MainViewModel : INotifyPropertyChanged { public ObservableCollection<QaItemViewModel> QaItems { get; set; } public ICommand UpdateRowHeightCommand { get; set; } public MainViewModel() { QaItems = new ObservableCollection<QaItemViewModel>(); // 初始化示例数据,为每个项分配行索引 for (int i = 0; i < 20; i++) { QaItems.Add(new QaItemViewModel { Question = $"问题{i+1}", Answer = $"这是问题{i+1}的详细答案,内容较长,展开后需要显示全部内容。重复内容模拟长文本:这是问题{i+1}的详细答案,内容较长,展开后需要显示全部内容。", RowIndex = i, ExpandCommand = new Command<QaItemViewModel>(item => { UpdateRowHeightCommand?.Execute(item.RowIndex); }) }); } } public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
- XAML布局:使用自定义ListView,绑定数据并控制答案显示状态:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:YourAppName" x:Class="YourAppName.MainPage"> <ContentPage.Resources> <local:BoolToTextConverter x:Key="BoolToTextConverter"/> </ContentPage.Resources> <ContentPage.BindingContext> <local:MainViewModel /> </ContentPage.BindingContext> <local:AutoSizeListView ItemsSource="{Binding QaItems}" HasUnevenRows="True" UpdateRowHeightCommand="{Binding UpdateRowHeightCommand}"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <VerticalStackLayout Padding="12,8"> <Label Text="{Binding Question}" FontSize="16" FontAttributes="Bold"/> <Button Margin="0,8,0,0" Text="{Binding IsExpanded, Converter={StaticResource BoolToTextConverter}, ConverterParameter='收起|展开'}" Command="{Binding ExpandCommand}" CommandParameter="{Binding .}"/> <Label Text="{Binding Answer}" FontSize="14" IsVisible="{Binding IsExpanded}" LineBreakMode="WordWrap"/> </VerticalStackLayout> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </local:AutoSizeListView> </ContentPage>
4. 补充:布尔值转文本转换器
实现转换器让按钮文本随展开状态切换:
public class BoolToTextConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is bool isExpanded && parameter is string param) { var parts = param.Split('|'); return isExpanded ? parts[0] : parts[1]; } return string.Empty; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
方案优势
- 仅刷新指定行的高度,避免
ForceUpdateSize()导致的全局重绘,大幅提升iOS端性能 - 贴合iOS原生UITableView的布局逻辑,适配更稳定
- 保留MAUI MVVM架构,无需修改原有业务逻辑
内容的提问来源于stack exchange,提问作者Shaurya Shikhar
相关产品推荐
相关产品推荐

