在 Xamarin 中使用 tvOS 文本和搜索字段

如果需要,Xamarin.tvOS 应用可以使用文本字段和屏幕键盘从用户(如用户 ID 和密码)请求少量文本:

示例搜索字段

可以选择使用搜索字段提供应用内容的关键字搜索功能:

示例搜索结果

本文档将介绍在 Xamarin.tvOS 应用中使用文本和搜索字段的详细信息。

关于文本和搜索字段

如上所述,如果需要,Xamarin.tvOS 可以显示一个或多个文本字段,以使用屏幕(或可选的蓝牙键盘,具体取决于用户已安装的 tvOS 版本)从用户收集少量文本。

此外,如果应用向用户呈现大量内容(如音乐、电影或图片集合),你可能希望包含一个搜索字段,允许用户输入少量文本来筛选可用项列表。

文本字段

在 tvOS 中,文本字段显示为固定高度的圆角输入框,当用户单击屏幕键盘时,该框将显示屏幕键盘:

tvOS 中的文本字段

当用户焦点移动到给定的文本字段时,它将变大并显示深阴影。 在设计用户界面时,你需要牢记这一点,因为文本字段在获得焦点时可能会覆盖其他用户界面元素。

Apple 在使用文本字段方面提出了以下建议:

  • 谨慎使用文本输入 - 由于屏幕键盘的特性,输入长篇文本或填写多个文本字段对用户来说很繁琐。 更好的解决方案是通过使用选择列表或按钮来限制文本输入量。
  • 使用提示来传达目的 - 当文本框为空时,可以显示占位符“提示”。 如果适用,请使用提示(而非单独的标签)来描述文本字段的目的。
  • 选择适当的默认键盘类型 - tvOS 提供了几种不同的专用内置键盘类型,可以指定用于文本字段。 例如,电子邮件地址键盘可以通过允许用户从最近输入的地址列表中选择来简化输入。
  • 适当时使用安全文本字段 - 安全文本字段显示输入为点(而非实际字母)的字符。 收集敏感信息(如密码)时,请始终使用安全文本字段。

键盘

每当用户单击用户界面中的文本字段时,屏幕上会显示线性键盘。 用户使用 Touch Surface Siri Remote 从键盘中选择单个字母并输入请求的信息:

Siri 远程键盘

如果当前视图中有多个文本字段,将自动显示“下一步”按钮,以将用户带到下一个文本字段。 将为最后一个文本字段显示“完成”按钮,用于结束文本输入并将用户返回到上一个屏幕。

用户可以随时按 Siri Remote 上的“菜单”按钮结束文本输入,并再次返回到上一个屏幕。

Apple 在使用屏幕键盘方面提出了以下建议:

  • 选择适当的默认键盘类型 - tvOS 提供了几种不同的专用内置键盘类型,可以指定用于文本字段。 例如,电子邮件地址键盘可以通过允许用户从最近输入的地址列表中选择来简化输入。
  • 适当时使用键盘附件视图 - 除了始终显示的标准信息外,还可以将可选的附件视图(如图像或标签)添加到屏幕键盘,以阐明文本输入的目的或帮助用户输入所需的信息。

有关使用屏幕键盘的详细信息,请参阅 Apple UIKeyboardType管理键盘数据输入自定义视图iOS 文本编程指南文档。

搜索字段提供一个专用屏幕,其中提供文本字段和屏幕键盘,允许用户筛选键盘下方显示的项集合:

示例搜索结果

当用户在搜索字段中输入字母时,下面的结果将自动反映搜索结果。 用户可随时将焦点移动到结果,并选择其中一个项。

Apple 在使用搜索字段方面提出了以下建议:

  • 提供最近搜索 - 由于使用 Siri Remote 输入文本可能很繁琐,用户倾向于重复搜索请求,因此请考虑在键盘区域下的当前结果之前添加最近搜索结果的一部分。
  • 尽可能限制结果数 - 由于用户难以分析和导航大型项列表,因此请考虑限制返回的结果数。
  • 适当时提供搜索结果筛选器 - 如果应用提供的内容适合自己,请考虑添加范围栏以允许用户进一步筛选返回的搜索结果。

