在 Xamarin 中使用 tvOS 集合视图

集合视图支持使用任意布局显示一组内容。 使用内置支持,它们允许轻松创建类似网格的布局或线性布局,同时支持自定义布局。

示例集合视图

集合视图使用委托和数据源来维护项集合,以提供用户交互和集合内容。 由于集合视图基于独立于视图本身的布局子系统,因此提供不同的布局可以轻松地动态更改集合视图数据的呈现。

关于集合视图

如上所述,集合视图 (UICollectionView) 管理项的有序集合,并呈现具有可自定义布局的项。 集合视图的工作方式与表视图 (UITableView) 类似,但是可以使用布局来显示不仅仅是一列的项目。

在 tvOS 中使用集合视图时,你的应用负责使用数据源 (UICollectionViewDataSource) 来提供与集合关联的数据。 可以选择将集合视图数据组织并呈现为不同的组(节)。

集合视图使用单元格 (UICollectionViewCell) 在屏幕上显示各个项,单元格提供集合中给定部分信息的呈现(如图像及其标题)。

(可选)可将补充视图添加到集合视图的展示中,以充当节和单元格的页眉和页脚。 集合视图的布局负责定义这些视图以及各个单元格的位置。

集合视图可以使用委托 (UICollectionViewDelegate) 来响应用户交互。 此委托还负责确定给定单元格是否可以获得焦点、是否已突出显示单元格或是否已选择单元格。 在某些情况下,委托可确定各个单元格的大小。

集合视图布局

集合视图的一个关键功能是将所呈现数据与其布局分离。 集合视图布局 (UICollectionViewLayout) 负责在集合视图的屏幕展示中提供单元格(以及任何补充视图)的组织结构和位置。

各个单元格由集合视图从其附加的数据源创建,然后由给定的集合视图布局排列和显示。

创建集合视图时,通常提供集合视图布局。 但是,可以随时更改集合视图布局,将使用提供的新布局自动更新集合视图数据的屏幕展示。

集合视图布局提供了几种方法,可用于对两个不同布局之间的转换进行动画处理(默认情况下没有完成动画)。 此外,集合视图布局可以使用手势识别器进一步对用户交互进行动画处理,从而更改布局。

创建单元格和补充视图

集合视图的数据源不仅负责提供支持集合项的数据,还负责提供用于显示内容的单元格。

由于集合视图旨在处理大型项集合,因此可以取消排队并重复使用各个单元格,防止内存溢出限制。 为视图取消排队有两种不同的方法:

  • DequeueReusableCell - 创建或返回给定类型的单元格(如应用的情节提要中指定)。
  • DequeueReusableSupplementaryView - 创建或返回给定类型的补充视图(如应用的情节提要中指定)。

在调用上述任一方法之前,必须注册类、情节提要或用于使用集合视图创建单元格视图的 .xib 文件。 例如:

public CityCollectionView (IntPtr handle) : base (handle)
{
    // Initialize
    RegisterClassForCell (typeof(CityCollectionViewCell), CityViewDatasource.CardCellId);
    ...
}

其中 typeof(CityCollectionViewCell) 提供支持视图的类,CityViewDatasource.CardCellId 提供在为单元格(或视图)取消排队时使用的 ID。

为单元格取消排队后,使用它所展示的项的数据对其进行配置,并返回到集合视图以供显示。

关于集合视图控制器

集合视图控制器 (UICollectionViewController) 是一种专用视图控制器 (UIViewController),具有以下行为:

  • 它负责从情节提要或 .xib 文件加载集合视图并实例化视图。 如果是在代码中创建,则会自动创建一个未配置的新集合视图。
  • 加载集合视图后,控制器会尝试从情节提要或 .xib 文件加载其数据源和委托。 如果都不可用,它会将自身设置为这两者的来源。
  • 确保在首次显示时填充集合视图之前加载数据,并在每次后续显示时重新加载和清除选择。

此外,集合视图控制器还提供可替代的方法,可用于管理集合视图的生命周期,例如 AwakeFromNibViewWillDisplay

集合视图和情节提要

