本文章是由機器翻譯。

觸控與執行

方位圖的觸控介面

Charles Petzold

下載代碼示例

Charles Petzold
每當我有點迷失在一個購物中心或博物館,我搜索一張地圖,但在同一時間,我常覺得我會找到一些焦慮。我敢肯定地圖功能將標有箭頭,"你在這裡,"但地圖如何將著眼呢?如果垂直裝入該映射,則右側的地圖實際上對應于我的右邊和底部對應于在我的後面是什麼?或不會在地圖需要從其裝載弱智中刪除和扭曲空間線對齊的實際佈局中嗎?

安裝在某個角或與地板平行的地圖要好得多 — — 提供,也就是說,他們是面向正確開始。無論一個人的思維的敏捷與空間關係,地圖是易於閱讀時當平行于地球,或可以向前 swiveled,對準地球。之前 GPS 的時代,它是共同看到人們對付紙路地圖由瘋狂地扭動他們向右,左和顛倒了尋找適當的方向。

在手機和其他行動裝置上的軟體中實現的地圖有潛力東方本身基於一個閱讀的指南針。這是我尋求與手機旋轉的 Windows Phone 設備上顯示地圖背後的動力。這種地圖應能夠對齊本身與周圍景觀和可能會更有説明丟失我們當中。

定向地圖

我最初的目標是比旋轉地圖有點更加雄心勃勃。我所設想的程式將實際上浮法在 3D 空間中的一張地圖,因此它始終是平行于表面的地球,以及正在面向與羅盤。

小實驗,我確信這種做法是有點更奢侈不是我需要。雖然在汽車 GPS 顯示角度傾斜的地圖也很好,我認為這是因為地圖總是傾斜在同一程度和它有點模仿你看到出擋風玻璃。在行動裝置上的一幅地圖,擺式具有壓縮的地圖視覺效果而不提供任何附加資訊的效果。一個簡單的二維旋轉似乎是足夠。

在我 11 月專欄中,我討論了如何使用 Bing 地圖 SOAP 服務下載並裝配到地圖的 256 圖元廣場磚 ("裝配 Bing 地圖上的瓷磚 Windows Phone," msdn.microsoft.com/magazine/jj721603)。此 Web 服務可用的麻將牌被組織成縮放級別,其中每個更高級別具有雙原先的水準,這意味著每個圖塊作為下一步更高級別中的四個瓷磚面積相同的決議。

上個月的列包含的應用程式欄按鈕中的程式標記為加號和減號,增加和減少離散的跳轉的縮放級別。該類型的介面是足夠的地圖在網站上,但對於一個電話,只是描述似乎是適當的是"完全跛腳"。

這意味著現在是時候來實現真正觸摸介面,允許地圖中被不斷放大。

一旦我開始添加該觸摸介面 — — 一個單一的手指要平移、 兩個手指以放大和縮小 — — 我後天深遠和持久的尊重,Windows Phone 上的地圖應用程式和 Silverlight 映射控制。這些地圖顯然執行比我已經能夠管理一個複雜得多觸摸介面。

例如,我不認為我見過開放在地圖應用程式中,因為瓷磚是缺少黑色孔。它是我螢幕始終完全涵蓋的經驗 — — 雖然有時顯然與已經超出識別點緊張的圖塊。瓷磚被替換更好決議與淡入淡出動畫的瓷磚。慣性非常自然的方式,實現和 UI 從來沒有獲取心驚肉跳雖然正在下載瓷磚。

我 OrientingMap 程式 (這可以下載) 都無處接近真正的地圖應用程式。平移和擴張往往是心驚肉跳、 有沒有慣性和空白地區經常出現如果瓷磚不下載速度不夠快。

儘管這些不足之處,我的程式不會在維持與它所描繪的世界地圖的一種走向成功。

基本的問題

Bing 地圖 SOAP 服務給 256 圖元平方米地圖瓷磚從中可以興建較大複合地圖程式存取權限。對於道和空中視圖,Bing Maps 使得放大,可用 21 級別 1 級等等涵蓋四個瓷磚、 與 64 16 瓷磚、 3 級與 2 級與地球。每個級別提供兩倍的水準解析度和雙下一個較低級別的垂直解析度。