有关详细信息,请参阅 Apple UISearchController 类参考

使用文本字段

在 Xamarin.tvOS 应用中使用文本字段的最简单方法是使用 iOS 设计器将它们添加到用户界面设计中。

请执行以下操作:

  1. Solution Pad 中,双击 Main.storyboard 文件将其打开以进行编辑。

  2. 将一个或多个文本字段设计图面拖到视图上:

    文本字段

  3. 选择“文本字段”,在“Properties Pad”“小组件”选项卡中为每个字段指定唯一的“名称”

    Properties Pad 的“小组件”选项卡

  4. “文本字段”部分中,可以定义占位符提示和默认等元素:

    “文本字段”部分

  5. 向下滚动以定义拼写检查大写和默认键盘类型等属性:

    拼写检查、大写和默认键盘类型

  6. 保存对情节提要所做的更改。

在代码中,可以使用其 Text 属性获取或设置文本字段的值:

Console.WriteLine ("User ID {0} and Password {1}", UserId.Text, Password.Text);

可以选择使用 StartedEnded 文本字段事件来响应文本输入的开始和结束。

使用搜索字段

在 Xamarin.tvOS 应用中使用搜索字段的最简单方法是使用 Interface Designer 将它们添加到用户界面设计中。

请执行以下操作:

  1. Solution Pad 中,双击 Main.storyboard 文件将其打开以进行编辑。

  2. 将新的集合视图控制器拖到情节提要中,显示用户搜索的结果:

    集合视图控制器

  3. “Properties Pad”“小组件”选项卡中,对“类” 使用SearchResultsViewController,对“情节提要 ID”使用 SearchResults

    Visual Studio for Mac 中的“小组件”选项卡,可在其中指定类和情节提要 I D。

  4. 在设计图面上选择“单元格原型”

  5. “Properties Explorer”“小组件”选项卡中,对“类”使用 SearchResultCell,对“标识符”使用 ImageCell

    Visual Studio for Mac 中的“小组件”选项卡,可在其中指定类和标识符。

  6. 设置“单元格原型”的设计布局,并在“Properties Explorer”“小组件”选项卡中以唯一名称公开每个元素:

    设置单元格原型设计的布局

  7. 保存对情节提要所做的更改。

提供数据模型

接下来,需要提供一个类来充当用户要搜索的结果的数据模型。 在解决方案资源管理器中,右键单击项目名称并选择“添加”>“新文件...”>“常规”>“空类”并提供“名称”

选择“空类”并提供名称

例如,允许用户按标题和关键字搜索图片集合的应用可能如下所示:

using System;
using Foundation;

namespace tvText
{
    public class PictureInformation : NSObject
    {
        #region Computed Properties
        public string Title { get; set;}
        public string ImageName { get; set;}
        public string Keywords { get; set;}
        #endregion

        #region Constructors
        public PictureInformation (string title, string imageName, string keywords)
        {
            // Initialize
            this.Title = title;
            this.ImageName = imageName;
            this.Keywords = keywords;
        }
        #endregion
    }
}

集合视图单元格

在数据模型到位后,编辑原型单元 (SearchResultViewCell.cs),使其如下所示:

using Foundation;
using System;
using UIKit;

namespace tvText
{
    public partial class SearchResultViewCell : UICollectionViewCell
    {
        #region Private Variables
        private PictureInformation _pictureInfo = null;
        #endregion

        #region Computed Properties
        public PictureInformation PictureInfo {
            get { return _pictureInfo; }
            set {
                _pictureInfo = value;
                UpdateUI ();
            }
        }
        #endregion

        #region Constructors
        public SearchResultViewCell (IntPtr handle) : base (handle)
        {
            // Initialize
            UpdateUI ();
        }
        #endregion

        #region Private Methods
        private void UpdateUI ()
        {
            // Anything to process?
            if (PictureInfo == null) return;

            try {
                Picture.Image = UIImage.FromBundle (PictureInfo.ImageName);
                Picture.AdjustsImageWhenAncestorFocused = true;
                Title.Text = PictureInfo.Title;
                TextColor = UIColor.LightGray;
            } catch {
                // Ignore errors if view isn't fully loaded
            }
        }
        #endregion
    }

}

