跨平台

借助 Xamarin.Forms 跨移动平台共享 UI 代码

Jason Smith

借助 Xamarin,您可以使用 C# 构建美观的本机移动应用和在平台之间共享您的大部分代码。传统上,您必须对每个目标平台设计单独的 UI。但是,借助 Xamarin.Forms,您可以构建一个在所有平台中本机呈现的 UI。

Xamarin.Forms 是跨平台 UI 抽象层。可以使用它在平台间共享 UI 和后端代码,而同时仍提供完全本机 UI 体验。因为它们是本机的,所以您的控件和小组件将具有每个目标平台的外观。

Xamarin.Forms 与 Model-View-ViewModel (MVVM) 设计模式完全兼容,因此可以在视图模型类中将页面元素绑定到属性和命令。

如果您愿意以声明方式设计您的页面,则可以使用标记语言 XAML,XAML 具有资源字典、动态资源、数据绑定、命令、触发器和行为等功能。

Xamarin.Forms 有一个小巧、易于使用的 API。如果您需要更深入地访问平台的本机 UI,可以创建自定义视图和特定于平台的呈现器。这尽管听起来很复杂,但它实际上只是一种可访问本机 UI 的方法,并且 Xamarin 网站有充足示例来帮助您完成该任务。

您可以使用 Xamarin.Forms 附带的任何现成页面、布局和视图开始操作。在您的应用逐渐完善并且您发现新用例和设计机会时,您可能马上就需依赖 Xamarin.Forms 支持 XAML、MVVM、特定于自定义平台的呈现器和各种其他功能(如动画和数据模板)。

我将使用特定示例概述 Xamarin 功能,演示如何在不同的目标平台之间共享大部分 UI 代码,以及如何在必要时合并特定于平台的代码。

开始使用

首先,打开 Visual Studio 或 Xamarin Studio 中的 NuGet 包管理器,并检查 Xamarin.Forms 是否为新版本。因为仅仅在任一 IDE 中打开 Xamarin.Forms 解决方案,不会告知您新版本,所以检查更新的 NuGet 包是否为确保获得最新增强功能的唯一方法。

创建 Xamarin.Forms 解决方案确保您已获得最新版本 Xamarin.Forms 后,创建一个空白应用 (Xamarin.Forms Portable) 解决方案。

您的解决方案具有三个特定于平台的项目和一个可移植类库 (PCL)。在 PCL 中创建您的页面。首先创建一个基本的登录页面。

使用 C# 创建页面向 PCL 项目中添加一个类。然后添加控件(在 Xamarin 中称为“视图”),如图 1 中所示。

图 1 添加视图(控件)

public class LogInPage : ContentPage
{
  public LogInPage()
  {
    Entry userEntry = new Entry { Placeholder = "Username" };
    Entry passEntry =
      new Entry { Placeholder = "Password", IsPassword = true };
    Button submit = new Button { };
    Content = new StackLayout
    {
      Padding = 20,
      VerticalOptions = LayoutOptions.Center,
      Children = { userEntry, passEntry, submit }
    };
  }
}

若要在应用启动时显示页面,打开 MyApp 类并将其中一个实例分配给 MainPage 属性:

public class MyApp : Application
{
  public MyApp()
  {
    MainPage = new LogInPage();
  }
}

这是讨论应用程序类的好时机。从 v1.3.0 开始,所有 Xamarin.Forms 应用将都包含此类。它是 Xamarin.Forms 应用的入口点,除此之外,它还提供了生命周期事件以及所有可序列化数据的永久数据存储区(属性字典)。如果需要从应用任意位置访问此类的实例,您可以使用静态 Application.Current 属性。

在前面的示例中,我已删除应用程序类内部的默认代码,并用一行代码代替,从而使 LogInPage 显示您何时运行应用。它显示应用何时运行,因为此代码将页面 (LogInPage) 分配给了应用程序类的 MainPage 属性。您必须在应用程序类的构造函数中对其进行设置。

您还可以重写此类中的三个方法:

  • OnStart 方法,第一次启动应用时调用。
  • OnSleep 方法,当应用要进入后台状态时调用。
  • OnResume 方法,当从后台状态返回应用时调用。

一般情况下,页面不是非常有趣,直到您将他们与某种形式的数据或行为连接,因此我将介绍如何执行该操作。

将页面绑定到数据如果您使用 MVVM 设计模式,则可以创建一个(如图 2 中所示)实现 INotifyPropertyChanged 接口的类。

图 2 实现 INotifyPropertyChanged 接口

