Xamarin.Forms之ListView

介绍

ListView是用于显示数据列表的视图,特别是需要滚动的长列表。

CollectionView 是使用不同布局规范显示数据列表的视图(eg:可以水平排列),它旨在提供更灵活、更高的性能替代方法ListView,但是UWP下现在支持不是很好

用例

在您要显示可滚动数据列表的任何情况下,都可以使用ListView控件, ListView类支持上下文操作和数据绑定

ListView控件不应与TableView控件混淆。每当您具有选项或数据的非绑定列表时,TableView控件都是一个更好的选择,因为它允许在XAML中指定预定义的选项。例如,具有大多数预定义选项集的iOS设置应用程序比ListView更适合使用TableView。

ListView类不支持在XAML中定义列表项,您必须使用ItemsSource属性或与ItemTemplate的数据绑定来定义列表中的项。

ListView最适合由单个数据类型组成的集合。此要求是因为列表中的每一行只能使用一种类型的单元格。 TableView控件可以支持多种单元格类型,因此当您需要显示多种数据类型时,它是一个更好的选择。

组件

ListView控件具有许多可用于行使每个平台的本机功能的组件。 这些组件在以下各节中定义。

1、Headers、footers

页眉和页脚组件显示在列表的开头和结尾,与列表数据分开,页眉和页脚可以绑定到与ListView的数据源不同的数据源。

2、Groups

可以对ListView中的数据进行分组,以简化导航,组通常是数据绑定的,以下屏幕截图显示了具有分组数据的ListView:

 

3、Cells

ListView中的数据项称为单元格,每个单元对应于一行数据,有内置单元供您选择,或者您可以定义自己的自定义单元,内置单元格和自定义单元格都可以在XAML或代码中使用/定义。

  • 内置单元格(例如TextCell和ImageCell)对应于本机控件,并且性能特别好
  1. TextCell显示文本字符串,还可以显示详细信息文本。详细信息文本以较小的字体显示为第二行,带有强调色。
  2. ImageCell显示带有文本的图像。 显示为带有左侧图像的TextCell。
  • 自定义单元格用于呈现复杂数据。 例如,自定义单元格可用于呈现包括专辑和歌手在内的歌曲列表。

以下屏幕快照显示了具有ImageCell项的ListView:

 

功能性
ListView类支持多种交互样式。

  • Pull-to-refresh允许用户下拉ListView刷新内容。
  • Context actions上下文操作允许开发人员在单个列表项上指定自定义操作。 例如,您可以在iOS上实施滑动操作,或者在Android上实施长按操作。
  • Selection 选择允许开发人员将功能附加到列表项上的选择和取消选择事件。

以下屏幕截图显示了具有上下文操作的ListView:

 

数据源

ItemsSource
ListItem使用ItemsSource属性填充数据,该属性可以接受实现IEnumerable的任何集合,填充ListView的最简单方法是使用字符串数组:

<ListView>
      <ListView.ItemsSource>
          <x:Array Type="{x:Type x:String}">
            <x:String>mono</x:String>
            <x:String>monodroid</x:String>
            <x:String>monotouch</x:String>
            <x:String>monorail</x:String>
            <x:String>monodevelop</x:String>
            <x:String>monotone</x:String>
            <x:String>monopoly</x:String>
            <x:String>monomodal</x:String>
            <x:String>mononucleosis</x:String>
          </x:Array>
      </ListView.ItemsSource>
</ListView>
View Code

此方法将使用字符串列表填充ListView。 默认情况下,ListView将调用ToString并将结果显示在每一行的TextCell中。

由于ItemsSource已发送到数组,因此内容不会随着基础列表或数组的更改而更新。 如果希望ListView在基础列表中添加,删除和更改项时自动更新,则需要使用ObservableCollection。 ObservableCollection是在System.Collections.ObjectModel中定义的,与List一样,除了 它可以将任何更改通知ListView之外:

ObservableCollection<Employee> employees = new ObservableCollection<Employee>();
listView.ItemsSource = employees;

//Mr. Mono will be added to the ListView because it uses an ObservableCollection
employees.Add(new Employee(){ DisplayName="Mr. Mono"});
View Code

Data Binding

