Windows Phone

Escrevendo um aplicativo de bússola para o Windows Phone

Donn Morse

Baixar o código de exemplo

Como editor responsável pela documentação da plataforma de sensor do Windows 8, quero que o maior número possível de desenvolvedores adote nossa nova plataforma. E, como os aplicativos estilo Metro podem ser escritos usando XAML e C#, o desenvolvedor do Windows Phone é um candidato ideal para essa migração. Os desenvolvedores do Windows Phone já têm experiência com XAML, e vários deles também têm experiência com sensores (porque os sensores acelerômetro, bússola, giro e GPS estão expostos na versão mais recente do Windows Phone).

Para me ajudar a entender melhor os desenvolvedores do Windows Phone e sua plataforma de desenvolvimento, decidi escrever um aplicativo de bússola simples no inverno passado. Quando acabei de escrevê-lo, enviei uma versão gratuita para o Windows Phone Marketplace usando o App Hub. Desde sua aceitação, o aplicativo foi baixado por usuários do Windows Phone de lugares tão distantes quanto Suíça e Malásia.

Este artigo aborda o desenvolvimento do aplicativo.

O aplicativo de bússola

O aplicativo de bússola usa a bússola, ou magnetômetro, interna ao dispositivo Windows Phone. Esse aplicativo fornece um rumo com relação ao norte verdadeiro além de um rumo recíproco que pode ser útil ao navegar em um barco ou ao se orientar em uma área remota com um mapa. Além disso, o aplicativo permite ao usuário alternar de um rumo numérico (por exemplo, “090” graus) para um rumo alfabético (por exemplo, “E” para este). O aplicativo também permite ao usuário bloquear o rumo atual. Isso é útil quando os usuários precisam de que o ponteiro permaneça estacionário para que possam determinar a posição de um marco ou ponto de referência específico em um mapa ou gráfico.

A Figura 1 mostra o aplicativo sendo executado em um Samsung Focus. A imagem à esquerda exibe um rumo numérico e a imagem à direita exibe um rumo alfabético.

The Running App, Showing a Numeric Heading (Left) and an Alpha Heading (Right)
Figura 1 O aplicativo em execução, mostrando um rumo numérico (à esquerda) e um rumo alfabético (à direita)

Projetando a interface do usuário

Como desenvolvedor acostumado a escrever aplicativos para o PC, inicialmente, me senti limitado pelo espaço de tela reduzido do telefone. No entanto, isso não foi um empecilho. Precisei apenas pensar mais um pouco e melhor sobre os recursos em meu aplicativo com relação às novas dimensões de tela. Meu aplicativo de bússola tem duas telas: uma tela de calibragem e a tela de navegação principal.

A tela de calibragem A bússola, ou magnetômetro, instalada em um dispositivo Windows Phone precisa de calibragem depois que o dispositivo é ligado. Além disso, esses sensores podem precisar de nova calibragem periódica. Para ajudá-lo a detectar de forma programática quando a calibragem é necessária, a plataforma dá suporte a uma propriedade HeadingAccuracy que pode ser usada para detectar a calibragem atual. Além disso, a plataforma dá suporte a um evento Calibrate que é disparado se a bússola necessitar de calibragem.

Meu aplicativo manipula o evento Calibrate, que, por sua vez, exibe uma tela de calibragem (calibrationStackPanel) que solicita ao usuário calibrar manualmente o dispositivo varrendo o telefone em um movimento na forma de 8. Enquanto o usuário varre o telefone, a precisão atual é exibida no CalibrationTextBlock com uma fonte vermelha até que a precisão desejada seja atingida, conforme mostrado na Figura 2. Quando a precisão retornada for menor ou igual a 10°, os valores numéricos serão apagados e um “Complete!” verde aparecerá.

The Calibration Screen
Figura 2 A tela de calibragem

O código correspondente que dá suporte à calibragem é encontrado no módulo MainPage.xaml.cs dentro do manipulador de eventos compass_Current­ValueChanged, conforme mostrado na Figure 3.

Figura 3 Calibrando a bússola

...
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");
 }
}