在 Xamarin.tvOS 应用中使用集合视图的最简单方法是向其情节提要添加一个集合视图。 为快速展示,我们创建一个示例应用,使其提供图像、标题和选择按钮。 如果用户单击选择按钮,将显示一个集合视图,允许用户选择新图像。 选择图像后,将关闭集合视图,并显示新的图像和标题。

请执行以下操作:

  1. 在 Visual Studio for Mac 中启动新的单视图 tvOS 应用

  2. 在“解决方案资源管理器”中,双击 Main.storyboard 文件,并在 iOS 设计器中将其打开。

  3. 将图像视图、标签和按钮添加到现有视图,并将其配置为如下所示:

    示例布局

  4. 在“属性资源管理器”的“小组件选项卡”中为图像视图和标签分配“名称”。 例如:

    设置名称

  5. 接下来,将集合视图控制器拖到情节提要上:

    集合视图控制器

  6. 从按钮拖动到集合视图控制器,然后从弹出窗口中选择“推送”:

    从弹出窗口中选择“推送”

  7. 运行应用时,每当用户单击按钮时,就会显示集合视图。

  8. 选择集合视图,并在“属性资源管理器”的“布局选项卡”中输入以下值:

    属性资源管理器

  9. 这将控制各个单元格的大小以及单元格与集合视图外边缘之间的边框。

  10. 在“小组件选项卡”中选择集合视图控制器并将其类设置为 CityCollectionViewController

    将类设置为 CityCollectionViewController

  11. 在“小组件选项卡”中选择集合视图并将其类设置为 CityCollectionView

    将类设置为 CityCollectionView

  12. 在“小组件选项卡”中选择集合视图单元格并将其类设置为 CityCollectionViewCell

    将类设置为 CityCollectionViewCell

  13. 在“小组件选项卡”中,确保集合视图的“布局”为 Flow,“滚动方向”为 Vertical

    “小组件”选项卡

  14. 在“小组件选项卡”中选择集合视图单元格并将其“标识”设置为 CityCell

    将“标识”设置为 CityCell

  15. 保存所做更改。

如果已为集合视图的“布局”选择 Custom,可以指定自定义布局。 Apple 提供内置 UICollectionViewFlowLayoutUICollectionViewDelegateFlowLayout,可以轻松地在基于网格的布局中呈现数据(flow 布局样式使用这些数据)。

有关使用情节提要的详细信息,请参阅你好,tvOS 快速入门指南

为集合视图提供数据

现在,我们已将集合视图(和集合视图控制器)添加到情节提要中,我们需要为集合提供数据。

数据模型

首先,我们将为数据创建一个模型,用于保存要显示的图像的文件名、标题和标志,以便选择城市。

创建 CityInfo 类,使其如下所示:

using System;

namespace tvCollection
{
    public class CityInfo
    {
        #region Computed Properties
        public string ImageFilename { get; set; }
        public string Title { get; set; }
        public bool CanSelect{ get; set; }
        #endregion

        #region Constructors
        public CityInfo (string filename, string title, bool canSelect)
        {
            // Initialize
            this.ImageFilename = filename;
            this.Title = title;
            this.CanSelect = canSelect;
        }
        #endregion
    }
}

集合视图单元格

现在,我们需要定义如何为每个单元格呈现数据。 编辑 CityCollectionViewCell.cs 文件(从“情节提要”文件自动创建),使其如下所示:

using System;
using Foundation;
using UIKit;
using CoreGraphics;

namespace tvCollection
{
    public partial class CityCollectionViewCell : UICollectionViewCell
    {
        #region Private Variables
        private CityInfo _city;
        #endregion

        #region Computed Properties
        public UIImageView CityView { get ; set; }
        public UILabel CityTitle { get; set; }

        public CityInfo City {
            get { return _city; }
            set {
                _city = value;
                CityView.Image = UIImage.FromFile (City.ImageFilename);
                CityView.Alpha = (City.CanSelect) ? 1.0f : 0.5f;
                CityTitle.Text = City.Title;
            }
        }
        #endregion

