Кросс-платформенный код

Совместное использование UI-кода на разных мобильных платформах с помощью Xamarin.Forms

Джейсон Смит

Продукты и технологии:

Xamarin.Forms

В статье рассматриваются:

  • подготовка простого проекта Xamarin.Forms;
  • использование при необходимости специфичного для платформы кода;
  • строительные блоки Xamarin.Forms;
  • отображение данных в списочном представлении;
  • навигация по страницам приложения;
  • применение анимации.

С помощью Xamarin можно использовать C# для создания красивых «родных» мобильных приложений и разделять большую часть кода между платформами. Традиционно приходилось проектировать отдельный UI для каждой целевой платформы. Но благодаря Xamarin.Forms можно создать один UI, который визуализируется как «родной» на любой платформе.

Xamarin.Forms является уровнем абстракции кросс-платформенного UI. На его основе вы можете использовать UI и его внутренний код между платформами, в то же время обеспечивая полностью «родную» пользовательскую среду (UX) на каждой платформе. Поскольку эти UI «родные», ваши элементы управления и виджеты полностью соответствуют духу и букве каждой целевой платформы.

Xamarin.Forms полностью совместим с проектировочным шаблоном Model-View-ViewModel (MVVM), поэтому вы можете связывать элементы страниц со свойствами и командами в классе модели представления.

Если вы предпочитаете декларативный дизайн своих страниц, то можете задействовать XAML — язык разметки с такими средствами, как словари ресурсов (resource dictionaries), динамические ресурсы, связывание с данными, команды, триггеры и поведения (behaviors).

Xamarin.Forms имеет компактный и простой в использовании API. Если вам нужен более глубокий доступ к «родному» UI платформы, вы можете создать собственные представления и рендеры, специфичные для платформы. Звучит устрашающе, но на самом деле это просто способ доступа к «родным» UI, и на веб-сайте Xamarin есть множество примеров, которые помогут вам сделать это.

Вы можете начать с использования любых из готовых страниц, разметок и представлений, поставляемых с Xamarin.Forms. Когда ваше приложение станет зрелым и вы обнаружите новые случаи применения и дизайнерские возможности, вы сможете опираться на поддержку Xamarin.Forms для XAML, MVVM, собственных рендеров, специфичных для платформ, и множество других средств, таких как анимации и шаблоны данных.

Я дам обзор функциональности Xamarin с конкретными примерами, который иллюстрируют, как делать общим для различных платформ основную часть вашего UI-кода и при необходимости включать в него специфичный для платформ код.

Приступаем

Сначала откройте диспетчер пакетов NuGet в Visual Studio или Xamarin Studio и проверьте наличие обновлений для Xamarin.Forms. Поскольку вы не получите уведомлений о новых выпусках при простом открытии решения Xamarin.Forms в любой из этих IDE, проверка наличия обновленных NuGet-пакетов является единственным способом гарантировать актуальность вашего ПО.

Создайте решение Xamarin.Forms Удостоверившись в наличии у вас новейшей версии Xamarin.Forms, создайте решение Blank App (Xamarin.Forms Portable).

В вашем решении содержатся три специфичных для платформ проекта и один проект Portable Class Library (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();
  }
}

Сейчас самое время обсудить класс Application. Начиная с версии v1.3.0, все приложения Xamarin.Forms будут содержать этот класс. Это входная точка приложения Xamarin.Forms, и он среди прочего предоставляет события жизненного цикла, а также постоянное хранилище (словарь Properties) для любых сериализуемых данных. Если вам нужен доступ к экземпляру этого класса из любой точки вашего приложения, используйте статическое свойство Application.Current.

В предыдущем примере я удалил код по умолчанию в классе приложения и заменил его одной строкой кода, которая вызывает появление страницы LogInPage при запуске приложения. Так происходит потому, что этот код назначает страницу (LogInPage) свойству MainPage класса Application. Вы должны задавать его в конструкторе класса Application.

В этом классе также можно переопределить три метода:

  • 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, см. «From Data Bindings to MVVM» на сайте Xamarin по ссылке bit.ly/1uMoIUX.

Используйте XAML для создания страницы В малых приложениях создание UI с помощью C# вполне оправданный подход. Однако по мере роста размера приложения вы можете обнаружить, что набираете слишком много повторяющегося кода. Этого можно избежать, используя XAML вместо кода на C#.

Добавьте элемент Forms XAML Page в свой проект PCL. Затем добавьте разметку на страницу, как показано на рис. 4.

Рис. 4. Добавление разметки в Forms XAML Page

<?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="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>

XAML на рис. 4 может показаться знакомым, если вы писали приложения Windows Presentation Foundation (WPF). Однако теги отличаются, поскольку они ссылаются на типы Xamarin.Forms. Кроме того, корневой элемент ссылается на подкласс класса Xamarin.Forms.Element. Все XAML-файлы в приложении Xamarin.Forms должны делать это.