数据绑定是将用户界面对象的属性绑定到某些CLR对象的属性(例如,视图模型中的类)的“胶水”。 数据绑定非常有用,因为它通过替换许多无聊的样板代码来简化用户界面的开发。

数据绑定通过使对象的绑定值更改而保持同步来进行。 您不必在控件的值每次更改时都编写事件处理程序,而只需建立绑定并在视图模型中启用绑定即可。

有关数据绑定的更多信息,请参见“数据绑定基础知识”

Binding Cells
单元格(和单元格的子级)的属性可以绑定到ItemsSource中对象的属性。 例如,可以使用ListView呈现员工列表。

创建一个ObservableCollection<Employee>,将其设置为ListView的 ItemsSource,并使用数据填充列表:

public class Employee
{
    public string DisplayName {get; set;}
}


ObservableCollection<Employee> employees = new ObservableCollection<Employee>();
public ObservableCollection<Employee> Employees { get { return employees; }}

public EmployeeListPage()
{
    EmployeeView.ItemsSource = employees;

    // ObservableCollection allows items to be added after ItemsSource
    // is set and the UI will react to changes
    employees.Add(new Employee{ DisplayName="Rob Finnerty"});
    employees.Add(new Employee{ DisplayName="Bill Wrestler"});
    employees.Add(new Employee{ DisplayName="Dr. Geri-Beth Hooper"});
    employees.Add(new Employee{ DisplayName="Dr. Keith Joyce-Purdy"});
    employees.Add(new Employee{ DisplayName="Sheri Spruce"});
    employees.Add(new Employee{ DisplayName="Burt Indybrick"});
}
View Code

注:ObservableCollection不是线程安全的。 修改ObservableCollection会导致UI更新在执行修改的同一线程上进行。 如果该线程不是主UI线程,则将导致异常。

以下代码段演示了绑定到员工列表的ListView:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:constants="clr-namespace:XamarinFormsSample;assembly=XamarinFormsXamlSample"
             x:Class="XamarinFormsXamlSample.Views.EmployeeListPage"
             Title="Employee List">
  <ListView x:Name="EmployeeView"
            ItemsSource="{Binding Employees}">
    <ListView.ItemTemplate>
      <DataTemplate>
        <TextCell Text="{Binding DisplayName}" />
      </DataTemplate>
    </ListView.ItemTemplate>
  </ListView>
</ContentPage>
View Code

Binding SelectedItem

通常,您将需要绑定到ListView的当前选定项,而不是使用事件处理程序来响应更改,为此,请在XAML中绑定SelectedItem属性: 

<ListView x:Name="listView"
          SelectedItem="{Binding Source={x:Reference SomeLabel},
          Path=Text}"></ListView>
View Code

假设listView的ItemsSource是字符串列表,SomeLabel将其Text属性绑定到SelectedItem。

自定义ListView单元格外观

Xamarin.Forms ListView类用于呈现可滚动列表,可通过使用ViewCell元素对其进行自定义,ViewCell元素可以显示文本和图像,指示是/否状态,并接收用户输入。

内置的单元格

Xamarin.Forms带有可用于许多应用程序的内置单元:

  • TextCell控件用于显示文本,并带有可选的第二行以显示详细文本。
  • ImageCell控件与TextCells相似,但在文本左侧包含一个图像。
  • SwitchCell控件用于呈现和捕获打开/关闭或真/假状态。
  • EntryCell控件用于呈现用户可以编辑的文本数据。

SwitchCell和EntryCell控件在TableView上下文中更常用。

1、TextCell

 

TextCell在运行时rendered(呈现)为本机控件,因此与自定义ViewCell相比,性能非常好。 TextCell可以自定义以下属性:

  • Text–第一行以大字体显示的文本。
  • Detail–第一行下方显示的文本,使用较小的字体。
  • TextColor –文本的颜色。
  • DetailColor –详细文本的颜色

2、ImageCell

像TextCell一样,ImageCell可以用于显示文本和辅助详细信息文本,并且通过使用每个平台的本机控件,它可以提供出色的性能。 ImageCell与TextCell的不同之处在于,它在文本的左侧显示一个图像。

当您需要显示具有可视外观的数据列表(例如联系人或电影列表)时,ImageCell很有用。 ImageCell是可自定义的,除了上面四个属性外,还有