public class LoginViewModel : INotifyPropertyChanged
{
  private string usrnmTxt;
  private string passWrd;
  public string UsernameText
  {
    get { return usrnmTxt; }
    set
    {
      if (usrnmTxt == value)
        return;
      usrnmTxt = value;
      OnPropertyChanged("UsernameText");
    }
  }
  public string PassWordText
  {
    get { return passWrd; }
    set
    {
      if (passWrd == value)
        return;
      passWrd = value;
      OnPropertyChanged("PassWrd");
    }
  }
  public event PropertyChangedEventHandler PropertyChanged;
  private void OnPropertyChanged(string propertyName)
  {
    if (PropertyChanged != null)
    {
      PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
  }
}

您可以将登录页面的视图绑定到该类的属性(如图 3 中所示)。

图 3 将视图绑定到类属性

public LogInPage()
{
  Entry userEntry = new Entry { Placeholder = "Username" };
  userEntry.SetBinding(Entry.TextProperty, "UsernameText");
  Entry passEntry =
    new Entry { Placeholder = "Password", IsPassword = true };
  passEntry.SetBinding(Entry.TextProperty, "PasswordText");
  Button submit = new Button { Text = "Submit" };
  Content = new StackLayout
    {
      Padding = 20,
      VerticalOptions = LayoutOptions.Center,
      Children = { userEntry, passEntry, submit }
    };
  BindingContext = new LoginViewModel();
}

若要阅读有关如何绑定到 Xamarin.Forms 应用中数据的详细信息,请参阅 Xamarin 站点 bit.ly/1uMoIUX 上的“从数据绑定到 MVVM”。

使用 XAML 创建页面对于较小的应用,使用 C# 创建 UI 是完全合理的方法。但是,随着应用大小的增长,您可能会发现自己键入了大量的重复代码。您可以通过使用 XAML 避免该问题,,而不是通过 C# 代码。

将窗体 XAML 页面项添加到您的 PCL 项目。然后将标记添加到页面,如图 4 中所示。

图 4 将标记添加到窗体 XAML 页面

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
  xmlns:x="https://schemas.microsoft.com/winfx/2009/xaml"
  x:Class="Jason3.LogInPage"
  xmlns:local="clr-namespace:XamarinForms;assembly=XamarinForms"> 
    <StackLayout VerticalOptions="Center">
      <StackLayout.BindingContext>
        <local:LoginViewModel />
      </StackLayout.BindingContext>
      <Entry Text="{Binding UsernameText}" Placeholder="Username" />
      <Entry Text="{Binding PasswordText}"
        Placeholder="Password" IsPassword="true" />
      <Button Text="Login" Command="{Binding LoginCommand}" />
    </StackLayout>
</ContentPage>

如果您编写过 Windows Presentation Foundation (WPF) 应用,那么会对图 4 中的 XAML 感到很熟悉。但是,这些标记是不同的,因为它们指的是 Xamarin.Forms 类型。此外,根元素引用 Xamarin.Forms.Element 类的子类。Xamarin.Forms 应用中的所有 XAML 文件必须都这样做。

若要了解有关使用 XAML 在 Xamarin.Forms 应用中创建页面的详细信息,请参阅 Xamarin 网站 bit.ly/1xAKvRN 上的“适用于 Xamarin.Forms 的 XAML”。

设计特定平台

与其他本机移动平台相比,Xamarin.Forms 具有相对较少的 API。这样您可以更轻松地设计页面,但有时 Xamarin.Forms 不按您想要的方式在一个或多个平台目标上呈现视图。

如果遇到此障碍,只需创建自定义视图,这只是一个可用于 Xamarin.Forms 的任何视图的子类。

若要使视图显示在页面上,请扩展视图呈现器。在更高级的情况下,您甚至可以从头创建自定义呈现器。自定义呈现器代码是特定于平台的,因此您不能共享此代码。但这种方法值这个价格,因为它可以引入本机功能和应用可用性。

创建自定义视图首先,创建 Xamarin.Forms 中提供的任一视图的子类。下面是两个自定义视图的代码:

public class MyEntry : Entry {}
public class RoundedBoxView : BoxView {}

扩展现有的呈现器图 5 扩展可以呈现 iOS 平台的条目视图的呈现器。您应将此类放在 iOS 平台项目中。此呈现器设置底层本机文本字段的颜色和样式。

图 5 扩展现有的呈现器

[assembly: ExportRenderer (typeof (MyEntry), typeof (MyEntryRenderer))]
namespace CustomRenderer.iOS
{
  public class MyEntryRenderer : EntryRenderer
  {
    protected override void OnElementChanged
      (ElementChangedEventArgs<Entry> e)
    {
      base.OnElementChanged (e);
      if (e.OldElement == null) {
        var nativeTextField = (UITextField)Control;
        nativeTextField.BackgroundColor = UIColor.Gray;
        nativeTextField.BorderStyle = UITextBorderStyle.Line;
      }
    }
  }
}

您可以在呈现器上执行几乎任何操作,因为您正在引用本机 API。如果您想要查看包含此代码段的示例,请参阅 bit.ly/1xTIjmR 上的“Xamarin.Forms 自定义呈现器”。

从头开始创建呈现器可以创建不扩展任何其他呈现器的全新呈现器。创建呈现器会需要您做更多的工作,但是这有益于执行以下任一操作:

