Написание приложения-компаса

Источник: MSDN Magazine
Донн Морс

Как технический писатель, ответственный за подготовку документации по сенсорной платформе для Windows 8, я хочу, чтобы нашу новую платформу приняли как можно больше разработчиков. И поскольку приложения в стиле Metro можно писать с применением XAML и C#, разработчики для Windows Phone - идеальные кандидаты для такой миграции. У них уже есть опыт работы с XAML, а некоторые умеют использовать датчики (в самом последнем выпуске Windows Phone был предоставлен доступ к таким датчикам, как акселерометр, компас, гироскоп и GPS).

Чтобы лучше понимать разработчиков Windows Phone и их платформу разработки, прошлой осенью я решил написать простое приложение-компас. Написав его, я передал бесплатную версию в Windows Phone Marketplace через App Hub. После принятия этой версии приложение активно скачивалось пользователями Windows Phone, даже из таких далеких мест, как Швейцария и Малайзия.

В этой статье обсуждается разработка данного приложения.

Приложение-компас

Это приложение использует компас, или магнитометр, встроенный в устройство Windows Phone. Программа показывает направление относительно истинного севера, а также обратный курс (reciprocal heading), который может быть полезен при навигации в лодке или ориентации на удаленной местности по карте. Кроме того, приложение позволяет переключаться из цифрового представления (например, "090" градусов) в буквенное (например, "E" - восток). Также можно фиксировать текущее направление. Это удобно, когда нужно, чтобы указатель оставался неподвижным, - это позволяет определять направление на некий ориентир на местности или отметку на карте.

На рис. 1 показано приложение, выполняемое на устройстве Samsung Focus. Изображение слева показывает направление в цифровом виде, а изображение справа - в буквенном.

*
Рис. 1. Выполняемое приложение, показывающее направление в цифровом виде (слева) и в буквенном (справа)

Проектирование UI

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

Калибровочный экран Компас, или магнитометр, установленный в устройстве с Windows Phone, требует калибровки после включения устройства. Кроме того, эти датчики могут требовать периодической повторной калибровки. Чтобы вы могли определять программным способом, когда необходима калибровка, платформа поддерживает свойство HeadingAccuracy, через которое можно выяснить состояние текущей калибровки. В дополнение платформа поддерживает событие Calibrate, срабатывающее, если компас нуждается в калибровке.

Мое приложение обрабатывает событие Calibrate, что в свою очередь приводит к выводу экрана калибровки (calibrationStackPanel), где пользователю предлагается вручную откалибровать устройство, описывая им горизонтальную восьмерку в воздухе. В течение этой процедуры текущая точность показывается в CalibrationTextBlock красным шрифтом, пока не будет достигнута подходящая точность (рис. 2). Как только требуемая точность укладывается в диапазон ошибки, равный или меньший 10°, цифровые значения стираются, и появляется зеленая надпись "Complete!".

*
Рис. 2. Калибровочный экран

Соответствующий код, поддерживающий калибровку, находится в модуле MainPage.xaml.cs в обработчике событий compass_Current ValueChanged, как показано на рис. 3.

Рис. 3. Калибровка компаса

...
else
{
  if (HeadingAccuracy <= 10)
  {
    CalibrationTextBlock.Foreground =
      new SolidColorBrush(Colors.Green);
    CalibrationTextBlock.Text = "Complete!";
  }
  else
  {
    CalibrationTextBlock.Foreground =
      new SolidColorBrush(Colors.Red);
    CalibrationTextBlock.Text =
      HeadingAccuracy.ToString("0.0");
  }
}

Как только достигнута нужная точность, пользователю предлагается нажать кнопку Done, которая скрывает экран калибровки и выводит основной экран приложения.

Основной экран Этот экран отображает направление в числовой или буквенной форме и обратное значение (reciprocal value). Кроме того, здесь визуализируется картушка компаса (compass face), ориентированная относительно истинного севера. Наконец, основной экран показывает четыре элемента управления (кнопки), позволяющие изменять вывод, а также фиксировать направление и картушку компаса.

На рис. 4 показан основной экран моего приложения (MainPage.xaml) в том виде, в каком он появляется в Visual Studio.

*
Рис. 4. Основной экран приложения в Visual Studio

Большинство UI-элементов на основном экране представляют собой простые элементы управления TextBlock и Button. В текстовых блоках сообщаются направление и его обратное значение. Кнопки позволяют управлять выводом. Однако картушка компаса - штука посложнее.

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

Фоновое изображение В XAML фоновое изображение называется CompassFace (на переменную с этим именем ссылается код, отвечающий за поворачивание картушки компаса):

<Image Height="263" HorizontalAlignment="Left" Margin="91,266,0,0"
  Name="CompassFace" VerticalAlignment="Top" Width="263"  
  Source="/Compass71;component/compass.png" Stretch="None" />