Quando a precisão desejada é atingida, o usuário é solicitado a pressionar o botão Done, que oculta a tela de calibragem e exibe a tela principal do aplicativo.

A tela principal Essa tela exibe um rumo numérico ou alfabético e o valor recíproco. Além disso, ela renderiza o mostrador de uma bússola orientada com relação ao norte verdadeiro. Finalmente, a tela principal exibe os quatro controles (ou botões) que permitem ao usuário alterar a saída além de bloquear o valor de rumo e o mostrador da bússola.

A Figura 4 mostra a tela principal do meu aplicativo, MainPage.xaml, como ela aparece no Visual Studio.

The App’s Primary Screen in Visual Studio
Figura 4 A tela principal do aplicativo no Visual Studio

A maior parte dos elementos da interface do usuário na tela principal são controles TextBlock e Button simples. Os blocos de texto identificam o rumo e seu valor recíproco. Os botões permitem ao usuário controlar a saída. No entanto, o mostrador da bússola está um pouco mais envolvido.

O mostrador da bússola O mostrador da bússola consiste em três componentes: uma imagem de plano de fundo, uma imagem de primeiro plano e uma elipse maior, com bordas, na qual as imagens de primeiro plano e de plano de fundo giram. A imagem de plano de fundo contém as letras correspondentes aos quatro pontos em uma bússola, uma linha horizontal e uma linha vertical. A imagem em primeiro plano cria o efeito de vidro esfumaçado.

A imagem de plano de fundo No XAML, a imagem de plano de fundo é chamada de CompassFace (esse nome de variável é referenciado posteriormente no código que gira o mostrador da bússola):

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

A imagem de primeiro plano O primeiro plano do mostrador da bússola, EllipseGlass, é definido no próprio XAML. O efeito de vidro esfumaçado é criado usando um pincel de gradiente linear. Criei essa elipse usando o Microsoft Expression Blend 4. A ferramenta Expression Blend é compatível com o Visual Studio e permite carregar o XAML de seu aplicativo e aprimorar a interface do usuário personalizando os elementos gráficos. A Figura 5 mostra o editor Expression Blend como ele apareceu ao criar a elipse sombreada.

Creating a Shaded Ellipse in Expression Blend
Figura 5 Criando uma elipse sombreada no Expression Blend

Depois de terminar a edição da elipse no Expression Blend, o XAML em meu projeto Visio Studio foi atualizado com o seguinte 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>

Observe que as dimensões de EllipseGlass (263 x 263 pixels) são uma correspondência exata das dimensões de compass.png. Observe também que o nome do objeto, EllipseGlass, é referenciado posteriormente no código que executa a rotação do mostrador da bússola.

A borda do mostrador da bússola O mostrador da bússola gira dentro de uma elipse branca maior com uma borda vermelha. Essa elipse é definida no XAML e é chamada de EllipseBorder:

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

O código por trás da interface do usuário

O código reside no arquivo MainPage.xaml.cs no download de código fornecido, e ele acessa os namespaces requeridos pelo aplicativo, inicializa o sensor, define um intervalo de relatórios e lida com os vários recursos do aplicativo: calibragem, girar o mostrador da bússola, alternar entre saída numérica e alfabética e assim por diante.

Acessando a bússola em seu código A primeira etapa ao escrever um aplicativo de bússola (ou qualquer aplicativo que acesse um dos sensores do telefone) é obter acesso aos objetos de sensor expostos pelo namespace Microsoft.Devices.Sensors. Isso é executado com a seguinte diretiva using em MainPage.xaml.cs:

using Microsoft.Devices.Sensors;

Quando essa diretiva using aparece no arquivo, posso criar uma variável compass que me dá acesso programático ao dispositivo real no telefone:

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

Usarei essa variável para iniciar a bússola, pará-la, recuperar a precisão de rumo atual, definir o intervalo de relatório e assim por diante.

Iniciando a bússola e definindo a frequência dos relatórios Depois de criar a variável compass, posso começar a invocar os métodos e definir as propriedades no objeto. O primeiro método que invoquei é o método Start, que me permite começar a receber dados do sensor. Depois de iniciar a bússola, defino o intervalo de relatório, o tempo entre atualizações do sensor, para 400 ms (observe que a propriedade TimeBetweenUpdates requer um múltiplo de 20 ms):