        #region Constructors
        public CityCollectionViewCell (IntPtr handle) : base (handle)
        {
            // Initialize
            CityView = new UIImageView(new CGRect(22, 19, 320, 171));
            CityView.AdjustsImageWhenAncestorFocused = true;
            AddSubview (CityView);

            CityTitle = new UILabel (new CGRect (22, 209, 320, 21)) {
                TextAlignment = UITextAlignment.Center,
                TextColor = UIColor.White,
                Alpha = 0.0f
            };
            AddSubview (CityTitle);
        }
        #endregion


    }
}

对于 tvOS 应用,我们将显示图像和可选标题。 如果无法选择给定的城市,我们将使用以下代码将图像视图灰显:

CityView.Alpha = (City.CanSelect) ? 1.0f : 0.5f;

当用户将包含图像的单元格引入焦点时,我们希望对它使用内置的视差效果来设置以下属性:

CityView.AdjustsImageWhenAncestorFocused = true;

有关导航和焦点的详细信息,请参阅我们的使用导航和焦点Siri 远程和蓝牙控制器文档。

集合视图数据提供程序

创建数据模型并定义单元格布局后,让我们为集合视图创建数据源。 数据源不仅负责提供支持数据,还负责为单元格取消排队以在屏幕上显示各个单元格。

创建 CityViewDatasource 类,使其如下所示:

using System;
using System.Collections.Generic;
using UIKit;
using Foundation;
using CoreGraphics;
using ObjCRuntime;

namespace tvCollection
{
    public class CityViewDatasource : UICollectionViewDataSource
    {
        #region Application Access
        public static AppDelegate App {
            get { return (AppDelegate)UIApplication.SharedApplication.Delegate; }
        }
        #endregion

        #region Static Constants
        public static NSString CardCellId = new NSString ("CityCell");
        #endregion

        #region Computed Properties
        public List<CityInfo> Cities { get; set; } = new List<CityInfo>();
        public CityCollectionView ViewController { get; set; }
        #endregion

        #region Constructors
        public CityViewDatasource (CityCollectionView controller)
        {
            // Initialize
            this.ViewController = controller;
            PopulateCities ();
        }
        #endregion

        #region Public Methods
        public void PopulateCities() {

            // Clear existing cities
            Cities.Clear();

            // Add new cities
            Cities.Add(new CityInfo("City01.jpg", "Houses by Water", false));
            Cities.Add(new CityInfo("City02.jpg", "Turning Circle", true));
            Cities.Add(new CityInfo("City03.jpg", "Skyline at Night", true));
            Cities.Add(new CityInfo("City04.jpg", "Golden Gate Bridge", true));
            Cities.Add(new CityInfo("City05.jpg", "Roads by Night", true));
            Cities.Add(new CityInfo("City06.jpg", "Church Domes", true));
            Cities.Add(new CityInfo("City07.jpg", "Mountain Lights", true));
            Cities.Add(new CityInfo("City08.jpg", "City Scene", false));
            Cities.Add(new CityInfo("City09.jpg", "House in Winter", true));
            Cities.Add(new CityInfo("City10.jpg", "By the Lake", true));
            Cities.Add(new CityInfo("City11.jpg", "At the Dome", true));
            Cities.Add(new CityInfo("City12.jpg", "Cityscape", true));
            Cities.Add(new CityInfo("City13.jpg", "Model City", true));
            Cities.Add(new CityInfo("City14.jpg", "Taxi, Taxi!", true));
            Cities.Add(new CityInfo("City15.jpg", "On the Sidewalk", true));
            Cities.Add(new CityInfo("City16.jpg", "Midnight Walk", true));
            Cities.Add(new CityInfo("City17.jpg", "Lunchtime Cafe", true));
            Cities.Add(new CityInfo("City18.jpg", "Coffee Shop", true));
            Cities.Add(new CityInfo("City19.jpg", "Rustic Tavern", true));
        }
        #endregion

        #region Override Methods
        public override nint NumberOfSections (UICollectionView collectionView)
        {
            return 1;
        }

        public override nint GetItemsCount (UICollectionView collectionView, nint section)
        {
            return Cities.Count;
        }

        public override UICollectionViewCell GetCell (UICollectionView collectionView, NSIndexPath indexPath)
        {
            var cityCell = (CityCollectionViewCell)collectionView.DequeueReusableCell (CardCellId, indexPath);
            var city = Cities [indexPath.Row];

            // Initialize city
            cityCell.City = city;

            return cityCell;
        }
        #endregion
    }
}