Основное изображение Основное изображение картушки компаса, EllipseGlass, определено в самом XAML. Эффект дымчатого стекла создается с использованием кисти с линейным градиентом. Я создал этот эллипс, используя Microsoft Expression Blend 4. Этот инструмент совместим с Visual Studio и позволяет загружать XAML вашего приложения и улучшать UI настройкой графики. На рис. 5 показан редактор Expression Blend, в котором создается затененный эллипс.

Большинство UI-элементов на основном экране представляют собой простые элементы управления TextBlock и Button.

*
Рис. 5. Создание затененного эллипса в Expression Blend

После редактирования эллипса в Expression Blend я обновил XAML в проекте Visual Studio с помощью следующего XML:

<Ellipse Height="263"  Width="263" x:Name="EllipseGlass" 
  Margin="91,266,102,239" Stroke="Black" StrokeThickness="1">
  <Ellipse.Fill>
    <LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">
    <GradientStop Color="#A5000000" Offset="0" />
    <GradientStop Color="#BFFFFFFF" Offset="1" />
    </LinearGradientBrush>
  </Ellipse.Fill>

Заметьте, что размеры EllipseGlass (263×263 пикселя) точно соответствуют размерам изображения в compass.png. Также обратите внимание, что на объект EllipseGlass ссылается код, выполняющий поворачивание картушки компаса.

Граница картушки компаса Картушка вращается внутри большего белого эллипса с красной границей. Этот эллипс определен в XAML и называется EllipseBorder:

<Ellipse Height="385" HorizontalAlignment="Left" Margin="31,0,0,176"
  Name="EllipseBorder" Stroke="#FFF80D0D" StrokeThickness="2"
  VerticalAlignment="Bottom" Width="385" Fill="White" />

Отделенный код UI

Этот код содержится в файле MainPage.xaml.cs в пакете исходного кода, который можно скачать для этой статьи, и он предоставляет доступ кпространствам имен, необходимым приложению, инициализирует датчик, задает интервал отчетов и обрабатывает различные функции приложения: калибровку, вращение картушки компаса, переключение между форматами вывода (цифровой/буквенный) и т. д.

Обращение к компасу из кода Первый шаг в написании приложения-компаса (и любого приложения, которое обращается к одному из датчиков смартфона) - получение доступа к объектам датчиков, предоставляемым пространством имен Microsoft.Devices.Sensors. Для этого в MainPage.xaml.cs используется следующая директива:

using Microsoft.Devices.Sensors;

Как только эта директива появляется в файле, можно создать переменную compass, через которую вы получаете программный доступ к реальному устройству в смартфоне:

namespace Compass71
{
  public partial class MainPage : PhoneApplicationPage
  {
    Compass compass = new Compass();

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

Запуск компаса и задание частоты отчетов После создания переменной compass можно вызывать методы и настраивать свойства этого объекта. Первый вызываемый мной метод - Start, который позволяет мне начать получение данных от датчика. После запуска компаса я задаю интервал отчетов - время между обновлениями датчика - равным 400 мс (заметьте, что свойство TimeBetweenUpdates требует указывать значение, кратное 20 мс):

compass.TimeBetweenUpdates =
  TimeSpan.FromMilliseconds(400);  // Должно быть кратно 20
compass.Start();

Значение 400 было подобрано методом проб и ошибок. Интервал по умолчанию слишком мал. Если вы попытаетесь запустить приложение со значением по умолчанию, картушка компаса будет вращаться так часто, что покажется неустойчивой.

Установка обработчиков событий компаса Приложение-компас поддерживает два обработчика событий: один из них - отображает страницу калибровки (calibration StackPanel), а другой - визуализирует текущее направление и поворачивает картушку компаса.

Определение и подключение обработчика событий калибровки Этот обработчик содержит сравнительно немного кода (рис. 6) и выполняет две основные задачи: отображает экран калибровки, определенный в файле MainPage.xaml, и устанавливает булеву переменную calibrating в true.

Рис. 6. Обработчик событий калибровки

void compass_Calibrate(object sender,
  CalibrationEventArgs e)
{
  try
  {
    Dispatcher.BeginInvoke(() =>
    { calibrationStackPanel.Visibility =
      Visibility.Visible; 
    });
    calibrating = true;
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message.ToString(),
       "Error!", MessageBoxButton.OK);
  }
}

Поскольку этот обработчик событий вызывается из фонового потока, у него нет прямого доступа к UI-потоку. Поэтому, чтобы отобразить экран калибровки, мне нужно вызвать метод BeginInvoke объекта Dispatcher.

Булева переменная calibrating проверяется в коде обработчиком событий изменения значения (compass_CurrentValueChanged). Когда эта переменная установлена в true, компас игнорируется и экран калибровки обновляется последними калибровочными данными. Если же эта переменная равна false, показания компаса обновляются и выполняются соответствующие повороты картушки компаса.

Первый шаг в написании приложения-компаса (и любого приложения, которое обращается к одному из датчиков смартфона) - получение доступа к объектам датчиков, предоставляемым пространством имен Microsoft.Devices.Sensors.

Этот обработчик событий подключается в конструкторе MainPage следующей строкой кода:

compass.Calibrate += new EventHandler<CalibrationEventArgs>(compass_Calibrate);

Определение и подключение обработчика событий изменения значения Этот обработчик (compass_CurrentValueChanged) вызывается всякий раз, когда от компаса поступает новое значение. В зависимости от состояния переменной calibrating он обновляет либо экран калибровки, либо основной экран.

При обновлении основного экрана этот обработчик выполняет следующие задачи:

  • вычисляет истинное и обратное направления относительно истинного севера;
  • вращает картушку компаса;
  • визуализирует текущее и обратное направления.

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

TrueHeading = e.SensorReading.TrueHeading;
  if ((180 <= TrueHeading) && (TrueHeading <= 360))
    ReciprocalHeading = TrueHeading - 180;
  Else
    ReciprocalHeading = TrueHeading + 180;

На рис. 7 показано, как обработчик обновляет текущее и обратное направления.

Рис. 7. Обновление текущего и обратного направлений

if (!Alphabetic) // визуализируем направление в числовом виде
{
  HeadingTextBlock.Text = TrueHeading.ToString();
  RecipTextBlock.Text = ReciprocalHeading.ToString();
}
else // визуализируем направление в буквенном виде
{
  if (((337 <= TrueHeading) && (TrueHeading < 360)) //
    ((0 <= TrueHeading) && (TrueHeading < 22)))
  {
    HeadingTextBlock.Text = "N";
    RecipTextBlock.Text = "S";
  }
  else if ((22 <= TrueHeading) && (TrueHeading < 67))
  {
    HeadingTextBlock.Text = "NE";
    RecipTextBlock.Text = "SW";
  }
  else if ((67 <= TrueHeading) && (TrueHeading < 112))
  {
    HeadingTextBlock.Text = "E";
    RecipTextBlock.Text = "W";
  }
  else if ((112 <= TrueHeading) && (TrueHeading < 152))
  {
    HeadingTextBlock.Text = "SE";
    RecipTextBlock.Text = "NW";
  }
  else if ((152 <= TrueHeading) && (TrueHeading < 202))
  {
    HeadingTextBlock.Text = "S";
    RecipTextBlock.Text = "N";
  }
  else if ((202 <= TrueHeading) && (TrueHeading < 247))
  {
    HeadingTextBlock.Text = "SW";
    RecipTextBlock.Text = "NE";
  }
  else if ((247 <= TrueHeading) && (TrueHeading < 292))
  {
    HeadingTextBlock.Text = "W";
    RecipTextBlock.Text = "E";
  }
  else if ((292 <= TrueHeading) && (TrueHeading < 337))
  {
    HeadingTextBlock.Text = "NW";
    RecipTextBlock.Text = "SE";
  }
}

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

CompassFace.RenderTransformOrigin = new Point(0.5, 0.5);
EllipseGlass.RenderTransformOrigin = new Point(0.5, 0.5);
transform.Angle = 360 - TrueHeading;
CompassFace.RenderTransform = transform;
EllipseGlass.RenderTransform = transform;

Переменная CompassFace соответствует фоновому изображению, которое содержит четыре точки компаса (N, E, W и S), а также горизонтальную и вертикальную линии. Переменная EllipseGlass соответствует слою с дымчатым стеклом.

Приложение-компас поддерживает два обработчика событий.

Прежде чем применять преобразование вращения, мне нужно убедиться, что преобразование центрируется на двух объектах, которые я буду поворачивать. Для этого вызывается метод RenderTransformOrigin каждого объекта и передаются координаты (0.5, 0.5). (Подробнее об этом методе и его использовании см. в MSDN Library страницу "UIElement.RenderTransformOrigin Property" по ссылке bit.ly/KIn8Zh.)

После этого можно вычислять угол и выполнять поворачивание. Я вычисляю угол, вычитая текущее направление из 360. (Это направление, которое я только что получил в обработчике событий.) Новый угол применяется через свойство RenderTransform.

Фиксация и разблокировка компаса Эта функциональность предназначена для навигации на открытой местности с картой в руке. Она весьмапроста; я вызываю метод Stop объекта compass для фиксации направления, а для возобновления приема значений направления - метод Start.

Метод Stop вызывается, когда пользователь нажимает LockButton:

private void LockButton_Click(object sender,
 RoutedEventArgs e)
{
  try
  {
    compass.Stop();
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message.ToString(),
      "Error!", MessageBoxButton.OK);
  }
}

