Este artigo foi traduzido por máquina.

Toque e ouça

Uma interface de toque para um mapa de orientação

Charles Petzold

Baixar o código de exemplo

Charles PetzoldSempre que estou um pouco perdido em um shopping ou um museu, posso procurar um mapa, mas ao mesmo tempo muitas vezes eu sinto certa ansiedade sobre o que vai encontrar. Tenho certeza de que o mapa contará com uma seta rotulada, "Você está aqui", mas como o mapa será orientado? Se o mapa é montado verticalmente, o lado direito do mapa correspondem realmente à minha direita e a inferior correspondem ao que está atrás de mim? Ou o mapa precisa ser removido mentalmente de sua montaria e torcida no espaço para alinhar com o layout real?

Mapas que são montados em um ângulo ou paralela com o chão são muito melhores — fornecidas, ou seja, eles são orientados corretamente para começar com. Independentemente a agilidade mental com relações espaciais, os mapas são mais fáceis de ler quando está paralelos à terra, ou pode ser giradas para a frente para alinhar-se com a terra. Antes da idade de GPS, era comum ver pessoas lutando com roteiros de papel por descontroladamente torcê-las para a direita, esquerda e cabeça para baixo em busca de orientação adequada.

Mapas que são implementados no software em celulares e outros dispositivos móveis têm o potencial de orientar-se com base em uma leitura da bússola. Este é o impulso por trás de minha busca para exibir um mapa em um dispositivo Windows Phone que gira em relação ao telefone. Esse mapa deve ser capaz de alinhar-se com a paisagem circundante e, potencialmente, ser mais útil para os perdidos entre nós.

Orientar o mapa

Meu objetivo inicial era um pouco mais ambicioso do que um mapa de rotação. O programa que eu imaginado realmente flutuaria mapa no espaço 3D, então seria sempre paralela à superfície da terra, como sendo orientada com a bússola.

Um pouco experimentação convenceu-me que esta abordagem foi um pouco mais extravagante do que eu precisava. Embora um mapa inclinado com perspectiva é bom para GPS exibe nos automóveis, acho que é porque o mapa é sempre inclinado o mesmo grau, e que imita um pouco o que você está vendo fora do pára-brisa. Com um mapa em um dispositivo móvel, a inclinação tem o efeito de comprimir os visuais do mapa sem fornecer qualquer informação adicional. Uma simples rotação bidimensional parecia ser suficiente.

Na minha coluna de novembro, discuti como usar os serviços do Bing Maps SOAP para baixar e montar os azulejos quadrados de 256 pixels em um mapa ("montagem Bing mapa telhas no Windows Phone," msdn.microsoft.com/magazine/jj721603). As telhas disponíveis a partir deste serviço Web são organizadas em níveis de zoom, onde cada nível superior tem o dobro da resolução do nível anterior, que significa que cada telha cobre a mesma área como quatro telhas no nível mais elevado seguinte.

O programa em botões de barra de aplicativo contido no mês passado, coluna rotulada com mais e menos sinais para aumentar e diminuir o nível de zoom em saltos discretos. Que tipo de interface é adequado para um mapa em um site, mas para um telefone, a única descrição que parece apropriada é "totalmente coxo."

Isso significa que agora é hora de implementar uma interface de toque real que permite que o mapa a ser ampliada continuamente.

Depois que eu comecei a adicionar a interface de toque — um único dedo para pan, dois dedos para zoom in e out — adquiri um respeito profundo e duradouro para o aplicativo de mapas no Windows Phone e para o controle de mapa do Silverlight. Estes mapas, obviamente, implementam uma interface de toque muito mais sofisticada do que o que eu tenho sido capaz de gerenciar.

Por exemplo, eu não acho que eu já vi um furo preto abrir o app de mapas porque uma telha está ausente. É minha experiência que a tela é sempre inteiramente coberta — embora, obviamente, às vezes com uma telha que foi esticada além do ponto de reconhecimento. Telhas são substituídas por telhas de melhor resolução com uma animação de desvanecimento. Inércia é implementada de uma forma muito natural, e a interface do usuário nunca fica nervoso enquanto as telhas estão sendo baixadas.