ImageSource – the image to display next to the text

自定义单元格

自定义单元格使您可以创建内置单元格不支持的单元格布局。 例如,您可能要显示一个带有两个权重相等的标签的单元格。 TextCell不足,因为TextCell的标签较小。 大多数单元格自定义添加其他只读数据(例如其他标签,图像或其他显示信息)。

所有自定义单元格都必须派生自ViewCell,后者是所有内置单元格类型使用的相同基类。

Xamarin.Forms在ListView控件上提供了一种缓存行为,该行为可以提高某些类型的自定义单元格的滚动性能。

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="demoListView.ImageCellPage">
    <ContentPage.Content>
        <ListView  x:Name="listView">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <StackLayout BackgroundColor="#eee"
                        Orientation="Vertical">
                            <StackLayout Orientation="Horizontal">
                                <Image Source="{Binding image}" />
                                <Label Text="{Binding title}" TextColor="#f35e20" />
                                <Label Text="{Binding subtitle}"   HorizontalOptions="EndAndExpand"  TextColor="#503026" />
                            </StackLayout>
                        </StackLayout>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </ContentPage.Content>
</ContentPage>
View Code

XAML的工作方式如下:

  • 自定义单元格嵌套在DataTemplate内部,该数据模板位于ListView.ItemTemplate内部,此过程与使用任何内置单元相同。
  • ViewCell是自定义单元格的类型, DataTemplate元素的子级必须是ViewCell类或从ViewCell类派生
  • 在ViewCell内部,布局可以由任何Xamarin.Forms布局管理,在此示例中,布局由StackLayout管理,该布局允许自定义背景色。

ListView中的 <DataTemplate>的单元项有3种写法
1、写在同一个页面:直接在当前ListView中写<ViewCell>
2、分离,新建一个CellTemplate页面,继承自ViewCell,但是里面没有写资源Resources,资源需要写成全局的
3、分离,新建一个CellTemplate页面,继承自ContentView,里面可以写资源Resources。

列表外观

除了列表中每一行的ViewCell实例之外,Xamarin.Forms ListView还允许您自定义列表的表示形式。

分组

当在连续滚动列表中显示大量数据时,可能会变得笨拙。在这种情况下,启用分组可以通过更好地组织内容并激活特定平台的控件来改善用户体验,这些控件使导航数据更加容易。

为ListView激活分组后,将为每个组添加标题行。

要启用分组:

  • 创建列表列表(组列表,每个组是元素列表)。
  • 将ListView的ItemsSource设置到该列表。
  • 将IsGroupingEnabled设置为true。
  • 设置GroupDisplayBinding以绑定到用作组标题的组属性。
  • [可选]将GroupShortNameBinding设置为绑定到用作组的简称的组的属性。简称用于跳转列表(iOS的右侧列)。

Headers and footers

ListView可能会显示可与列表元素一起滚动的页眉和页脚。 页眉和页脚可以是文本字符串,也可以是更复杂的布局。 此行为与节组是分开的。

可以将“页眉”和/或“页脚”设置为字符串值,也可以将它们设置为更复杂的布局。 还有HeaderTemplate和FooterTemplate属性,可让您为支持数据绑定的页眉和页脚创建更复杂的布局。

<ListView.Header>
    <StackLayout Orientation="Horizontal">
        <Label Text="Header" TextColor="Olive"  BackgroundColor="Red" />
    </StackLayout>
</ListView.Header>
<ListView.Footer>
    <StackLayout Orientation="Horizontal">
        <Label Text="Footer" TextColor="Gray" BackgroundColor="Blue" />
    </StackLayout>
</ListView.Footer>

滚动条可见性

ListView类具有Horizo​​ntalScrollBarVisibility和VerticalScrollBarVisibility属性,它们获取或设置一个ScrollBarVisibility值,该值表示水平或垂直滚动​​条何时可见,可以将这两个属性设置为以下值:

  • Default:指示平台的默认滚动条行为,并且是Horizo​​ntalScrollBarVisibility和VerticalScrollBarVisibility属性的默认值。
  • Always:指示滚动条将可见,即使内容适合视图。
  • Never:表示即使内容不适合视图,也不显示滚动条。