瓷磚有父-子關係:除了級別 21 中的瓷磚,每平鋪了下一步更高級別中共同覆蓋同一地區本身,但雙該決議的四個孩子。

當一個程式堅持整體縮放級別 — — 在最後一個月的專欄仲介紹的程式一樣 — — 各個圖塊可以顯示在它們的實際圖元大小。上個月的程式總是顯示 25 瓷磚在 5 × 5 陣列中,總大小的方形 1,280 圖元。該程式始終定位此陣列的瓷磚這樣,在螢幕的中心對應于手機的位置地圖,這是中心平鋪在某一個位置上。做數學題,你會發現即使中心瓷磚一角坐在螢幕的中心,此 1,280 圖元平方米大小是足夠的 480 × 800 螢幕大小的手機,無論它旋轉的方式。

因為上個月的程式支援只有離散的縮放級別和始終居中的麻將牌,基於手機的位置,它完全取代這些 25 瓷磚出現更改時實現極為簡單的邏輯。幸運的是,下載快取使這一過程相當快,如果正在替換以前下載的瓷磚瓷磚。

觸摸介面,這種簡單的方法不再是可以接受的。

難的部分是絕對縮放比例:例如,假設程式開始通過其圖元尺寸顯示地圖平鋪的 12 級。現在使用者將兩個手指放在螢幕上並移動手指分開擴大螢幕。該程式必須回應縮放超出其 256 圖元大小的麻將牌。它可以做到與本身的牆磚上的 ScaleTransform 或 ScaleTransform 應用於畫布的瓷磚進行組裝。

但您不想無限期地縮放這些瓦片 !有些時候,您想要每個圖塊替換四個兒童瓷磚的下一個較高的級別和一半的縮放因數。如果兒童瓷磚被立即可供使用,但是,當然,他們不是此更換過程會相當微不足道。它們是必須下載的這意味著兒童瓷磚必須以可視方式置於父,並已下載所有四個兒童麻將牌時,才可以父從刪除繪圖畫布。

相反的過程必須發生在縮小。作為使用者捏在一起的兩個手指,可以縮小整個陣列的瓷磚,但在某些點每一組的四個瓷磚,應取代與父直觀地底下四個瓷磚拼貼。已下載該父圖塊時,才可以刪除四個孩子。

其他類

在上個月的專欄中討論時,Bing Maps 使用一個叫做"quadkey"的編號系統唯一標識地圖瓦片。Quadkey 是一個基地 4 數位:在 quadkey 中的位數指示的縮放級別,和自己的數位編碼交錯的經度和緯度。

為協助 OrientingMap 程式與 quadkeys 合作,該專案包括一個 QuadKey 類,定義屬性以獲取父和子 quadkeys。

OrientingMap 專案也已從 UserControl 派生的新的 MapTile 類。此控制項的 XAML 檔所示圖 1。它具有圖像元素的源屬性設置為用於向上或向下縮放整個平鋪顯示點陣圖平鋪,以及 ScaleTransform BitmapImage 物件。(在實踐中,各個圖塊只縮放的積極和消極的積分權力的 2)。對於調試,我把文字區塊中的 XAML 檔的顯示 quadkey,和我離開,在:只需更改的可見度屬性為可見,才能看到它。

圖 1 來自 OrientingMap 的 MapTile.xaml 檔

<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>

MapTile 的代碼隱藏檔定義了幾個有用的屬性:QuadKey 屬性允許 MapTile 類本身獲得的 URI 用於訪問地圖底圖 ; 分攤比額表屬性允許外部代碼設置的縮放係數 ; IsImageOpened 屬性指示當已下載點陣圖 ; 並提供了外部訪問到的 ImageOpened 事件中的 BitmapImage 物件的 ImageOpened 屬性。 這最後兩個屬性説明確定以便程式可以刪除任何圖像取代的瓷磚時已載入圖像的程式。