Чтобы узнать больше о применении XAML для создания страниц в приложении Xamarin.Forms, см. «XAML for Xamarin.Forms» на сайте Xamarin по ссылке bit.ly/1xAKvRN.

Разработка для конкретной платформы

Xamarin.Forms имеет сравнительно небольшое количество API по сравнению с другими «родными» мобильными платформами. Это упрощает проектирование ваших страниц, но иногда Xamarin.Forms визуализирует представление не совсем так, как вы хотели, на одной или более целевых платформах.

Если вы уперлись в этот барьер, просто создайте пользовательское представление, которое является подклассом любого представления, доступного в Xamarin.Forms.

Чтобы представление появилось на странице, расширьте рендер представлений (view renderer). В более сложных случаях вы можете создать с нуля даже собственный рендер. Код такого рендера специфичен для платформы, поэтому его нельзя сделать общим. Но этот подход может стоить того, чтобы ввести «родные» средства и пользовательскую среду в приложение.

Создайте пользовательское представление Сначала создайте подкласс любого представления, доступного в Xamarin.Forms. Вот код для двух пользовательских представлений:

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

Расширьте существующий рендер На рис. 5 рендер расширяется для того, чтобы визуализировать представление Entry на платформе 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. Если вы хотите изучить пример, содержащий этот фрагмент кода, см. «Xamarin.Forms Custom Renderer» по ссылке bit.ly/1xTIjmR.

Создание рендера с нуля Вы можете создать совершенно новый рендер, который не расширяет любые другие рендеры. В этом случае потребуется больше работы, но это имеет смысл, если вам нужно делать любые из следующих вещей:

  • заменить рендер представления;
  • добавить новый тип представления в свое решение;
  • добавить «родной» элемент управления или страницу в свое решение.

Например, если вы хотите добавить «родной» элемент управления 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)
      {
        // Здесь отключаем любые события от e.OldElement
      }
      var rbv = e.NewElement;
      if (rbv != null)
      {
        var shadowView = new UIView();
        // Set properties on the UIView here.
        SetNativeControl(shadowView);
        // Здесь подключаем любые события от e.NewElement
      }
    }
  }
}

Универсальный шаблон, просматривающийся в этом рендере, гарантирует, что вы сможете использовать его в виртуализованных разметках, таких как списочное представление (list view), о которых мы поговорим позже.

Если вы хотите изучить пример, содержащий этот фрагмент кода, см. bit.ly/xf-customrenderer.

Добавьте свойства в пользовательское представление Вы можете добавлять свойства в пользовательское представление, но обязательно сделайте их связываемыми (bindable), чтобы их можно было связывать со свойствами в модели представления или с другими типами данных. Вот связываемое свойство в пользовательском представлении 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;
}

Узнать больше о создании собственных представлений и рендеров можно в «Customizing Controls for Each Platform» по ссылке bit.ly/11pSFhL.

Страницы, разметки и представления: строительные блоки Xamarin.Forms

Я продемонстрировал несколько элементов, но их гораздо больше. Сейчас самое время обсудить их.

Приложения Xamarin.Forms содержат страницы, разметки и представления. Страница содержит одну или более разметок, а разметка — одно или более представлений. Термин «представление» (view) в Xamarin описывает то, что вы привыкли называть элементом управления (control). В целом, инфраструктура Xamarin.Forms поддерживает пять типов страниц, семь типов разметок и 24 типа представлений. Подробности см. по ссылке xamarin.com/forms. О некоторых важных типах страниц я расскажу позже, а сейчас остановлюсь на обзоре некоторых разметок, которые вы можете применять в своем приложении. Xamarin.Forms содержит четыре основные разметки.

  • StackLayout Размещает дочерние элементы в одну строку, которую можно ориентировать горизонтально или вертикально. Один StackLayout можно вкладывать в другой для создания сложной иерархии визуальных элементов. Вы также можете управлять тем, как представления размещаются в StackLayout, используя свойства VerticalOptions и HorizontalOptions каждого дочернего представления.
  • Grid Размещает представления по строкам и столбцам. Эта разметка похожа на ту, которую вы получаете с WPF и Silverlight с тем исключением, что можно добавлять интервалы между строками и столбцами. Для этого используются свойства RowSpacing и ColumnSpacing в Grid.
  • RelativeLayout Размещает представления, используя ограничения относительно других представлений.
  • AbsoluteLayout Позволяет размещать дочерние представления двумя способами: в абсолютной позиции или пропорционально относительной по отношению к предку. Это полезно, если вы планируете создать разделенную (split) и перекрытую (overlaid) иерархии. Пример приведен на рис. 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 разметки Grid легко добавлять и удалять строки и столбцы, а также указывать интервалы для строк и столбцов.

Отображение данных в списке с прокруткой

Вы можете показывать данные в списке с прокруткой (scrolling list), используя форму списочного представления (list view form) (ListView). Это представление хорошо работает, потому что рендеры для каждой ячейки в списке виртуализованы. Ввиду этого важно, чтобы вы корректно обрабатывали событие OnElementChanged любого из собственных рендеров, которые вы создаете для ячеек или списков по шаблону, аналогичному приведенному ранее.