UpdateUI 方法将用于在每次更新属性时,在命名的 UI 元素中显示 PictureInformation 项(PictureInfo 属性)的各个字段。 例如,与图片关联的图像和标题。

集合视图控制器

接下来,编辑搜索结果集合视图控制器 (SearchResultsViewController.cs),使其如下所示:

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

namespace tvText
{
    public partial class SearchResultsViewController : UICollectionViewController , IUISearchResultsUpdating
    {
        #region Constants
        public const string CellID = "ImageCell";
        #endregion

        #region Private Variables
        private string _searchFilter = "";
        #endregion

        #region Computed Properties
        public List<PictureInformation> AllPictures { get; set;}
        public List<PictureInformation> FoundPictures { get; set; }
        public string SearchFilter {
            get { return _searchFilter; }
            set {
                _searchFilter = value.ToLower();
                FindPictures ();
                CollectionView?.ReloadData ();
            }
        }
        #endregion

        #region Constructors
        public SearchResultsViewController (IntPtr handle) : base (handle)
        {
            // Initialize
            this.AllPictures = new List<PictureInformation> ();
            this.FoundPictures = new List<PictureInformation> ();
            PopulatePictures ();
            FindPictures ();

        }
        #endregion

        #region Private Methods
        private void PopulatePictures ()
        {
            // Clear list
            AllPictures.Clear ();

            // Add images
            AllPictures.Add (new PictureInformation ("Antipasta Platter","Antipasta","cheese,grapes,tomato,coffee,meat,plate"));
            AllPictures.Add (new PictureInformation ("Cheese Plate", "CheesePlate", "cheese,plate,bread"));
            AllPictures.Add (new PictureInformation ("Coffee House", "CoffeeHouse", "coffee,people,menu,restaurant,cafe"));
            AllPictures.Add (new PictureInformation ("Computer and Expresso", "ComputerExpresso", "computer,coffee,expresso,phone,notebook"));
            AllPictures.Add (new PictureInformation ("Hamburger", "Hamburger", "meat,bread,cheese,tomato,pickle,lettus"));
            AllPictures.Add (new PictureInformation ("Lasagna Dinner", "Lasagna", "salad,bread,plate,lasagna,pasta"));
            AllPictures.Add (new PictureInformation ("Expresso Meeting", "PeopleExpresso", "people,bag,phone,expresso,coffee,table,tablet,notebook"));
            AllPictures.Add (new PictureInformation ("Soup and Sandwich", "SoupAndSandwich", "soup,sandwich,bread,meat,plate,tomato,lettus,egg"));
            AllPictures.Add (new PictureInformation ("Morning Coffee", "TabletCoffee", "tablet,person,man,coffee,magazine,table"));
            AllPictures.Add (new PictureInformation ("Evening Coffee", "TabletMagCoffee", "tablet,magazine,coffee,table"));
        }

        private void FindPictures ()
        {
            // Clear list
            FoundPictures.Clear ();

            // Scan each picture for a match
            foreach (PictureInformation picture in AllPictures) {
                if (SearchFilter == "") {
                    // If no search term, everything matches
                    FoundPictures.Add (picture);
                } else if (picture.Title.Contains (SearchFilter) || picture.Keywords.Contains (SearchFilter)) {
                    // If the search term is in the title or keywords, we've found a match
                    FoundPictures.Add (picture);
                }
            }
        }
        #endregion

        #region Override Methods
        public override nint NumberOfSections (UICollectionView collectionView)
        {
            // Only one section in this collection
            return 1;
        }

        public override nint GetItemsCount (UICollectionView collectionView, nint section)
        {
            // Return the number of matching pictures
            return FoundPictures.Count;
        }

        public override UICollectionViewCell GetCell (UICollectionView collectionView, NSIndexPath indexPath)
        {
            // Get a new cell and return it
            var cell = collectionView.DequeueReusableCell (CellID, indexPath);
            return (UICollectionViewCell)cell;
        }