行分隔符

默认情况下,iOS和Android上ListView元素之间显示分隔线。如果您想在iOS和Android上隐藏分隔线,请在ListView上设置SeparatorVisibility属性,SeparatorVisibility的选项为:

  • Default-在iOS和Android上显示分隔线。
  • None-在所有平台上隐藏分隔符。

还可以通过SeparatorColor属性设置分隔线的颜色。

行高

默认情况下,ListView中的所有行都具有相同的高度,ListView具有两个可用于更改该行为的属性:

  • HasUnevenRows(不均匀) –真/假值,如果设置为true,则行的高度会有所不同。 默认为false。
  • RowHeight –设置HasUnevenRows为false时每行的高度。

在运行时调整行大小
只要HasUnevenRows属性设置为true,就可以在运行时以编程方式调整单个ListView行的大小,Cell.ForceUpdateSize方法更新单元格的大小,即使当前不可见它也是如此,如以下代码所示

void OnImageTapped (object sender, EventArgs args)
{
    var image = sender as Image;
    var viewCell = image.Parent.Parent as ViewCell;

    if (image.HeightRequest < 250) {
        image.HeightRequest = image.Height + 100;
        viewCell.ForceUpdateSize ();
    }
}
View Code

OnImageTapped事件处理程序是响应于轻按单元格中的一个Image而执行的,并增加了该单元格中显示的Image的大小,以便于查看。

注:运行时行大小调整的过度使用会导致性能下降

交互性

选择和点击

通过将ListView.SelectionMode属性设置为ListViewSelectionMode枚举的值,可以控制ListView选择模式:

  • Single 表示可以选中一个项目,并突出显示所选项目,这是默认值。
  • None 表示 项无法被选中。

当用户点击一个项目时,将触发两个事件:

  • 选择新项目时将触发ItemSelected。
  • 点击项目时触发ItemTapped。

轻按两次相同的项目将触发两个ItemTapped事件,但只会触发一个ItemSelected事件

注:ItemTappedEventArgs类包含ItemTapped事件的事件参数,具有Group和Item属性,以及一个ItemIndex属性,该属性的值表示所窃听项目的ListView中的索引。 类似地,包含ItemSelected事件的事件参数的SelectedItemChangedEventArgs类具有SelectedItem属性和SelectedItemIndex属性,该属性的值表示所选项目的ListView中的索引。

当SelectionMode属性设置为Single时,可以选择ListView中的项目,将触发ItemSelected和ItemTapped事件,并且SelectedItem属性将设置为所选项目的值。

当SelectionMode属性设置为None时,将无法选择ListView中的项目,将不会触发ItemSelected事件,并且SelectedItem属性将保持为null。 但是,在点击过程中,仍会触发ItemTapped事件,并且被轻按的项目将短暂突出显示。

当选择一个项目并将SelectionMode属性从Single更改为None时,SelectedItem属性将设置为null,并且将使用空项触发ItemSelected事件。

上下文操作

通常,用户会希望对ListView中的项目执行操作。  在iOS上,您可以滑动以删除消息。上下文动作可以在C#和XAML中实现。

上下文操作是使用MenuItem元素创建的,MenuItems对象的点击事件是MenuItems引发的而不是ListView,这与处理单元格的轻拍事件的方式不同,在列表事件中,ListView引发事件而不是单元格,由于ListView引发了事件,因此会为其事件处理程序提供关键信息,例如选择或点击了哪个项目。

默认情况下,MenuItem无法知道它属于哪个单元格,MenuItem上的CommandParameter属性可用于存储对象,例如MenuItem的ViewCell后面的对象。 可以在XAML和C#中设置CommandParameter属性。

Xaml:

可以在XAML集合中创建MenuItem元素。 下面的XAML演示了一个自定义单元,其中实现了两个上下文操作:

<ListView x:Name="ContextDemoList">
  <ListView.ItemTemplate>
    <DataTemplate>
      <ViewCell>
         <ViewCell.ContextActions>
            <MenuItem Clicked="OnMore"
                      CommandParameter="{Binding .}"
                      Text="More" />
            <MenuItem Clicked="OnDelete"
                      CommandParameter="{Binding .}"
                      Text="Delete" IsDestructive="True" />
         </ViewCell.ContextActions>
         <StackLayout Padding="15,0">
              <Label Text="{Binding title}" />
         </StackLayout>
      </ViewCell>
    </DataTemplate>
  </ListView.ItemTemplate>
