一百行代码写一个WP8路线跟踪应用

目录

介绍

我从Windows Phone7 还在测试阶段的时候就开始开发了,所以在Windows Phone 8 SDK开放后第一时间下载了。为了有趣,我决定创建一个简单的跑步定位追踪的应用来展示大量的特性,并且我将挑战在100行代码内完成此功能(不使用紧凑和可读性差的代码)

本文将通过我所开发的这个应用引导你深入探究  Windows Phone 8 中的以下特性:

虽然使用Windows Phone 7 开发一个这样的路线跟踪应用也是完全可以(商城里已经有很多很棒的例子了),不过Windows Phone 8 中新增的特性与功能可以让它的功能更加丰富。

注意哦: 我最初将本文发表在诺基亚开发者百科(Nokia Developer Wiki)上,不过在CodeProject 上我也分享了,在那里你可以找到我写的其他一些文章

应用程序界面

这个程序的UI十分简单,主要由一张全屏的地图组成,地图上面显示跑步的统计信息,正如下面的截图所示:

程序界面的XAML代码如下:

<Grid util:GridUtils.RowDefinitions="Auto, *">

  <!-- title -->
  <StackPanel Grid.Row="0" Margin="12,17,0,28">
    <StackPanel Orientation="Horizontal">
      <Image Source="/Assets/ApplicationIconLarge.png" Height="50"/>
      <TextBlock Text="WP8Runner" VerticalAlignment="Center"
                  Margin="10 0 0 0"
                  FontSize="{StaticResource PhoneFontSizeLarge}"/>
    </StackPanel>
  </StackPanel>

  <!--ContentPanel - place additional content here-->
  <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">

    <!-- the map -->
    <maps:Map x:Name="Map"
          ZoomLevel="16"/>

    <!-- 步行统计 -->
    <Grid Background="#99000000" Margin="20" 
          VerticalAlignment="Bottom">
      <Grid Margin="20"
            util:GridUtils.RowDefinitions="40, 40, Auto"
            util:GridUtils.ColumnDefinitions="*, *, *, *">
          
        <!-- 距离 -->
        <TextBlock Text="Distance:"/>
        <TextBlock Text="0 km" Grid.Column="1" x:Name="distanceLabel"
              HorizontalAlignment="Center"/>

        <!-- 时间 -->
        <TextBlock Text="Time:" Grid.Column="2"/>
        <TextBlock Text="00:00:00" Grid.Column="3" x:Name="timeLabel"
              HorizontalAlignment="Center"/>

        <!-- 卡路里 -->
        <TextBlock Text="Calories:" Grid.Row="1"/>
        <TextBlock Text="0" Grid.Column="1" x:Name="caloriesLabel"
              HorizontalAlignment="Center" Grid.Row="1"/>

        <!-- 步幅 -->
        <TextBlock Text="Pace:" Grid.Column="2" Grid.Row="1"/>
        <TextBlock Text="00:00" Grid.Column="3" x:Name="paceLabel"
              HorizontalAlignment="Center" Grid.Row="1"/>

        <Button Content="Start"
                Grid.Row="2" Grid.ColumnSpan="4"
                Click="StartButton_Click"
                x:Name="StartButton"/>
      </Grid>
    </Grid>
  </Grid>
</Grid>
GridUtilsis 是我几年前写的一个工具类,可方便定义网格(Grid)的行列属性(该类适用于 WPF,Silverlight 以及Windows Phone)。如果你依序从头编写这个应用,为了能使用地图,就得加入下面的命名空间定义:
xmlns:maps="clr-namespace:Microsoft.Phone.Maps.Controls;assembly=Microsoft.Phone.Maps" 

在构建与运行程序之前,你得先开启地图功能。打开 '''WPAppManifest.xml''',找到 Capabilities 标签然后选中 ID_CAP_MAP,这时ID_CAP_LOCATION也(自动)被选中:

Capabilites 用于决定你的应用中打算加入的手机功能,从而方便用户了解这个应用能做些什么。

选中以上功能后,构建运行程序你就可以看到如上所述的界面。

地图控件的其中一项改进即它是完全基于矢量的(Windows Phone 7中的地图是基于位图),这便使得地图在缩放时的过渡更加平滑,而且还可以进行3D变换(稍后将会看到)。对我们的路线跟踪应用而言,地图控件还有其他一些有用的功能,如步行与路标。 可以通过如下的代码设置:

<!-- the map -->
<maps:Map x:Name="Map"
      PedestrianFeaturesEnabled="True"
      LandmarksEnabled="True"
      ZoomLevel="16"/>

经过以上设置,地图将会显示一些有用的东西,比如街道、路标等信息:

(顺便插一句,我可没想把这50行XAML算到我的代码总行数里哈 :-P)

Windows Phone 8中的地图还有很多新特性我没在这个程序里用到。比如你可以用下新的ColorMode属性,它能让你渲染一幅‘暗色模式’的地图以便在低光环境下查看,甚至可以让这个路线跟踪应用根据当天的时间自动选择ColorMode