Meu programa OrientingMap (que você pode fazer o download) chega em nenhum lugar perto para a real aplicação de mapas. O panning e expansão é muitas vezes saltitante, não há nenhuma inércia e áreas em branco aparecem com freqüência se telhas não são baixadas rapidamente o suficiente.

Apesar destas deficiências, meu programa consegue manter uma orientação do mapa com o mundo que retrata.

A questão básica

Os serviços do Bing Maps sabão dar acesso a um programa para telhas de 256 pixels quadrados mapa do qual ele pode construir mapas compostos maiores. Para a estrada e vistas aéreas, mapas do Bing torna disponíveis 21 níveis de zoom, onde a nível 1 cobre a terra com quatro telhas, nível 2, com 16 telhas, nível 3 com 64 e assim por diante. Cada nível oferece o dobro da resolução horizontal e o dobro da resolução vertical do próximo nível inferior.

Telhas têm uma relação pai-filho: Exceto para telhas em nível 21, cada telha tem quatro filhos no nível mais elevado seguinte que juntos cobrem a mesma área como próprio, mas com o dobro da resolução.

Quando um programa varas para níveis de zoom integral — como faz o programa apresentado na coluna do mês passado — os azulejos individuais podem ser exibidos no seu tamanho real do pixel. Programa do mês passado sempre exibe 25 telhas em uma matriz de 5 × 5, para um tamanho total de 1.280 pixels quadrados. O programa sempre posições essa matriz de telhas, para que o centro da tela corresponde à localização do telefone no mapa, que é um local em algum lugar na telha centro. Faça as contas e verá que, mesmo se um canto de azulejo centro fica no centro da tela, este tamanho de 1.280 pixel quadrado é adequado para o tamanho de tela de 480 × 800 do telefone, independentemente como ela é girada.

Porque o programa do mês passado suporta níveis de zoom apenas discreta e centros sempre as telhas com base na localização do telefone, ele implementa uma lógica extremamente simplista substituindo completamente estas 25 telhas, sempre que ocorrer uma alteração. Felizmente, o cache de download torna este processo bastante rápido se as telhas estão sendo substituídas com telhas baixadas anteriormente.

Com uma interface de toque, essa abordagem simples não é mais aceitável.

A parte mais difícil é definitivamente o dimensionamento: Por exemplo, suponha que o programa começa exibindo o mapa telhas de nível 12 no seu tamanho de pixel. Agora o usuário coloca dois dedos na tela e move os dedos para ampliar a tela. O programa deve responder por escala as telhas para além de seus tamanhos de 256 pixels. Ele pode fazer isso com um ScaleTransform sobre as telhas em si, ou com um ScaleTransform aplicado a uma tela em que são montadas as telhas.

Mas você não quer expandir estas telhas indefinidamente! Em algum momento você deseja substituir cada telha com quatro telhas de filho de nível superior para a próximo e meia o fator de escala. Este processo de substituição seria bastante trivial se as telhas de criança foram imediatamente disponíveis, mas, é claro, não eles. Eles devem ser baixados, que significa que telhas de criança devem ser posicionadas visualmente em cima do pai, e somente quando todos os quatro criança telhas foram baixadas pode o pai ser removido da tela.

O processo oposto deve ocorrer em um zoom out. Como o usuário aperta dois dedos juntos, todo o conjunto de telhas pode ser reduzido, mas em algum momento cada grupo de quatro peças deve ser substituído com uma telha de pai visualmente debaixo das quatro telhas. Somente quando a telha que pai foi baixada podem ser removidas a quatro crianças.

Classes adicionais

Conforme discutido na coluna do mês passado, Bing Maps usa um sistema de numeração, chamado um "quadkey" para identificar com exclusividade o mapa telhas. Um quadkey é um número de base-4: O número de dígitos no quadkey indica o nível de zoom, e os dígitos se codificam um intercalado de longitude e latitude.

