Специальный выпуск Windows 10 2015

ТОМ 30, НОМЕР 11

Дизайн UI - Адаптивные приложения для Windows 10

Клинт Руткас | Windows 2015

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

Universal Windows Platform, API Contracts, XAML, Continuum

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

  • улучшения в XAML в Windows 10, позволяющие создавать адаптивный UI;
  • применение обнаружения API Contracts для поддержки средств и возможностей, подходящих для клиента;
  • роль API Contracts в поддержке функциональности между устройствами.

С появлением универсальной платформы Windows (Universal Windows Platform, UWP) в Windows 10 приложения могут теперь выполняться на разнообразных семействах устройств и автоматически масштабироваться под разные экраны и размеры окон, поддерживаемых элементами управления платформы. Как эти семейства устройств поддерживают взаимодействие пользователей с вашими приложениями и как ваши приложения адаптируются к устройству, на котором они выполняются? Мы исследовали эти вопросы, а также средства и ресурсы, предоставляемые Microsoft на этой платформе, чтобы вам не приходилось писать и сопровождать сложный код для приложений, работающих на устройствах разных типов.

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

Прежде чем изучать элементы управления, API и код, разберемся в семействах устройств, о которых мы говорим. Если в двух словах, то семейство устройств (device family) — это группа устройств со специфическим форм-фактором: от IoT-устройств, смартфонов, планшетов и настольных ПК до игровых консолей Xbox, устройств Surface Hub с большим экраном и даже носимой электроники. Приложения будут работать на всех этих семействах устройств, но при проектировании приложений важно учитывать, на каких семействах устройств они могли бы использоваться.

Хотя семейств устройств много, UWP спроектирован так, чтобы 85% его API были полностью доступны любому приложению независимо от того, на каком устройстве оно выполняется. Более того, если посмотреть на 1000 самых популярных приложений, 96,2% всех используемых API приходится на базовый набор Universal Windows API. Основная функциональность присутствует и доступна как часть UWP наряду со специализированными API, доступными на каждом устройстве для дальнейшей адаптации вашего приложения.

С возвращением, Windows

Одно из самых больших изменений в том, как приложения используются в Windows, — нечто, с чем вы уже знакомы: выполнение приложений в окне. Windows 8 и Windows 8.1 позволяли запускать приложения в полноэкранном режиме или в окнах бок о бок (до четырех приложений одновременно). Windows 10, напротив, позволяет упорядочивать, изменять размеры и позицию окон приложений как угодно. Новый подход в Windows 10 обеспечивает более высокую гибкость UI, но может потребовать от вас некоторой работы для его оптимизации. Благодаря улучшениям XAML в Windows 10 появился ряд способов реализации адаптивных методик в приложениях, поэтому они отлично выглядят независимо от размера экрана или окна. Давайте исследуем три таких подхода.

VisualStateManager В Windows 10 класс VisualStateManager был расширен двумя механизмами для реализации адаптивного дизайна в ваших XAML-приложениях. Новые VisualState.StateTriggers и VisualState.Setters API позволяют определять визуальные состояния, которые соответствуют определенным условиям. Визуальные состояния можно изменять на основе высоты и ширины окна приложения, используя встроенный AdaptiveTrigger как StateTrigger в VisualState и настраивая свойства MinWindowHeight и MinWindowWidth. Кроме того, вы можете расширять Windows.UI.Xaml.StateTriggerBase, чтобы создавать собственные триггеры, например для срабатывания на некоем семействе устройств или при каком-то типе ввода. Взгляните на код на рис. 1.

Рис. 1. Создание собственных триггеров состояний