</ListView>
View Code

注:Android的NavigationPageRenderer具有可重写的UpdateMenuItemIcon方法,该方法可用于从自定义Drawable加载图标。 通过此覆盖,可以将SVG图像用作Android上MenuItem实例上的图标

可以将多个上下文操作添加到一个单元格,但是只有一个应将IsDestructive设置为true(为true时,项目在iOS上的呈现方式有所不同)

Pull to刷新

要启用“拉动刷新”功能,请将IsPullToRefreshEnabled设置为true:

刷新期间会出现一个微调框,默认情况下为黑色。 但是,可以通过将RefreshControlColor属性设置为Color来在iOS和Android上更改微调器颜色:

ListView触发Refreshing事件以启动刷新,并且IsRefreshing属性将设置为true【自动的】,刷新ListView内容所需的任何代码均应由Refreshing事件的事件处理程序执行,或由RefreshCommand执行的方法执行。 刷新ListView之后,应将IsRefreshing属性设置为false,或应调用EndRefresh方法,以指示刷新已完成。

注:定义RefreshCommand时,可以指定命令的CanExecute方法来启用或禁用命令。

eg:

<ListView  RefreshCommand="{Binding ComListRefreshCommand}" IsRefreshing="{Binding ComListIsRefreshing}">

在刷新命令中,执行一些操作后,将IsRefreshing设置为false,

private void ComListRefreshCommandAsync()
        {
            //....
            ComListIsRefreshing = false;  //刷新完成
        }

检测滚动

ListView定义了一个Scrolled事件,该事件被触发以指示发生滚动。 以下XAML示例显示了一个ListView,它为Scrolled事件设置了一个事件处理程序:

<ListView Scrolled="OnListViewScrolled">
    ...
</ListView>
void OnListViewScrolled(object sender, ScrolledEventArgs e)
{
    Debug.WriteLine("ScrollX: " + e.ScrollX);
    Debug.WriteLine("ScrollY: " + e.ScrollY);  
}

性能

 

在编写移动应用程序时,性能至关重要。 用户已经期望平滑的滚动和快速的加载时间。 无法满足用户的期望将使您在应用程序商店中的评分成本下降,或者对于业务线应用程序,这将花费组织时间和金钱。

 

Xamarin.Forms ListView是用于显示数据的强大视图,但是它有一些限制。 使用自定义单元格时,滚动性能可能会受到影响,尤其是当它们包含深度嵌套的视图层次结构或使用需要复杂测量的某些布局时。 幸运的是,可以使用一些技术来避免性能下降。

 

缓存策略

ListView通常用于显示比屏幕显示更多的数据。 例如,音乐应用程序可能具有包含数千个条目的歌曲库,为每个条目创建一个项目将浪费宝贵的内存并且性能不佳,不断创建和销毁行将要求应用程序不断实例化和清理对象,这也会导致性能下降。

为了节省内存,每个平台的本机ListView等效项都具有用于重复使用行的内置功能。 仅将屏幕上可见的单元格加载到内存中,并将内容加载到现有单元格中,此模式可防止应用程序实例化数千个对象,从而节省时间和内存

Xamarin.Forms允许通过ListViewCachingStrategy枚举重用ListView单元,该枚举具有以下值:

  • RetainElement, // the default value
  • RecycleElement,
  • RecycleElementAndDataTemplate

注:通用Windows平台(UWP)忽略了RetainElement缓存策略,因为它始终使用缓存来提高性能。 因此,默认情况下,它的行为就像应用了RecycleElement缓存策略一样。

RetainElement保留元素
RetainElement缓存策略指定ListView将为列表中的每个项目生成一个单元格,并且是默认的ListView行为。在以下情况下应使用它:

  • 每个细胞具有大量的结合(20-30 +)。
  • 单元模板经常更改。
  • 测试表明,RecycleElement缓存策略导致执行速度降低。

