一触即发

供定向地图使用的触摸界面

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