Сначала определите ячейку, как показано на рис. 8. Все шаблоны данных для ListView должны использовать Cell в качестве корневого элемента.

Рис. 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 }
    };
  }
}

Затем определите источник данных и назначьте свойству ItemTemplate в ListView новый шаблон данных. Шаблон данных базируется на классе MyCell, созданном ранее:

var items = new[] {
  new { Name = "Flower", Description = "A lovely pot of flowers." },
  new { Name = "Tuna", Description = "A can of tuna!" },
  // ... добавьте другие элементы
};
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" }
  }
};

В этом случае важно задать свойства Title и Icon каждой страницы так, чтобы на вкладках страницы что-то появилось. Значки визуализируются не на всех платформах. Это зависит от дизайна вкладок на конкретной платформе.

Если открыть эту страницу на мобильном устройстве, по умолчанию выбирается первая вкладка. Но вы можете изменить это через свойство CurrentPage страницы TabbedPage.

NavigationPage Управляет навигацией по стеку страниц. Эта страница предлагает самый распространенный тип навигационного шаблона для мобильных приложений. Вот как вы добавляли бы в нее свои страницы:

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

Обратите внимание на использование PushAsync как на способ перехода пользователей на конкретную страницу (в данном случае — на DirectoryPage). В NavigationPage вы «заталкиваете» страницы в стек, а затем «выталкиваете» их оттуда по мере того, как пользователи возвращаются на предыдущие страницы.

Методы PushAsync и PopAsync в NavigationPage являются асинхронными, поэтому ваш код должен ожидать их через await и не выталкивать или заталкивать никакие новые страницы, пока выполняется данная задача. Задача каждого метода возвращается по завершении анимации заталкивания или выталкивания.

Вы создаете более привлекательный UI, анимируя представления на странице, что можно делать двумя способами.

Для удобства все представления, разметки и страницы в Xamarin.Forms содержат свойство Navigation. Это свойство является прокси-интерфейсом, содержащим методы PushAsync и PopAsync экземпляра NavigationPage. Вы могли бы использовать для перехода на страницу это свойство вместо вызова методов PushAsync и PopAsync экземпляра NavigationPage. Кроме того, NavigationProperty можно задействовать, чтобы получить доступ к методам PushModalAsync и PopModalAsync. Это полезно, если вы хотите заменить содержимое всего экрана новой модальной страницей. Вам не обязательно иметь NavigationPage в родительском стеке представления, чтобы использовать свойство Navigation представления, но немодальные операции PushAsync/PopAsync могут закончиться неудачей.

Примечание по проектировочным шаблонам В целом, подумайте о добавлении страниц NavigationPage как дочерних к страницам TabbedPage, а TabbedPage — как дочерних к MasterDetailPage. Некоторые типы шаблонов могут отрицательно сказаться на пользовательской среде. Например, на большинстве платформ не рекомендуют добавлять TabbedPage как дочернюю страницу к NavigationPage.

Анимация представлений на странице

Вы создаете более привлекательный UI, анимируя представления на странице, что можно делать двумя способами. Либо выберите одну из встроенных анимаций, поставляемых с Xamarin.Forms, либо создайте собственную, используя API анимации.

Например, вы могли бы создать эффект затухания вызовом анимации 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);
}

В этом примере каждая анимация включается в один экземпляр Animation, а затем вся последовательность анимаций запускается методом Commit. Поскольку такая анимация не привязана к конкретному представлению, вы можете применять ее к любому представлению.

Заключение

Xamarin.Forms — новый захватывающий способ создания кросс-платформенных «родных» мобильных приложений. Вы создаете UI, который визуализируется как «родной» в iOS, Android и Windows Phone. Почти весь ваш код можно сделать общим для разных платформ.

Xamarin Inc. создала Xamarin и Xamarin.Forms, чтобы разработчики на C# могли буквально за один вечер перейти к разработке для мобильных платформ. Если у вас есть опыт разработки для Windows Runtime, WPF или Silverlight, вы найдете Xamarin.Forms удобным мостиком в мир кросс-платформенной разработки «родных» мобильных приложений. Вы можете сегодня же установить Xamarin и немедленно начать использовать C# для создания красивых «родных» приложений, выполняемых на устройствах под управлением iOS, Android и Windows Phone.


Джейсон Смит (Jason Smith) — руководитель инженерно-технологических разработок в Xamarin Inc. (Сан-Франциско), в настоящее время возглавляет проект Xamarin.Forms. Был одним из ведущих архитекторов Xamarin.Forms, а до этого участвовал в проекте Xamarin Studio и был членом первой исследовательской группы, разработки которой привели к созданию Xamarin Test Cloud.

Выражаю благодарность за рецензирование статьи эксперту Microsoft Норму Эстабруку (Norm Estabrook).