定时执行

当"Start"按钮按下后程序就开始的GPS接收器就开始追踪用户的位置来标注用户的行动轨迹,如果有兴趣的话还可以计算已经跑步的时间和统计各种数据,我们先从两项功能中简单的一项开始,定时执行.当我们点击开始按钮后 DispatcherTimer就开始执行了并且记录下按钮按下的时间,另一个timer定时器来更新显示已经执行了多长时间.

public partial class MainPage : PhoneApplicationPage
{
  private DispatcherTimer _timer = new DispatcherTimer();
  private long _startTime;

  public MainPage()
  {
    InitializeComponent();

    _timer.Interval = TimeSpan.FromSeconds(1);
    _timer.Tick += Timer_Tick;
  }

  private void Timer_Tick(object sender, EventArgs e)
  {
    TimeSpan runTime = TimeSpan.FromMilliseconds(System.Environment.TickCount - _startTime);
    timeLabel.Text = runTime.ToString(@"hh\:mm\:ss");
  }

  private void StartButton_Click(object sender, RoutedEventArgs e)
  {
    if (_timer.IsEnabled)
    {
      _timer.Stop();
      StartButton.Content = "Start";
    }
    else
    {
      _timer.Start();
      _startTime = System.Environment.TickCount;
      StartButton.Content = "Stop";
    }
  }
}
以上代码完成后,点击"start"按钮后定时器就开始执行了

位置追踪

下一步需要做的就是在计时器运行的同时来记录位置信息。Window Phone的API 库有GeoCoordinateWatcherclass 类, 其中的一个方法 PositionChangedevent 可以用来记录用户的位置和路径。 而后用MapPolyLine方法可以很简单地在地图上用地理坐标来渲染出用户的路径。每一次这个事件被激活,就会在地图上增加一个点,具体代码如下:

public partial class MainPage : PhoneApplicationPage
{
  private GeoCoordinateWatcher _watcher = new GeoCoordinateWatcher(GeoPositionAccuracy.High);
  private MapPolyline _line;
  private DispatcherTimer _timer = new DispatcherTimer();
  private long _startTime;

  public MainPage()
  {
    InitializeComponent();

    //初始化一个line类用来记录路径
    _line = new MapPolyline();
    _line.StrokeColor = Colors.Red;
    _line.StrokeThickness = 5;
    Map.MapElements.Add(_line);

    _watcher.PositionChanged += Watcher_PositionChanged;

    //..计时器代码在此处省略..
  }

  //.. 计时器代码此处被省略 ...

  private void StartButton_Click(object sender, RoutedEventArgs e)
  {
    if (_timer.IsEnabled)
    {
      _watcher.Stop();
      _timer.Stop();
      StartButton.Content = "Start";
    }
    else
    {
      _watcher.Start();
      _timer.Start();
      _startTime = System.Environment.TickCount;
      StartButton.Content = "Stop";
    }
  }


  private void Watcher_PositionChanged(object sender, GeoPositionChangedEventArgs<GeoCoordinate> e)
  {
    var coord = new GeoCoordinate(e.Position.Location.Latitude, e.Position.Location.Longitude);

    Map.Center = coord;
    _line.Path.Add(coord);
  }
}

上面的代码可以就可以实现把用户的路径层加到地图上,如下图所示。

PositionChanged 事件可以进一步的扩展成计算总运行距离,卡路里消耗和速度.使用GeoCoordinate.GetDistanceTo可以用来计算两个地点之间的距离:

private double _kilometres;
private long _previousPositionChangeTick;

private void Watcher_PositionChanged(object sender, GeoPositionChangedEventArgs<GeoCoordinate> e)
{
  var coord = new GeoCoordinate(e.Position.Location.Latitude, e.Position.Location.Longitude);

  if (_line.Path.Count > 0)
  {
    // find the previos point and measure the distance travelled
    var previousPoint = _line.Path.Last();
    var distance = coord.GetDistanceTo(previousPoint);

    // compute pace
    var millisPerKilometer = (1000.0 / distance) * (System.Environment.TickCount - _previousPositionChangeTick);

    // compute total distance travelled
    _kilometres += distance / 1000.0;

    paceLabel.Text = TimeSpan.FromMilliseconds(millisPerKilometer).ToString(@"mm\:ss");
    distanceLabel.Text = string.Format("{0:f2} km", _kilometres);
    caloriesLabel.Text = string.Format("{0:f0}", _kilometres * 65);
  }
  

  Map.Center = coord;
  
  _line.Path.Add(coord);
  _previousPositionChangeTick = System.Environment.TickCount;
}
Runner's不去测量每小时的速度是多少英里或公里.相反,速度用来计算一段跑完距离里所需的时间.这种计算方法可以更轻松的确定你整体的跑步时间,例如:如果你跑步的速度是四分钟一公里,那么你会在20分钟后跑完五公里.

