加载中

Contents

  • Introduction
  • The Application User Interface
  • Timing The Run
  • Location Tracking
  • Setting The Map Pitch and Heading
  • Background Location Tracking
  • A Live Tile
  • Conclusion 

Introduction 

I have worked with Windows Phone 7 since it was in beta, so as you can imagine, I downloaded the Windows Phone 8 SDK as soon as it went live. For a bit of fun I decided to create a simple run-tracking application that showcases a number of these features ... and for an extra challenge do it all within 100 lines of code! (without resorting to writing compact and cryptic code).

目录

  • 介绍
  • 应用程序界面
  • 定时执行
  • 定位追踪
  • 设置地图Pitch和Heading属性
  • 后台位置追踪
  • Live Tile
  • 总结

介绍

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

This article guides you through the application that I developed, delving into the following Windows Phone 8 features:
  • The new map control, with pedestrian and landmark features.
  • How to track the users location, including tracking their location in the background while other apps are running
  • Adding line annotations to the map
  • Some 3D mapping features, setting the pitch and heading
  • Live-tiles making use of one of the new tile formats

Whilst it was perfectly possible to develop a run-tracking app with Windows Phone 7 (and there are a number of good examples in the Marketplace), the new features and capabilities of Windows Phone 8 can be used to make a much more feature-rich application.

NOTE: I originally published this article on the Nokia Developer Wiki, but thought I would share it on CodeProject also, where most of my other articles have been published.

本文将通过我所开发的这个应用引导你深入探究  Windows Phone 8 中的以下特性:
  • 新增的地图控件(拥有步行与三维路标功能)。
  • 如何跟踪用户的位置,包括当其他程序运行时在后台跟踪的情况。
  • 在地图上添加线条注释
  • 一些3D地图的特性,设置 pitch 与 heading 属性。
  • 使用新增的模板创建动态Tile

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

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

The Application User Interface

This application has quite a basic UI, which is composed of full-screen map, which has the run statistics overlayed on top of it as shown in the screenshot below:

The application UI is defined in XAML as follows:

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

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

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

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

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

应用程序界面

这个程序的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 a utility class, which I wrote a number of years ago, that provides convenient shorthand for defining grid columns and rows (for WPF, Silverlight and WindowsPhone). If you are following along, by building this running app from scratch, then in order to add the map, you will have to include the following namespace definition:
xmlns:maps="clr-namespace:Microsoft.Phone.Maps.Controls;assembly=Microsoft.Phone.Maps" 

Before building and running the application, you have to include the mapping ‘capability’. To do this open up '''WPAppManifest.xml''', navigate to the Capabilities tab and check theID_CAP_MAPcheckbox. While you’re there, you may as well includeID_CAP_LOCATIONas well:

Capabilites are used to determine the phone features that your application uses so that users can more easily determine what an application does.

With these capabilities included, build and run the application and you should see the same UI that was illustrated above.

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 用于决定你的应用中打算加入的手机功能,从而方便用户了解这个应用能做些什么。

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

One of the improvements in the maps control is that it is fully vector-based (The Windows Phone 7 map is image-tile-based), this creates a much more smooth transition when the map is zoomed, and also allows for 3D transformations (as we will see a little later on). The map control also has a few other useful features for our running app, pedestrian-features and landmarks. These can be enabled as follows:

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

With these features enabled the map illustrates useful features such as stairs, crossings and 3D landmarks:

(By the way, I’m not counting the ~50 lines of XAML in my total lines-of-code count!)

The Windows Phone 8 maps have many more new features that I have not used in this application. You could for example use the newColorMode, which allows you to render a 'dark' map which is easier on the eyes in low light conditions. You could even make the run-tracking app choose theColorModebased on the time of day!

地图控件的其中一项改进即它是完全基于矢量的(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

Timing The Run

When the '''Start''' button is tapped the application tracks the user’s location using the phone’s built in GPS receiver, in order to mark their path on the map. It also times their run duration and generates various statistics of interest. We’ll start with the simpler of the two, timing the run. When the start button is clicked aDispatcherTimeris started and the time of the button tap recorded. On each timer ‘tick’ the label which indicates the elapsed run time is updated:

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

With the above code in place, tapping the '''start''' button starts the timer.

定时执行

当"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"按钮后定时器就开始执行了

Location Tracking

The next step is to track the location whilst the timer is running. The Windows Phone API has aGeoCoordinateWatcherclass which fires aPositionChangedevent which can be used to track the user’s location. It is very easy to render the user’s movements on a map via aMapPolyLine, which is a line path which is defined in terms of geocoordinates. Each time the event is fired, a new point is added to the line as follows:

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

    // create a line which illustrates the run
    _line = new MapPolyline();
    _line.StrokeColor = Colors.Red;
    _line.StrokeThickness = 5;
    Map.MapElements.Add(_line);

    _watcher.PositionChanged += Watcher_PositionChanged;

    //.. timer code omitted ...
  }

  //.. timer code omitted ...

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

With these few lines of extra code, the path of the user’s run is added to the map:

位置追踪

下一步需要做的就是在计时器运行的同时来记录位置信息。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);
  }
}

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

The PositionChangedevent handler can be developed further to compute the total run distance, calories burnt and pace. This makes use of theGeoCoordinate.GetDistanceTomethod which can be used to compute the distance between two locations:
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 do not measure pace in miles or kilometers per hour. Instead, pace is measured in terms of the time taken to travel a set distance. This method of measurement makes it much easier to determine your overall race time, e.g. if you are running at 4:00 minute-kilometers pace, you will complete a 5k race in 20 minutes.

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分钟后跑完五公里.

NOTE: The code above uses a pretty basic calorie calculation, assuming a burn rate of 65 calories per kilometer. A more accurate calculation would incorporate the runner's weight and pace, and other environmental factors. I'll leave this as an exercise for the reader!

For developing applications that involve tracking a user’s location the emulator has some very useful features. You can record points along a route, then replay them at set intervals. You can also save the route as an XML file so that it can be replayed in future sessions:

It takes a while to create a realistic dataset that emulates a real run, but at least you only have to do this once!

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

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

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

Setting The Map Pitch and Heading

Because of the vector nature of the Windows Phone 8 map it is possible to transform the view using the Pitch and Heading properties. The Pitch property sets the viewing angle of the map, providing a perspective rendering, rather than a top-down rendering, while the Heading property allows you to rotate the map. Most sat-nav systems use a combination of these effects to render the map so that it looks the same as the view directly in front of you. Many people find this type of map view much easier to understand (they do not have to perform rotate transforms in their head!).

Adding this feature to the running app is really easy, firstly setting the map Pitch is simply done in the XAML:

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

设置地图视角和指向

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

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

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