        public override void WillDisplayCell (UICollectionView collectionView, UICollectionViewCell cell, NSIndexPath indexPath)
        {
            // Grab the cell
            var currentCell = cell as SearchResultViewCell;
            if (currentCell == null)
                throw new Exception ("Expected to display a `SearchResultViewCell`.");

            // Display the current picture info in the cell
            var item = FoundPictures [indexPath.Row];
            currentCell.PictureInfo = item;
        }

        public override void ItemSelected (UICollectionView collectionView, NSIndexPath indexPath)
        {
            // If this Search Controller was presented as a modal view, close
            // it before continuing
            // DismissViewController (true, null);

            // Grab the picture being selected and report it
            var picture = FoundPictures [indexPath.Row];
            Console.WriteLine ("Selected: {0}", picture.Title);
        }

        public void UpdateSearchResultsForSearchController (UISearchController searchController)
        {
            // Save the search filter and update the Collection View
            SearchFilter = searchController.SearchBar.Text ?? string.Empty;
        }

        public override void DidUpdateFocus (UIFocusUpdateContext context, UIFocusAnimationCoordinator coordinator)
        {
            var previousItem = context.PreviouslyFocusedView as SearchResultViewCell;
            if (previousItem != null) {
                UIView.Animate (0.2, () => {
                    previousItem.TextColor = UIColor.LightGray;
                });
            }

            var nextItem = context.NextFocusedView as SearchResultViewCell;
            if (nextItem != null) {
                UIView.Animate (0.2, () => {
                    nextItem.TextColor = UIColor.Black;
                });
            }
        }
        #endregion
    }
}

首先,IUISearchResultsUpdating 接口添加到类,以处理用户正在更新的搜索控制器筛选器:

public partial class SearchResultsViewController : UICollectionViewController , IUISearchResultsUpdating

还定义了一个常量,用于指定原型单元的 ID(与上述 Interface Designer 中定义的 ID 匹配)的 ID,在集合控制器请求新单元格时将用到:

public const string CellID = "ImageCell";

为搜索的项的完整列表、搜索筛选器词和匹配该词的项列表创建存储:

private string _searchFilter = "";
...

public List<PictureInformation> AllPictures { get; set;}
public List<PictureInformation> FoundPictures { get; set; }
public string SearchFilter {
    get { return _searchFilter; }
    set {
        _searchFilter = value.ToLower();
        FindPictures ();
        CollectionView?.ReloadData ();
    }
}

更改 SearchFilter 时,匹配项的列表将会更新,并重新加载集合视图的内容。 FindPictures 例程负责查找与新搜索词匹配的项:

private void FindPictures ()
{
    // Clear list
    FoundPictures.Clear ();

    // Scan each picture for a match
    foreach (PictureInformation picture in AllPictures) {
        if (SearchFilter == "") {
            // If no search term, everything matches
            FoundPictures.Add (picture);
        } else if (picture.Title.Contains (SearchFilter) || picture.Keywords.Contains (SearchFilter)) {
            // If the search term is in the title or keywords, we've found a match
            FoundPictures.Add (picture);
        }
    }
}

当用户更改搜索控制器中的筛选器时,SearchFilter 的值将会更新(这将更新结果集合视图):

public void UpdateSearchResultsForSearchController (UISearchController searchController)
{
    // Save the search filter and update the Collection View
    SearchFilter = searchController.SearchBar.Text ?? string.Empty;
}

PopulatePictures 方法最初会填充可用项的集合:

private void PopulatePictures ()
{
    // Clear list
    AllPictures.Clear ();

    // Add images
    AllPictures.Add (new PictureInformation ("Antipasta Platter","Antipasta","cheese,grapes,tomato,coffee,meat,plate"));
    ...
}

为了本示例,集合视图控制器加载时,将在内存中创建所有示例数据。 在实际应用中,此数据可能是从数据库或 Web 服务读取的,并且仅在需要时才能防止超过 Apple TV 的有限内存。

NumberOfSectionsGetItemsCount 方法提供匹配的项数:

public override nint NumberOfSections (UICollectionView collectionView)
{
    // Only one section in this collection
    return 1;
}

public override nint GetItemsCount (UICollectionView collectionView, nint section)
{
    // Return the number of matching pictures
    return FoundPictures.Count;
}

GetCell 方法为集合视图中每个项返回新的原型单元(基于上面在情节提要中定义的 CellID):

public override UICollectionViewCell GetCell (UICollectionView collectionView, NSIndexPath indexPath)
{
    // Get a new cell and return it
    var cell = collectionView.DequeueReusableCell (CellID, indexPath);
    return (UICollectionViewCell)cell;
}

在显示单元格之前调用 WillDisplayCell 方法,以便对其进行配置:

public override void WillDisplayCell (UICollectionView collectionView, UICollectionViewCell cell, NSIndexPath indexPath)
{
    // Grab the cell
    var currentCell = cell as SearchResultViewCell;
    if (currentCell == null)
        throw new Exception ("Expected to display a `SearchResultViewCell`.");

    // Display the current picture info in the cell
    var item = FoundPictures [indexPath.Row];
    currentCell.PictureInfo = item;
}

DidUpdateFocus 方法向用户提供视觉反馈,因为它们突出显示了结果集合视图中的项:

public override void DidUpdateFocus (UIFocusUpdateContext context, UIFocusAnimationCoordinator coordinator)
{
    var previousItem = context.PreviouslyFocusedView as SearchResultViewCell;
    if (previousItem != null) {
        UIView.Animate (0.2, () => {
            previousItem.TextColor = UIColor.LightGray;
        });
    }

    var nextItem = context.NextFocusedView as SearchResultViewCell;
    if (nextItem != null) {
        UIView.Animate (0.2, () => {
            nextItem.TextColor = UIColor.Black;
        });
    }
}

最后,ItemSelected 方法在结果集合视图中处理用户选择项(单击带有 Siri Remote 的 Touch Surface):

public override void ItemSelected (UICollectionView collectionView, NSIndexPath indexPath)
{
    // If this Search Controller was presented as a modal view, close
    // it before continuing
    // DismissViewController (true, null);

    // Grab the picture being selected and report it
    var picture = FoundPictures [indexPath.Row];
    Console.WriteLine ("Selected: {0}", picture.Title);
}

如果搜索字段显示为模式对话框视图(在调用它的视图顶部),请使用 DismissViewController 方法在用户选择项时消除搜索视图。 在此示例中,搜索字段显示为“选项卡视图”选项卡的内容,因此它在这里不会消除。

有关集合视图的详细信息,请参阅我们的使用集合视图文档。

显示搜索字段

搜索字段(及其关联的屏幕键盘和搜索结果)可通过两种主要方式呈现给 tvOS 中的用户:

  • 模式对话框视图 - 搜索字段可以作为全屏模式对话框视图显示在当前视图和视图控制器上。 这通常是为了响应用户单击按钮或其他 UI 元素。 当用户从搜索结果中选择项时,该对话框将消除。
  • 视图内容 - 搜索字段是给定视图的直接部分。 例如,作为选项卡视图控制器中“搜索”选项卡的内容。

对于上面给出的可搜索图片列表的示例,搜索字段在“搜索”选项卡中显示为“视图内容”,搜索选项卡视图控制器如下所示:

using System;
using UIKit;

namespace tvText
{
    public partial class SecondViewController : UIViewController
    {
        #region Constants
        public const string SearchResultsID = "SearchResults";
        #endregion

        #region Computed Properties
        public SearchResultsViewController ResultsController { get; set;}
        #endregion

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