注意:上面的代码使用了一个非常简单的卡路里消耗算法,假设每跑一公里消耗65卡路里,一个更精确的算法会根据跑步者的体重和速度和其他环境因素来进行计算,将这个功能作为一个小练习留给读者.

对于开发应用程序,模拟器有一些包括跟踪用户的位置在内的非常有用的特性。你可以沿着路线记录一些点,然后每隔一段时间重复一下,你也可以将这些模拟数据保存成一个xml,以便以后重复执行.

这可能需要一定时间来创建一个真实的数据来模拟真实的运行情况,但是你至少有一次来做这件事.

设置地图视角和指向

由于 Windows Phone8 地图的向量性质,它可以使用视角和指向属性转换视图。 视角属性设置地图的观察角度,提供了一个透视的图像,而非一个自上而下的图像,而指向属性则使你可以旋转地图。 多数卫星导航系统将这些特效组合起来呈现地图,这样它看起来就像直接在你的面前一样。很多人觉得这样的地图比较容易理解(他们不需要在大脑里旋转地图)。

你可以很容易的将这些功能添加到你的应用中,首先,在 XAML 中设置地图视角:

<!-- the map -->
<maps:Map x:Name="Map"
      PedestrianFeaturesEnabled="True"
      LandmarksEnabled="True"
      Pitch="55"
      ZoomLevel="18"/> 

指向的计算有些复杂。在上节中,当前及过去的坐标被用来计算行驶的距离和速度。这两个坐标同样也可以用来计算指向,尽管计算更复杂。 幸运的是,我发现了一个包含了很多有用的地理定位工具的 .NET 库,其中也包含了指向的计算。使用这个 .NET 扩展库, 查询和设置指向将十分简单:

PositionHandler handler = new PositionHandler();
var heading = handler.CalculateBearing(new Position(previousPoint), new Position(coord));
Map.SetView(coord, Map.ZoomLevel, heading, MapAnimationKind.Parabolic);

另外,注意上面的代码使用了 SetView 方法,而不是单个进行属性配置。 如果你直接设置属性,地图状态将会立刻改变,这也意味着,视图将会从一个坐标/指向“跳转”到另一个上。使用 SetView, UI 将会变得更加流畅。

在下面,你将看到在纽约中央公园中的指向和地图视角的展示:

 

后台位置跟踪

WP7 可以在锁屏后(继续)运行前台应用,这对运动跟踪类应用来说是十分重要的一项功能,它可以让用户在锁机后其位置信息依然能被记录。而WP8 则更进一步,应用可以在后台进行跟踪,也就意味着,当用户在使用其他应用比如检查邮件、听音乐时,程序可以在后台跟踪记录用户的地理位置信息。

为了开启这个功能你得手动更改'''WMAppManifest.xml''', 在上面右键选择“查看代码”,然后定位到 Tasks 标签添加如下代码:

<Tasks>
  <DefaultTask Name="_default" NavigationPage="MainPage.xaml">
    <BackgroundExecution>
      <ExecutionType Name="LocationTracking" />
    </BackgroundExecution>
  </DefaultTask>
</Tasks>

这就搞定了! 

程序开始在后台运行的时候就会触发RunningInBackground 事件,借此可以显示一个Toast 通知消息,不过接下来的小节中我们会采用一种更加有趣的方式来告知用户其正被追踪记录的情况。

活动瓷贴 

WP8 新增了许多瓷贴模板,而这里我们将用到‘Iconic Template’。打开 '''WMAppManifest.xml''' (这次得用视觉编辑器了!), 选中模板 Iconic template。

更新瓷贴状态就跟发消息通知一样简单。每次位置变更的时候就会执行下面的代码:

ShellTile.ActiveTiles.First().Update(new IconicTileData()
{
  Title = "WP8Runner",
  WideContent1 = string.Format("{0:f2} km", _kilometres),
  WideContent2 = string.Format("{0:f0} calories", _kilometres * 65),
});
如果你现在把程序 pin 到开始屏幕并使用宽瓷贴样式,那么当位置在后台被跟踪记录时,瓷贴将更新如下:

添加完如上代码,这个应用就算完成了!

总结

Windows Phone 8 有很多非常酷的功能可以用来增强你的程序。在本文中我就展示了一个简单的路线跟踪应用是如何从中受益的。同样,强大的框架和API 能让你只用非常少的代码就可以写出复杂的程序。

很显然本文所说的应用还有待完善!不过你何不亲自尝试着改进下呢,试试用独立存储(Isolated Storage)记录跑步历史,或者增加一个统计信息图表,你也可以用下Windows Phone 8 API中其他新增的功能,比如说使用语音命令来控制开始/结束!祝玩得开心!

那么该应用是否真的用了100行代码呢?你可以从这里下载源代码 WP8Runner.zip,然后就会发现 '''MainPage.xaml.cs'''恰好用了 100 行。