Метод Start вызывается, когда пользователь нажимает UnlockButton:

private void UnlockButton_Click(object sender,
  RoutedEventArgs e)
{
  try
  {
    compass.Start();
    compass.TimeBetweenUpdates =
      TimeSpan.FromMilliseconds(400);
 // должно быть кратно 20
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message.ToString(),
      "Error!", MessageBoxButton.OK);
  }
}

Заметьте, что в дополнение к перезапуску компаса я переустанавливаю интервал отчетов в 400 мс, чтобы обеспечить согласованное поведение.

Переключение между цифровым и буквенным форматами вывода направлений Код такого переключения управляется единственной булевой переменной Alphabetic, значение которой задается, когда пользователь нажимает AlphaButton или NumericButton. При выборе AlphaButton эта переменная устанавливается в true, а при выборе NumericButton - в false.

Ниже приведен код обработчика события щелчка кнопки AlphaButton:

private void AlphaButton_Click(
    object sender, RoutedEventArgs e)
  {
    try
    {
      Alphabetic = true;
    }
    catch (Exception ex)
    {
      MessageBox.Show(
         ex.Message.ToString(), "Error!",
        MessageBoxButton.OK);
    }
  }

Код в обработчике compass_CurrentValueChanged проверяет значение Alphabetic и определяет формат вывода значений направления.

Поддержка тем с повышенной и пониженной яркостью Создав это приложение и отправив его в App Hub для сертификации, я был крайне удивлен уведомлению о том, что мое приложение не прошло сертификацию из-за того, что некоторые UI-элементы исчезали при выборе темы с повышенной яркостью (light visibility theme). (Я работал исключительно с темой пониженной яркости [dark visibility theme] и не удосужился проверить тему с повышенной яркостью.)

Чтобы устранить эту проблему, я добавил код в конструктор MainPage, которые получает текущую тему, а затем устанавливает основной цвет UI-элементов (текстовых блоков и кнопок) таким, чтобы они были видны при настройках текущей темы. Если установлена тема с повышенной яркостью, основные цвета элементов задаются черными и красными. А если действует тема с пониженной яркостью, основные цвета элементов становятся темно- и светло-серыми. Этот код приведен на рис. 8.

Рис. 8. Координация тем и цветов

Visibility isLight = (Visibility)Resources ["PhoneLightThemeVisibility"]; // для светлой темы
if (isLight == System.Windows.Visibility.Visible)
{
  // Операции в конструкторе
  SolidColorBrush scb = new SolidColorBrush(Colors.Black);
  SolidColorBrush scb2 = new SolidColorBrush(Colors.Red);
  RecipLabelTextBlock.Foreground = scb;
  HeadingLabelTextBlock.Foreground = scb;
  RecipTextBlock.Foreground = scb2;
  HeadingTextBlock.Foreground = scb2;
  LockButton.Foreground = scb;
  UnlockButton.Foreground = scb;
  AlphaButton.Foreground = scb;
  NumericButton.Foreground = scb;
}
else // выбрана темная цветовая схема - задаем соответствующие цвета текста
{
  // Операции в конструкторе
  SolidColorBrush scb = new SolidColorBrush(Colors.DarkGray);
  SolidColorBrush scb2 = new SolidColorBrush(Colors.LightGray);
  RecipLabelTextBlock.Foreground = scb;
  HeadingLabelTextBlock.Foreground = scb;
  RecipTextBlock.Foreground = scb2;
  HeadingTextBlock.Foreground = scb2;
  LockButton.Foreground = scb;
  UnlockButton.Foreground = scb;
  AlphaButton.Foreground = scb;
  NumericButton.Foreground = scb;
}

Интересно и полезно

Создавать это приложение было очень интересно; к тому же, она весьма полезное. Поработав с датчиками на платформе Windows Phone, я теперь лучше понимаю различия между этой платформой и поддержкой датчиков в Windows 8. Но что удивило меня больше всего, так это множество похожих средств. Уверен, что разработчик для Windows Phone, так или иначе имевший дело с пространством имен датчиков, найдет процесс перехода на Windows 8 исключительно простым. А в Windows 8 есть дополнительные датчики, такие как угломер (inclinometer), датчик ориентации и простой датчик ориентации (simple orientation sensor). (Датчик ориентации - это сплав нескольких датчиков, которые возвращают Quaternion, или матрицу вращения, которую можно использовать для управления сложными играми. Простой датчик ориентации позволяет распознавать, находится ваше устройство в портретном или альбомном режиме, а также смотрит оно экраном вниз или вверх.)

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


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=30896