让我们详细查看此类。 首先,我们从 UICollectionViewDataSource 中继承并提供单元格 ID 的快捷方式(我们已在 iOS 设计器中分配):

public static NSString CardCellId = new NSString ("CityCell");

接下来,我们将为集合数据提供存储,并提供用于填充数据的类:

public List<CityInfo> Cities { get; set; } = new List<CityInfo>();
...

public void PopulateCities() {

    // Clear existing cities
    Cities.Clear();

    // Add new cities
    Cities.Add(new CityInfo("City01.jpg", "Houses by Water", false));
    Cities.Add(new CityInfo("City02.jpg", "Turning Circle", true));
    ...
}

然后,我们替代 NumberOfSections 方法,并返回集合视图具有的节(项组)数。 在这种情况下,只有一个:

public override nint NumberOfSections (UICollectionView collectionView)
{
    return 1;
}

接下来,我们使用以下代码返回集合中的项数:

public override nint GetItemsCount (UICollectionView collectionView, nint section)
{
    return Cities.Count;
}

最后,使用以下代码在集合视图请求时为可重用的单元格取消排队:

public override UICollectionViewCell GetCell (UICollectionView collectionView, NSIndexPath indexPath)
{
    var cityCell = (CityCollectionViewCell)collectionView.DequeueReusableCell (CardCellId, indexPath);
    var city = Cities [indexPath.Row];

    // Initialize city
    cityCell.City = city;

    return cityCell;
}

获取 CityCollectionViewCell 类型的集合视图单元格后,使用给定的项填充它。

响应用户事件

由于我们希望用户能够从集合中选择项,因此我们需要提供集合视图委托来处理此交互。 我们需要提供一种方法来让调用视图知道用户选择了哪些项。

应用委托

我们需要一种方法,将当前选定的项从集合视图关联回调用视图。 我们将在我们的 AppDelegate 上使用自定义属性。 编辑 AppDelegate.cs 文件并添加以下代码:

public CityInfo SelectedCity { get; set;} = new CityInfo("City02.jpg", "Turning Circle", true);

这将定义属性并设置最初将显示的默认城市。 稍后,我们将使用此属性来显示用户的选择并允许更改选择。

集合视图委托

接下来,向项目添加新的 CityViewDelegate 类,使其如下所示:

using System;
using System.Collections.Generic;
using UIKit;
using Foundation;
using CoreGraphics;

namespace tvCollection
{
    public class CityViewDelegate : UICollectionViewDelegateFlowLayout
    {
        #region Application Access
        public static AppDelegate App {
            get { return (AppDelegate)UIApplication.SharedApplication.Delegate; }
        }
        #endregion

        #region Constructors
        public CityViewDelegate ()
        {
        }
        #endregion

        #region Override Methods
        public override CGSize GetSizeForItem (UICollectionView collectionView, UICollectionViewLayout layout, NSIndexPath indexPath)
        {
            return new CGSize (361, 256);
        }

        public override bool CanFocusItem (UICollectionView collectionView, NSIndexPath indexPath)
        {
            if (indexPath == null) {
                return false;
            } else {
                var controller = collectionView as CityCollectionView;
                return controller.Source.Cities[indexPath.Row].CanSelect;
            }
        }

        public override void ItemSelected (UICollectionView collectionView, NSIndexPath indexPath)
        {
            var controller = collectionView as CityCollectionView;
            App.SelectedCity = controller.Source.Cities [indexPath.Row];

            // Close Collection
            controller.ParentController.DismissViewController(true,null);
        }
        #endregion
    }
}

我们来详细了解此类。 首先,我们从 UICollectionViewDelegateFlowLayout 中继承。 我们从此类而不从 UICollectionViewDelegate 继承的原因是我们使用内置 UICollectionViewFlowLayout 来呈现项而不是自定义布局类型。

接下来,使用此代码返回各个项的大小:

public override CGSize GetSizeForItem (UICollectionView collectionView, UICollectionViewLayout layout, NSIndexPath indexPath)
{
    return new CGSize (361, 256);
}

然后,我们决定给定单元格是否可以使用以下代码获得焦点:

public override bool CanFocusItem (UICollectionView collectionView, NSIndexPath indexPath)
{
    if (indexPath == null) {
        return false;
    } else {
        var controller = collectionView as CityCollectionView;
        return controller.Source.Cities[indexPath.Row].CanSelect;
    }
}

我们检查以查看给定的部分支持数据是否将其 CanSelect 标志设置为 true 并返回该值。 有关导航和焦点的详细信息,请参阅我们的使用导航和焦点Siri 远程和蓝牙控制器文档。

最后,我们使用以下代码对用户选择项做出响应:

public override void ItemSelected (UICollectionView collectionView, NSIndexPath indexPath)
{
    var controller = collectionView as CityCollectionView;
    App.SelectedCity = controller.Source.Cities [indexPath.Row];

    // Close Collection
    controller.ParentController.DismissViewController(true,null);
}

在这里,我们将 AppDelegateSelectedCity 属性设置为用户选择的项,并关闭集合视图控制器,返回到调用视图。 我们尚未定义集合视图的 ParentController 属性,接下来将执行此操作。

配置集合视图

现在,我们需要编辑集合视图并分配数据源和委托。 编辑 CityCollectionView.cs 文件(从情节提要自动创建),使其如下所示:

using System;
using Foundation;
using UIKit;

namespace tvCollection
{
    public partial class CityCollectionView : UICollectionView
    {
        #region Application Access
        public static AppDelegate App {
            get { return (AppDelegate)UIApplication.SharedApplication.Delegate; }
        }
        #endregion

        #region Computed Properties
        public CityViewDatasource Source {
            get { return DataSource as CityViewDatasource;}
        }

        public CityCollectionViewController ParentController { get; set;}
        #endregion

        #region Constructors
        public CityCollectionView (IntPtr handle) : base (handle)
        {
            // Initialize
            RegisterClassForCell (typeof(CityCollectionViewCell), CityViewDatasource.CardCellId);
            DataSource = new CityViewDatasource (this);
            Delegate = new CityViewDelegate ();
        }
        #endregion

        #region Override Methods
        public override nint NumberOfSections ()
        {
            return 1;
        }

        public override void DidUpdateFocus (UIFocusUpdateContext context, UIFocusAnimationCoordinator coordinator)
        {
            var previousItem = context.PreviouslyFocusedView as CityCollectionViewCell;
            if (previousItem != null) {
                Animate (0.2, () => {
                    previousItem.CityTitle.Alpha = 0.0f;
                });
            }

            var nextItem = context.NextFocusedView as CityCollectionViewCell;
            if (nextItem != null) {
                Animate (0.2, () => {
                    nextItem.CityTitle.Alpha = 1.0f;
                });
            }
        }
        #endregion
    }
}

首先,我们提供访问 AppDelegate 的快捷方式:

public static AppDelegate App {
    get { return (AppDelegate)UIApplication.SharedApplication.Delegate; }
}

接下来,提供集合视图数据源的快捷方式和用于访问集合视图控制器的属性(在用户做出选择时,上面所述的委托使用它来关闭集合):

public CityViewDatasource Source {
    get { return DataSource as CityViewDatasource;}
}

public CityCollectionViewController ParentController { get; set;}

然后,使用以下代码初始化集合视图并分配单元格类、数据源和委托:

public CityCollectionView (IntPtr handle) : base (handle)
{
    // Initialize
    RegisterClassForCell (typeof(CityCollectionViewCell), CityViewDatasource.CardCellId);
    DataSource = new CityViewDatasource (this);
    Delegate = new CityViewDelegate ();
}

最后,我们希望图像下的标题仅在用户将其突出显示(聚焦)时才可见。 使用以下代码执行此操作:

public override void DidUpdateFocus (UIFocusUpdateContext context, UIFocusAnimationCoordinator coordinator)
{
    var previousItem = context.PreviouslyFocusedView as CityCollectionViewCell;
    if (previousItem != null) {
        Animate (0.2, () => {
            previousItem.CityTitle.Alpha = 0.0f;
        });
    }

    var nextItem = context.NextFocusedView as CityCollectionViewCell;
    if (nextItem != null) {
        Animate (0.2, () => {
            nextItem.CityTitle.Alpha = 1.0f;
        });
    }
}