<Page>
  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup>
        <VisualState>
          <VisualState.StateTriggers>
          <!-- VisualState, который должен сработать, когда
            ширина окна >=720 пикселей -->
            <AdaptiveTrigger MinWindowWidth="720" />
          </VisualState.StateTriggers>
          <VisualState.Setters>
            <Setter Target="myPanel.Orientation"
                    Value="Horizontal" />
          </VisualState.Setters>
        </VisualState>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <StackPanel x:Name="myPanel" Orientation="Vertical">
      <TextBlock Text="This is a block of text. It is text block 1. "
                 Style="{ThemeResource BodyTextBlockStyle}"/>
      <TextBlock Text="This is a block of text. It is text block 2. "
                 Style="{ThemeResource BodyTextBlockStyle}"/>
      <TextBlock Text="This is a block of text. It is text block 3. "
                 Style="{ThemeResource BodyTextBlockStyle}"/>
    </StackPanel>
  </Grid>
</Page>

В примере на рис. 1 на странице отображаются три элемента TextBlock, расположенные поверх друг друга в своем состоянии по умолчанию. В VisualStateManager имеется AdaptiveTrigger, определенный с MinWindowWidth равным 720, что заставляет изменять ориентацию StackPanel на Horizontal, когда окно имеет ширину минимум 720 рабочих пикселей. Это позволяет использовать дополнительное пространство экрана по горизонтали, когда пользователь изменяет размер окна или переключается из книжной в альбомную ориентацию на смартфоне или планшете. Учтите: если вы определите как свойство ширины, так и свойство высоты, ваш триггер будет срабатывать, только когда приложение соответствует обоим условиям одновременно. Изучите пример триггеров State в GitHub (wndw.ms/XUneob), чтобы увидеть больше сценариев использования триггеров, в том числе ряда пользовательских триггеров.

RelativePanel В примере на рис. 1 для изменения свойства Orientation объекта StackPanel применялся StateTrigger. Многие элементы-контейнеры в XAML в сочетании с StateTrigger позволяют манипулировать UI самыми разными способами, но не дают возможности легко создавать сложные адаптируемые UI, где элементы размещаются относительно друг друга. Здесь и вступает в игру новый RelativePanel. Как видно на рис. 2, с помощью RelativePanel можно размещать элементы, выражая пространственные связи между элементами. Это означает, что можно легко использовать RelativePanel совместно с AdaptiveTrigger, чтобы создавать адаптивный UI, где элементы перемещаются на основе доступного экранного пространства.

Рис. 2. Выражение пространственных связей с помощью RelativePanel

<RelativePanel BorderBrush="Gray" BorderThickness="10">
  <Rectangle x:Name="RedRect" Fill="Red" MinHeight="100" MinWidth="100"/>
  <Rectangle x:Name="BlueRect" Fill="Blue" MinHeight="100" MinWidth="100"
             RelativePanel.RightOf="RedRect" />
  <!-- Ширина не задается в зеленом и желтом прямоугольниках.
       Она определяется свойствами RelativePanel. -->
  <Rectangle x:Name="GreenRect" Fill="Green"
             MinHeight="100" Margin="0,5,0,0"
             RelativePanel.Below="RedRect"
             RelativePanel.AlignLeftWith="RedRect"
             RelativePanel.AlignRightWith="BlueRect"/>
  <Rectangle Fill="Yellow" MinHeight="100"
             RelativePanel.Below="GreenRect"
             RelativePanel.AlignLeftWith="BlueRect"
             RelativePanel.AlignRightWithPanel="True"/>
</RelativePanel>

Помните, что синтаксис, используемый вами для подключаемых свойств, включает дополнительные скобки, как показано ниже:

<VisualStateManager.VisualStateGroups>
  <VisualStateGroup>
    <VisualState>
      <VisualState.StateTriggers>
        <AdaptiveTrigger MinWindowWidth="720" />
      </VisualState.StateTriggers>
      <VisualState.Setters>
        <Setter Target="GreenRect.(RelativePanel.RightOf)"
                Value="BlueRect" />
      </VisualState.Setters>
    </VisualState>

Вы можете изучить дополнительные сценарии применения RelativePanel в примере «Responsiveness techniques» в GitHub (wndw.ms/cbdL0q).