compass.TimeBetweenUpdates = 
  TimeSpan.FromMilliseconds(400);  // Must be multiple of 20
compass.Start();

O valor de 400 ms foi escolhido por tentativa e erro. O intervalo de relatório padrão é extremamente curto. Se você tentar executar o aplicativo com esse valor padrão, o mostrador da bússola é girado com tanta frequência que ele parece estar instável.

Estabelecendo os manipuladores de eventos da bússola O aplicativo de bússola dá suporte a dois manipuladores de eventos: um que exibe a página de calibragem (calibration­StackPanel) e outro que renderiza o rumo atual e gira o mostrador da bússola.

Definindo e estabelecendo o manipulador de eventos Calibration O manipulador de eventos Calibration contém relativamente poucas linhas de código. Esse código, mostrado na Figura 6, executa duas tarefas principais: Primeira, exibe a tela de calibragem que é definida no arquivo MainPage.xaml; segunda, define uma variável booliana de calibragem como true.

Figura 6 O manipulador de eventos Calibration

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);
  }
}

Como esse manipulador de eventos é chamado de um thread de segundo plano, ele não tem acesso direto ao thread da interface do usuário. Assim, para exibir a tela de calibragem, preciso chamar o método BeginInvoke do objeto Dispatcher.

A variável booliana de calibragem é examinada dentro do código do manipulador de eventos de valor alterado (compass_CurrentValueChanged). Quando essa variável for true, ignoro a bússola e atualizo a tela de calibragem com os dados de calibragem mais recentes. Quando a variável for false, atualizo as leituras da bússola e executo rotações do mostrador da bússola.

Esse manipulador de eventos é estabelecido no construtor MainPage com a seguinte linha de código:

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

Definindo e estabelecendo o manipulador de valor alterado O manipulador de eventos de valor alterado (compass_CurrentValueChanged) é invocado todas as vezes que uma nova leitura chega da bússola. E, dependendo do estado da variável de calibragem, atualiza a tela de calibragem ou atualiza a tela principal.

Quando está atualizando a tela principal, o manipulador de eventos executa as seguintes tarefas:

  • Ele calcula os rumos verdadeiro e recíproco com relação ao norte verdadeiro.
  • Ele gira o mostrador da bússola.
  • Ele renderiza os rumos atual e recíproco.

Calculando os rumos O código a seguir demonstra como o manipulador de eventos recupera o rumo em relação ao norte verdadeiro usando a propriedade TrueHeading do objeto SensorReading:

 

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

A Figura 7 demonstra como o manipulador de eventos atualiza os rumos atual e recíproco.

Figura 7 Atualizando os rumos atual e recíproco

if (!Alphabetic) // Render numeric heading
{
  HeadingTextBlock.Text = TrueHeading.ToString();
  RecipTextBlock.Text = ReciprocalHeading.ToString();
}
else // Render alpha heading
{
  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";
  }
}

Girando o mostrador da bússola O seguinte trecho de código demonstra como o aplicativo gira as duas elipses que constituem o plano de fundo e o primeiro plano do mostrador da bússola:

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;

A variável CompassFace corresponde à imagem do plano de fundo que contém os quatro pontos da bússola (N, E, W e S) e as linhas vertical e horizontal. A variável EllipseGlass corresponde à camada do vidro esfumaçado.

Antes que eu possa aplicar uma transformação de rotação, preciso assegurar que a transformação esteja centralizada nos dois objetos que irei girar. Isso é feito invocando o método RenderTransformOrigin em cada objeto e fornecendo as coordenadas (0,5, 0,5). (Para obter mais informações sobre esse método e seu uso, consulte a página da Biblioteca MSDN, “Propriedade UIElement.RenderTransformOrigin,” em bit.ly/KIn8Zh.)

Depois de centralizada a transformação, posso calcular o ângulo e executar a rotação. Calculo o ângulo subtraindo o rumo atual de 360. (Esse é o rumo que acabei de receber no manipulador de eventos.) Aplico esse novo ângulo com a propriedade RenderTransform.