我们将上一项的透明度设置为零 (0),下一项的透明度设置为 100%。 这些转换也进行动画处理。

配置集合视图控制器

现在,我们需要对集合视图执行最终配置,并允许控制器设置我们定义的属性,以便在用户做出选择后关闭集合视图。

编辑 CityCollectionViewController.cs 文件(从情节提要自动创建),使其如下所示:

// This file has been autogenerated from a class added in the UI designer.

using System;

using Foundation;
using UIKit;

namespace tvCollection
{
    public partial class CityCollectionViewController : UICollectionViewController
    {
        #region Computed Properties
        public CityCollectionView Collection {
            get { return CollectionView as CityCollectionView; }
        }
        #endregion

        #region Constructors
        public CityCollectionViewController (IntPtr handle) : base (handle)
        {
        }
        #endregion

        #region Override Methods
        public override void AwakeFromNib ()
        {
            base.AwakeFromNib ();

            // Save link to controller
            Collection.ParentController = this;
        }
        #endregion
    }
}

整合到一起

现在,我们已将所有部分整合到一起以填充和控制集合视图,因此我们需要对主视图进行最终编辑,以便将所有内容组合在一起。

编辑 ViewController.cs 文件(从情节提要自动创建),使其如下所示:

using System;
using Foundation;
using UIKit;
using tvCollection;

namespace MySingleView
{
    public partial class ViewController : UIViewController
    {
        #region Application Access
        public static AppDelegate App {
            get { return (AppDelegate)UIApplication.SharedApplication.Delegate; }
        }
        #endregion

        #region Constructors
        public ViewController (IntPtr handle) : base (handle)
        {
        }
        #endregion

        #region Override Methods
        public override void ViewDidLoad ()
        {
            base.ViewDidLoad ();
            // Perform any additional setup after loading the view, typically from a nib.
        }

        public override void ViewWillAppear (bool animated)
        {
            base.ViewWillAppear (animated);

            // Update image with the currently selected one
            CityView.Image = UIImage.FromFile(App.SelectedCity.ImageFilename);
            BackgroundView.Image = CityView.Image;
            CityTitle.Text = App.SelectedCity.Title;
        }

        public override void DidReceiveMemoryWarning ()
        {
            base.DidReceiveMemoryWarning ();
            // Release any cached data, images, etc that aren't in use.
        }
        #endregion
    }
}

以下代码最初显示 AppDelegateSelectedCity 属性中选择的项,并在用户从集合视图中做出选择时重新显示它:

public override void ViewWillAppear (bool animated)
{
    base.ViewWillAppear (animated);

    // Update image with the currently selected one
    CityView.Image = UIImage.FromFile(App.SelectedCity.ImageFilename);
    BackgroundView.Image = CityView.Image;
    CityTitle.Text = App.SelectedCity.Title;
}

测试应用

完成所有操作后,如果生成并运行应用,则主视图将显示默认城市:

主屏幕

如果用户单击“选择视图”按钮,则会显示集合视图:

集合视图

CanSelect 属性设置为 false 的任何城市都将灰显,用户将无法将焦点集中到该城市。 当用户突出显示(聚焦)某个项时,将显示标题,并且他们可以使用视差效果以 3D 方式稍微倾斜图像。

当用户单击选择图像时,集合视图将关闭,主视图将重新显示新图像:

主屏幕上的新图像

创建自定义布局并为项重新排序

使用集合视图的主要功能之一是创建自定义布局。 由于 tvOS 继承自 iOS,因此创建自定义布局的过程相同。 有关详细信息,请参阅我们的集合视图简介文档。

最近添加到 iOS 9 的集合视图的功能是允许为集合中的项轻松重新排序。 同样,由于 tvOS 9 是 iOS 9 的子集,因此操作方式相同。 有关更多详细信息,请参阅我们的集合视图更改文档。

总结

本文介绍了如何设计和使用 Xamarin.tvOS 应用中的集合视图。 首先,它讨论了构成集合视图的所有元素。 接下来,它演示了如何使用情节提要来设计和实现集合视图。 最后,它提供了有关创建自定义布局和为项重新排序的信息的链接。