  • 替换视图的呈现器。
  • 将新的视图类型添加到您的解决方案。
  • 将本机控件或本机页面添加到您的解决方案。

例如,如果您想要将本机 UIView 控件添加到 iOS 版本应用的页面中,则应将自定义呈现器添加到您的 iOS 项目,如图 6 中所示。

图 6 将本机 UIView 控件添加到配备自定义呈现器的 iOS 应用

[assembly: ExportRendererAttribute(typeof(RoundedBoxView),
  typeof(RoundedBoxViewRenderer))]
namespace RBVRenderer.iOS
{
  public class RoundedBoxViewRenderer :
    ViewRenderer<RoundedBoxView,UIView>
  {
    protected override void OnElementChanged(
      ElementChangedEventArgs<RoundedBoxView> e)
    {
      base.OnElementChanged(e);
      var rbvOld = e.OldElement;
      if (rbvOld != null)
      {
        // Unhook any events from e.OldElement here.
      }
      var rbv = e.NewElement;
      if (rbv != null)
      {
        var shadowView = new UIView();
        // Set properties on the UIView here.
        SetNativeControl(shadowView);
        // Hook up any events from e.NewElement here.
      }
    }
  }
}

此呈现器中显示的通用模式,可确保您可以在虚拟化布局(如列表视图)中使用此模式,稍后我将对此进行讨论。

如果您想要查看包含此代码段的示例,请参阅 bit.ly/xf-customrenderer

将属性添加到自定义视图您可以将属性添加到自定义视图,但要确保这些属性可绑定,以便您可以将属性绑定到视图模型中的属性或其他类型的数据。下面是一个 RoundedBoxView 自定义视图中可绑定的属性:

public class RoundedBoxView : BoxView
{
  public static readonly BindableProperty CornerRadiusProperty =
    BindableProperty.Create<RoundedBoxView, double>(p => p.CornerRadius, 0);
  public double CornerRadius
  {
    get { return (double)base.GetValue(CornerRadiusProperty);}
    set { base.SetValue(CornerRadiusProperty, value);}
  }
}

若要将新属性连接到您的呈现器,请重写呈现器的 OnElementPropertyChanged 方法,并添加在属性更改时运行的代码:

protected override void OnElementPropertyChanged(object sender,    
  System.ComponentModel.PropertyChangedEventArgs e)
{
  base.OnElementPropertyChanged(sender, e);
  if (e.PropertyName ==
    RoundedBoxView.CornerRadiusProperty.PropertyName)
      childView.Layer.CornerRadius = (float)this.Element.CornerRadius;
}

若要了解有关创建自定义视图和自定义呈现器的详细信息,请参阅 bit.ly/11pSFhL 上的“自定义每个平台的控件”。

页面、布局和视图:Xamarin.Forms 的构建基块

我已经展示了几个元素,但还有更多。这是介绍这些元素的好时机。

Xamarin.Forms 应用包含页面、布局和视图。一个页面包含一个或多个布局,一个布局包含一个或多个视图。在 Xamarin 中使用术语视图来描述您调用控件的习惯。总的来说,Xamarin.Forms 框架包含五种页面类型、七种布局类型和 24 种视图类型。通过 xamarin.com/forms,您可以获得更多信息。稍后,我将介绍一些重要的页面类型,但首先我将花一点时间回顾一些您可以在应用中使用的布局。Xamarin.Forms 包含四种主要布局:

  • StackLayout:StackLayout 在单个垂直或水平行中定位子元素。您可以嵌套 StackLayouts 来创建复杂的可视层次结构。通过使用每个子视图的 VerticalOptions 和 Horizontal­Options 属性,可以控制视图在 StackLayout 中的排列方式。
  • 网格:网格将视图排列入行和列。此布局类似于使用 WPF 和 Silverlight 获得的布局,但您不可以在行和列之间添加间距。通过使用网格的 RowSpacing 和 ColumnSpacing 属性来做到这一点。
  • RelativeLayout:通过使用相对于其他视图的约束,RelativeLayout 可以定位视图。
  • AbsoluteLayout:AbsoluteLayout 可以通过两种方式定位子视图:绝对位置定位或相对于父级按比例定位。这有助于创建拆分并叠加层次结构。图 7 显示了一个示例。

图 7 使用 AbsoluteLayout

void AbsoluteLayoutView()
{
  var layout = new AbsoluteLayout();
  var leftHalfOfLayoutChild = new BoxView { Color = Color.Red };
  var centerAutomaticallySizedChild =
    new BoxView { Color = Color.Green };
  var absolutelyPositionedChild = new BoxView { Color = Color.Blue };
  layout.Children.Add(leftHalfOfLayoutChild,
    new Rectangle(0, 0, 0.5, 1),
    AbsoluteLayoutFlags.All);
  layout.Children.Add(centerAutomaticallySizedChild,
    new Rectangle(
    0.5, 0.5, AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize),
    AbsoluteLayoutFlags.PositionProportional);
  layout.Children.Add(
    absolutelyPositionedChild, new Rectangle(10, 20, 30, 40));
}

请注意,所有布局都为您提供了名为 Children 的属性。该属性可用于访问其他成员。例如,可以使用网格布局的 Children 属性来添加和删除行和列,以及指定行和列间距。

在滚动列表中显示数据

通过使用列表视图窗体(名为 ListView),可以在滚动列表中显示数据。此视图性能良好,因为列表中每个单元格的呈现器均被虚拟化。由于每个单元格均被虚拟化,因此通过使用与前面类似的模式,正确处理为单元格或列表创建的任何自定义呈现器的 OnElementChanged 事件是非常重要的。

首先,定义某一单元格,如图 8 中所示。ListView 的所有数据模板都必须都使用单元格作为根元素。

图 8 定义 ListView 单元格

public class MyCell : ViewCell
{
  public MyCell()
  {
    var nameLabel = new Label();
    nameLabel.SetBinding(Label.TextProperty, "Name");
    var descLabel = new Label();
    descLabel.SetBinding(Label.TextProperty, "Description");
    View = new StackLayout
    {
      VerticalOptions = LayoutOptions.Center,
      Children = { nameLabel, descLabel }
    };
  }
}

接下来,定义数据源,并对新的数据模板设置 ListView 的 ItemTemplate 属性。数据模板基于前面创建的 MyCell 类:

var items = new[] {
  new { Name = "Flower", Description = "A lovely pot of flowers." },
  new { Name = "Tuna", Description = "A can of tuna!" },
  // ... Add more items
};
var listView = new ListView
{
  ItemsSource = items,
  ItemTemplate = new DataTemplate(typeof(MyCell))
};

您可以借助以下标记在 XAML 中执行此操作:

<ListView ItemsSource="{Binding Items}">
  <ListView.ItemTemplate>
    <ViewCell>
      <StackLayout VerticalOptions="center">
      <Label Text="{Binding Name}" />
      <Label Text="{Binding Description}" />
      </StackLayout>
    </ViewCell>
  </ListView.ItemTemplate>
</ListView>

在页面之间导航

大多数应用包含多个页面,因此您需要使用户能够从一个页面导航到另一个页面。以下页面具有适用于页面导航的内置支持,并支持全屏模式页面演示文稿:

  • TabbedPage
  • MasterDetailPage
  • NavigationPage
  • CarouselPage

您可以将页面作为子级添加到这四个页面的任何一个,并免费获得导航。

Tabbed Page TabbedPage 横跨屏幕顶部显示选项卡的数组。假设 PCL 项目包含名为 LogInPage、DirectoryPage 和 AboutPage 的页面,则可以使用以下代码将这些页面全部添加到 TabbedPage:

var tabbedPage = new TabbedPage
{
  Children =
  {
    new LoginPage { Title = "Login", Icon = "login.png" },
    new DirectoryPage { Title = "Directory", Icon = "directory.png" },
    new AboutPage { Title = "About", Icon = "about.png" }
  }
};

在这种情况下,设置每个页面的标题和图标属性是非常重要的,这可以使某些内容显示在页面选项卡上。并非所有平台都呈现图标。这取决于平台的选项卡设计。

如果您在移动设备上打开此页面,第一个选项卡将显示为选中状态。但是,可以通过设置 TabbedPage 页面的 CurrentPage 属性更改此状态。

NavigationPageNavigationPage 管理大量页面的导航和 UX。此页为您提供了最常见的移动应用导航模式类型。下面是如何向它添加您的页面:

var loginPage = new LoginPage();
var navigationPage = new NavigationPage(loginPage);
loginPage.LoginSuccessful += async (o, e) => await
  navigationPage.PushAsync(new DirectoryPage());

请注意,PushAsync 方法用于导航用户到特定页面(在本例中为 DirectoryPage)。在 NavigationPage 中,可以将页面“推送”到堆栈,然后在用户向后导航到先前页面时将其“弹出”。

NavigationPage 的 PushAsync 和 PopAsync 方法是异步的,因此您的代码应等待它们,而不是运行任务时推送或弹出任何新页面。推送或弹出的动画完成后,返回每种方法的任务。

为方便起见,所有 Xamarin.Forms 视图、布局和页面都包含导航属性。此属性是一个代理接口,包含 NavigationPage 实例的 PushAsync 和 PopAsync 方法。该属性可用于导航到页面而不是直接调用 NavigationPage 实例的 PushAsync 和 PopAsync 方法。NavigationProperty 还可用于获取 PushModalAsync 和 PopModalAsync 方法。这有助于将整个屏幕的内容替换为新的模式页面。视图的父堆栈中不一定必须有一个 NavigationPage 来使用视图的导航属性,但无模式 PushAsync/PopAsync 操作可能会失败。

关于设计模式的注释通常,将 NavigationPages 作为子级添加到 TabbedPages,将 TabbedPages 作为子级添加到 MasterDetailPages。某些类型的模式可能会导致意外的 UX。例如,大多数平台建议您不要将 TabbedPage 添加为 NavigationPage 的子级。

对页面中的视图进行动画处理

有两种方法对页面上的视图设置动画效果,来创建更具吸引力的用户体验。选择 Xamarin.Forms 附带的内置动画,或通过使用动画 API 自己构建。

例如,您可以通过调用一个视图的 FadeTo 动画创建淡化效果。FadeTo 动画内置于视图中,因此简单易用:

async Task SpinAndFadeView(View view)
{
  await view.FadeTo(20, length: 200, easing: Easing.CubicInOut);
}

可以通过使用 await 关键字一起链接一系列的动画。在前一个完成后,将执行每个动画。例如,可以在将视图淡化为焦点之前对其进行旋转:

async Task SpinAndFadeView(View view)
{
  await view.RotateTo(180);
  await view.FadeTo(20, length: 200, easing: Easing.CubicInOut);
}

如果在实现所需效果时遇到问题,可以使用完整的动画 API。在下面的代码中,通过旋转视图淡化一半:

void SpinAndFadeView(View view)
{
  var animation = new Animation();
  animation.Add(0, 1, new Animation(
    d => view.Rotation = d, 0, 180, Easing.CubicInOut));
  animation.Add(0.5, 1, new Animation(
    d => view.Opacity = d, 1, 0, Easing.Linear));
  animation.Commit(view, "FadeAndRotate", length: 250);
}

此示例将每个动画组合成单个动画实例,然后使用 Commit 方法运行整个动画序列。因为此动画不能局限于特定视图,所以您可以将此动画应用于任何视图。

总结

Xamarin.Forms 是构建跨平台本机移动应用的令人激动的新方法。使用它可以构建跨 iOS、Android 和 Windows Phone 本机呈现的 UI。您可以在平台之间共享几乎所有代码。

Xamarin Inc. 构建 Xamarin 和 Xamarin.Forms,使 C# 开发人员几乎能够迅速跳转到移动开发。如果您已经为 Windows 运行时、WPF 或 Silverlight 进行开发,您会发现 Xamarin.Forms 是跨平台本机移动开发领域的便捷桥梁。您可以立即安装 Xamarin 并立即开始使用 C#,以构建在 iOS、Android 和 Windows Phone 设备上运行的美观本机应用。


Jason Smith* 是旧金山 Xamarin Inc. 的工程技术负责人,目前负责 Xamarin.Forms 项目。他是一位 Xamarin.Forms 首席架构师,在这之前,他致力于开发 Xamarin Studio 项目并且该项目是初始研究的一部分,将促成 Xamarin 测试云的创建。*

衷心感谢以下 Microsoft 技术专家对本文的审阅:Norm Estabrook
Norm Estabrook 是一位内容开发人员,在 Microsoft 任职已有 10 多年,主要致力于帮助开发人员通过使用 Visual Studio 构建 Office 扩展和本机移动应用。他为 MSDN 库发表了许多文章。现在与妻子、两个孩子一起居住在华盛顿西北部,家里有一只非常可爱的视障迷你雪纳瑞