在開發此程式時,在我最初進行一項計畫那裡每個 MapTile 物件將使用其規模屬性來確定何時應被替換一組的四個兒童 MapTile 物件或父 MapTile。 MapTile 本身會處理創造和定位的這些新的物件,設置的 ImageOpened 事件的處理常式,並還將負責從畫布中刪除本身。

但是我沒有得到這項計畫能夠工作得很好。 考慮使用者通過觸摸介面擴展的 25 地圖瓦片的陣列。 這些 25 的瓦片都被替換 100 瓷磚、 和 400 瓷磚然後替換的 100 瓦。 這樣清楚了吧? 不,不,因為縮放已有效地移動這些潛在的新瓷磚的許多太遠了螢幕是可見的。 大多數人都不應該創建,或在所有下載 !

相反,我這種邏輯轉向首頁。 此類維護類型字典 < QuadKey、 MapTile > 的 currentMapTiles 欄位。 這所有的 MapTile 物件目前上存儲顯示,即使它們仍在正在下載。 名為 RefreshDisplay 的方法使用當前地圖和比例因數的位置裝配類型清單 <QuadKey> 的 validQuadKeys 欄位。 如果在 validQuadKeys 中存在一個 QuadKey 物件,但不是在 currentMapTiles,新的 MapTile 是創建並添加到畫布和 currentMapTiles。

RefreshDisplay 不會刪除不再需要的 MapTile 物件或者因為他們是已經關閉螢幕移動全景說明的或者是替換為父母或兒童。 這是一個名為清理的第二個重要方法的責任。 此方法比較具有 currentMapTiles 的 validQuadKeys 集合。 如果它不在 validQuadKeys 中的 currentMapTiles 中找到專案,它只刪除該 MapTile 如果 validQuadKeys 有沒有孩子,或如果在 validQuadKeys 中的兒童都已下載,或如果 validQuadKeys 包含的父項,MapTile 和該父已被下載。

製作的 RefreshDisplay 和清理的方法更有效 — — 和經常援引這些較少 — — 是提高性能的 OrientingMap 的一種辦法。

嵌套的畫布

OrientingMap 程式的 UI 需要的圖形轉換的兩種類型:單手指平移和縮放的兩個手指捏的翻譯業務。 此外,面向北的方向與地圖需要旋轉變換。 要實現這些與高效 Silverlight 轉換,MainPage.xaml 檔中所示包含三個層次的換行面板, 圖 2

圖 2 MainPage.xaml 的大部分的檔 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>

名為 ContentPanel 的網格包含在最外面的畫布,以及始終顯示在螢幕上的固定位置的三個元素:TextBlock 報告初始化錯誤,包含一個旋轉箭頭邊框顯示的北的方向和另一個邊框來顯示 Bing 徽標。

在最外面的畫布有其 HorizontalAlignment 和垂直­對齊屬性設置為中心,其中將畫布縮小到零在網格的中心位置的大小。 (0,0) 這個畫布的座標是因此顯示的中心。 此居中方便的定位瓷磚,並且還允許縮放和旋轉圍繞原點發生。

在最外面的畫布是那個旋轉基於北的方向。 此最外面的畫布內是有 TranslateTransform 的第二個畫布。 這是為平移。 每當單個手指橫掃螢幕,只是通過設置此 TranslateTransform 的 X 和 Y 屬性可以移動整個地圖。

這第二個畫布內是用來指示當前位置相對於地圖的中心電話的橢圓。 當使用者平移地圖,此橢圓也會移動。 如果手機的 GPS 報告位置,單獨的翻譯中的變化,但是­變換在橢圓上的移動它相對於地圖。

在最裡面的畫布被命名為 imageCanvas,和它在這裡地圖底圖都是實際組裝。 應用於此畫布的 ScaleTransform 允許程式在增加或減少這一整個集會的基於使用者捏推拿放大或縮小地圖瓦片。

為了適應連續變焦,程式會保留一個 zoomFactor 類型的欄位雙。 此 zoomFactor 作為圖塊級別具有相同的範圍 — — 從 1 到 21 — — 這意味著它是實際的總的地圖縮放因數的基地 2 對數。 ZoomFactor 增加 1,每當地圖的縮放比例增加一倍。