        #region Private Methods
        public void ShowSearchController ()
        {
            // Build an instance of the Search Results View Controller from the Storyboard
            ResultsController = Storyboard.InstantiateViewController (SearchResultsID) as SearchResultsViewController;
            if (ResultsController == null)
                throw new Exception ("Unable to instantiate a SearchResultsViewController.");

            // Create an initialize a new search controller
            var searchController = new UISearchController (ResultsController) {
                SearchResultsUpdater = ResultsController,
                HidesNavigationBarDuringPresentation = false
            };

            // Set any required search parameters
            searchController.SearchBar.Placeholder = "Enter keyword (e.g. coffee)";

            // The Search Results View Controller can be presented as a modal view
            // PresentViewController (searchController, true, null);

            // Or in the case of this sample, the Search View Controller is being
            // presented as the contents of the Search Tab directly. Use either one
            // or the other method to display the Search Controller (not both).
            var container = new UISearchContainerViewController (searchController);
            var navController = new UINavigationController (container);
            AddChildViewController (navController);
            View.Add (navController.View);
        }
        #endregion

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

            // If the Search Controller is being displayed as the content
            // of the search tab, include it here.
            ShowSearchController ();
        }

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

            // If the Search Controller is being presented as a modal view,
            // call it here to display it over the contents of the Search
            // tab.
            // ShowSearchController ();
        }
        #endregion
    }
}

首先,定义一个常量,该常量与 Interface Designer 中分配给搜索结果集合视图控制器的情节提要标识符匹配:

public const string SearchResultsID = "SearchResults";

接下来,ShowSearchController 方法创建新的搜索视图收集控制器并按需要显示它:

public void ShowSearchController ()
{
    // Build an instance of the Search Results View Controller from the Storyboard
    ResultsController = Storyboard.InstantiateViewController (SearchResultsID) as SearchResultsViewController;
    if (ResultsController == null)
        throw new Exception ("Unable to instantiate a SearchResultsViewController.");

    // Create an initialize a new search controller
    var searchController = new UISearchController (ResultsController) {
        SearchResultsUpdater = ResultsController,
        HidesNavigationBarDuringPresentation = false
    };

    // Set any required search parameters
    searchController.SearchBar.Placeholder = "Enter keyword (e.g. coffee)";

    // The Search Results View Controller can be presented as a modal view
    // PresentViewController (searchController, true, null);

    // Or in the case of this sample, the Search View Controller is being
    // presented as the contents of the Search Tab directly. Use either one
    // or the other method to display the Search Controller (not both).
    var container = new UISearchContainerViewController (searchController);
    var navController = new UINavigationController (container);
    AddChildViewController (navController);
    View.Add (navController.View);
}

在上述方法中,在 SearchResultsViewController 从情节提要实例化后,将创建一个新的 UISearchController,向用户显示搜索字段和屏幕键盘。 搜索结果集合(由 SearchResultsViewController 定义)将显示在此键盘下。

接下来,SearchBar 使用占位符提示等信息配置。 这为用户提供了关于正在执行的搜索类型的信息。

然后,搜索字段以以下两种方式之一向用户显示:

  • 模式对话框视图 - 调用 PresentViewController 方法以在现有视图上全屏显示搜索。
  • 查看内容 - 创建 UISearchContainerViewController 以包含搜索控制器。 创建 UINavigationController 以包含搜索容器,然后将导航控制器添加到视图控制器 AddChildViewController (navController),并显示视图 View.Add (navController.View)

最后,根据演示文稿类型,ViewDidLoadViewDidAppear 方法将调用 ShowSearchController 方法以向用户显示搜索:

public override void ViewDidLoad ()
{
    base.ViewDidLoad ();

    // If the Search Controller is being displayed as the content
    // of the search tab, include it here.
    ShowSearchController ();
}

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

    // If the Search Controller is being presented as a modal view,
    // call it here to display it over the contents of the Search
    // tab.
    // ShowSearchController ();
}

当应用运行并且用户选择了“搜索”选项卡时,将向用户显示完整的未筛选项列表:

默认搜索结果

当用户开始输入搜索词时,结果列表将按该词进行筛选并自动更新:

筛选的搜索结果

用户可随时将焦点切换到搜索结果中的某个项,然后单击 Siri Remote 的 Touch Surface 以选择它。

总结

本文介绍了如何设计和使用 Xamarin.tvOS 应用中的文本和搜索字段。 它演示了如何在 Interface Designer 中创建文本和搜索集合内容,并通过两种不同的方式向 tvOS 中的用户显示搜索字段。