Udostępnij za pośrednictwem


Creating a Universal Templatable Compass Control

At the Microsoft //Build/ Conference last April, the Windows Phone 8.1 SDK (WP8.1) was released as a preview. With the release of this SDK one of the new templates added to Visual Studios now allows you to create universal apps. Universals apps allow you to build an app for Windows and Windows Phone while at the same time being able to share code, user controls, styles, strings, and other assets between the two projects in Visual Studio. This saves time having to develop code for each platform. You can find more information on creating universal apps here.

In this blog post we are going to create a reusable control that wraps the compass sensor and provides with an easy method for changing its style using templates. The API for the compass sensor is available to both Windows and Windows Phone apps. This allows us to use this sensor inside of a universal app with ease. The compass sensor is easy to use and returns heading value between 0 and 360 in degrees relative to true or magnetic north. However not everyone understands headings in degrees and are likely more familiar with a handheld compass. There are a number of different ways we can make the compass work. We could have a compass face that rotates such that readings are always aligned with their respective headings. Alternatively we could rotate a needle such that it points to the heading value on the compass that the device is facing. This blog post will show how to accomplish both.

Documentation on how to use the compass sensor can be found in the MSDN documentation here. This blog post is a Universal app version of a code sample from Chapter 10 of the free eBook; Location Intelligence for Windows Store Apps. Download a copy of this book here.

Setting up the project

To get started open up Visual Studios and create a new Universal App project in C#. Select the Blank App template and call the application UniversalCompass and press OK.

clip_image002

When the solution loads you will see three projects. The first two will be Windows and Windows Phone projects. The third project is a shared project used by the first two projects.

clip_image004

Since this is a simple universal app we are going to share the MainPage control as well. To do this drag and drop the MainPage.xaml and MainPage.xaml.cs files from either of the first two projects into the shared project. Next delete the references to these files in the first two projects. By doing this both apps will use the same MainPage control. Next add a new folder to the shared project called Assets. We will put our shared assets such as images into this folders. You solution should now look like this:

clip_image006

Creating the Compass Control

The compass control will wrap the compass sensor and expose a bindable heading property. When the control loads we will attach an event handler to the reading change event of the compass sensor. When the control is unloaded we will detach this event to prevent any memory leaks. When the compass reading changes the heading property will be updated with the heading for magnetic north. The compass in some devices does not return a heading value for true north, magnetic north is the most likely to be available. If we want to rotate the compass face such that readings are always aligned with their respective headings we will have to rotate the compass face by the negative of the heading. Rather than creating a property that exposes the negative heading value we can create a converter that modifies the heading value as needed through our binding from the compass template. Right click on the UniversalCompass.Shared project and select Add -> New Item and create a new class file called CompassControl.cs. The CompassControl class will derive from the Control class. In this class we will create a DependencyProperty for the heading so that we can easily bind to it. Update CompassControl.cs with the following code.

 using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace UniversalCompass
{
    public class CompassControl : Control
    {
        private Windows.Devices.Sensors.Compass compass;

        public CompassControl()
        {
            this.DefaultStyleKey = typeof(CompassControl);

            this.Loaded += CompassControl_Loaded;
            this.Unloaded += CompassControl_Unloaded;
        }

        public static readonly DependencyProperty HeadingProperty = DependencyProperty.Register("Heading",
            typeof(double), typeof(CompassControl), new PropertyMetadata(0.0));

        public double Heading
        {
            get
            {
                return (double)GetValue(HeadingProperty);
            }
            set
            {
                SetValue(HeadingProperty, value);
            }
        }

        private void CompassControl_Loaded(object sender, RoutedEventArgs e)
        {
            compass = Windows.Devices.Sensors.Compass.GetDefault();
            if (compass != null)
            {
                compass.ReadingChanged += CompassReadingChanged;
            }
        }

        private void CompassControl_Unloaded(object sender, RoutedEventArgs e)
        {
            if (compass != null)
            {
                compass.ReadingChanged -= CompassReadingChanged;
            }
        }

        private async void CompassReadingChanged(Windows.Devices.Sensors.Compass sender, Windows.Devices.Sensors.CompassReadingChangedEventArgs args)
        {
            try
            {
                await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, new Windows.UI.Core.DispatchedHandler(() =>
                {
                    Heading = args.Reading.HeadingMagneticNorth;
                }));
            }
            catch { }
        }
    }
}