Para ajudar o programa de OrientingMap trabalhar com quadkeys, o projeto inclui uma classe QuadKey que define as propriedades para obter quadkeys de pai e filho.

O projeto OrientingMap tem também uma nova classe de MapTile que deriva de UserControl. O arquivo XAML para este controle é mostrado na Figura 1. Ele tem um elemento de imagem com sua propriedade de fonte definida como um BitmapImage objeto para exibir o azulejo de bitmap como um ScaleTransform para dimensionamento a telha toda para cima ou para baixo. (Na prática, telhas individuais só são dimensionadas por positivas e negativas integrais potências de 2.) Para depuração, coloquei um TextBlock no arquivo XAML que exibe o quadkey, e eu deixei que em: Basta altere o atributo de visibilidade como Visible para vê-lo.

Figura 1: O arquivo de MapTile.xaml de OrientingMap

 

<UserControl x:Class="OrientingMap.MapTile" ...
>   <Grid>     <Image Stretch="None">       <Image.Source>         <BitmapImage x:Name="bitmapImage"                      ImageOpened="OnBitmapImageOpened" />       </Image.Source>            </Image>     <!-- Display quadkey for debugging purposes -->     <TextBlock Name="txtblk"                Visibility="Collapsed"                Foreground="Red" />   </Grid>   <UserControl.RenderTransform>     <ScaleTransform x:Name="scale" />   </UserControl.RenderTransform> </UserControl>

O arquivo code-behind para MapTile define várias propriedades úteis: A propriedade QuadKey permite que a classe de MapTile-se para obter o URI para acessar a telha do mapa; uma propriedade de escala permite código externo definir o fator de escala; uma propriedade IsImageOpened indica quando foi baixado o bitmap; e uma ImageOpened propriedade fornece acesso externo para o evento ImageOpened do objeto BitmapImage. Essas duas últimas Propriedades ajudam o programa a determinar quando uma imagem foi carregada para que o programa pode remover qualquer telhas que substitui a imagem.

Ao desenvolver este programa, eu inicialmente seguido um regime onde cada objeto MapTile usaria sua propriedade de escala para determinar quando deve ser substituído com um grupo de quatro objetos de MapTile filho, ou um pai MapTile. O MapTile si iria lidar com a criação e posicionamento desses novos objetos, definindo manipuladores para os eventos de ImageOpened e também seria responsável por retirar-se da tela.

Mas eu não poderia começar este esquema para funcionar muito bem. Considere uma matriz de 25 telhas de mapa que o usuário expande através da interface de toque. Estes 25 azulejos são substituídos com 100 telhas, e então as 100 telhas são substituídas com 400 telhas. Isso faz sentido? Não, não, porque a escala efetivamente mudou muitos destes potenciais telhas novas muito longe da tela para ser visível. A maioria deles não deve ser criada ou baixada em tudo!

Em vez disso, eu mudei essa lógica ao MainPage. Esta classe mantém um campo de currentMapTiles do tipo dicionário < QuadKey, MapTile >. Armazena todos os objetos de MapTile atualmente no visor, mesmo se eles estão ainda no processo de sendo baixado. Um método chamado RefreshDisplay usa o local atual do mapa e um fator de escala para montar um campo de validQuadKeys do tipo lista <QuadKey>. Se existir um objeto de QuadKey no validQuadKeys, mas não em currentMapTiles, um MapTile novo é criado e adicionado para a lona e o currentMapTiles.

RefreshDisplay não remove MapTile objetos que não são mais necessários, ou porque tenho sido criticados fora da tela ou substituídos com pais ou filhos. Que é a responsabilidade de um segundo método importante chamado limpeza. Este método compara a coleção de validQuadKeys com currentMapTiles. Se encontrar um item no currentMapTiles que não está no validQuadKeys, ele apenas remove que MapTile se validQuadKeys não tem filhos, ou se as crianças em validQuadKeys todos foram baixadas ou se validQuadKeys contém um pai do que MapTile e que o pai foi baixado.