SplitView Размер окна приложения влияет больше, чем контент, отображаемый на страницах приложения; он может потребовать, чтобы навигационные элементы реагировали на изменения размера самого окна. Новый элемент управления SplitView, введенный в Windows 10, как правило, используется для навигации верхнего уровня, поведение которой можно настраивать согласно размеру окна приложения. Хотя это распространенные случаи применения SplitView, его возможности не ограничены только этими случаями. SplitView применяется в двух областях: секции окна (pane) и контенте.

Для манипуляций отображением можно использовать ряд свойств этого элемента управления. Свойство DisplayMode указывает, как выполняется рендеринг секции окна относительно области контента, и поддерживает четыре доступных режима: Overlay, Inline, CompactOverlay и CompactInline. На рис. 3 показаны примеры режимов Inline, Overlay и CompactInline.

Навигационные элементы при разных значениях DisplayMode
Рис. 3. Навигационные элементы при разных значениях DisplayMode

Свойство PanePlacement отображает Pane либо слева (по умолчанию), либо справа от области Content. Свойство OpenPaneLength задает ширину секции окна при полном раскрытии (по умолчанию — 320 рабочих пикселей).

Заметьте, что элемент управления SplitView не включает встроенный UI-элемент для пользователей, чтобы переключать состояние секции окна наподобие распространенного меню «гамбургер», которое часто встречается в мобильных приложениях. Если вам нужно такое поведение, вы должны определить этот UI-элемент в своем приложении и написать код для переключения значения свойства IsPaneOpen в SplitView.

Хотите исследовать полный набор возможностей SplitView? Проверьте пример навигационного меню на XAML в GitHub (wndw.ms/qAUVr9).

Кнопка Back

Если вы разрабатывали приложения для более ранних версий Windows Phone, то, вероятно, привыкли к тому, что на каждом устройстве есть аппаратная или программная кнопка Back, позволяющая выполнять навигацию в приложении в обратном направлении. Однако в случае Windows 8 и 8.1 вы должны были создавать собственный UI для обратной навигации. Чтобы упростить все это при ориентации на несколько семейств устройств в вашем приложении Windows 10, теперь есть способ, обеспечивающий согласованный механизм обратной навигации для всех пользователей.

Чтобы включить системную кнопку возврата для вашего приложения, даже на семействах устройств, не имеющих аппаратной или программной кнопки Back (например, на лэптопах или настольных ПК), используйте свойство AppViewBackButtonVisibility класса SystemNavigationManager. Просто получите SystemNavigationManager для текущего представления и установите видимость кнопки возврата, как в следующем коде:

SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility =
  AppViewBackButtonVisibility.Visible;

Класс SystemNavigationManager также предоставляет событие BackRequested, которое генерируется, когда пользователь запрашивает системную кнопку, жест или голосовую команду для обратной навигации. Это означает, что вы должны обрабатывать это единственное событие для согласованного выполнения обратной навигации в своем приложении на всех семействах устройств.

Преимущества Continuum

Последнее, но не менее важное, о чем мы хотели бы упомянуть: Continuum в Windows 10. Благодаря Continuum Windows 10 подстраивает свою пользовательскую среду к тому, что вы желаете делать и как именно. Если ваше приложение выполняется на лэптопе «два в одном», то, например, реализация Continuum в этом приложении позволяет использовать как сенсорный ввод, так и мышь с клавиатурой для оптимизации продуктивности работы пользователей. С помощью свойства UserInteractionMode класса UIViewSettings приложение может определить, как пользователь взаимодействует с представлением — через сенсорный ввод или мышь и клавиатуру. Для этого достаточно одной строки кода:

// Возвращает UserInteractionMode.Mouse
// или UserInteractionMode.Touch
UIViewSettings.GetForCurrentView().UserInteractionMode;