第一次運行該程式時,zoomFactor 被初始化為 12,但使用者觸控式螢幕用兩個手指,第一次成為一個非整數值,並很可能此後仍然是一個非整數值。 該程式將 zoomFactor 作為一個使用者設置保存,並將在下次運行該程式時重新載入它。 初始積分增員的計算方法與簡單的截斷:

baseLevel = (int)zoomFactor;

此增員始終是一個整數 1 和 21 之間的範圍內,因此它適合直接用於檢索瓷磚。 從這兩個數字,程式計算 double 類型的非對數縮放因數:

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

這是適用于在最內層的畫布的縮放因數。 例如,如果 zoomFactor 是 10.5,然後用於檢索瓷磚的增員是 10,而 canvasScale 是 1.414。

如果初始的 zoomFactor 是 10.9,可能會更有意義,在 11 和 canvasZoom 在 0.933 設置增員。 該程式能做到這一點,但它顯然是可能的修改。

一個和兩個手指觸摸輸入

觸控式螢幕輸入,感覺更舒適的使用 XNA 觸控式螢幕比 Silverlight 操作事件。 首頁建構函式允許 XNA 手勢的四種的類型:(全景) FreeDrag、 DragComplete、 捏和 PinchComplete。 如中所示的 CompositionTarget.Rendering 事件的處理常式中的輸入檢查觸控式螢幕圖 3。 由於其複雜性,只有一點點捏的處理就在這裡。

圖 3 觸摸在 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;
    }
  }
}

FreeDrag 輸入伴隨著位置和增量值 (既有類型 Vector2) 指示當前位置的手指,並且自最後的觸控式螢幕事件如何移動手指。捏輸入補充這些具有 Position2 和 Delta2 的值的第二根手指。

但是,請記住這些 Vector2 值是在螢幕座標 !因為地圖相對於螢幕旋轉 — — 和使用者期望要隨著手指移動泛在同一方向的映射 — — 這些值必須旋轉基於當前的地圖旋轉,發生在名為 TransformGestureToMap 的小方法。

對於 FreeDrag 加工,增量值然後應用到中的 XAML 檔,以及兩個浮點欄位命名為 centerRelativeLongitude 和 centerRelativeLatitude TranslateTransform。這些值範圍從 0 到 1,並指示的經度和緯度對應到螢幕的中心。

在某一時刻,使用者可能會泛新瓷磚需要載入的足夠程度的映射。若要避免這種可能性與每個觸摸事件檢查,程式會保留兩個欄位命名為 accumulatedDeltaX 和 accumulatedDeltaY,並只調用 RefreshDisplay 時任一值高於 256,這是地圖底圖的圖元大小。

因為 RefreshDisplay 有大的工作要做 — — 確定什麼瓷磚應在螢幕上可見基於 centerRelativeLongitude 和 centerRelativeLatitude 和當前的 canvasScale,並創建新的瓷磚,如果有必要 — — 它是最佳的它不要求在觸控式螢幕輸入中的每個更改。有一項明確增強到程式會期間捏輸入限制 RefreshDisplay 調用。

在觸摸處理期間,清除方法只能調用時的手指離開螢幕。每當地圖的圖塊已完成下載也被稱為清理。

更改增員的標準 — — 從而啟動或由一位家長的兒童由父地圖平鋪的更換和 — — 是很放鬆。當 canvasScale 時大於 2,且遞減時 canvasScale 降到小於 0.5 只遞增增員。設置好過渡點是另一個明顯的增強。

程式現在有只有兩個應用程式欄按鈕:第一次切換路和鳥瞰圖和第二個立場之間地圖,使當前的位置是中心。

現在我只需要弄清楚如何使説明我導航購物商場和博物館的程式。

Charles Petzold 是 MSDN 雜誌和"程式設計視窗,第 6 版"的作者長期貢獻 (O'Reilly 媒體,2012年),一本關於編寫應用程式的 Windows 8 書。 他的網站是 charlespetzold.com

由於以下的技術專家對本文的審閱:湯瑪斯 Petchel