Fazendo com que os métodos RefreshDisplay e limpeza mais eficiente — e invoca-las menos freqüentemente — é uma abordagem para melhorar o desempenho de OrientingMap.

Telas aninhadas

A interface do usuário para o programa de OrientingMap requer dois tipos de transformações de gráficos: tradução para único-dedo, garimpando e escalonamento para dois dedos pitada operações. Além disso, orientar o mapa com a direção do Norte exige uma transformação de rotação. Para implementar estas com transformações de Silverlight eficientes, o arquivo MainPage XAML contém três níveis de painéis de lona, como mostrado na Figura 2.

Figura 2 muita a MainPage. XAML arquivo para OrientingMap

<phone:PhoneApplicationPage x:Class="OrientingMap.MainPage" ...
>   <Grid x:Name="LayoutRoot" Background="Transparent">     <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12">       <TextBlock Name="errorTextBlock"                  HorizontalAlignment="Center"                  VerticalAlignment="Top"                  TextWrapping="Wrap" />       <!-- Rotating Canvas with origin in center of screen -->       <Canvas HorizontalAlignment="Center"               VerticalAlignment="Center">         <!-- Translating Canvas for panning -->         <Canvas>           <!-- Scaled Canvas for images -->           <Canvas Name="imageCanvas"                   HorizontalAlignment="Center"                   VerticalAlignment="Center">               <Canvas.RenderTransform>                 <ScaleTransform x:Name="imageCanvasScale" />               </Canvas.RenderTransform>           </Canvas>           <!-- Circle to show location -->           <Ellipse Name="locationDisplay"                    Width="24"                    Height="24"                    Stroke="Red"                    StrokeThickness="3"                    HorizontalAlignment="Center"                    VerticalAlignment="Center"                    Visibility="Collapsed">             <Ellipse.RenderTransform>               <TranslateTransform x:Name="locationTranslate" />             </Ellipse.RenderTransform>           </Ellipse>           <Canvas.RenderTransform>             <TranslateTransform x:Name="imageCanvasTranslate" />           </Canvas.RenderTransform>         </Canvas>         <Canvas.RenderTransform>           <RotateTransform x:Name="imageCanvasRotate" />         </Canvas.RenderTransform>       </Canvas>       <!-- Arrow to show north -->       <Border HorizontalAlignment="Left"               VerticalAlignment="Top"               Background="Black"               Width="36"               Height="36"               CornerRadius="18">         <Path Stroke="White"               StrokeThickness="3"               Data="M 18 4 L 18 24 M 12 12 L 18 4 24 12">           <Path.RenderTransform>             <RotateTransform x:Name="northArrowRotate"                              CenterX="18"                              CenterY="18" />           </Path.RenderTransform>         </Path>       </Border>       <!-- "powered by bing" display -->       <Border Background="Black"               HorizontalAlignment="Center"               VerticalAlignment="Bottom"               CornerRadius="12"               Padding="3">         <StackPanel Name="poweredByDisplay"                     Orientation="Horizontal"                     Visibility="Collapsed">           <TextBlock Text=" powered by "                      Foreground="White"                      VerticalAlignment="Center" />           <Image Stretch="None">             <Image.Source>               <BitmapImage x:Name="poweredByBitmap" />             </Image.Source>           </Image>         </StackPanel>       </Border>     </Grid>   </Grid>   ...
</phone:PhoneApplicationPage>

O Grid chamado ContentPanel contém a lona ultraperiférica, bem como os três elementos que são sempre exibidos em locais fixos na tela: um TextBlock para relatório de erros de inicialização, uma borda que contém uma seta de rotação para exibir a direção do Norte e outra borda para exibir o logotipo do Bing.