После распознавания режима взаимодействия вы можете оптимизировать UI своего приложения, включая увеличение или уменьшение полей страниц, отображение или скрытие сложных функций и др. Посмотрите статью Ли Макферсона (Lee McPherson) в TechNet «Windows 10 Apps: Leverage Continuum Feature to Change UI for Mouse/Keyboard Users Using Custom StateTrigger» (wndw.ms/y3gB0J), которая демонстрирует, как можно комбинировать новые StateTrigger и UserInteractionMode для создания собственного Continuum StateTrigger.

Адаптивные приложения

Приложения, которые могут адаптироваться к изменениям в размере экрана и его ориентации, полезны, но для достижения настоящей кросс-платформенной функциональности UWP предоставляет разработчикам два дополнительных типа адаптивного поведения.

  • Адаптивные к версии приложения реагируют на разные версии UWP обнаружением доступных API и ресурсов. Например, вы хотите, чтобы приложение использовало некоторые более новые API, которые присутствуют только на устройствах с новейшими версиями UWP, и в то же время продолжало работать на пока еще не обновленных устройствах.
  • Адаптивные к платформе приложения реагируют на уникальные возможности, доступные на разных семействах устройств. Поэтому вы можете создать приложение, работающее на всех семействах устройств, но использующее некоторые специфичные API при выполнении на мобильном устройстве вроде смартфона.

Как отмечалось ранее, с появлением Windows 10 подавляющее большинство UWP API полностью доступно любому приложению независимо от устройства, на котором оно выполняется. А специализированные API, связанные с каждым семейством устройств, позволяют разработчикам в еще большей мере адаптировать свои устройства.

Фундаментальная идея адаптивных приложений заключается в том, что ваше приложение проверяет необходимую ему функциональность (или какой-то механизм) и пытается использовать ее, только если она доступна. В прошлом приложение проверяло версию ОС, а затем вызывало API, связанные с этой версией. В случае Windows 10 ваше приложение может проверять через исполняющую среду, поддерживается ли текущей ОС некий класс, метод, свойство, событие или контракт API. Если да, приложение вызывает соответствующий API. Класс ApiInformation в пространстве имен Windows.Foundation.Metadata содержит несколько статических методов (например, IsApiContractPresent, IsEventPresent и IsMethodPresent), применяемых для запроса на наличие этих API. Вот пример:

using Windows.Foundation.Metadata;
if(ApiInformation.IsTypePresent("Windows.Media.Playlists.Playlist"))
{
  await myAwesomePlaylist.SaveAsAsync( ... );
}

Этот код делает две вещи. Он заставляет исполняющую среду проверить наличие класса Playlist, затем вызывает метод SaveAsAsync экземпляра этого класса. Заметьте, насколько легко проверяется наличие какого-либо типа в текущей ОС, если используется IsTypePresent API. Раньше такая проверка могла бы потребовать вызова LoadLibrary, GetProcAddress, QueryInterface, обращения к механизму отражения или использования ключевого слова dynamic и т. д. в зависимости от языка и инфраструктуры. Также обратите внимание на строго типизированную ссылку при вызове метода. Используя отражение или dynamic, вы теряете статическую диагностику при компиляции, которая могла бы, например, сообщить вам, что вы неправильно указали имя метода.

Обнаружение с помощью API Contracts

По сути, контракт API — это набор API. Гипотетический контракт API мог бы представлять набор API, содержащих два класса, пять интерфейсов, одну структуру, два перечисления и т. д. Мы логически группируем связанные типы в контракт API. Во многих отношениях контракт API представляет определенные возможности — набор связанных API, которые совместно обеспечивают некую конкретную функциональность. Каждый Windows Runtime API, начиная с Windows 10 и далее, является членом какого-либо контракта API. В документации на msdn.com/dn706135 описываются все доступные API Contracts. Вы заметите, что большинство из них представляют некий набор функционально связанных API.

Использование API Contracts также дает разработчику некоторые дополнительные гарантии; самая важная из них в том, что, когда какая-либо платформа реализует любой API в контракте API, она должна реализовать все API в этом контракте. Иначе говоря, контракт API является атомарной единицей, и тестирование на поддержку данного контракта API эквивалентно тестированию на поддержку каждого API в наборе. Ваше приложение может вызывать любой API в обнаруженном контракте API, не заботясь о проверке на наличие каждого индивидуального API.