Bloqueando e desbloqueando a bússola O recurso de bloqueio e desbloqueio foi destinado a pessoas que estão ao ar livre e que estão usando o aplicativo para navegar (seja em um barco ou caminhando por uma trilha com um mapa nas mãos). Esse recurso é simples; ele invoca o método Stop na bússola para bloquear o rumo e, em seguida, invoca o método Start para retomar a recuperação do rumo.

O método Stop é invocado quando o usuário pressiona LockButton:

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

O método Start é invocado quando o usuário pressiona UnLockButton:

private void UnlockButton_Click(object sender, 
  RoutedEventArgs e)
{
  try
  {
    compass.Start();
    compass.TimeBetweenUpdates =
      TimeSpan.FromMilliseconds(400);  
      // Must be multiple of 20
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message.ToString(), 
      "Error!", MessageBoxButton.OK);
  }
}

Observe que além de reiniciar a bússola, redefini o intervalo de relatório para 400 ms para garantir um comportamento consistente.

Alternando entre rumos numéricos e alfabéticos O código que alterna entre rumos numéricos e alfabéticos é controlado por uma variável booliana simples chamada Alphabetic, definida quando um usuário pressiona AlphaButton ou NumericButton. Quando o usuário pressiona AlphaButton, essa variável é definida como True; quando o usuário pressiona NumericButton, ela é definida como False.

Veja a seguir o código do evento de clique AlphaButton:

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

O código no manipulador de eventos compass_CurrentValueChanged examina Alphabetic para determinar se ele deve renderizar os rumos numéricos ou alfabéticos.

Suportando os temas de visibilidade no claro e no escuro Depois de criar o aplicativo e enviá-lo ao App Hub para certificação, fiquei surpreso ao receber uma notificação de que ele não tinha passado porque certos elementos da interface do usuário desapareciam quando o tema de visibilidade no claro era testado. (Eu o executei exclusivamente com o tema no escuro e não o testei com o tema no claro.)

Para solucionar esse problema, adicionei código ao construtor MainPage, que recupera o tema atual e, então, define a cor do primeiro plano dos elementos da interface do usuário (blocos de texto e botões) para funcionar com o determinado tema. Se o tema no claro for definido, as cores de primeiro plano dos elementos serão definidas para preto e vermelho. Se o tema no escuro for definido, as cores de primeiro plano dos elementos serão definidas para cinza escuro e cinza claro. A Figura 8 mostra esse código.

Figura 8 Coordenando temas e cores

Visibility isLight = (Visibility)Resources["PhoneLightThemeVisibility"]; // For light theme
if (isLight == System.Windows.Visibility.Visible) // Light theme enabled
{
  // Constructor technique
  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 // Dark color scheme is selected—set text colors accordingly
{
  // Constructor technique
  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;
}

Divertido e valioso

A criação desse aplicativo foi muito divertida e também valiosa. Tendo trabalhado com um sensor na plataforma Windows Phone, agora tenho um entendimento mais claro das diferenças entre essa plataforma e o suporte a sensor do Windows 8. Mas o que mais me surpreendeu foram as similaridades. Meu palpite é que se você for um desenvolvedor do Windows Phone que passou algum tempo com o namespace sensor, achará a migração para o Windows 8 excepcionalmente simples. E, no Windows 8, você encontrará sensores adicionais como o inclinômetro, o sensor de orientação e o sensor de orientação simples. (O sensor de orientação é uma fusão de vários sensores que retorna um Quaternion, ou matriz de rotação, que pode ser usado para controlar jogos complexos. O sensor de orientação simples permite detectar se seu dispositivo está em modo retrato ou paisagem, assim como virado para cima ou para baixo.

As oportunidades de desenvolvimento proporcionadas por todos esses sensores são empolgantes, e espero ver as maneiras imaginativas que nossa comunidade criativa de desenvolvedores possa colocá-los em uso.

Donn Morse é um escritor técnico de programação sênior na equipe do Windows na Microsoft, que tem se concentrado nos últimos anos na plataforma do sensor, desde o lado do aplicativo até o driver. Ele é apaixonado e fascinado por sensores e seu uso.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Jason Scott