A lona ultraperiférica tem sua HorizontalAlignment e Vertical­Propriedades de alinhamento definido como centro, o que diminui a tela para um tamanho de zero posicionado no centro da grade. A (0, 0) coordenada desta tela, portanto, é o centro do visor. Esta centralização é conveniente para telhas de posicionamento e também permite que a escala e rotação ocorrer em torno da origem.

Tela mais externa é o que é girado com base na direção do Norte. Dentro desta tela ultraperiférica é uma segunda tela que tem um TranslateTransform. Isto é para garimpar. Sempre que um único dedo varre toda a tela, o mapa inteiro pode ser movido simplesmente por configuração as propriedades X e Y, deste TranslateTransform.

Dentro desta segunda tela é uma elipse usada para indicar o local atual do telefone em relação ao centro do mapa. Quando o usuário garimpa o mapa, esta elipse se move também. Mas se o GPS do telefone informa a mudança de localização, uma separado traduza­transformação na elipse move-lo em relação ao mapa.

A tela mais interna é chamada imageCanvas, e é aqui que as telhas de mapa realmente são montadas. O ScaleTransform aplicado a esta tela permite que o programa aumentar ou diminuir esta Assembléia inteira de telhas mapa baseado no usuário zoom dentro ou para fora com uma pitada de manipulação.

Para acomodar o zoom contínuo, o programa mantém um campo zoomFactor do tipo double. Este zoomFactor tem a mesma faixa como os níveis de telha — de 1 a 21 — o que significa que é na verdade o logaritmo de base 2 do fator de escala de mapa total. Sempre que o zoomFactor aumenta em 1, a escala do mapa de dobra.

A primeira vez que o programa é executado, zoomFactor é inicializado para 12, mas a primeira vez que o usuário toca a tela com dois dedos, torna-se um valor não-integral e continua a ser muito provável um valor não-integral posteriormente. O programa salva zoomFactor como uma configuração de usuário e recarrega-lo na próxima vez que o programa é executado. Um baseLevel integrante inicial é calculado com um simples truncamento:

baseLevel = (int)zoomFactor;

Este baseLevel é sempre um número inteiro no intervalo entre 1 e 21, e daí é diretamente apropriada para recuperação de telhas. Partir desses dois números, o programa calcula um fator de escala não-logarítmico do tipo double:

canvasScale = Math.Pow(2, zoomFactor - baseLevel);

Este é o fator de escala aplicado à tela mais íntima. Por exemplo, se o zoomFactor é 10.5, então o baseLevel usado para recuperar as telhas é 10, e canvasScale é 1.414.

Se a inicial zoomFactor é 10,9, pode fazer mais sentido definir baseLevel no 11 e canvasZoom em 0.933. O programa não faz isso, mas é obviamente um refinamento possível.

Entrada de toque de um e dois dedos

Para a entrada de toque, senti-me mais confortável usando o XNA TouchPanel do que o Silverlight manipulação de eventos. Construtor MainPage permite quatro tipos de gestos XNA: FreeDrag (pan), DragComplete, apertar e PinchComplete. O TouchPanel está marcada para a entrada em um manipulador para o evento CompositionTarget, como mostrado na Figura 3. Devido à sua complexidade, só um pouco da pitada processamento é mostrado aqui.

Figura 3 toque de processamento em OrientingMap