在使用自定义单元格时,认识到RetainElement缓存策略的后果很重要。每次创建单元都需要运行任何单元初始化代码,这可能是每秒多次。在这种情况下,在页面上很好的布局技术(例如使用多个嵌套的StackLayout实例)会在用户实时滚动设置并实时销毁它们时成为性能瓶颈。

RecycleElement回收元素
RecycleElement缓存策略指定ListView将尝试通过回收列表单元来最小化其内存占用量和执行速度。此模式并不总是可以提供性能上的改进,应该执行测试以确定是否有任何改进。但是,这是首选,应在以下情况下使用:

  • 每个单元具有少量至中等数量的结合。
  • 每个单元的BindingContext定义所有单元数据。
  • 每个单元在很大程度上相似,但单元模板不变。

在虚拟化期间,单元将更新其绑定上下文,因此,如果应用程序使用此模式,则必须确保正确处理了绑定上下文更新。有关单元的所有数据必须来自绑定上下文,否则可能会发生一致性错误。通过使用数据绑定显示单元格数据可以避免此问题。或者,应在OnBindingContextChanged重写中设置单元格数据,而不是在自定义单元格的构造函数中进行设置,如以下代码示例所示:

在子类ListView中设置缓存策略
在ListView的子类上从XAML设置CachingStrategy属性将不会产生所需的行为,因为ListView上没有CachingStrategy属性。 此外,如果启用了XAMLC,则将产生以下错误消息:找不到“ CachingStrategy”的属性,可绑定属性或事件

解决此问题的方法是在子类ListView上指定一个构造函数,该构造函数接受ListViewCachingStrategy参数并将其传递给基类

public class CustomListView : ListView
{
    public CustomListView (ListViewCachingStrategy strategy) : base (strategy)
    {
    }
    ...
}

然后可以使用x:Arguments语法从XAML指定ListViewCachingStrategy枚举值:

<local:CustomListView>
    <x:Arguments>
        <ListViewCachingStrategy>RecycleElement</ListViewCachingStrategy>
    </x:Arguments>
</local:CustomListView>

ListView性能建议

有许多技术可以改善ListView的性能,以下建议可能会改善ListView的性能

  • 将ItemsSource属性绑定到IList <T>集合而不是IEnumerable <T>集合,因为IEnumerable <T>集合不支持随机访问。
  • 尽可能使用内置单元(例如TextCell / SwitchCell)代替ViewCell。
  • 使用更少的元素。例如,考虑使用单个FormattedString标签而不是多个标签。
  • 显示非均匀数据(即不同类型的数据)时,用TableView替换ListView。
  • 限制使用Cell.ForceUpdateSize方法。如果使用过度,则会降低性能。
  • 在Android上,请避免在实例化ListView后设置其行分隔符的可见性或颜色,因为这会导致较大的性能损失。
  • 避免基于BindingContext更改单元格布局,更改布局会导致大量的测量和初始化成本。
  • 避免深层嵌套的布局层次结构,使用AbsoluteLayout或Grid可以帮助减少嵌套。
  • 避免使用除Fill之外的特定LayoutOptions(Fill是最便宜的计算方法)。
  • 出于以下原因,避免将ListView放在ScrollView中

ListView实现自己的滚动。
ListView将不会接收任何手势,因为它们将由父级ScrollView处理。
ListView可以显示一个自定义的页眉和页脚,可以与列表的元素一起滚动,从而可能提供ScrollView所使用的功能。有关更多信息,请参见页眉和页脚。

  • 如果需要在单元格中显示特定的复杂设计,请考虑使用自定义渲染器。

AbsoluteLayout有潜力执行布局而无需调用单个度量,从而使其具有高性能。如果不能使用AbsoluteLayout,请考虑RelativeLayout。如果使用RelativeLayout,则直接传递约束将比使用表达式API快得多。此方法更快,因为表达式API使用JIT,并且在iOS上必须解释树,这比较慢。表达式API适用于仅在初始布局和旋转时才需要的页面布局,但在ListView中(在滚动过程中不断运行)在列表视图中会降低性能。

为ListView或其单元格构建自定义渲染器是一种减少布局计算对滚动性能影响的方法。有关更多信息,请参见自定义ListView和自定义ViewCell。

 

posted @ 2019-11-13 18:37  peterYong  阅读(5938)  评论(0编辑  收藏  举报