Next right click on the shared project and select Add -> New Item and create a new class file called ReverseRotationConverter.cs and update it with the following code.

 using System;
using Windows.UI.Xaml.Data;

namespace UniversalCompass
{
    public class ReverseRotationConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            if (value is double)
            {
                return 360 - (double)value;
            }

            return value;
        }

        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            if (value is double)
            {
                return 360 - (double)value;
            }

            return value;
        }
    }
}

Creating the Compass Templates

We will create two different templates. For the first template we will create a compass where the face rotates such that all the compass headings stay pointing in the correct direction. We could create the compass face using pure XAML but it’s much fast and easier to use an image instead. We will use the following image as our compass face for this template.

clip_image008

CompassFace.png

Copy this image either from this blog post or from the code samples and add them to the Assets folder of the shared project. Open the App.xaml file in the shard project and update it with the following XAML.

 <Application
    x:Class="UniversalCompass.App"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:UniversalCompass">

    <Application.Resources>
        <local:ReverseRotationConverter x:Key="reverseRotationConverter"/>

        <Style TargetType="local:CompassControl">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="local:CompassControl">
                        <Image Source="ms-appx:///Assets/CompassFace.png" Stretch="None" RenderTransformOrigin="0.5,0.5">
                            <Image.RenderTransform>
                                <RotateTransform Angle="{Binding Path=Heading, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource reverseRotationConverter}}"/>
                            </Image.RenderTransform>
                        </Image>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Application.Resources>
</Application>

The second compass template we will create consists of a needle overlaid on a compass face. The needle will rotate such that it points to the current heading on the compass face. For this template we will use the following two images.

clip_image010

clip_image012

CompassFace2.png

Needle.png

Copy this image either from this blog post or from the code samples and add them to the Assets folder of the shared project. Next open the App.xaml file in the shared project and add the following style to the Application.Resources section of the document.

 <Style x:Name="NeedleCompassTemplate" TargetType="local:CompassControl">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:CompassControl">
                <Grid>
                    <Image Source="ms-appx:///Assets/CompassFace2.png" Stretch="None"/>
                    <Image Source="ms-appx:///Assets/Needle.png" Stretch="None" RenderTransformOrigin="0.5,0.5" 
                        HorizontalAlignment="Center" VerticalAlignment="Center">
                        <Image.RenderTransform>
                            <RotateTransform Angle="{Binding Path=Heading, RelativeSource={RelativeSource TemplatedParent}}"/>
                        </Image.RenderTransform>
                    </Image>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Applying the Templates to the Compass Control

At this point we have all the components we need to add a nicely templatable compass control to our application. Open the MainPage.xaml file in the shared project and update it with the following XMAL.

 <Page
    x:Class="UniversalCompass.MainPage"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:UniversalCompass"
    xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <ScrollViewer HorizontalScrollMode="Auto" HorizontalScrollBarVisibility="Auto" VerticalScrollMode="Disabled">
        <Grid>
            <TextBlock Text="Compass Sample" Style="{StaticResource HeaderTextBlockStyle}" Margin="40"/>

            <StackPanel Orientation="Horizontal" Width="900" HorizontalAlignment="Center" VerticalAlignment="Center">
                <local:CompassControl Margin="25"/>
                <local:CompassControl Style="{StaticResource NeedleCompassTemplate}" Margin="25"/>
            </StackPanel>
        </Grid>
    </ScrollViewer>
</Page>

At this point the app is complete and ready to be tested. When loading the app as a Windows Store app two compasses will be displayed in the middle of the screen. As you rotate your device the compass face on the left will rotate such that its headings are always pointing in the correct direction. The needle of the compass on the right will rotate such that it points to the current heading the device is facing.

clip_image014

When loading the Windows Phone 8.1 app you will likely only see one compass, scroll to the right to see the second one. Note that the Windows Phone Emulator does not provide a way to simulate the compass sensor. You will need to test this app on a device running Windows Phone 8.1 to see the compass rotate.

clip_image015

Full source code for this blog post can be downloaded from the MSDN Code Samples Gallery here.

Comments