void OnCompositionTargetRendering(object sender, EventArgs args) {   while (TouchPanel.IsGestureAvailable)   {     GestureSample gesture = TouchPanel.ReadGesture();     switch (gesture.GestureType)     {       case GestureType.FreeDrag:         // Adjust delta for rotation of canvas         Vector2 delta = TransformGestureToMap(gesture.Delta);         // Translate the canvas         imageCanvasTranslate.X += delta.X;         imageCanvasTranslate.Y += delta.Y;         // Adjust the center longitude and latitude         centerRelativeLongitude -= delta.X / (1 << baseLevel + 8) / canvasScale;         centerRelativeLatitude -= delta.Y / (1 << baseLevel + 8) / canvasScale;         // Accumulate the panning distance         accumulatedDeltaX += delta.X;         accumulatedDeltaY += delta.Y;         // Check if that's sufficient to warrant a screen refresh         if (Math.Abs(accumulatedDeltaX) > 256 ||             Math.Abs(accumulatedDeltaY) > 256)         {           RefreshDisplay();           accumulatedDeltaX = 0;           accumulatedDeltaY = 0;         }         break;       case GestureType.DragComplete:         Cleanup();         break;       case GestureType.Pinch:         // Get the old and new finger positions relative to canvas origin         Vector2 newPoint1 = gesture.Position - canvasOrigin;         Vector2 oldPoint1 = newPoint1 - gesture.Delta;         Vector2 newPoint2 = gesture.Position2 - canvasOrigin;         Vector2 oldPoint2 = newPoint2 - gesture.Delta2;         // Rotate in accordance with the current rotation angle         oldPoint1 = TransformGestureToMap(oldPoint1);         newPoint1 = TransformGestureToMap(newPoint1);         oldPoint2 = TransformGestureToMap(oldPoint2);         newPoint2 = TransformGestureToMap(newPoint2);         ...
RefreshDisplay();         break;       case GestureType.PinchComplete:         Cleanup();         break;     }   } }

A entrada FreeDrag é acompanhada por valores de posição e Delta (ambos do tipo Vector2) que indica a posição atual do dedo, e como o dedo se moveu desde o último evento de TouchPanel. A entrada de pitada suplementos com Position2 e Delta2 valores para o segundo dedo.

No entanto, tenha em mente que esses valores Vector2 são coordenadas na tela! Porque o mapa é girado em relação à tela, e o usuário espera o mapa para se deslocar no mesmo sentido, como um dedo se move — esses valores devem ser girados com a rotação atual do mapa, que ocorre em um pequeno método chamado TransformGestureToMap.

Para FreeDrag de processamento, o valor de delta é então aplicado ao TranslateTransform no arquivo XAML, como dois campos de ponto flutuante, denominados centerRelativeLongitude e centerRelativeLatitude. Esses valores variam de 0 a 1 e indicam a longitude e a latitude correspondente ao centro da tela.

Em algum momento, o usuário pode pan o mapa em grau suficiente que telhas novas precisam ser carregadas. Para evitar a verificação para essa possibilidade com cada evento de toque, o programa mantém dois campos chamados accumulatedDeltaX e accumulatedDeltaY e apenas chamadas de RefreshDisplay, quando qualquer valor vai acima de 256, que é o tamanho do pixel de telhas mapa.

Porque RefreshDisplay tem um grande trabalho para fazer — determinar que as telhas deve ser visível na tela baseada em centerRelativeLongitude e centerRelativeLatitude e o canvasScale atual e criando telhas novas, se necessário — é melhor que ele não será chamado para cada mudança na entrada por toque. Um acessório definitivo para o programa iria limitar chamadas de RefreshDisplay durante a entrada de pitada.

Durante o processamento de toque, o método de limpeza só é chamado quando o dedo ou dedos deixaram a tela. Limpeza também é chamada sempre que uma telha mapa concluiu Transferindo.

Os critérios para alterar baseLevel — e iniciando assim uma substituição de uma telha de mapa do pai pelos filhos, ou filhos de um pai — é muito descontraído. O baseLevel só é incrementado quando canvasScale torna-se maior que 2 e diminuído quando canvasScale cai para menos de 0,5. Definir pontos de transição melhores é outra melhoria óbvia.

O programa agora tem apenas dois botões de barra de aplicativos: O primeiro alterna entre estrada e vista aérea e as segunda posições do mapa para que o local atual é no centro.

Agora eu só preciso descobrir como fazer com que o programa me ajude a navegar, shoppings e museus.

Charles Petzold é um colaborador de longa data para MSDN Magazine e autor de "Programação Windows, 6ª edição" (o ' Reilly Media, 2012), um livro sobre como escrever aplicativos para Windows 8. Seu site é charlespetzold.com.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Thomas Petchel