Самый большой и наиболее часто используемый контракт API — Windows.Foundation.UniversalApiContract. Он содержит почти все API в Universal Windows Platform. Если бы вам понадобилось выяснить, поддерживает ли текущая ОС UniversalApiContract, вы могли бы написать такой код:

if (ApiInformation.IsApiContractPresent(
  "Windows.Foundation.UniversalApiContract"), 1, 0)
{
  // Все API в UniversalApiContract версии 1.0
  // доступны для использования
}

Прямо сейчас существует только одна версия UniversalApiContract — 1.0, так что эта проверка пока бессмысленна. Но в будущем обновлении Windows 10 могут быть введены дополнительные API, которые образуют UniversalApiContract версии 2.0, включающую новые универсальные API. В будущем приложению, которое должно работать на всех устройствах, а также использовать API новой версии (2.0), потребуется следующий код:

if (ApiInformation.IsApiContractPresent(
  "Windows.Foundation.UniversalApiContract"), 2, 0)
{
  // Это устройство поддерживает все API
  // в UniversalApiContract версии 2.0
}

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

Помимо UniversalApiContract, существуют и другие контракты API. Большинство из них представляет функциональность или набор API, которые не являются универсальными для всех платформ Windows 10 и вместо этого годятся лишь для одного или более специфичных семейств устройств. Как упоминалось ранее, вам больше незачем проверять конкретный тип устройства, а затем логически распознавать поддержку на нем какого-либо API. Просто проверяйте наличие набора API, необходимого вашему приложению.

Теперь я могу переписать исходный пример, чтобы проверять на наличие Windows.Media.Playlists.PlaylistsContract, а не просто класса Playlist:

if(ApiInformation.IsApiContractPresent(
  "Windows.Media.Playlists.PlaylistsContract"), 1, 0)
{
  // Теперь я могу использовать все Playlist API
}

Всякий раз, когда приложению нужно вызвать какой-то API, не присутствующий на всех семействах устройств, вы должны добавлять ссылку на соответствующий Extension SDK, где этот API определен. В Visual Studio 2015 откройте диалог Add Reference и перейдите на вкладку Extensions. Там вы найдете три важнейших расширения: Mobile Extension, Desktop Extension и IoT Extension.

Однако вашему приложению надо лишь проверить наличие требуемого контракта API и вызывать соответствующие API по условию. Беспокоиться о типе устройства нет никакой нужды. Теперь вопрос в следующем: я хочу вызвать Playlist API, но он не относится к универсально доступным API. В документации (bit.ly/1QkYqky) сообщается, в каком контракте API находится этот класс. Но в каком из Extension SDK он определен?

Оказывается, класс Playlist в настоящее время доступен только на настольных устройствах, но не на мобильных, Xbox и прочих семействах устройств. Поэтому вы должны добавить ссылку на Desktop Extension SDK, прежде чем компилировать любой код из предыдущих примеров.

Член группы Visual Studio и автор некоторых статей для «MSDN Magazine», Люсьен Вишик (Lucian Wischik), создал инструмент, который может помочь в этом деле. Он анализирует код приложения, когда тот вызывает специфичный для платформы API, и смотрит, обернут ли этот вызов в проверку на адаптивность. Если такой проверки нет, анализатор выводит предупреждение и предоставляет удобное «быстрое исправление» («quick-fix») для вставки корректной проверки в код простым нажатием клавиш Ctrl+точка или щелчком значка лампочки (подробности см. по ссылке bit.ly/1JdXTeV). Анализатор также можно установить через NuGet (bit.ly/1KU9ozj).

Давайте в заключение рассмотрим более полные примеры адаптивного кодирования для Windows 10. Для начала возьмем код, который некорректно использует адаптивность:

// Этот код рухнет, если будет вызван из IoT или Mobile
async private Task CreatePlaylist()
{
  StorageFolder storageFolder = KnownFolders.MusicLibrary;
  StorageFile pureRockFile = await storageFolder.CreateFileAsync("myJam.mp3");
  Windows.Media.Playlists.Playlist myAwesomePlaylist =
    new Windows.Media.Playlists.Playlist();
  myAwesomePlaylist.Files.Add(pureRockFile);
  // Здесь код рухнет, так как это вызов только для Desktop
  await myAwesomePlaylist.SaveAsAsync(KnownFolders.MusicLibrary,
    "My Awesome Playlist", NameCollisionOption.ReplaceExisting);
}

Теперь возьмем тот же код и добавим строку, которая проверяет, действительно ли дополнительный API поддерживается на целевом устройстве, прежде чем вызывать этот API. Это исключит крах в период выполнения. Заметьте, что вы скорее всего захотите усовершенствовать этот пример и не отображать UI, который вызывает метод CreatePlaylist, если приложение обнаруживает, что функциональность списка воспроизведения недоступна на этом устройстве:

async private Task CreatePlaylist()
{
  StorageFolder storageFolder = KnownFolders.MusicLibrary;
  StorageFile pureRockFile = await storageFolder.CreateFileAsync("myJam.mp3");
  Windows.Media.Playlists.Playlist myAwesomePlaylist =
    new Windows.Media.Playlists.Playlist();
  myAwesomePlaylist.Files.Add(pureRockFile);
  // Теперь вызов безопасен! Кешируем это значение,
  // если оно часто запрашивается.
  if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent(
    "Windows.Media.Playlists.Playlist"))
  {
      await myAwesomePlaylist.SaveAsAsync(
        KnownFolders.MusicLibrary, "My Awesome Playlist",
        NameCollisionOption.ReplaceExisting);
  }
}

Наконец, вот пример кода, который пытается обратиться к выделенной кнопке камеры, имеющейся на многих мобильных устройствах:

// Примечание: кешируйте значение вместо того,
// чтобы многократно запрашивать его
bool isHardwareButtonsAPIPresent =
  Windows.Foundation.Metadata.ApiInformation.IsTypePresent(
  "Windows.Phone.UI.Input.HardwareButtons");
if (isHardwareButtonsAPIPresent)
{
  Windows.Phone.UI.Input.HardwareButtons.CameraPressed +=
    HardwareButtons_CameraPressed;
}

Обратите внимание на этап обнаружения. Если бы я напрямую ссылался на объект HardwareButtons для события CameraPressed при работе на настольном ПК без проверки на наличие HardwareButtons, мое приложение потерпело бы крах.

Вокруг адаптивных UI и приложений в Windows 10 происходит еще много всякого. Хотите узнать больше? Послушайте отличное выступление Брента Ректора (Brent Rector) на тему API Contracts на конференции Build 2015 (wndw.ms/IgNy0I) и непременно посмотрите информативный видеоролик от Microsoft Virtual Academy по адаптивному коду (bit.ly/1OhZWGs), в котором эта тематика рассматривается подробнее.


Клинт Руткас (Clint Rutkas) — старший руководитель по продуктам Windows, основное внимание уделяет платформе разработки. Работал над Halo в 343 Industries и на Channel 9 в Microsoft, автор некоторых сногсшибательных проектов, использующих технологии Windows, например управляемой компьютером площадки для танцев в стиле «диско», модифицированного автомобиля Ford Mustang, роботизированных пушек, стреляющих майками, и др.

Раджен Кишна (Rajen Kishna) — в настоящее время работает старшим руководителем по маркетингу продуктов в группе Windows Platform Developer Marketing для Microsoft в Редмонде (штат Вашингтон). Ранее работал как консультант и идеолог Microsoft в Голландии.

Выражаем благодарность за рецензирование статьи экспертам Microsoft Сэму Джаравану (Sam Jarawan), Харини Каннану (Harini Kannan) и Бренту Ректору (Brent Rector).