使用Calcium为Xamarin Forms创建跨平台应用程序栏






4.94/5 (20投票s)
使用 Xamarin Forms 平台特定渲染来创建跨平台应用程序栏。
引言
Windows Phone 的应用程序栏是 Windows Phone 用户体验的标志性组成部分。我无法想象在不利用内置 ApplicationBar 的情况下构建 Windows Phone 应用程序。
在本文中,您将了解如何在 XAML 中定义自定义 AppBar;将其绑定到命令对象集合,以及直接将 AppBar 项绑定到 ViewModel 属性。您将看到如何利用平台特定渲染在 iOS、Android 和 Windows Phone 上显示 AppBar。您将了解到 Calcium AppBar 是多页面感知的,并且可以放置在由 CarouselPage
或 TabbedPage
托管的多个页面中。您将看到如何在 iOS 和 Android 中显示自定义菜单。最后,您将了解如何注册自定义平台特定视图渲染器。
Calcium 的 Windows Phone 平台 AppBar 利用 Windows Phone SDK 内置的 ApplicationBar
来提供绑定支持,并允许在通过 XAML 定义的 Pivot
和 Panorama
控件中使用多个应用程序栏。此外,Calcium 的 AppBar 还支持切换按钮和切换菜单项、导航按钮等。这超出了您在为 Windows Phone SDK 的 ApplicationBar
控件创建原生渲染器时所能期望的。
注意。本文介绍的 Xamarin Forms 的 AppBar
控件仍在开发中。就生产就绪而言,它可能还没有完全达到。
在开始之前,如果您还没有阅读过,我建议您在阅读本文之前先阅读本系列中的第一篇文章。
本系列文章
- 第一部分. 介绍适用于 Xamarin Forms 的 Calcium
- 第2部分。使用 MVVM 和 Calcium for Xamarin Forms 创建选项卡式界面
- 第三部分. 使用 Xamarin Forms 和 Calcium 构建本地化跨平台应用
- 第四部分. 使用 Xamarin Forms 和 Calcium 创建跨平台应用程序栏 (本文)
- 第五部分. 共享 Xamarin Forms 中图像资源的更好方法
- 第6部分。使用 Calcium for Windows Phone 创建用户选项页面
Calcium 和示例应用程序的源代码
Calcium for Xamarin.Forms 和示例的源代码位于 https://calcium.codeplex.com/SourceControl/latest
存储库中有各种解决方案。您感兴趣的是位于 \Trunk\Source\Calcium\Xamarin\Installation 目录下的 CalciumTemplates.Xamarin.sln。
CalciumTemplates.Xamarin 解决方案的结构如图 1 所示。您可以看到已突出显示的示例“模板”项目。这些项目包含本系列文章中呈现的大部分示例源代码。
图 1. CalciumTemplates.Xamarin 解决方案的结构
使用 AppBar
AppBar
包含一个按钮和菜单项的集合,并且根据应用程序运行的平台,其渲染方式不同。如果您的应用程序在 Windows Phone 上运行,AppBar
将使用内置的 ApplicationBar
控件进行渲染。在 iOS 和 Android 上运行时,AppBar
将使用 Xamarin Forms 工具栏(用于按钮)和对话框(用于菜单项)进行渲染。
AppBar
可以绑定到按钮和菜单项命令的列表,或者纯粹在 XAML 中定义,如列表 1 所示。
列表 1. 在 XAML 中定义 AppBar
<calcium:AppBar>
<calcium:AppBar.Buttons>
<calcium:AppBarItem Text="Foo" Tap="FooButtonHandleTap"
IconUri="/Views/MainView/Images/AppBar/Check.png" />
<calcium:AppBarItem Text="Bah" />
</calcium:AppBar.Buttons>
<calcium:AppBar.MenuItems>
<calcium:AppBarItem Text="MenuItem1" />
</calcium:AppBar.MenuItems>
</calcium:AppBar>
当我开始为本文编写代码时,我决定超越我在 Windows Phone 版 Calcium 中已经创建的内容。我以一种允许开发人员更好地将 UI 与其 ViewModel 分离的方式构建了 AppBar
。AppBar
可以绑定到按钮命令的集合和菜单项命令的集合。我选择允许绑定到命令集合,而不是专有模型类,因为命令 API 是众所周知的,并且 Calcium 的 UICommand
对象拥有表示按钮或菜单项所需的一切。
在 WPF 中,RoutedUICommands
可用于封装命令的文本显示属性。其优点是,例如按钮的文本可以在命令逻辑中更新。当 Silverlight 到来时,将 UI 特定属性放在 ICommand
实现中的想法被认为是不受欢迎的。事实上,RoutedUICommand
未包含在 FCL 中。它完全留给 UI 根据其自身状态或 ViewModel 来显示文本。我认为,这导致了业务逻辑倾向于泄露到视图层。命令的概念可能与文本和图标的概念是正交的,但支持文本或图标的按钮控件的命令是如此普遍,以至于您不能否认它是一个适合合并到单个类型中的好候选。我想,这就是感知到的最佳实践和实用主义发生冲突的地方。
在评估这类设计决策时,在我看来,如果成本可以忽略不计,但有回报,那么您应该倾向于实用主义,并做出有利于简化开发的 API 决策,而不是可能导致内部复杂性的感知价值。
让我们继续概述 AppBar
的绑定功能。可下载示例代码中的 MainViewModel
包含两个 ObservableCollection
,其中包含 IUICommand
类型的对象。IUICommands
扩展了熟悉的 ICommand
接口,以包含一个用于文本、可见性和图标 URL 的属性。实现 IUICommand
的对象的属性可以映射到 AppBar
中的按钮或菜单项。在以下摘录中,IUICommand
允许用户点按以导航到 HubView
页面
navigateToHubCommand = new UICommand(NavigateToHub)
{
Text = AppResources.MainView_AppBar_Buttons_Hub
};
appBarButtonCommands.Add(navigateToHubCommand);
NavigateToHub
方法解析 HubView
页面(由 IoC 容器构建),并且 INavigation
实例导航到 HubView
,如图所示
async void NavigateToHub(object arg)
{
var hubView = Dependency.Resolve<HubView>();
Navigation.PushAsync(hubView);
}
通过创建另一个 UICommand
对象,将菜单项添加到 AppBar
中。这次,Calcium 的对话框服务向用户显示一个消息提示。DialogService
负责在 Windows Phone、iOS 和 Android 上显示对话框。
var menuCommand = new UICommand(_ => DialogService.ShowMessageAsync("Menu Item 1 tapped."))
{
Text = "Menu Item 1"
};
appBarMenuCommands.Add(menuCommand);
在 MainView 页面上包含 XML 命名空间声明,如下所示
xmlns:calcium="clr-namespace:Outcoder.UI.Xaml;assembly=Outcoder.Calcium"
最后,在 MainView 页面上定义了一个 AppBar
,它被数据绑定到两个 ObservableCollection
,如所示
<calcium:AppBar
ButtonCommands="{Binding AppBarButtonCommands}"
MenuCommands="{Binding AppBarMenuCommands}" />
这在所有三个视图中都实现了,如图 2 所示。请注意三个平台上渲染的 AppBar
按钮的位置。
图 2. 在 Windows Phone、iOS 和 Android 上显示的 AppBar
。
构建 Xamarin Forms 自定义 AppBar
您可以创建一个自定义的 Xamarin Forms 控件或视图,因为它们被称为,方法是继承 Xamarin.Forms.View
类。
适用于 Xamarin Forms 的 Calcium AppBar
由按钮的 ObservableCollection
和菜单项的 ObservableCollection
组成。按钮和菜单项都由实现 IAppBarItem
接口的 AppBarItem
类表示。IAppBarItem
具有以下成员
- string Text
- Uri IconUri
- bool IsEnabled
- void PerformTap()
IAppBarItem
接口的基本实现是 AppBarItemBase
抽象类。参见图 3。AppBarItem
类增加了命令支持。
图 3. AppBar 包含 IAppBarItem
对象的集合。
AppBarItemBase
类包含两个可绑定属性,它们在类的静态构造函数中进行初始化,如下所示
static AppBarItemBase()
{
TextProperty = BindableProperty.Create(
"Text", typeof(string), typeof(AppBarItemBase), string.Empty, BindingMode.TwoWay);
IconUriProperty = BindableProperty.Create(
"IconUri", typeof(Uri), typeof(AppBarItemBase), null, BindingMode.TwoWay);
}
请注意,IconUri
的类型是 Uri
而不是 string。在 Xamarin XAML VisualElements 中,使用字符串而不是专用类型作为属性是普遍存在的。Xamarin Forms 尚不支持隐式类型转换。为了允许在 XAML 中将 IconUri
属性设置为字符串值,有必要用 TypeConverter
属性装饰该属性,如以下摘录所示
[TypeConverter(typeof(UriTypeConverter))]
public Uri IconUri
{
get
{
return (Uri)GetValue(IconUriProperty);
}
set
{
SetValue(IconUriProperty, value);
}
}
可以使用 AppBarItemBase
类的 PerformTap
方法以编程方式启动 Tap
事件。这允许您从原生实现传递点击事件,如以下各节所示。
AppBarItem
类扩展了 AppBarItemBase
类以添加命令支持。AppBarItem
类中包含另外两个可绑定属性:Command
属性和 CommandParameter
属性。
使用 AppBarItem
对象时,命令是可选的。如果您愿意,可以仅依赖 Tap
事件来检测用户操作。
当设置了 AppBarItem
的 Command
属性时,我的意图是自动将 AppBarItem
绑定到命令的各种属性。不幸的是,我在这里遇到了一个障碍。 在撰写本文时,Xamarin Forms Binding
对象没有 RelativeSource
属性,这阻止了绑定到命令的 Text
和 IconUri
属性。参见列表 2。 方法体在存储库中被注释掉了。
列表 2. AppBarItem.HandleCommandChanged 方法的第一次尝试
static void BindToCommand(AppBarItem item, ICommand command)
{
if (command != null)
{
var newUICommand = command as IUICommand;
if (newUICommand != null)
{
/* Without a relative source binding capability we can't bind to the command. */
{
if (string.IsNullOrEmpty(item.Text))
{
item.SetBinding(TextProperty, new Binding("Command.Text", BindingMode.OneWay));
}
if (!string.IsNullOrEmpty(newUICommand.IconUrl))
{
item.SetBinding(IconUriProperty, "Command.IconUrl", BindingMode.OneWay,
new StringToUriConverter());
}
item.SetBinding(IsEnabledProperty, new Binding("Command.Enabled", BindingMode.OneWay));
}
}
}
}
因此,绑定到命令的 Text
属性,例如,是在绑定表达式中完成的;如列表 3 所示。
列表 3. AboutView.xaml AppBar 摘录
<calcium:AppBar Grid.Row="1">
<calcium:AppBar.Buttons>
<calcium:AppBarItem Command="{Binding ExampleCommand}"
Text="{Binding ExampleCommand.Text}" />
</calcium:AppBar.Buttons>
</calcium:AppBar>
当用户点击 AppBarItem
的原生表示时,事件会通过 AppBarItem
可覆盖的 PerformTap
方法“流经”。如果为 AppBarItem
分配了命令,则会调用命令的 Execute
方法,如以下摘录所示
protected override void OnTap(EventArgs e)
{
base.OnTap(e);
var command = Command;
if (command != null)
{
command.Execute(CommandParameter);
}
}
注意。命令的存在不会覆盖 Tap
事件。在执行命令之前,任何订阅者仍会收到点击事件的通知。
将命令适配到 AppBar 项目
回顾一下,您可以将 AppBar
的 Buttons
和 MenuItems
属性绑定到 IUICommand
集合。显然,有一个中间步骤来解析要用于表示特定 IUICommand
的 IAppBarItem
类型。这时就轮到 IAppBarItemFactory
了。IAppBarItemFactory
对象的工作是将命令映射到应用程序栏的对应项。IAppBarItemFactory
有一个方法,它接受一个 IUICommand
并返回一个 IAppBarItem
。默认实现目前作用不大,只是创建一个 AppBarItem
并为其分配指定的命令,如下所示
public virtual IAppBarItem BuildItem(IUICommand command)
{
ArgumentValidator.AssertNotNull(command, "command");
var result = new AppBarItem {Command = command};
return result;
}
IAppBarItemFactory
是一个可扩展点,因为您可以替换 IAppBarItemFactory
实现来执行您想要的任何自定义映射。AppBar
类的 AdaptItems
方法从 IoC 容器中检索 IAppBarItemFactory
,如果未注册任何项,则回退到默认实现 AppBarItemFactory
。参见列表 4。
列表 4. AppBar.AdaptItems 方法
List<IAppBarItem> AdaptItems(IEnumerable<IUICommand> commands)
{
IAppBarItemFactory factory = Dependency.Resolve<IAppBarItemFactory, AppBarItemFactory>();
List<IAppBarItem> result = new List<IAppBarItem>();
foreach (var command in commands)
{
IAppBarItem item = factory.BuildItem(command);
result.Add(item);
}
return result;
}
在本节中,您将学习如何创建一个自定义视觉元素。AppBar
的渲染方式必须取决于您的应用程序运行的平台。在下一节中,您将看到如何创建平台特定渲染器。
采用平台特定渲染,又称原生渲染
让我们深入了解 AppBar
如何在多个平台上实现,而无需您进行自定义管道代码。执行平台特定渲染的任务是使用 Xamarin Forms 渲染 API。对于 Calcium 的 AppBar
,它使用了三个不同的渲染器:一个用于 iOS,一个用于 Android,一个用于 Windows Phone。在本节中,您将看到每个渲染器的实现方式,并且您将学习如何向 Xamarin Forms 渲染系统注册原生渲染器。
在 Xamarin Forms 中,使用自定义 ViewRenderer<TElement, TNativeElement>
实现来使用平台特定控件,其中 TElement
是 Xamarin Forms 自定义视图的类型,TNativeElement
是平台特定元素(如 Windows Phone 自定义控件)的类型。
注意:尽管 ViewRenderer
的泛型参数命名暗示 VisualElement
就足够了,但 ViewRenderer
的类型约束要求使用派生自 Xamarin.Forms.View
的类。
使用 View Renderer 显示 Windows Phone 应用程序栏
内置的 Windows Phone 应用程序栏是 Windows Phone 的关键组件之一。大多数基于 XAML 的应用程序都使用它,并为用户体验的关键方面提供了统一性。然而,如果您想要任何非标准行为,它确实会带来一些挑战。例如,Windows Phone 8 ApplicationBar
不支持数据绑定。与 Windows Phone Silverlight 应用程序中提供的其他丰富的开箱即用控件不同,它使用起来很笨拙,充其量只是底层原生控件的一个包装器。一个特别的限制是它不支持为 Pivot
或 Panorama
项创建多个应用程序栏。
很久以前,我决定为内置应用程序栏创建一个包装器,我在许多商业应用程序中都使用了它。它已成为我工具箱中有用的补充。我利用了这个自定义应用程序栏的功能来为 Xamarin Page
提供相同的多栏支持,这些页面放置在 MultiPage
容器(如 TabbedPage
)中。
自定义 Windows Phone AppBar
类由 Windows Phone 特定的 AppBarRenderer
类渲染。 您可以在 Calcium 源存储库的 Outcoder.Calcium.XamarinForms.WindowsPhone 项目中找到适用于 Windows Phone 的 AppBarRenderer
类。该类存在于 Outcoder.UI.Xaml.Renderers 命名空间中。
public class AppBarRenderer : ViewRenderer<Outcoder.UI.Xaml.AppBar, Outcoder.UI.Xaml.Controls.AppBar>
{ ... }
创建自定义渲染器时,需要做两件事。第一件事是您应该通过重写 ViewRenderer<TView,TNative>
类的 OnElementChanged
方法来响应 ElementChanged
事件。当调用 OnElementChanged
方法时,它提供了初始化控件的机会。第二件事是,一旦初始化了原生控件,它就会通过渲染器的 SetNativeControl
方法提供给渲染器。
在列表 5 中,您可以看到该方法负责分离旧的 AppBar
(取消订阅其事件)并附加新的 AppBar
实例。此外,当前 Xamarin Page 实例是使用自定义 GetHostPage
方法找到的。渲染器订阅页面的 Disappearing
和 Appearing
事件,以知道何时隐藏和重新构建 AppBar
。SetNativeControl
方法将 Xamarin Forms 基础结构要显示的元素提供给平台特定的界面。
列表 5. 重写 Windows Phone AppBar 的 OnElementChanged
方法
protected override void OnElementChanged(ElementChangedEventArgs<AppBar> e)
{
base.OnElementChanged(e);
var oldElement = e.OldElement;
if (oldElement != null)
{
DetachAppBar(oldElement);
}
if (!initialized)
{
initialized = true;
Page page = GetHostPage();
page.Disappearing += HandlePageDisappearing;
page.Appearing += HandlePageAppearing;
var wpAppBar = new Outcoder.UI.Xaml.Controls.AppBar();
SetNativeControl(wpAppBar);
}
AppBar newAppBar = e.NewElement;
AttachAppBar(newAppBar);
}
AppBarRenderer
维护对当前 Page 的弱引用,以检测何时另一页面接管了应用程序栏。以下摘录显示了 HandlePageAppearing
方法,该方法创建了一个新的 WeakReference
对象
void HandlePageAppearing(object sender, EventArgs e)
{
Page ownerPage = GetHostPage();
appBarOwner = new WeakReference<Page>(ownerPage);
UpdateAppBar();
}
相反,当用户导航离开页面时,将调用 HandlePageDisappearing
方法。如果新页面与页面出现时注册的页面不对应,则表示不同的页面现在拥有应用程序栏。跟踪活动页面的原因是新页面的 Appearing
事件在当前页面的 Disappearing
事件之前触发。
void HandlePageDisappearing(object sender, EventArgs e)
{
Page ownerPage;
if (appBarOwner != null && appBarOwner.TryGetTarget(out ownerPage))
{
var hostContentPage = GetHostPage();
if (ownerPage != hostContentPage)
{
return;
}
}
HideAppBar();
}
通过遍历视觉树,调用自定义方法 GetParentOfType<Page>()
来检索宿主页面。参见列表 6。我打算将此视觉树逻辑合并到 Calcium 基础库中,类似于您在 Calcium 的 WPF 和 Windows Phone 特定核心中看到的视觉树帮助逻辑。
列表 6. AppBarRenderer.GetParentOfType
T GetParentOfType<T>() where T : class
{
T page = null;
Element parent = Element;
while (page == null)
{
parent = parent.Parent;
if (parent == null)
{
break;
}
page = parent as T;
}
return page;
}
当向渲染器呈现新的 AppBar
时,AttachAppBar
方法会订阅 AppBar
的 ButtonCollectionChanged
和 MenuItemCollectionChanged
事件。参见列表 7。
列表 7. Windows Phone AppBarRenderer.AttachAppBar 方法
void AttachAppBar(AppBar appBar)
{
if (appBar == null)
{
return;
}
DetachAppBar(appBar);
appBar.ButtonCollectionChanged += HandleButtonsChanged;
appBar.MenuItemCollectionChanged += HandleMenuItemsChanged;
}
当添加或删除按钮或菜单项时,AppBar
会通过调用渲染器的 UpdateAppBar
方法来重建。参见列表 8。使用基类 ViewRenderer
的 Control
属性来检索您之前提供的原生控件。
如果适用于 Xamarin Forms 的 AppBar
包含菜单项但没有按钮,则 Windows Phone 应用程序栏将设置为最小化状态;缩小其尺寸,同时仍允许展开菜单。
为适用于 Xamarin Forms 的 AppBar
中的每个项目创建一个新的原生 AppBar
项目。AppBarRenderer
为每个原生项目的 Click 事件分配一个处理程序,该处理程序将事件传递给 IAppBarItem
的 PerformTap
方法。
您可能会注意到,要响应 AppBarItem
对象中的更改,还需要做更多工作。我计划让 Xamarin Forms IAppBarItem
对象中的属性更改流向原生 AppBar
项目。
列表 8. Windows Phone AppBarRenderer.UpdateAppBar 方法
void UpdateAppBar()
{
var wpAppBar = Control;
if (wpAppBar == null)
{
return;
}
wpAppBar.Buttons.Clear();
wpAppBar.MenuItems.Clear();
var xamAppBar = Element;
var xamButtons = xamAppBar.Buttons.ToList();
var xamMenuItems = xamAppBar.MenuItems.ToList();
if (!xamButtons.Any())
{
if (!xamMenuItems.Any())
{
wpAppBar.IsVisible = false;
return;
}
wpAppBar.Mode = Microsoft.Phone.Shell.ApplicationBarMode.Minimized;
}
wpAppBar.IsVisible = true;
foreach (var item in xamAppBar.Buttons)
{
var button = new Outcoder.UI.Xaml.Controls.AppBarIconButton();
button.Text = item.Text;
button.IconUri = item.IconUri;
IAppBarItem i = item;
button.Click += (sender, args) => i.PerformTap();
wpAppBar.Buttons.Add(button);
}
foreach (IAppBarItem item in xamAppBar.MenuItems)
{
var menuItem = new Outcoder.UI.Xaml.Controls.AppBarMenuItem();
menuItem.Text = item.Text;
/* Menu item icons are not supported on Windows Phone. */
//menuItem.IconUri = item.IconUri;
IAppBarItem i = item;
menuItem.Click += (sender, args) => i.PerformTap();
wpAppBar.MenuItems.Add(menuItem);
}
}
使用 View Renderer 在 iOS 上显示工具栏
顺便说一句,当我开始构建跨平台 AppBar
时,Xamarin API 非常不同。Xamarin 的开发人员进行了一些重构,以统一三个平台上的渲染系统;这倒不是坏事。
我最初的意图是利用平台特定 API 在 iOS 和 Android 上填充工具栏。然而,事实证明,我的代码和 Xamarin 的代码开始相互干扰。您可以看到,在 Xamarin Forms 中创建工具栏的方法是填充 Xamarin Forms Page 的 ToolbarItems
属性。然后,Page
渲染器在构建页面时会构造工具栏。这干扰了我的 AppBar
。我改变了策略,不再手动构建工具栏,而是决定利用 Xamarin Forms ToolbarItems
属性;根据 AppBar
的内容来填充它。
您会注意到,在列表 9 中,iOS AppBarRenderer
将其原生控件设置为一个标签。这是因为没有我需要操作的原生后备控件,但我仍然需要向 Xamarin 渲染子系统提供一些东西。我希望 Xamarin 团队能给我一个提供伪控件的替代方案。
每个平台上的图像解析方式存在差异,这使得提供统一的方法来放置和定位图像变得困难。我相信我已经找到了一些巧妙的解决方案。您将在本系列后面的文章中看到一个专门介绍此内容的章节。IImageUrlTransformer
对象与此方面相关,因此请暂时忽略它,稍后我们将返回讨论它。
填充工具栏的触发器几乎与 Windows Phone 实现相同;AppRenderer
依赖于活动 Page
的 Appearing
和 Disappearing
事件来确定何时应填充当前页面的 Xamarin Forms ToolbarItems
属性。
列表 9. iOS AppBarRenderer.OnElementChanged 方法
protected override void OnElementChanged(ElementChangedEventArgs<AppBar> e)
{
base.OnElementChanged(e);
var newAppBar = e.NewElement;
if (!initialized)
{
initialized = true;
imageUrlTransformer = Dependency.Resolve<IImageUrlTransformer, ImageUrlTransformer>(true);
/* An Exception is raised if a control is not provided. */
SetNativeControl(new UILabel(RectangleF.Empty));
Page page = GetHostContentPage();
page.Disappearing += (sender, args) =>
{
DetachAppBar(newAppBar);
Page ownerPage;
if (toolbarOwner != null && toolbarOwner.TryGetTarget(out ownerPage))
{
var hostContentPage = GetHostContentPage();
if (ownerPage != hostContentPage)
{
return;
}
}
RemoveToolbar();
};
page.Appearing += (sender, args) =>
{
Page ownerPage = GetHostContentPage();
toolbarOwner = new WeakReference<Page>(ownerPage);
UpdateAppBar();
AttachAppBar(newAppBar);
};
}
}
现在您将看到 AppBar
如何使用 Xamarin Forms ToolbarItems
集合表示。为 AppBar
中的每个按钮创建一个 ToolbarItem
。参见列表 10。当 ToolBarItem
的 Activated
事件触发时,事件处理程序会调用 AppBarItem
的 PerformTap
方法。
工具栏不存在菜单项集合。因此,AppBarRenderer
会构造一个特殊的菜单按钮,这类似于 Windows Phone 应用程序栏上的省略号按钮。当激活菜单按钮时,将调用自定义的 DisplayMenu
方法。接下来,我们将讨论该方法。
列表 10. iOS AppRenderer.UpdateAppBar 方法
void UpdateAppBar()
{
var appBar = Element;
Page page = GetParentOfType<ContentPage>();
if (page == null)
{
return;
}
List<ToolbarItem> items = null;
foreach (IAppBarItem appBarItem in appBar.Buttons)
{
string text;
if (!AppBarItemPropertyResolver.TryGetItemText(appBarItem, out text))
{
throw new Exception("Unable to resolve text for button.");
}
ToolbarItem item = new ToolbarItem { Name = text };
IAppBarItem itemForClosure = appBarItem;
item.Activated += (sender, e) => itemForClosure.PerformTap();
string iconUrl;
if (AppBarItemPropertyResolver.TryGetItemUrl(appBarItem, out iconUrl))
{
string transformedUrl = imageUrlTransformer.TransformForCurrentPlatform(iconUrl);
item.Icon = transformedUrl;
}
if (items == null)
{
items = new List<ToolbarItem>();
}
items.Add(item);
}
var menuItemsEnumerable = appBar.MenuItems;
if (menuItemsEnumerable != null)
{
var menuItems = menuItemsEnumerable.ToList();
int menuItemCount = menuItems.Count;
if (menuItemCount > 0)
{
string[] buttonTitles = new string[menuItemCount];
for (int i = 0; i < menuItemCount; i++)
{
string text;
if (!AppBarItemPropertyResolver.TryGetItemText(menuItems[i], out text))
{
throw new Exception("Unable to resolve text for menu item.");
}
buttonTitles[i] = text;
}
ToolbarItem item = new ToolbarItem { Name = menuToolbarItemText };
item.Activated += async (sender, e) =>
{
var viewController = UIApplication.SharedApplication.Windows[0].RootViewController;
var uiView = viewController.View;
var action = await DisplayMenu(uiView, "Cancel", buttonTitles);
if (action > -1)
{
var selectedItem = menuItems[action];
selectedItem.PerformTap();
}
};
if (items == null)
{
items = new List<ToolbarItem>();
}
items.Add(item);
}
}
if (items != null && items.Any())
{
var toolbarItems = page.ToolbarItems;
toolbarItems.Clear();
items.Reverse();
toolbarItems.AddRange(items);
}
}
AppBarItemPropertyResolver
类用于检索 AppBarItem
的文本和图标 URL。它首先尝试直接从 AppBarItem
检索属性。如果为 null,则从 AppBarItem
的命令中检索属性。参见列表 11。
列表 11. AppBarItemPropertyResolver.TryGetItemText 方法。
internal static bool TryGetItemText(IAppBarItem appBarItem, out string text)
{
bool resolvedText = false;
text = appBarItem.Text;
if (!string.IsNullOrEmpty(text))
{
resolvedText = true;
}
else
{
var commandItem = appBarItem as AppBarItem;
if (commandItem != null && commandItem.Command != null)
{
var uiCommand = commandItem.Command as IUICommand;
if (uiCommand != null)
{
text = uiCommand.Text;
resolvedText = true;
}
}
}
return resolvedText;
}
在 Xamarin.iOS 中显示自定义菜单
iOS AppBarRenderer
依赖 UIActionSheet
类来显示选项列表。使用 UIActionSheet
对象的 AddButton
方法将其按钮添加到其中。参见列表 12。
DisplayMenu
方法是可等待的,UI 线程在显示 UIActionSheet
时不会被阻塞。TaskCompletionSource<int>
用于等待 UIActionSheet
关闭,结果 int 值是点击的按钮的索引,如果取消则为 -1。
列表 12. iOS AppBarRenderer.DisplayMenu 方法
Task<int> DisplayMenu(UIView uiView, string cancelText, params string[] buttons)
{
var sheet = new UIActionSheet();
foreach (var button in buttons)
{
sheet.AddButton(button);
}
int cancelButtonIndex = sheet.ButtonCount;
sheet.AddButton(cancelText);
sheet.CancelButtonIndex = cancelButtonIndex;
TaskCompletionSource<int> source = new TaskCompletionSource<int>();
try
{
sheet.Clicked += delegate(object sender, UIButtonEventArgs args)
{
int result;
if (args != null)
{
int buttonIndex = args.ButtonIndex;
if (buttonIndex != cancelButtonIndex)
{
result = buttonIndex;
}
else
{
result = -1;
}
}
else
{
result = -1;
}
source.SetResult(result);
};
sheet.ShowInView(uiView);
}
catch (Exception ex)
{
source.SetException(ex);
}
return source.Task;
}
当用户点击我们自定义 AppBar
中的省略号按钮时,将显示 UIActionSheet
。参见图 4。每个 AppBarItem
的 Tap
事件都会被触发,如果 AppBarItem
关联了命令,则执行该命令。
图 4. 使用 UIActionSheet
显示菜单。
使用 View Renderer 显示 Android 工具栏
在 AppBar 视图渲染器概述的最后一部分,我们将介绍 Android 的 AppBar
ViewRenderer
实现。
Android 的 AppBarRenderer
位于 Calcium 源代码存储库的 Outcoder.Calcium.Android 项目中。它与 iOS AppBarRenderer
非常相似,因为它也利用了活动页面的 Xamarin Forms ToolbarItems
属性。因此,原生活动很少。关键区别在于菜单显示给用户的方式。参见列表 13。
Calcium ActionDialog
用于向用户显示选项列表。DisplayMenu
方法是可等待的,并返回一个 Task<int>
。方法的返回值是点击的按钮的索引。
列表 13. Android AppBarRenderer.DisplayMenu 方法
Task<int> DisplayMenu(params string[] buttons)
{
ActionDialogArguments arguments = new ActionDialogArguments(null, null, null, buttons);
ActionDialog dialog = new ActionDialog(arguments, Context);
dialog.Show();
return arguments.Result.Task;
}
ActionDialog
类似于 Xamarin.Android ActionSheet
类。但是,有几个显著的区别。Calcium 的 ActionDialog
使用基于索引的系统而不是基于字符串的系统来标识哪个按钮被点击。它还允许隐藏对话框的标题。我认为这对于 AppBar
场景很有意义。它通过调用基类 Dialog
类的 RequestWindowFeature
来实现,如 ActionDialog
的 OnCreate
方法的以下摘录所示
protected override void OnCreate(Bundle savedInstanceState)
{
string actionSheetTitle = arguments.ActionSheetTitle;
bool showTitle = !string.IsNullOrWhiteSpace(actionSheetTitle);
if (!showTitle)
{
RequestWindowFeature((int)WindowFeatures.NoTitle);
}
base.OnCreate(savedInstanceState);
…
}
注意。如果您创建自己的自定义 Android 对话框,则需要在调用 base.OnCreate
方法之前调用 RequestWindowFeature
。
我将不再在此详细介绍 ActionDialog
的其余代码。如果您对此感兴趣,可以在 Calcium 源代码存储库的 Outcoder.Calcium.Android 项目中找到 ActionDialog
类。
点击 Android 上的省略号按钮会显示菜单项。参见图 5。
图 5. Android 中的 AppBar 菜单
注册 View Renderer
要注册 Xamarin Forms 原生渲染器,您可以使用 ExportRenderer
属性,如下所示
[assembly: ExportRenderer(typeof(Outcoder.UI.Xaml.AppBar),
typeof(Outcoder.UI.Xaml.Renderers.AppBarRenderer))]
在撰写本文时,导出渲染器存在平台差异。Xamarin.Android 和 Xamarin.iOS 项目都允许您从类库中导出渲染器。Windows Phone 的情况并非如此。要在 Windows Phone 项目中使用 AppBar
,您必须在代码中显式添加 ExportRenderer
属性。在示例中,我选择在 Windows Phone 项目的 MainPage.xaml.cs 文件中进行此操作。
结论
在本文中,您了解了如何在 XAML 中定义自定义 AppBar
;将其绑定到命令对象集合,以及直接将 AppBar
项绑定到 ViewModel 属性。您看到了如何利用平台特定渲染在 iOS、Android 和 Windows Phone 上显示 AppBar
。您了解到 Calcium AppBar
是多页面感知的,并且可以放置在 CarouselPage 或 TabbedPage 托管的多个页面中。您看到了如何在 iOS 和 Android 中显示自定义菜单。最后,您了解了如何注册自定义视图渲染器。
在下一篇文章中,您将学习如何在共享项目中将图像放置为任何地方的内容资源,就像在 Windows Phone 中一样,并在所有三个平台上以相同的方式使用它们。
我希望您觉得这个项目有用。如果有用,我将不胜感激您能对其进行评分和/或在下方留下反馈。这将帮助我写出更好的下一篇文章。
历史
2014 年 10 月
- 首次发布。