Pedometer (Walking Motion)

Apedometer counts how many steps you take. It is a handy device for people who are interested in getting enough exercise and perhaps need a little motivation. Pedometers—especially good ones—are expensive. Thanks to the built-in accelerometer, the Pedometer app enables you to turn your phone into a pedometer without the need for a separate device.

Current Windows phones do not report accelerometer data while the screen is turned off!

Although this app runs while the phone is locked, phones do not report accelerometer data while the screen is off. Unfortunately, this means that no steps can be registered while the screen is off.You must keep the screen on (and therefore the phone unlocked) for the entire time you use this app.This app disables the screen time-out, so you don’t have to worry about your phone automatically locking in your pocket.You do, however, have to worry about accidentally bumping buttons. For the best results, the screen should face away from your body.

The Main Page

This app has a main page, a settings page, and an instructions page . The main page shows the current number of steps and converts that number to miles and kilometers, based on a stride-length setting customized on the settings page. It starts out in a “paused” state, shown in Figure 50.1, in which no steps are registered. This cuts down on the reporting of bogus steps. The idea is that users press the start button while the phone is close to their pocket, and then they slide the phone in carefully. (The step-detection algorithm can easily register two bogus steps with a little bit of jostling of the phone.)

This page also shows a welcome message urging the user to calibrate the pedometer, but it only shows it the first time the page is loaded (on the first run of the app).

Listing 50.1 contains the XAML for the main page, and Listing 50.2 contains its code-behind.

The app starts out in a paused state to avoid registering bogus steps.
FIGURE 50.1 The app starts out in a paused state to avoid registering bogus steps.

LISTING 50.1 MainPage.xaml—The User Interface for Pedometer’s Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Portrait” shell:SystemTray.IsVisible=”True”>
<!– The application bar, with four buttons –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.5”
BackgroundColor=”{StaticResource PhoneAccentColor}”>
<shell:ApplicationBarIconButton Text=”start”
IconUri=”/Shared/Images/appbar.play.png”
Click=”StartPauseButton_Click”/>
<shell:ApplicationBarIconButton Text=”settings” Click=”SettingsButton_Click”
IconUri=”/Shared/Images/appbar.settings.png”/>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBarIconButton Text=”reset”
IconUri=”/Shared/Images/appbar.delete.png”
Click=”ResetButton_Click”/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<!– The accent-colored foot –>
<Rectangle Opacity=”.5” Fill=”{StaticResource PhoneAccentBrush}” Width=”277”
Height=”648” Margin=”0,12,12,0” VerticalAlignment=”Top”
HorizontalAlignment=”Right”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/foot.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<StackPanel>
<!– PAUSED –>
<TextBlock x:Name=”PausedTextBlock” FontFamily=”Segoe WP Black”
HorizontalAlignment=”Center” Text=”PAUSED”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”/>
<!– Steps –>
<TextBlock x:Name=”StepsTextBlock” Margin=”12,12,0,0” Opacity=”.5”
FontSize=”130”/>
<TextBlock Text=”steps” Margin=”18,-24” FontWeight=”Bold”
FontSize=”{StaticResource PhoneFontSizeMedium}”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<!– Miles –>
<TextBlock x:Name=”MilesTextBlock” Margin=”12,72,0,0” Opacity=”.5”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”/>
<TextBlock Text=”miles” Margin=”18,-12” FontWeight=”Bold”
FontSize=”{StaticResource PhoneFontSizeMedium}”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<!– Kilometers –>
<TextBlock x:Name=”KilometersTextBlock” Margin=”12,72,0,0” Opacity=”.5”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”/>
<TextBlock Text=”kilometers” Margin=”18,-12” FontWeight=”Bold”
FontSize=”{StaticResource PhoneFontSizeMedium}”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
</StackPanel>
<!– The special one-time message –>
<Border x:Name=”FirstRunPanel” Visibility=”Collapsed” Width=”300”
Margin=”0,0,24,0” HorizontalAlignment=”Right” VerticalAlignment=”Center”
Background=”{StaticResource PhoneAccentBrush}” Padding=”24”>
<TextBlock TextWrapping=”Wrap” Text=”Welcome! Be sure to calibrate …”/>
</Border>
</Grid>
</phone:PhoneApplicationPage>

[/code]

Both the foot and the application bar are given the phone theme’s accent color, but with 50% opacity to make them more subtle.

LISTING 50.2 MainPage.xaml.cs—The Code-Behind for Pedometer’s Main Page

[code]

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Applications.Common; // For AccelerometerHelper
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
IApplicationBarIconButton startPauseButton;
IApplicationBarIconButton settingsButton;
IApplicationBarIconButton instructionsButton;
IApplicationBarIconButton resetButton;
bool pastPositiveThreshold = true;
public MainPage()
{
InitializeComponent();
this.startPauseButton = this.ApplicationBar.Buttons[0]
as IApplicationBarIconButton;
this.settingsButton = this.ApplicationBar.Buttons[1]
as IApplicationBarIconButton;
this.instructionsButton = this.ApplicationBar.Buttons[2]
as IApplicationBarIconButton;
this.resetButton = this.ApplicationBar.Buttons[3]
as IApplicationBarIconButton;
SoundEffects.Initialize();
// Use the accelerometer via Microsoft’s helper
AccelerometerHelper.Instance.ReadingChanged
+= Accelerometer_ReadingChanged;
// Allow the app to run when the phone is locked.
// Once disabled, you cannot re-enable the default behavior!
PhoneApplicationService.Current.ApplicationIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
RefreshDisplay();
// Show the welcome message if this is the first time ever
if (Settings.FirstRun.Value)
{
this.FirstRunPanel.Visibility = Visibility.Visible;
Settings.FirstRun.Value = false;
}
else
{
this.FirstRunPanel.Visibility = Visibility.Collapsed;
}
// While on this page, don’t allow the screen to auto-lock
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Restore the ability for the screen to auto-lock when on other pages
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Enabled;
}
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
if (AccelerometerHelper.Instance.Active)
{
MessageBox.Show(“You must pause the pedometer before leaving. …”,
“Pedometer Still Collecting Data”, MessageBoxButton.OK);
e.Cancel = true;
}
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerHelperReadingEventArgs e)
{
bool newStep = false;
double magnitude = e.OptimalyFilteredAcceleration.Magnitude;
if (!pastPositiveThreshold && magnitude > 1 + Settings.Threshold.Value)
{
newStep = true;
pastPositiveThreshold = true;
}
if (magnitude < 1 – Settings.Threshold.Value)
pastPositiveThreshold = false;
if (newStep)
{
// We only know about one leg, so count each new step as two
Settings.NumSteps.Value += 2;
this.Dispatcher.BeginInvoke(delegate()
{
RefreshDisplay();
if (Settings.PlaySound.Value)
SoundEffects.Ding.Play();
});
}
}
void RefreshDisplay()
{
this.StepsTextBlock.Text = Settings.NumSteps.Value.ToString(“N0”);
double totalInches = Settings.NumSteps.Value * Settings.Stride.Value;
this.MilesTextBlock.Text = (totalInches / 63360).ToString(“##0.####”);
this.KilometersTextBlock.Text =
(totalInches * 0.0000254).ToString(“##0.####”);
}
// Application bar handlers
void StartPauseButton_Click(object sender, EventArgs e)
{
if (AccelerometerHelper.Instance.Active)
{
// Stop the accelerometer with Microsoft’s helper
AccelerometerHelper.Instance.Active = false;
this.startPauseButton.Text = “start”;
this.startPauseButton.IconUri
= new Uri(“/Shared/Images/appbar.play.png”, UriKind.Relative);
this.PausedTextBlock.Text = “PAUSED”;
this.StepsTextBlock.Opacity = .5;
this.MilesTextBlock.Opacity = .5;
this.KilometersTextBlock.Opacity = .5;
this.settingsButton.IsEnabled = true;
this.instructionsButton.IsEnabled = true;
this.resetButton.IsEnabled = true;
}
else
{
// Start the accelerometer with Microsoft’s helper
AccelerometerHelper.Instance.Active = true;
this.startPauseButton.Text = “pause”;
this.startPauseButton.IconUri
= new Uri(“/Shared/Images/appbar.pause.png”, UriKind.Relative);
this.PausedTextBlock.Text = “”;
this.StepsTextBlock.Opacity = 1;
this.MilesTextBlock.Opacity = 1;
this.KilometersTextBlock.Opacity = 1;
this.settingsButton.IsEnabled = false;
this.instructionsButton.IsEnabled = false;
this.resetButton.IsEnabled = false;
}
}
void SettingsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(
new Uri(“/SettingsPage.xaml”, UriKind.Relative));
}
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(
new Uri(“/InstructionsPage.xaml”, UriKind.Relative));
}
void ResetButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to clear your data?”, “Reset”,
MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
Settings.NumSteps.Value = 0;
RefreshDisplay();
}
}
}
}

[/code]

  • This listing uses the following settings defined in a separate Settings.cs file as follows:
    [code]
    public static class Settings
    {
    // Configurable settings
    public static readonly Setting<double> Threshold =
    new Setting<double>(“Threshold”, .43);
    public static readonly Setting<bool> PlaySound =
    new Setting<bool>(“PlaySound”, true);
    public static readonly Setting<double> Stride =
    new Setting<double>(“Stride”, 28);
    // Current state
    public static readonly Setting<int> NumSteps =
    new Setting<int>(“NumSteps”, 0);
    // A special flag that is set to false during the app’s first run
    public static readonly Setting<bool> FirstRun =
    new Setting<bool>(“FirstRun”, true);
    }
    [/code]
  • The walking detection, done inside Accelerometer_ReadingChanged, uses the magnitude of the acceleration vector reported by the AccelerometerHelper library (the square root of X2 + Y2 + Z2). Therefore, the direction of the motion doesn’t matter; just the amount of motion. A phone at rest has a magnitude of 1 (pointing toward the Earth), so this algorithm looks for a sufficiently smaller magnitude followed by a sufficiently larger magnitude. This represents the motion of the single leg holding the phone, so the detection of each step is actually counted as two steps, one per leg. The definition of sufficient is determined by the Threshold setting customized on the settings page.

The Settings Page

The settings page must be visited frequently in order to calibrate the pedometer just right.
FIGURE 50.2 The settings page must be visited frequently in order to calibrate the pedometer just right.

The settings page, shown in Figure 50.2, enables the user to change the values of the Threshold, PlaySound, and Stride settings used in the previous listing.

Users can try different sensitivity values (which map to Threshold) and then manually count their steps while Pedometer runs to see if the two counts match. To make this process even easier, this app supports playing a sound every time a step is detected. When the value is correct, the user will hear a sound every other step (when the leg carrying the phone makes a step). Once a good sensitivity value is found, the user can turn off the sound.

The best sensitivity value can vary based on where the user puts the phone, the depth of the pocket containing the phone, and other factors. Therefore, a sensitivity value that works best one day may not work well the next.

To give accurate values for miles and kilometers, users must enter their stride length at the bottom of the page. If users are unable to measure their stride length, they can multiply their height (in inches) by .413 for a woman or .415 for a man. The average stride length for a woman is 26.4 inches, and the average stride length for a man is 30 inches. This information is explained on this app’s instructions page.

Listing 50.3 contains the XAML for the settings page, and Listing 50.4 contains its codebehind.

LISTING 50.3 SettingsPage.xaml—The User Interface for Pedometer’s Settings Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.SettingsPage” x:Name=”Page”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
xmlns:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<Grid Background=”Transparent”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard settings header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”SETTINGS” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”pedometer”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<StackPanel Margin=”{StaticResource PhoneMargin}”>
<TextBlock Text=”Tap &amp; hold …” TextWrapping=”Wrap” Margin=”12,0”/>
<!– The slider with three supporting text blocks –>
<Grid Height=”150”>
<TextBlock HorizontalAlignment=”Right” Margin=”0,12,12,0”
VerticalAlignment=”Center” Text=”report more steps”/>
<TextBlock HorizontalAlignment=”Left” Margin=”12,12,0,0”
VerticalAlignment=”Center” Text=”report fewer steps”/>
<TextBlock x:Name=”ThresholdTextBlock” Margin=”12,6,0,0”
Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Left” VerticalAlignment=”Top”
FontSize=”{StaticResource PhoneFontSizeLarge}”/>
<Slider x:Name=”ThresholdSlider” Minimum=”.1” Maximum=”1”
LargeChange=”.001” IsDirectionReversed=”True”
Value=”{Binding Threshold, Mode=TwoWay, ElementName=Page}”/>
</Grid>
<!– reset –>
<Button Content=”reset sensitivity” Click=”ResetButton_Click”
local:Tilt.IsEnabled=”True”/>
<!– Play a sound toggle switch –>
<toolkit:ToggleSwitch x:Name=”PlaySoundToggleSwitch” Margin=”0,30,0,0”
Header=”Play a sound every other step”
IsChecked=”{Binding PlaySound, Mode=TwoWay, ElementName=Page}”/>
<!– Stride length –>
<TextBlock Style=”{StaticResource LabelStyle}”
Text=”Stride length (in inches)”/>
<TextBox x:Name=”StrideLengthTextBox” InputScope=”Number”
Text=”{Binding StrideLength, Mode=TwoWay, ElementName=Page}”/>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 50.4 SettingsPage.xaml.cs—The Code-Behind for Pedometer’s Settings Page

[code]

using System.Windows;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class SettingsPage : PhoneApplicationPage
{
public SettingsPage()
{
InitializeComponent();
this.ThresholdTextBlock.Text =
(Settings.Threshold.Value * 1000).ToString(“N0”);
}
// Simple property bound to the slider
public double Threshold
{
get { return Settings.Threshold.Value; }
set { this.ThresholdTextBlock.Text = (value * 1000).ToString(“N0”);
Settings.Threshold.Value = value; }
}
// Simple property bound to the toggle switch
public bool PlaySound
{
get { return Settings.PlaySound.Value; }
set { Settings.PlaySound.Value = value; }
}
// Simple property bound to the text box
public string StrideLength
{
get { return Settings.Stride.Value.ToString(); }
set
{
try { Settings.Stride.Value = int.Parse(value); }
catch { }
}
}
void ResetButton_Click(object sender, RoutedEventArgs e)
{
this.ThresholdSlider.Value = Settings.Threshold.DefaultValue;
}
}
}

[/code]

The Finished Product

Pedometer (Walking Motion)

 

 

Balance Test (2D)

Test your hand’s coordination and ability to hold your phone still in Balance Test, a fun little 2D-accelerometerbased game. You’ve got one minute to line up the images as many times as possible. You must keep the phone still at the correct angle for 3 seconds to earn each point. If the images become unaligned for just a moment, the hold-still time resets. This app keeps track of your best score, average score, and number of attempts, so you can keep track of your performance and watch it improve over time.

Balance Test is a lot like a 2D level app, in that you can move an image around the screen by tilting your phone in the X and Y dimensions. However, the image always “sinks” toward the corner of the screen closest to the Earth, whereas a bubble in a level would float to the highest corner.

From a code perspective, Balance Test is like a cross between the Moo Cow and Reflex Test apps. It uses Moo Cow’s accelerometer-based motion extended in an additional dimension, and its scoring feature (which keeps track of the best and average scores) is almost identical to Reflex Test.

The Main Page

This app uses a single page (ignoring the standard calibration and about pages, as well as an instructions page). It features the two images that need to be aligned to earn points, shown in Figure 49.1. The one shown in white is referred to as the target, and gets randomly placed on the screen. The one shown in blue is referred to as the moving piece and is controlled by tilting the phone.

When you’re not actively playing, you can see the score stats, shown in Figure 49.2. This app does something a little different—it gives the application bar a background color matching the theme accent color. The application bar is shown expanded in Figure 49.3.

Listing 49.1 contains the XAML for the main page, and Listing 49.2 contains its code-behind.

The images must be aligned for three seconds to get a point for your good balance.
FIGURE 49.1 The images must be aligned for three seconds to get a point for your good balance.
The score display is just like the one used in the Reflex Test app.
FIGURE 49.2 The score display is just like the one used in the Reflex Test app.
The application bar makes a bold statement with its accent-colored background.
FIGURE 49.3 The application bar makes a bold statement with its accent-colored background.

LISTING 49.1 MainPage.xaml—The User Interface for Balance Test’s Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Portrait”>
<!– Add four animations to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– Move the target and scale both images –>
<Storyboard x:Name=”MoveTargetStoryboard”
Storyboard.TargetName=”TargetTransform”>
<DoubleAnimation x:Name=”TargetXAnimation”
Storyboard.TargetProperty=”TranslateX” Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuarticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation x:Name=”TargetYAnimation”
Storyboard.TargetProperty=”TranslateY” Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuarticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation x:Name=”TargetScaleXAnimation”
Storyboard.TargetProperty=”ScaleX” Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuarticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation x:Name=”TargetScaleYAnimation”
Storyboard.TargetProperty=”ScaleY” Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuarticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation x:Name=”MovingPieceScaleXAnimation”
Storyboard.TargetName=”MovingPieceTransform”
Storyboard.TargetProperty=”ScaleX” Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuadraticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation x:Name=”MovingPieceScaleYAnimation”
Storyboard.TargetName=”MovingPieceTransform”
Storyboard.TargetProperty=”ScaleY” Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuadraticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Rotate the target, just for fun –>
<DoubleAnimation Storyboard.TargetProperty=”Rotation” By=”360”
Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuarticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Slide the best score out then back in –>
<Storyboard x:Name=”SlideBestScoreStoryboard”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName=”BestScoreTransform”
Storyboard.TargetProperty=”TranslateX”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”0”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.4” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:.4” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.8” Value=”0”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName=”BestScoreTextBlock”
Storyboard.TargetProperty=”Visibility”>
<!– Ensure the score is visible on the way in,
even if collapsed on the way out –>
<DiscreteObjectKeyFrame KeyTime=”0:0:.4” Value=”Visible”/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<!– Slide the average score out then back in –>
<Storyboard x:Name=”SlideAvgScoreStoryboard”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName=”AvgScoreTransform”
Storyboard.TargetProperty=”TranslateX”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”0”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.4” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:.4” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.8” Value=”0”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName=”AvgScoreTextBlock”
Storyboard.TargetProperty=”Visibility”>
<!– Ensure the score is visible on the way in,
even if collapsed on the way out –>
<DiscreteObjectKeyFrame KeyTime=”0:0:.4” Value=”Visible”/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<!– Animate in (then out) a message –>
<Storyboard x:Name=”ShowMessageStoryboard”
Storyboard.TargetName=”MessageTransform”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=”TranslateY”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.5” Value=”50”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:2.5” Value=”50”/>
<EasingDoubleKeyFrame KeyTime=”0:0:3” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– The application bar, with four menu items and one button –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.999”
BackgroundColor=”{StaticResource PhoneAccentColor}”>
<shell:ApplicationBarIconButton Text=”start”
IconUri=”/Shared/Images/appbar.play.png”
Click=”StartButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”instructions”
Click=”InstructionsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”calibrate”
Click=”CalibrateMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”clear scores”
Click=”ClearScoresMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<!– These two elements are shown while the images are aligned –>
<ProgressBar x:Name=”ProgressBar” Maximum=”3000” VerticalAlignment=”Top”/>
<Rectangle x:Name=”HighlightRectangle” Opacity=”0”
Fill=”{StaticResource PhoneAccentBrush}”/>
<!– The canvas with the two images –>
<Canvas>
<Rectangle x:Name=”Target” Fill=”{StaticResource PhoneAccentBrush}”
Width=”480” Height=”482”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/target.png”/>
</Rectangle.OpacityMask>
<Rectangle.RenderTransform>
<CompositeTransform x:Name=”TargetTransform” ScaleX=”0” ScaleY=”0”/>
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Fill=”{StaticResource PhoneForegroundBrush}”
Width=”480” Height=”482”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/movingPiece.png”/>
</Rectangle.OpacityMask>
<Rectangle.RenderTransform>
<CompositeTransform x:Name=”MovingPieceTransform”
ScaleX=”0” ScaleY=”0”/>
</Rectangle.RenderTransform>
</Rectangle>
</Canvas>
<!– A display for the best score, average score, and # of tries –>
<StackPanel x:Name=”ScorePanel” VerticalAlignment=”Bottom”
HorizontalAlignment=”Right” Margin=”12,72”>
<TextBlock Text=”BEST SCORE” Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”BestScoreTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-15,0,30”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”BestScoreTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock x:Name=”AvgScoreHeaderTextBlock” Text=”AVG SCORE”
Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”AvgScoreTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-15,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”AvgScoreTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
<!– An animated message –>
<Grid RenderTransformOrigin=”.5,.5” HorizontalAlignment=”Center”
VerticalAlignment=”Top”>
<Grid.RenderTransform>
<CompositeTransform x:Name=”MessageTransform” TranslateY=”800”/>
</Grid.RenderTransform>
<TextBlock x:Name=”MessageTextBlockShadow” FontWeight=”Bold” FontSize=”90”
Margin=”4,4,0,0” Foreground=”{StaticResource PhoneBackgroundBrush}”/>
<TextBlock x:Name=”MessageTextBlock” FontWeight=”Bold” FontSize=”90”/>
</Grid>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • Notice that the application bar is given an opacity of .999. This is used instead of an opacity of 1 (which looks the same) to make the code-behind a little simpler. This is explained after the next listing.
  • The page isn’t using two Image elements, but rather the familiar trick of using theme-colored rectangles with image brush opacity masks. This gives the target the appropriate accent color, and the moving piece the appropriate foreground color.

LISTING 49.2 MainPage.xaml.cs—The Code-Behind for Balance Test’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Media.Animation;
using System.Windows.Navigation;
using Microsoft.Phone.Applications.Common; // For AccelerometerHelper
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// The bounds for the moving piece
double minX, maxX, lengthX, midX;
double minY, maxY, lengthY, midY;
DateTime? timeEntered;
DateTime beginTime;
Random random;
// Persistent settings
Setting<int> bestScore = new Setting<int>(“BestScore”, 0);
Setting<double> avgScore = new Setting<double>(“AvgScore”, 0);
Setting<int> numTries = new Setting<int>(“NumTries”, 0);
int score;
bool isRunning;
const double TOLERANCE = 6;
public MainPage()
{
InitializeComponent();
// Use the accelerometer via Microsoft’s helper
AccelerometerHelper.Instance.ReadingChanged += Accelerometer_ReadingChanged;
this.random = new Random();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the persisted values
UpdateLabels(true);
// Reset
this.isRunning = false;
// Start the accelerometer with Microsoft’s helper
AccelerometerHelper.Instance.Active = true;
// While on this page, don’t allow the screen to auto-lock
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Restore the ability for the screen to auto-lock when on other pages
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Enabled;
}
void UpdateLabels(bool animateBestScore)
{
if (this.numTries.Value > 0)
{
// Ensure the panel is visible and update the text blocks
this.ScorePanel.Visibility = Visibility.Visible;
this.BestScoreTextBlock.Text = this.bestScore.Value.ToString();
this.AvgScoreTextBlock.Text = this.avgScore.Value.ToString(“##0.##”);
if (this.numTries.Value == 1)
this.AvgScoreHeaderTextBlock.Text = “AVG SCORE (1 TRY)”;
else
this.AvgScoreHeaderTextBlock.Text = “AVG SCORE (“ + this.numTries.Value
+ “ TRIES)”;
// Animate the text blocks out then in. The animations take care of
// showing the text blocks if they are collapsed.
this.SlideAvgScoreStoryboard.Begin();
if (animateBestScore)
this.SlideBestScoreStoryboard.Begin();
else
this.BestScoreTextBlock.Visibility = Visibility.Visible;
}
else
{
// Hide everything
this.ScorePanel.Visibility = Visibility.Collapsed;
this.BestScoreTextBlock.Visibility = Visibility.Collapsed;
this.AvgScoreTextBlock.Visibility = Visibility.Collapsed;
}
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerHelperReadingEventArgs e)
{
// Transition to the UI thread
this.Dispatcher.BeginInvoke(delegate()
{
if (!this.isRunning)
return;
// End the game after 1 minute
if ((DateTime.Now – beginTime).TotalMinutes >= 1)
{
GameOver();
return;
}
// Move the object based on the horizontal and vertical forces
this.MovingPieceTransform.TranslateX =
Clamp(this.midX + this.lengthX * e.LowPassFilteredAcceleration.X,
this.minX, this.maxX);
this.MovingPieceTransform.TranslateY =
Clamp(this.midY – this.lengthY * e.LowPassFilteredAcceleration.Y,
this.minY, this.maxY);
// Check if the two elements are aligned, with a little bit of wiggle room
if (Math.Abs(this.MovingPieceTransform.TranslateX
– this.TargetTransform.TranslateX) <= TOLERANCE &&
Math.Abs(this.MovingPieceTransform.TranslateY
– this.TargetTransform.TranslateY) <= TOLERANCE)
{
if (this.timeEntered == null)
{
// Start tracking the time
this.timeEntered = DateTime.Now;
this.HighlightRectangle.Opacity = .5;
}
// Show the progress
this.ProgressBar.Value =
(DateTime.Now – this.timeEntered.Value).TotalMilliseconds;
if (this.ProgressBar.Value >= this.ProgressBar.Maximum)
{
// Success!
this.score++;
// Animate in the score
this.MessageTextBlock.Text = this.score.ToString();
this.MessageTextBlockShadow.Text = this.score.ToString();
this.ShowMessageStoryboard.Begin();
// Move the target to the next location
MoveTarget();
}
}
else
{
// The elements are not aligned, so reset everything
this.HighlightRectangle.Opacity = 0;
this.timeEntered = null;
this.ProgressBar.Value = 0;
}
});
}
void GameOver()
{
this.isRunning = false;
this.ApplicationBar.IsVisible = true;
if (this.MoveTargetStoryboard.GetCurrentState() == ClockState.Active)
this.MoveTargetStoryboard.Stop();
// Shrink both elements to nothing
this.TargetScaleXAnimation.To = 0;
this.TargetScaleYAnimation.To = 0;
this.MovingPieceScaleXAnimation.To = 0;
this.MovingPieceScaleYAnimation.To = 0;
this.MoveTargetStoryboard.Begin();
// Record this attempt and update the UI
double oldTotal = this.avgScore.Value * this.numTries.Value;
// New average
this.avgScore.Value = (oldTotal + this.score) / (this.numTries.Value + 1);
// New total number of tries
this.numTries.Value++;
if (this.score > this.bestScore.Value)
{
// New best score
this.bestScore.Value = this.score;
UpdateLabels(true);
// Animate in a congratulations message
this.MessageTextBlock.Text = “NEW BEST!”;
this.MessageTextBlockShadow.Text = “NEW BEST!”;
this.ShowMessageStoryboard.Begin();
}
else
{
UpdateLabels(false);
}
}
void MoveTarget()
{
// Choose a random scale for the images, from .1 to .5
double scale = (random.Next(5) + 1) / 10d;
// Adjust the horizontal bounds of the moving piece accordingly
this.maxY = this.ActualHeight – 379 * scale;
this.minY = -104 * scale;
this.lengthY = Math.Abs(this.minY) + this.maxY;
this.midY = this.minY + this.lengthY / 2;
// Adjust the vertical bounds of the moving piece accordingly
this.maxX = this.ActualWidth – 280 * scale;
this.minX = -224 * scale;
this.lengthX = Math.Abs(this.minX) + this.maxX;
this.midX = this.minX + this.lengthX / 2;
// Prepare and begin the animation to a new location & size
this.TargetScaleXAnimation.To = this.TargetScaleYAnimation.To = scale;
this.MovingPieceScaleXAnimation.To = scale;
this.MovingPieceScaleYAnimation.To = scale;
this.TargetXAnimation.To = this.minX + random.Next((int)this.lengthX);
this.TargetYAnimation.To = this.minY + random.Next((int)this.lengthY);
this.MoveTargetStoryboard.Begin();
}
// “Clamp” the incoming value so it’s no lower than min & no larger than max
static double Clamp(double value, double min, double max)
{
return Math.Max(min, Math.Min(max, value));
}
// Application bar handlers
void StartButton_Click(object sender, EventArgs e)
{
// Get started
this.isRunning = true;
this.ApplicationBar.IsVisible = false;
this.ScorePanel.Visibility = Visibility.Collapsed;
// Center the target before it animates to a new location
this.TargetTransform.TranslateX = this.ActualWidth – this.Target.Width / 2;
this.TargetTransform.TranslateY = this.ActualHeight – this.Target.Height / 2;
MoveTarget();
this.score = 0;
this.beginTime = DateTime.Now;
}
void InstructionsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void CalibrateMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/Calibrate/CalibratePage.xaml?appName=Balance Test&”
+ “calibrateX=true&calibrateY=false”, UriKind.Relative));
}
void ClearScoresMenuItem_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to clear your scores?”,
“Clear scores”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
this.numTries.Value = 0;
this.bestScore.Value = 0;
UpdateLabels(true);
}
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Balance Test”, UriKind.Relative));
}
}
}

[/code]

  • This app uses the low-pass-filtered acceleration data from the AccelerometerHelper library. This gives a nice balance of smoothness and the right amount of latency. (Using the optimally filtered data ends up being a bit too jumpy in this case.)
  • The calculations that adjust the moving piece’s position should look familiar from the Moo Can app. Notice that the acceleration-based offset is added to the X midpoint, whereas it’s subtracted from the Y midpoint. As illustrated back in Figure 44.1, the Y-acceleration dimension grows in the opposite direction than Silverlight’s typical Y dimension.
  • The page’s ActualWidth and ActualHeight properties are used throughout. If the application bar had an opacity of 1, the page’s ActualHeight would be 728 while it is visible versus 800 while it is hidden. However, in StartButton_Click, accessing ActualHeight after hiding the application bar would still report 728, as it hasn’t given the layout system a chance to make the change. Thanks to the application bar’s custom opacity that enables the page to extend underneath it, the timing of this is no longer a concern; ActualHeight reports 800 at all times.

The Finished Product

Balance Test (2D)

Level (Determining Angle)

No book covering the use of an accelerometer would be complete without showing you how to create a level! This chapter’s Level app not only features four classic tubular bubble levels (one on each edge), but it also shows the current angle of the phone with little accent lines that line up with companion lines when one of the edges of the phone is parallel to the ground. This makes it even easier to visually align the phone exactly as you wish.

Getting smooth, stable results from the accelerometer is important for an app such as this that relies on slow, small movements. Therefore, as in the preceding chapter, Level makes use of Microsoft’s AccelerometerHelper class to perform data smoothing. The key to this app is to use a little bit of trigonometry in order to determine the angle of the phone based on the accelerometer data.

The BubbleWindow User Control

The classic bubble display, shown in Figure 48.1, is implemented as a user control. That’s because Level’s main page uses four instances of this display. Listing 48.1 contains its XAML and Listing 48.2 contains its code-behind.

The BubbleWindow user control displays a colored bubble inside a marked rectangle.
FIGURE 48.1 The BubbleWindow user control displays a colored bubble inside a marked rectangle.

LISTING 48.1 BubbleWindow.xaml—The User Interface for the BubbleWindow User Control

[code]

<UserControl x:Class=”WindowsPhoneApp.BubbleWindow”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”>
<!– Add one storyboard to the control’s resource dictionary –>
<UserControl.Resources>
<Storyboard x:Name=”BubbleStoryboard” Storyboard.TargetName=”BubbleTransform”>
<!– Stretch the bubble horizontally –>
<DoubleAnimation Storyboard.TargetProperty=”ScaleX” By=”.5”
Duration=”0:0:.8” AutoReverse=”True”>
<DoubleAnimation.EasingFunction>
<QuadraticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Shrink the bubble horizontally –>
<DoubleAnimation Storyboard.TargetProperty=”ScaleY” By=”-.2”
Duration=”0:0:.8” AutoReverse=”True”>
<DoubleAnimation.EasingFunction>
<QuadraticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</UserControl.Resources>
<Canvas Background=”#333”>
<Ellipse x:Name=”Bubble” Width=”115” Height=”115” Visibility=”Collapsed”
Fill=”{StaticResource PhoneAccentBrush}” RenderTransformOrigin=”.5,.5”>
<Ellipse.RenderTransform>
<!– Used in the animations –>
<CompositeTransform x:Name=”BubbleTransform”/>
</Ellipse.RenderTransform>
</Ellipse>
<Line x:Name=”Line1” Stroke=”White”/>
<Line x:Name=”Line2” Stroke=”White”/>
<Rectangle x:Name=”Rectangle” Stroke=”#A555” StrokeThickness=”6”/>
</Canvas>
</UserControl>

[/code]

LISTING 48.2 BubbleWindow.xaml.cs—The Code-Behind for the BubbleWindow User Control

[code]

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace WindowsPhoneApp
{
public partial class BubbleWindow : UserControl
{
public BubbleWindow()
{
InitializeComponent();
this.Loaded += BubbleWindow_Loaded;
}
void BubbleWindow_Loaded(object sender, RoutedEventArgs e)
{
// Adjust the two centered lines and the rectangular border to fit
// the size given to this instance of the control
this.Line1.X1 = this.Line1.X2 =
(this.ActualWidth / 2) – (this.Bubble.ActualWidth / 2);
this.Line1.Y2 = this.ActualHeight;
this.Line2.X1 = this.Line2.X2 =
(this.ActualWidth / 2) + (this.Bubble.ActualWidth / 2);
this.Line2.Y2 = this.ActualHeight;
this.Rectangle.Width = this.ActualWidth;
this.Rectangle.Height = this.ActualHeight;
// Don’t allow the bubble to render past this control’s border
this.Clip = new RectangleGeometry {
Rect = new Rect(0, 0, this.ActualWidth, this.ActualHeight) };
this.Bubble.Visibility = Visibility.Visible;
}
// Set the horizontal position of the bubble on a scale from 0 to 100
public void SetXPercentage(double percentage)
{
percentage = percentage / 100;
double left = (-this.Bubble.ActualWidth/2) + this.ActualWidth*percentage;
Canvas.SetLeft(this.Bubble, left);
}
// Set the vertical position of the bubble on a scale from 0 to 100
public void SetYPercentage(double percentage)
{
percentage = percentage / 100;
// Allow the bubble to go more off-screen with a taller possible range
double range = this.ActualHeight + 20 * 2;
double top = -20 + range * percentage – range / 2;
Canvas.SetTop(this.Bubble, top);
}
public void Animate()
{
// Stretch out the bubble if the animation isn’t already running
if (this.BubbleStoryboard.GetCurrentState() != ClockState.Active)
{
this.BubbleStoryboard.Stop();
this.BubbleStoryboard.Begin();
}
}
}
}

[/code]

This user control has nothing to do with the accelerometer. It simply enables its consumer to do three things:

  • Set the horizontal position of the bubble from 0% (all the way to the left) to 100% (all the way to the right).
  • Set the vertical position of the bubble from 0% (all the way to the top) to 100% (all the way to the bottom).
  • Trigger an animation that stretches the bubble, appropriate to use when the bubble is moving quickly to give it more realism. This stretching is shown in Figure 48.2.
The stretched bubble, simulating the expected deformation from fast motion.
FIGURE 48.2 The stretched bubble, simulating the expected deformation from fast motion.

Note that the manual layout code in Listing 48.2 that adjusts the lines and rectangle could be eliminated by leveraging a grid’s automatic layout rather than using a canvas.

The Main Page

Besides the same calibration page used by Moo Can and shown in the preceding chapter, Level only has the single main page. Listing 48.3 contains the XAML for the main page, and Listing 48.4 contains its code-behind.

LISTING 48.3 MainPage.xaml—The User Interface for Level’s Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
SupportedOrientations=”Portrait”>
<Grid>
<Grid.Background>
<!– The metallic background –>
<ImageBrush ImageSource=”Images/background.png”/>
</Grid.Background>
<!– The four lines in the middle that don’t move –>
<Line Y1=”240” Y2=”290” X1=”240” X2=”240” Stroke=”#555” StrokeThickness=”6”/>
<Line Y1=”510” Y2=”560” X1=”240” X2=”240” Stroke=”#555” StrokeThickness=”6”/>
<Line Y1=”400” Y2=”400” X1=”80” X2=”130” Stroke=”#555” StrokeThickness=”6”/>
<Line Y1=”400” Y2=”400” X1=”350” X2=”400” Stroke=”#555” StrokeThickness=”6”/>
<!– The four lines that tilt based on the phone’s angle –>
<Canvas RenderTransformOrigin=”.5,.5” Width=”480” Height=”800”>
<Canvas.RenderTransform>
<RotateTransform x:Name=”CenterTransform” Angle=”45”/>
</Canvas.RenderTransform>
<Line Y1=”240” Y2=”290” X1=”240” X2=”240”
Stroke=”{StaticResource PhoneAccentBrush}” StrokeThickness=”6”/>
<Line Y1=”510” Y2=”560” X1=”240” X2=”240”
Stroke=”{StaticResource PhoneAccentBrush}” StrokeThickness=”6”/>
<Line Y1=”400” Y2=”400” X1=”80” X2=”130”
Stroke=”{StaticResource PhoneAccentBrush}” StrokeThickness=”6”/>
<Line Y1=”400” Y2=”400” X1=”350” X2=”400”
Stroke=”{StaticResource PhoneAccentBrush}” StrokeThickness=”6”/>
</Canvas>
<!– The display for the exact angle –>
<TextBlock x:Name=”AngleTextBlock” Foreground=”#555”
HorizontalAlignment=”Center” VerticalAlignment=”Center”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
RenderTransformOrigin=”.5,.5”>
<TextBlock.RenderTransform>
<RotateTransform x:Name=”AngleTextBlockTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
<!– The circle in the center –>
<Ellipse Width=”220” Height=”220” Stroke=”#555” StrokeThickness=”6”/>
<!– The four bubble level displays –>
<local:BubbleWindow x:Name=”TopWindow” Width=”275” Height=”75”
VerticalAlignment=”Top”/>
<local:BubbleWindow x:Name=”BottomWindow” Width=”275” Height=”75”
VerticalAlignment=”Bottom”/>
<local:BubbleWindow x:Name=”LeftWindow” Width=”622” Height=”75”
HorizontalAlignment=”Left” Margin=”-274,0,0,0” RenderTransformOrigin=”.5,.5”>
<local:BubbleWindow.RenderTransform>
<CompositeTransform Rotation=”-90”/>
</local:BubbleWindow.RenderTransform>
</local:BubbleWindow>
<local:BubbleWindow x:Name=”RightWindow” Width=”622” Height=”75”
HorizontalAlignment=”Right” Margin=”0,0,-274,0” RenderTransformOrigin=”.5,.5”>
<local:BubbleWindow.RenderTransform>
<CompositeTransform Rotation=”-90”/>
</local:BubbleWindow.RenderTransform>
</local:BubbleWindow>
<!– The calibrate pseudo-button that tilts based on the phone’s angle –>
<Border Margin=”0,0,0,120” HorizontalAlignment=”Center”
VerticalAlignment=”Bottom” local:Tilt.IsEnabled=”True”
MouseLeftButtonUp=”Calibrate_MouseLeftButtonUp”>
<TextBlock Text=”calibrate” Padding=”36” Foreground=”#555”
FontSize=”{StaticResource PhoneFontSizeExtraLarge}”
RenderTransformOrigin=”.5,.5”>
<TextBlock.RenderTransform>
<RotateTransform x:Name=”CalibrateTextBlockTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
</Border>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 48.4 MainPage.xaml.cs—The Code-Behind for Level’s Main Page

[code]

using System;
using System.Windows.Input;
using System.Windows.Navigation;
using Microsoft.Phone.Applications.Common; // For AccelerometerHelper
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Used to track every 5th call to Accelerometer_ReadingChanged
double readingCount = 0;
// The angle 5 calls ago
double previousAngle = 0;
public const double RADIANS_TO_DEGREES = 180 / Math.PI;
public MainPage()
{
InitializeComponent();
// Use the accelerometer via Microsoft’s helper
AccelerometerHelper.Instance.ReadingChanged += Accelerometer_ReadingChanged;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Start the accelerometer with Microsoft’s helper
AccelerometerHelper.Instance.Active = true;
// While on this page, don’t allow the screen to auto-lock
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Restore the ability for the screen to auto-lock when on other pages
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Enabled;
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerHelperReadingEventArgs e)
{
// This is the key formula
double rawAngle =
Math.Atan2(e.AverageAcceleration.X, e.AverageAcceleration.Y)
* RADIANS_TO_DEGREES;
// Express the angle from 0-90°, used by some calculations
double angle0to90 = Math.Abs(rawAngle) % 90;
// Calculate the horizontal % of the left & right bubbles by
// using the angle as an offset from the midpoint (50%)
double landscapeXPercentage = Clamp(Math.Abs(rawAngle) > 90
? 50 + angle0to90
: 50 – (90 – angle0to90), 0, 100);
// The horizontal % of the top & bottom bubbles requires more cases
double portraitXPercentage;
// When the bottom bubble window is on top
if (Math.Abs(rawAngle) <= 25)
portraitXPercentage = Clamp(rawAngle > 0
? 50 – angle0to90 * 2
: 50 + angle0to90 * 2, 0, 100);
// When the top bubble window is on top
else if (Math.Abs(rawAngle) >= 155)
portraitXPercentage = Clamp(rawAngle > 0
? 50 – (90 – angle0to90) * 2
: 50 + (90 – angle0to90) * 2, 0, 100);
// When the right bubble window is on top
else if (rawAngle < 0)
portraitXPercentage = 100;
// When the left bubble window is on top
else
portraitXPercentage = 0;
// The Y% for the left/right bubbles is the same
// as the X% for the top/bottom bubbles
double landscapeYPercentage = portraitXPercentage;
// The Y% for the top/bottom bubbles is the same
// as the inverse of the X% for the left/right bubbles
double portraitYPercentage = 100 – landscapeXPercentage;
this.Dispatcher.BeginInvoke(delegate()
{
// Set the primary (horizontal) position of each bubble
this.TopWindow.SetXPercentage(portraitXPercentage);
this.BottomWindow.SetXPercentage(portraitXPercentage);
this.LeftWindow.SetXPercentage(landscapeXPercentage);
this.RightWindow.SetXPercentage(landscapeXPercentage);
// Set the vertical position of each bubble
this.TopWindow.SetYPercentage(portraitYPercentage);
this.BottomWindow.SetYPercentage(portraitYPercentage);
this.LeftWindow.SetYPercentage(landscapeYPercentage);
this.RightWindow.SetYPercentage(landscapeYPercentage);
// On every 5th call, check to see if significant motion has occurred.
// If the angle has changed by more than 8 degrees, trigger the
// animation on any bubbles that are vertically close to the top
// or bottom of their window (< 10% or > 90%).
readingCount = (readingCount + 1) % 5;
if (readingCount == 0)
{
if (Math.Abs(previousAngle – rawAngle) > 8)
{
if (portraitYPercentage < 10 || portraitYPercentage > 90)
{
this.TopWindow.Animate();
this.BottomWindow.Animate();
}
if (landscapeYPercentage < 10 || landscapeYPercentage > 90)
{
this.LeftWindow.Animate();
this.RightWindow.Animate();
}
}
this.previousAngle = rawAngle;
}
// Update the exact angle display, using values from 0 to 45°
double angle0to45 = angle0to90 <= 45 ? angle0to90 : 90 – angle0to90;
this.AngleTextBlock.Text = angle0to45.ToString(“##0.0°”);
// Tilt the non-bubble pieces of UI accordingly
this.AngleTextBlockTransform.Angle = rawAngle – 180;
this.CalibrateTextBlockTransform.Angle = rawAngle – 180;
this.CenterTransform.Angle = rawAngle – 180;
});
}
void Calibrate_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
this.NavigationService.Navigate(
new Uri(“/Shared/Calibrate/CalibratePage.xaml?” +
“appName=Level&calibrateX=true&calibrateY=true”, UriKind.Relative));
}
// “Clamp” the incoming value so it’s no lower than min & no larger than max
static double Clamp(double value, double min, double max)
{
return Math.Max(min, Math.Min(max, value));
}
}
}

[/code]

  • The most important calculation in this app is done as the first line of Accelerometer_ReadingChanged. The X and Y acceleration vectors are perpendicular to each other. Therefore, you can imagine the X and Y values from the accelerometer as two sides of a right triangle, as pictured in Figure 48.3. Trigonometry tells us that the tangent of an angle is the length of the opposite side divided by the length of the adjacent side (the “TOA” in the classic “SOHCAH- TOA” mnemonic). Therefore, the angle equals the arctangent of the opposite length divided by the adjacent length. Math.Atan2 performs this calculation for us, and even handles the potential divide-by-zero case so we don’t have to.
In a right triangle, the tangent of an angle equals the opposite length divided by the adjacent length.
FIGURE 48.3 In a right triangle, the tangent of an angle equals the opposite length divided by the adjacent length.

The trigonometric functions in System.Math return angles specified in radians!

This is why Listing 48.4 multiplies the result of Math.Atan2 by the RADIANS_TO_DEGREES constant.This converts the value expressed in radians to a value expressed in degrees by multiplying the value by 180 / π.

  • For the call to Math.Atan2, the X axis is chosen as the opposite side of the right triangle and the Y axis is chosen as the adjacent side. This choice is arbitrary, but it does impact the resultant angle. Using X as the opposite side produces the values for rawAngle illustrated in Figure 48.4.
The raw angle reported by Math.Atan2, where each arrow indicates the direction that must point toward the sky.
FIGURE 48.4 The raw angle reported by Math.Atan2, where each arrow indicates the direction that must point toward the sky.
  • The bulk of the code in Accelerometer_ReadingChanged determines the appropriate horizontal percentage for the four BubbleWindow user controls based on the current angle. (The two “portrait” bubbles—top and bottom— always have the same values as each other, as do the two “landscape” bubbles.)
  • Because of the symmetry between the raw angle reported when the left side is facing the sky and when the right side is facing the sky, calculating the horizontal percentage for the landscape bubbles is relatively straightforward. Each degree away from 90° (or –90°) adds or subtracts one percentage point from the 50% midpoint.
  • Because of the asymmetry of the top/bottom raw angles, the horizontal percentage calculation for the portrait bubbles is more complex.
  • The angle displayed in the middle of the screen is 0.0° when any of the four sides is exactly level with the ground. Therefore, the displayed angle never gets higher than 45°. At such a point, the value starts decreasing as the next side gets closer to becoming level.
  • Rather than checking on every ReadingChanged event, this listing checks for large motion that should trigger the bubble animation every fifth event. This is a tradeoff between performance and latency.

To get even more stability in its readings, the Level app tweaks the code for AccelerometerHelper to make AverageAcceleration examine the previous 50 samples rather than 25.This was done by simply changing the value of a SamplesCount static variable from 25 to 50. This has the side effect of doubling the latency in the reported data, but this is acceptable for a level app.The latency simply makes the liquid in each bubble window act a little “thicker.”

The Finished Product

Level (Determining Angle)

Moo Can (Turn Over)

Do you remember those cans that moo when you turn them upside down? The Moo Can app brings this classic children’s toy back to life in digital form! Moo Can makes a moo sound when you turn your phone upside down. Gravity also affects the cow on the screen, which rotates and falls toward whatever edge of the screen is currently on the bottom.

Just like the real cans, you can shake the phone to make a harsh sound that is different from the desired “moo” caused by gently turning the phone upside down. You can also change the cow to a sheep or cat, each of which makes its own unique sounds.

The Main Page

Moo Can has a main page, a calibration page, and an instructions page (not shown in this chapter). Listing 47.1 contains the XAML for the main page, and Listing 47.2 contains its code-behind.

LISTING 47.1 MainPage.xaml—The User Interface for Moo Can’s Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
SupportedOrientations=”Portrait”>
<!– The application bar, with five menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar BackgroundColor=”#9CB366” ForegroundColor=”White”>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”cow” Click=”AnimalMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”sheep” Click=”AnimalMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”cat” Click=”AnimalMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”instructions”
Click=”InstructionsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”calibrate”
Click=”CalibrateMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Canvas>
<Canvas.Background>
<ImageBrush ImageSource=”Images/background.png”/>
</Canvas.Background>
<!– The cow, sheep, or cat –>
<Image x:Name=”AnimalImage” RenderTransformOrigin=”.5,.5”
Canvas.Left=”32” Canvas.Top=”50” Width=”434” Height=”507”>
<Image.RenderTransform>
<CompositeTransform x:Name=”AnimalTransform”/>
</Image.RenderTransform>
</Image>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

The user interface contains an animal image (whose source is set in code-behind) placed over the background image. It also uses an application bar menu, shown in Figure 47.1, for switching the animal or navigating to either of the other two pages. The application bar is given hard-coded colors, so it blends in with the grass in the background image when the menu is closed.

The application bar menu on the main page.
FIGURE 47.1 The application bar menu on the main page.

LISTING 47.2 MainPage.xaml.cs—The Code-Behind for Moo Can’s Main Page

[code]

using System;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using Microsoft.Phone.Applications.Common; // For AccelerometerHelper
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
Setting<string> chosenAnimal = new Setting<string>(“ChosenAnimal”, “cow”);
bool upsideDown = false;
// The start, middle, end, and length of the
// vertical path that the animal moves along
const double MAX_Y = 170;
const double MIN_Y = -75;
const double MID_Y = 47.5;
const double LENGTH_Y = 245;
// The start, middle and end of the clockwise rotation of the animal
const double MAX_ANGLE_CW = 180;
const double MIN_ANGLE_CW = 0;
const double MID_ANGLE_CW = 90;
// The start, middle and end of the counter-clockwise rotation of the animal
const double MIN_ANGLE_CCW = -180;
const double MAX_ANGLE_CCW = 0;
const double MID_ANGLE_CCW = -90;
// The length of the rotation, regardless of which direction
const double LENGTH_ANGLE = 180;
public MainPage()
{
InitializeComponent();
// Use the accelerometer via Microsoft’s helper
AccelerometerHelper.Instance.ReadingChanged += Accelerometer_ReadingChanged;
SoundEffects.Initialize();
// Allow the app to run (producing sounds) even when the phone is locked.
// Once disabled, you cannot re-enable the default behavior!
PhoneApplicationService.Current.ApplicationIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Restore the chosen animal
ChangeAnimal();
// Start the accelerometer with Microsoft’s helper
AccelerometerHelper.Instance.Active = true;
// While on this page, don’t allow the screen to auto-lock
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Restore the ability for the screen to auto-lock when on other pages
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Enabled;
}
void ChangeAnimal()
{
switch (this.chosenAnimal.Value)
{
case “cow”:
this.AnimalImage.Source = new BitmapImage(
new Uri(“Images/cow.png”, UriKind.Relative));
break;
case “sheep”:
this.AnimalImage.Source = new BitmapImage(
new Uri(“Images/sheep.png”, UriKind.Relative));
break;
case “cat”:
this.AnimalImage.Source = new BitmapImage(
new Uri(“Images/cat.png”, UriKind.Relative));
break;
}
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerHelperReadingEventArgs e)
{
// Transition to the UI thread
this.Dispatcher.BeginInvoke(delegate()
{
// Move the animal vertically based on the vertical force
this.AnimalTransform.TranslateY =
Clamp(MID_Y – LENGTH_Y * e.AverageAcceleration.Y, MIN_Y, MAX_Y);
// Clear the upside-down flag, only when completely upright
if (this.AnimalTransform.TranslateY == MAX_Y)
this.upsideDown = false;
// Rotate the animal to always be upright
if (e.AverageAcceleration.X <= 0)
this.AnimalTransform.Rotation = Clamp(MID_ANGLE_CW +
LENGTH_ANGLE * e.AverageAcceleration.Y, MIN_ANGLE_CW, MAX_ANGLE_CW);
else
this.AnimalTransform.Rotation = Clamp(MID_ANGLE_CCW –
LENGTH_ANGLE * e.AverageAcceleration.Y, MIN_ANGLE_CCW, MAX_ANGLE_CCW);
// Play the appropriate shake sound when shaken
if (ShakeDetection.JustShook(e.OriginalEventArgs))
{
switch (this.chosenAnimal.Value)
{
case “cow”: SoundEffects.MooShake.Play(); break;
case “sheep”: SoundEffects.BaaShake.Play(); break;
case “cat”: SoundEffects.MeowShake.Play(); break;
}
}
// Play the normal sound when first turned upside-down
if (!this.upsideDown && this.AnimalTransform.TranslateY == MIN_Y)
{
this.upsideDown = true;
switch (this.chosenAnimal.Value)
{
case “cow”: SoundEffects.Moo.Play(); break;
case “sheep”: SoundEffects.Baa.Play(); break;
case “cat”: SoundEffects.Meow.Play(); break;
}
}
});
}
// “Clamp” the incoming value so it’s no lower than min & no larger than max
static double Clamp(double value, double min, double max)
{
return Math.Max(min, Math.Min(max, value));
}
// Application bar handlers
void AnimalMenuItem_Click(object sender, EventArgs e)
{
this.chosenAnimal.Value = (sender as IApplicationBarMenuItem).Text;
ChangeAnimal();
}
void InstructionsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void CalibrateMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/Calibrate/CalibratePage.xaml?appName=Moo Can”, UriKind.Relative));
}
}
}

[/code]

  • This app moves and rotates the animal based on the accelerometer’s data, as shown in Figure 47.2. However, the raw data has a lot of noise, so using it directly would produce a jerky result. (This noise didn’t matter for the previous three apps because the punch, throw, and shake detection are coarse.) Therefore, this listing makes use of an AccelerometerHelper class published by Microsoft that performs data smoothing for you. It also simplifies the starting/stopping interaction with the accelerometer.
The chosen animal rotates and falls as you turn the phone upside down.
FIGURE 47.2 The chosen animal rotates and falls as you turn the phone upside down.
  • AccelerometerHelper exposes its functionality via a static Instance property, so the constructor uses this to attach a handler to its ReadingChanged event. This event is just like the ReadingChanged event from the preceding chapters, but with richer data passed to handlers.
  • This project uses a SoundEffects class just like the one from previous apps but with six properties for six possible sounds: Moo, MooShake, Baa, BaaShake, Meow, and MeowShake.
  • This app runs while locked and this page prevents the screen from auto-locking. This way, you can make the noises without keeping the phone screen on. At the same time, you can continue to watch the cow fall up and down without having to periodically tap the screen to keep it on.
  • To start the accelerometer, OnNavigatedTo simply sets AccelerometerHelper’s Instance’s Active property to true. Internally, this calls Start (if not already started) with the same sort of exception handling done in preceding chapters. This app doesn’t bother stopping the accelerometer, but this could be done by setting Active to false inside OnNavigatedFrom.
  • Inside Accelerometer_ReadingChanged, the AccelerometerHelper-specific AccelerometerHelperReadingEventArgs instance is leveraged to get the average acceleration in the X and Y directions. This class is different from the AccelerometerReadingEventArgs used by the preceding apps. It exposes several properties for getting the data with various types of smoothing applied.

AccelerometerHelperReadingEventArgs exposes four properties that enable you to choose how raw or smooth you want your data to be:

  • RawAcceleration—The same noisy data you would get from the Accelerometer class’s ReadingChanged event (but with calibration potentially applied, as described later in this chapter).
  • LowPassFilteredAcceleration—Applies a first-order low-pass filter over the raw data. The result is smoother data with a little bit of latency.
  • OptimalyFilteredAcceleration [sic]—This uses the same low-pass filter, but only when the current value is close enough to a rolling average value. If the value is sufficiently greater, the raw value is reported instead. (This algorithm is done independently for all three axes.) This gives a nice balance between having smooth results and low latency. Small changes are handled smoothly and large changes are reported quickly.
  • AverageAcceleration—Reports the mean of the most recent 25 data points collected from the “optimally filtered” algorithm.This gives the smoothest result of any of the choices, but it also has the highest latency.

Each property exposes X, Y, Z, and Magnitude properties (but unlike AccelerometerReadingEventArgs, no Timestamp property). Magnitude reports the length of the 3D vector formed by the other three values, which is the square root of X2 + Y2 + Z2. For more details about the algorithms behind these properties, see http://bit.ly/accelerometerhelper.

  • This app uses the same ShakeDetection class shown in the preceding chapter. Because the JustShook method is defined to accept an instance of AccelerometerReadingEventArgs—not AccelerometerHelperReadingEventArgs—this listing uses an OriginalEventArgs property to retrieve it. This property actually isn’t exposed by the AccelerometerHelperReadingEventArgs class; I added it directly to my copy of the source code.
  • A turn upside down is detected by noticing the first moment that the Y acceleration value is large enough after a point in time when it has been small enough. In other words, after the upside-down orientation has been detected, the user must turn the phone right side up to clear the value of upsideDown before another upside-down turn will be detected. Because the animal transform’s TranslateY value is proportional to the Y-axis acceleration (and restricted to a range of MIN_Y to MAX_Y), the phone is completely upside down when TranslateY is MIN_Y and the phone is completely upright when TranslateY is MAX_Y.
  • This app purposely does not make any noise when turned right side up, because a real can does not either. (I didn’t realize this until I bought a few of the cans and tried for myself.)

The Calibration Page

Moo Can enables users to calibrate the accelerometer in case its notion of right side up or upside down are slightly askew from reality. The process of calibration simply involves asking the user when they believe the phone is level, remembering the accelerometer’s reading at that moment, and then using those values as an offset to the accelerometer data from that point onward.

One benefit of using AccelerometerHelper rather than the raw accelerometer APIs is that calibration functionality is built in. It collects the data, stores it as isolated storage application settings (named “AccelerometerCalibrationX” and “AccelerometerCalibrationY”), and automatically offsets the data it returns—even the supposedly “raw” data.

The calibration page, designed to be shared among multiple apps, shows you how to take advantage of it. Listing 47.3 contains the XAML and Listing 47.4 contains the codebehind. It produces the page shown in Figure 47.3.

The calibration page enables calibrating just the X dimension, just the Y dimension, or both, but only when the phone is held fairly still and level.
FIGURE 47.3 The calibration page enables calibrating just the X dimension, just the Y dimension, or both, but only when the phone is held fairly still and level.

LISTING 47.3 CalibratePage.xaml—The User Interface for The Accelerometer Calibration Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.CalibratePage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock x:Name=”ApplicationName”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”calibrate” Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<StackPanel>
<TextBlock Margin=”24” TextWrapping=”Wrap” Text=”Tap the button …”/>
<Button x:Name=”CalibrateButton” Content=”calibrate” IsEnabled=”False”
Height=”150” Click=”CalibrateButton_Click”
local:Tilt.IsEnabled=”True”/>
<TextBlock x:Name=”WarningText” Visibility=”Collapsed” Margin=”24,0”
TextWrapping=”Wrap” FontWeight=”Bold”
Text=”Your phone is not still or level enough!”/>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 47.4 CalibratePage.xaml.cs—The Code-Behind for the Accelerometer Calibration Page

[code]

using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Applications.Common; // For AccelerometerHelper
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class CalibratePage : PhoneApplicationPage
{
bool calibrateX = true, calibrateY = true;
public CalibratePage()
{
InitializeComponent();
// Use the accelerometer via Microsoft’s helper
AccelerometerHelper.Instance.ReadingChanged += Accelerometer_ReadingChanged;
// Ensure it is active
AccelerometerHelper.Instance.Active = true;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Set the application name in the header
if (this.NavigationContext.QueryString.ContainsKey(“appName”))
{
this.ApplicationName.Text =
this.NavigationContext.QueryString[“appName”].ToUpperInvariant();
}
// Check for calibration parameters
if (this.NavigationContext.QueryString.ContainsKey(“calibrateX”))
{
this.calibrateX =
bool.Parse(this.NavigationContext.QueryString[“calibrateX”]);
}
if (this.NavigationContext.QueryString.ContainsKey(“calibrateY”))
{
this.calibrateY =
bool.Parse(this.NavigationContext.QueryString[“calibrateY”]);
}
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerHelperReadingEventArgs e)
{
this.Dispatcher.BeginInvoke(delegate()
{
bool canCalibrateX = this.calibrateX &&
AccelerometerHelper.Instance.CanCalibrate(this.calibrateX, false);
bool canCalibrateY = this.calibrateY &&
AccelerometerHelper.Instance.CanCalibrate(false, this.calibrateY);
// Update the enabled state and text of the calibration button
this.CalibrateButton.IsEnabled = canCalibrateX || canCalibrateY;
if (canCalibrateX && canCalibrateY)
this.CalibrateButton.Content = “calibrate (flat)”;
else if (canCalibrateX)
this.CalibrateButton.Content = “calibrate (portrait)”;
else if (canCalibrateY)
this.CalibrateButton.Content = “calibrate (landscape)”;
else
this.CalibrateButton.Content = “calibrate”;
this.WarningText.Visibility = this.CalibrateButton.IsEnabled ?
Visibility.Collapsed : Visibility.Visible;
});
}
void CalibrateButton_Click(object sender, RoutedEventArgs e)
{
if (AccelerometerHelper.Instance.Calibrate(this.calibrateX,
this.calibrateY) ||
AccelerometerHelper.Instance.Calibrate(this.calibrateX, false) ||
AccelerometerHelper.Instance.Calibrate(false, this.calibrateY))
{
// Consider it a success if we were able to
// calibrate in either direction (or both)
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
else
{
MessageBox.Show(“Unable to calibrate. Make sure you’re holding your “ +
“phone still, even when tapping the button!”, “Calibration Error”,
MessageBoxButton.OK);
}
}
}
}

[/code]

  • This page enables calibration in just one dimension—or both—based on the query parameters passed when navigating to it. Listing 47.2 simply passes “appName=Moo Can” as the query string, so this app will enable any kind of calibration. Most likely, the user will be holding the phone in the portrait orientation when attempting to calibrate.
  • The calibration method— AccelerometerHelper.Instance.Calibrate—only succeeds if the phone is sufficiently still and at least somewhat-level in the relevant dimension(s). The AccelerometerHelper.Instance.CanCalibrate method tells you whether calibration will succeed, although the answer can certainly change between the time you call CanCalibrate and the time you call Calibrate, so you should always be prepared for Calibrate to fail.
  • The ReadingChanged event handler continually checks CanCalibrate so it can enable/disable the calibration button appropriately. CanCalibrate has two Boolean parameters that enable you to specify whether you care about just the X dimension, just the Y dimension, or both. (Calibrating the Z axis is not meaningful.) Listing 47.4 checks each dimension individually (if the app cares about the dimension) so it can display a helpful message to the user. The only way you can calibrate both X and Y dimensions simultaneously is by placing the phone flat on a surface parallel to the ground.
  • CalibrateButton_Click tries to calibrate whatever will succeed. Calibrate has the same two parameters as CanCalibrate, so this listing first attempts to calibrate both dimensions, but if that fails (by returning false), it tries to calibrate each dimension individually.

Moo Can (Turn Over)

Noise Maker (Shake)

Noise Maker makes a loud, annoying noise when you shake the phone. This app is meant for sporting events, much like the vuvuzelas that became infamous during the 2010 FIFA World Cup. You can use it to help cheer on your team, or to distract the opposition. The main lesson of this chapter—figuring out when the phone is being shaken— can be useful for many apps. You might wish to use the same shake-detection algorithm for more serious apps, perhaps as a gesture to refresh data.

The Main Page

Noise Maker has a main page, a settings page, and an about page (not shown in this chapter). Listing 46.1 contains the XAML for the main page. This produces the user interface shown to the right. Listing 46.2 contains its code-behind.

LISTING 46.1 MainPage.xaml—The User Interface for Noise Maker’s Main Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
SupportedOrientations=”Portrait”>
<!– The application bar, with two menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”settings”
Click=”SettingsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<StackPanel>
<!– An accent-colored image –>
<Rectangle Fill=”{StaticResource PhoneAccentBrush}” Width=”456” Height=”423”
Margin=”{StaticResource PhoneMargin}”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/logo.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<!– Accent-colored rotated text –>
<TextBlock FontFamily=”Segoe WP Black” Text=”SHAKE TO MAKE NOISE!”
FontSize=”100” Foreground=”{StaticResource PhoneAccentBrush}”
TextWrapping=”Wrap” TextAlignment=”Center” Margin=”0,20,0,0”
LineHeight=”80” LineStackingStrategy=”BlockLineHeight”
RenderTransformOrigin=”.5,.5”>
<TextBlock.RenderTransform>
<RotateTransform Angle=”-10”/>
</TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
</phone:PhoneApplicationPage>

[/code]

LISTING 46.2 MainPage.xaml.cs—The Code-Behind for Noise Maker’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Navigation;
using Microsoft.Devices.Sensors;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
Accelerometer accelerometer;
public MainPage()
{
InitializeComponent();
// Initialize the accelerometer
this.accelerometer = new Accelerometer();
this.accelerometer.ReadingChanged += Accelerometer_ReadingChanged;
SoundEffects.Initialize();
// Allow the app to run (producing sounds) even when the phone is locked.
// Once disabled, you cannot re-enable the default behavior!
PhoneApplicationService.Current.ApplicationIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Start the accelerometer
try
{
this.accelerometer.Start();
}
catch
{
MessageBox.Show(
“Unable to start your accelerometer. Please try running this app again.”,
“Accelerometer Error”, MessageBoxButton.OK);
}
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Stop the accelerometer
try
{
this.accelerometer.Stop();
}
catch { /* Nothing to do */ }
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerReadingEventArgs e)
{
if (ShakeDetection.JustShook(e))
{
// We’re on a different thread, so transition to the UI thread
this.Dispatcher.BeginInvoke(delegate()
{
// Play each sound, which builds on top
// of previously-playing sound effects
if (Settings.IsLowChosen.Value)
SoundEffects.Low.Play();
if (Settings.IsMediumChosen.Value)
SoundEffects.Medium.Play();
if (Settings.IsHighChosen.Value)
SoundEffects.High.Play();
});
}
}
// Application bar handlers
void SettingsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/AboutPage.xaml”,
UriKind.Relative));
}
}
}

[/code]

  • This listing makes use of three persisted settings defined in a separate Settings.cs file as follows:

    [code]
    public static class Settings
    {
    public static readonly Setting<bool> IsLowChosen =
    new Setting<bool>(“IsLowChosen”, false);
    public static readonly Setting<bool> IsMediumChosen =
    new Setting<bool>(“IsMediumChosen”, true);
    public static readonly Setting<bool> IsHighChosen =
    new Setting<bool>(“IsHighChosen”, false);
    }
    [/code]

  • This app supports playing one, two, or three sounds simultaneously when a shake is detected. Any previously playing sound effects are not stopped by the calls to Play. The resulting additive effect means that shaking more vigorously (technically, more frequently) causes the sound to be louder.
  • The shake detection is done with a simple JustShook method defined in Listing 46.3.

LISTING 46.3 ShakeDetection.cs—The Shake Detection Algorithm

[code]

using System;
using Microsoft.Devices.Sensors;
namespace WindowsPhoneApp
{
public static class ShakeDetection
{
static AccelerometerReadingEventArgs previousData;
static int numShakes;
// Two properties for controlling the algorithm
public static int RequiredConsecutiveShakes { get; set; }
public static double Threshold { get; set; }
static ShakeDetection()
{
RequiredConsecutiveShakes = 1;
Threshold = .7;
}
// Call this with the accelerometer data
public static bool JustShook(AccelerometerReadingEventArgs e)
{
if (previousData != null)
{
if (IsShaking(previousData, e, Threshold))
{
numShakes++;
if (numShakes == RequiredConsecutiveShakes)
{
// Just shook!
numShakes = 0;
return true;
}
}
else if (!IsShaking(previousData, e, .2))
numShakes = 0;
}
previousData = e;
return false;
}
// It’s a shake if the values in at least two dimensions
// are different enough from the previous values
static bool IsShaking(AccelerometerReadingEventArgs previous,
AccelerometerReadingEventArgs current, double threshold)
{
double deltaX = Math.Abs(previous.X – current.X);
double deltaY = Math.Abs(previous.Y – current.Y);
double deltaZ = Math.Abs(previous.Z – current.Z);
return (deltaX > threshold && deltaY > threshold) ||
(deltaY > threshold && deltaZ > threshold) ||
(deltaX > threshold && deltaZ > threshold);
}
}
}

[/code]

  • The core part of the algorithm—the IsShaking method—simply checks for two out of the three data points being sufficiently different from the previous set of data points. “Sufficiently different” is determined by a threshold that defaults to .7 but can be changed by the consumer of this class.
  • JustShook keeps track of the previous data and calls IsShaking. It uses the RequiredConsecutiveShakes property to support specifying a number of consecutive shakes required before considering that a shake has happened.
  • Although it’s a good idea for apps to provide calibration functionality when the accelerometer is used, it’s not really necessary for something as coarse as shake detection. Any differences between phones are not likely to be noticed.

The Settings Page

The settings page enables customization of the sound effects.
FIGURE 46.1 The settings page enables customization of the sound effects.

The settings page, shown in Figure 46.1, enables the user to turn on/off any of the three noises with simple check boxes. Listing 46.4 contains the XAML, and Listing 46.5 contains the codebehind.

LISTING 46.4 SettingsPage.xaml—The User Interface for Noise Maker’s Settings Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.SettingsPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<Grid Background=”{StaticResource PhoneBackgroundBrush}”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard settings header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”SETTINGS” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”noise maker”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<StackPanel Margin=”{StaticResource PhoneMargin}”>
<TextBlock Text=”Choose one, two, or three noises”
Foreground=”{StaticResource PhoneSubtleBrush}”
Margin=”{StaticResource PhoneMargin}”/>
<CheckBox x:Name=”LowCheckBox” Content=”low”
Checked=”CheckBox_IsCheckedChanged”
Unchecked=”CheckBox_IsCheckedChanged” local:Tilt.IsEnabled=”True”/>
<CheckBox x:Name=”MediumCheckBox” Content=”medium”
Checked=”CheckBox_IsCheckedChanged”
Unchecked=”CheckBox_IsCheckedChanged” local:Tilt.IsEnabled=”True”/>
<CheckBox x:Name=”HighCheckBox” Content=”high”
Checked=”CheckBox_IsCheckedChanged”
Unchecked=”CheckBox_IsCheckedChanged” local:Tilt.IsEnabled=”True”/>
<!– A warning for when no sounds are checked –>
<StackPanel x:Name=”WarningPanel” Visibility=”Collapsed”
Orientation=”Horizontal” Margin=”12,4,0,0”>
<!– Use the image as an opacity mask for the rectangle, so the image
visible in both dark and light themes –>
<Rectangle Fill=”{StaticResource PhoneForegroundBrush}”
Width=”48” Height=”48”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Shared/Images/normal.error.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<TextBlock Text=”No sounds will be made unless you check at least one!”
TextWrapping=”Wrap” Width=”350”
Margin=”{StaticResource PhoneMargin}”/>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 46.5 SettingsPage.xaml.cs—The Code-Behind for Noise Maker’s Settings Page

[code]

using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class SettingsPage : PhoneApplicationPage
{
public SettingsPage()
{
InitializeComponent();
ShowOrHideWarning();
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Save the settings
Settings.IsLowChosen.Value = this.LowCheckBox.IsChecked.Value;
Settings.IsMediumChosen.Value = this.MediumCheckBox.IsChecked.Value;
Settings.IsHighChosen.Value = this.HighCheckBox.IsChecked.Value;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the settings
this.LowCheckBox.IsChecked = Settings.IsLowChosen.Value;
this.MediumCheckBox.IsChecked = Settings.IsMediumChosen.Value;
this.HighCheckBox.IsChecked = Settings.IsHighChosen.Value;
}
void CheckBox_IsCheckedChanged(object sender, RoutedEventArgs e)
{
ShowOrHideWarning();
}
void ShowOrHideWarning()
{
if (!this.LowCheckBox.IsChecked.Value &&
!this.MediumCheckBox.IsChecked.Value &&
!this.HighCheckBox.IsChecked.Value)
this.WarningPanel.Visibility = Visibility.Visible;
else
this.WarningPanel.Visibility = Visibility.Collapsed;
}
}
}

[/code]

Other than a straightforward mapping of three check boxes to three settings, this page contains a warning that gets shown when all three check boxes are unchecked. This is shown in Figure 46.2.

The settings page shows a warning if all check boxes are unchecked.
FIGURE 46.2 The settings page shows a warning if all check boxes are unchecked.

The Finished Product

Noise Maker (Shake)

Boxing Glove (Accelerometer Basics)

Boxing Glove is an app for people who feel like being immature (or people who simply are immature). With it, you can throw punches into the air and hear a variety of punching/groaning sound effects. The punching sound effects occur right when you hit your imaginary target.

Boxing Glove supports right- or left-handed punching, and has a few entertaining features, such as a button that makes a “ding ding ding” bell sound as if to signify the start of a fight.

To provide the effect of making a punching sound at the appropriate time, this app uses the phone’s accelerometer.

The Accelerometer

Several times a second, the phone’s accelerometer reports the direction and magnitude of the total force being applied to the phone. This force is expressed with three values—X, Y, and Z—where X is horizontal, Y is vertical, and Z is perpendicular to the screen. This is illustrated in Figure 44.1.

The three accelerometer dimensions, relative to the phone screen.
FIGURE 44.1 The three accelerometer dimensions, relative to the phone screen.

The magnitude of each value is a multiplier of g (the gravitational force on the surface of Earth). Each value is restricted to a range from -2 to 2. If the phone is resting flat on a table with the screen up, the values reported for X and Y are roughly zero, and the value of Z is roughly -1 (1 g into the screen toward the ground). That’s because the only force being applied to the phone in this situation is gravity. By shifting the phone’s angle and orientation and then keeping it roughly still, the values of X, Y, and Z reveal which way is down in the real world thanks to the ever-present force of gravity. When you abruptly move or shake the phone, the X, Y, and Z values are able to reveal this activity as well.

Regardless of how you contort your phone, the X,Y, and Z axes used for the accelerometer data remain fixed to the phone. For example, the Y axis always points toward the top edge of the phone.

To get the accelerometer data, you create an instance of the Accelerometer class from the Microsoft.Devices.Sensors namespace in the Microsoft.Devices.Sensors assembly.

This assembly is not referenced by Windows Phone projects by default, so you must add it via the Add Reference dialog in Visual Studio.

The Accelerometer class exposes Start and Stop methods and—most importantly— a ReadingChanged event. This event gets raised many times a second (after Start is called) and reports the data via properties on the event-args parameter passed to handlers. These properties are X, Y, Z, and Timestamp. The physical accelerometer is always running; Start and Stop simply start/stop the data reporting to your app.

Sounds pretty simple, right? The class is indeed simple although, as you’ll see in the remaining chapters, interpreting the data in a satisfactory way can be complicated.

To get the best performance and battery life, it’s good to stop the accelerometer data reporting when you don’t need the data and then restart it when you do.

The main page, with its application bar menu expanded.
FIGURE 44.2 The main page, with its application bar menu expanded.

The User Interface

Boxing Glove has a main page, a settings page, an instructions page, and an about page. The latter two pages aren’t interesting and therefore aren’t shown in this chapter, but Listing 44.1 contains the XAML for the main page. The page, with its application bar menu expanded, is shown in Figure 44.2.

LISTING 44.1 MainPage.xaml—The User Interface for Boxing Glove’s Main Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
SupportedOrientations=”Portrait”>
<!– The application bar, with two buttons and three menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.5”>
<shell:ApplicationBarIconButton Text=”ring bell”
IconUri=”/Images/appbar.bell.png” Click=”RingBellButton_Click” />
<shell:ApplicationBarIconButton Text=”switch hand”
IconUri=”/Images/appbar.leftHand.png”
Click=”SwitchHandButton_Click” />
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”instructions”
Click=”InstructionsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”settings”
Click=”SettingsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”about”
Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Border Background=”{StaticResource PhoneAccentBrush}”>
<Image Source=”Images/hand.png” RenderTransformOrigin=”.5,.5”>
<Image.RenderTransform>
<!– ScaleX is 1 for right-handed or -1 for left-handed –>
<CompositeTransform x:Name=”ImageTransform” ScaleX=”1”/>
</Image.RenderTransform>
</Image>
</Border>
</phone:PhoneApplicationPage>

[/code]

Besides the application bar, this page basically contains an image that instructs the user how to hold the phone. The background takes on the theme accent color, so the screen in Figure 44.2 is from a phone whose accent color is set to red.

The application bar contains a button for performing a “ding ding ding” bell sound on demand (to mimic the start of a fight in a boxing ring), and a button for swapping between right-handed mode and left-handed mode. The composite transform flips the image horizontally (in code-behind) when left-handed mode is in use.

The Code-Behind

Listing 44.2 contains the code-behind for the main page. It makes use of two persisted settings defined in a separate Settings.cs file as follows:

[code]

public static class Settings
{
public static readonly Setting<bool> IsRightHanded =
new Setting<bool>(“IsRightHanded”, true);
public static readonly Setting<double> Threshold =
new Setting<double>(“Threshold”, 1.5);
}

[/code]

LISTING 44.2 MainPage.xaml.cs—The Code-Behind for Boxing Glove’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Devices.Sensors;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
Accelerometer accelerometer;
IApplicationBarIconButton switchHandButton;
DateTimeOffset acceleratingQuicklyForwardTime = DateTimeOffset.MinValue;
Random random = new Random();
double currentThreshold;
public MainPage()
{
InitializeComponent();
this.switchHandButton = this.ApplicationBar.Buttons[1]
as IApplicationBarIconButton;
// Initialize the accelerometer
this.accelerometer = new Accelerometer();
this.accelerometer.ReadingChanged += Accelerometer_ReadingChanged;
SoundEffects.Initialize();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Start the accelerometer
try
{
this.accelerometer.Start();
}
catch
{
MessageBox.Show(
“Unable to start your accelerometer. Please try running this app again.”,
“Accelerometer Error”, MessageBoxButton.OK);
}
// Also ensures the threshold is updated on return from settings page
UpdateForCurrentHandedness();
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Stop the accelerometer
try
{
this.accelerometer.Stop();
}
catch { /* Nothing to do */ }
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerReadingEventArgs e)
{
// Only pay attention to large-enough magnitudes in the X dimension
if (Math.Abs(e.X) < Math.Abs(this.currentThreshold))
return;
// See if the force is in the same direction as the threshold
// (forward punching motion)
if (e.X * this.currentThreshold > 0)
{
// Forward acceleration
this.acceleratingQuicklyForwardTime = e.Timestamp;
}
else if (e.Timestamp – this.acceleratingQuicklyForwardTime
< TimeSpan.FromSeconds(.2))
{
// This is large backward force shortly after the forward force.
// Time to make the punching noise!
this.acceleratingQuicklyForwardTime = DateTimeOffset.MinValue;
// We’re on a different thread, so transition to the UI thread.
// This is a requirement for playing the sound effect.
this.Dispatcher.BeginInvoke(delegate()
{
switch (this.random.Next(0, 4))
{
case 0: SoundEffects.Punch1.Play(); break;
case 1: SoundEffects.Punch2.Play(); break;
case 2: SoundEffects.Punch3.Play(); break;
case 3: SoundEffects.Punch4.Play(); break;
}
switch (this.random.Next(0, 10)) // Only grunt some of the time
{
case 0: SoundEffects.Grunt1.Play(); break;
case 1: SoundEffects.Grunt2.Play(); break;
case 2: SoundEffects.Grunt3.Play(); break;
}
});
}
}
void UpdateForCurrentHandedness()
{
this.currentThreshold = (Settings.IsRightHanded.Value ?
Settings.Threshold.Value :
-Settings.Threshold.Value);
this.ImageTransform.ScaleX = (Settings.IsRightHanded.Value ? 1 : -1);
// Show the opposite hand on the application bar button
if (Settings.IsRightHanded.Value)
this.switchHandButton.IconUri = new Uri(“/Images/appbar.leftHand.png”,
UriKind.Relative);
else
this.switchHandButton.IconUri = new Uri(“/Images/appbar.rightHand.png”,
UriKind.Relative);
}
// Application bar handlers
void RingBellButton_Click(object sender, EventArgs e)
{
SoundEffects.DingDingDing.Play();
}
void SwitchHandButton_Click(object sender, EventArgs e)
{
Settings.IsRightHanded.Value = !Settings.IsRightHanded.Value;
UpdateForCurrentHandedness();
}
void InstructionsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void SettingsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/AboutPage.xaml”,
UriKind.Relative));
}
}
}

[/code]

  • The constructor contains the code for initializing the accelerometer. It constructs an instance of Accelerometer and attaches a handler to its ReadingChanged event. The SoundEffects class, defined in Listing 44.3, is also initialized.
  • OnNavigatedTo contains the code for starting the accelerometer, and OnNavigatedTo stops it. If it weren’t stopped, the handler would still be called (and the sound effects would still be made) while the main page is on the back stack. This would happen when the user visits the settings, instructions, or about pages. Leaving the accelerometer running would not be a problem, and may even be desirable for some apps. However, Listing 44.1 stops it to avoid two threads potentially reading/writing the threshold variable at the same time, as discussed in a later sidebar.

The calls to Accelerometer.Start and Accelerometer.Stop can throw an exception!

This can happen at development-time if you omit the ID_CAP_SENSORS capability from your app manifest. For a published app in the marketplace (which automatically gets the appropriate capability), this should not happen unless you have previously called the Accelerometer instance’s Dispose method.Of course, there’s not much that many accelerometer-based apps can do when the accelerometer fails to start, so Boxing Glove simply instructs to user to try closing and reopening the app and hope for the best.

  • Inside Accelerometer_ReadingChanged, the handler for the ReadingChanged event, only two properties of the AccelerometerReadingEventArgs instance are examined: X and Timestamp. The algorithm is as follows: If the app detects a strong forward horizontal force followed quickly by a strong backward horizontal force, it’s time to make a punching sound. Making the sound when the forward force is detected isn’t good enough, because the sound would be made too early. The sound should occur when the punching motion stops (i.e. hits the imaginary target of the punch). The detected backward force does not result from the phone being moved backward, but rather from the fast deceleration that occurs when the user stops their flying fist in mid-air.
  • The definition of “strong” used by Accelerometer_ReadingChanged’s algorithm is determined by a threshold that is configurable by the user on the settings page. The absolute value of the threshold ranges from almost 0 (.1) to almost 2 (1.9). The sign of the threshold depends on whether the app is in right-handed mode or lefthanded mode. In right-handed mode, forward motion means pushing to phone toward its left, so the threshold of forward force is negative. In left-handed mode, forward motion means pushing the phone toward its right, so the threshold of forward force is positive. This adjustment is made inside UpdateForCurrentHandedness.
  • When it’s time to make a punching noise, the sound is randomly chosen, potentially along with a randomly chosen grunting sound.

The accelerometer’s ReadingChanged event is raised on a non-UI thread!

This is great for processing the data without creating a bottleneck on the UI thread, but it does mean that you must explicitly transition to the UI thread before performing any work that requires it.This includes updating any UI elements or, as in this app, playing a sound effect. Listing 44.2 uses the page dispatcher’s BeginInvoke method to play the sound effect on the UI thread.

Although you can start the accelerometer from a page’s constructor, it’s normally better to wait until an event such as Loaded.That’s because starting it within the constructor could cause the ReadingChanged event to be raised earlier than when you’re prepared to handle it. For example, it can be raised before (or during) the deserialization of persisted settings, causing a failure if a setting is accessed in the event handler. It can be raised before the CompositionTarget.Rendering event is raised,which would be a problem if the implementation of SoundEffects (shown in the next listing) waited for the first Rendering event to call XNA’s FrameworkDispatcher.Update method.

Boxing Glove chooses to start the accelerometer in OnNavigatedTo and stop it in OnNavigatedFrom so the ReadingChanged event-handler thread doesn’t try to access the threshold member while the call to UpdateForCurrentHandedness inside OnNavigatedTo potentially updates it on the UI thread.

The SoundEffects class used by Listing 44.2 is shown in Listing 44.3. It encapsulates the work of setting up the sound effects, with code similar to the apps from Part V, “Audio & Video.”

LISTING 44.3 SoundEffects.cs—Initializes and Exposes Boxing Glove’s Eight Sound Effects

[code]

using System;
using System.Windows.Resources;
using Microsoft.Xna.Framework.Audio; // For SoundEffect
namespace WindowsPhoneApp
{
public static class SoundEffects
{
public static void Initialize()
{
StreamResourceInfo info;
info = App.GetResourceStream(new Uri(“Audio/punch1.wav”, UriKind.Relative));
Punch1 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/punch2.wav”, UriKind.Relative));
Punch2 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/punch3.wav”, UriKind.Relative));
Punch3 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/punch4.wav”, UriKind.Relative));
Punch4 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/grunt1.wav”, UriKind.Relative));
Grunt1 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/grunt2.wav”, UriKind.Relative));
Grunt2 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/grunt3.wav”, UriKind.Relative));
Grunt3 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/dingDingDing.wav”,
UriKind.Relative));
DingDingDing = SoundEffect.FromStream(info.Stream);
CompositionTarget.Rendering += delegate(object sender, EventArgs e)
{
// Required for XNA Sound Effect API to work
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
};
// Call also once at the beginning
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
}
public static SoundEffect Punch1 { get; private set; }
public static SoundEffect Punch2 { get; private set; }
public static SoundEffect Punch3 { get; private set; }
public static SoundEffect Punch4 { get; private set; }
public static SoundEffect Grunt1 { get; private set; }
public static SoundEffect Grunt2 { get; private set; }
public static SoundEffect Grunt3 { get; private set; }
public static SoundEffect DingDingDing { get; private set; }
}
}

[/code]

The Settings Page

The settings page, shown in Figure 44.3, contains a slider and a reset button for adjusting the threshold value from .1 to 1.9. The XAML is shown in Listing 44.4 and its codebehind is in Listing 44.5.

The settings page enables the user to adjust the accelerometer threshold, described as “required punching strength.”
FIGURE 44.3 The settings page enables the user to adjust the accelerometer threshold, described as “required punching strength.”

LISTING 44.4 SettingsPage.xaml—The User Interface for Boxing Glove’s Settings Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.SettingsPage” x:Name=”Page”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape”
shell:SystemTray.IsVisible=”True”>
<Grid Background=”{StaticResource PhoneBackgroundBrush}”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard settings header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”SETTINGS” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”boxing glove”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<StackPanel Margin=”{StaticResource PhoneMargin}”>
<TextBlock Text=”Required punching strength”
Foreground=”{StaticResource PhoneSubtleBrush}”
Margin=”{StaticResource PhoneMargin}”/>
<Slider x:Name=”StrengthSlider” Minimum=”.1” Maximum=”1.9”
LargeChange=”.1”
Value=”{Binding Threshold, Mode=TwoWay, ElementName=Page}”/>
<Button Content=”reset” Click=”ResetButton_Click”
local:Tilt.IsEnabled=”True”/>
<TextBlock TextWrapping=”Wrap” Margin=”{StaticResource PhoneMargin}”
Text=”…”/>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 44.5 SettingsPage.xaml.cs—The Code-Behind for Boxing Glove’s Settings Page

[code]

using System.Windows;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class SettingsPage : PhoneApplicationPage
{
public SettingsPage()
{
InitializeComponent();
}
// Simple property bound to the slider
public double Threshold
{
get { return Settings.Threshold.Value; }
set { Settings.Threshold.Value = value; }
}
void ResetButton_Click(object sender, RoutedEventArgs e)
{
this.StrengthSlider.Value = Settings.Threshold.DefaultValue;
}
}
}

[/code]

The Finished Product

Boxing Glove (Accelerometer Basics)

Spin the Bottle! (Rotate Gesture & Simulating Inertia)

The Spin the Bottle! app enables you to play the classic kissing game even if you don’t have a bottle handy. In this game, people sit in a circle and take turns spinning the bottle. When someone spins, he or she must kiss whomever the bottle ends up pointing toward. Even if you have no plans to play the game, you could still use this app as a fun time-waster. You could even find work-related applications for this, such as using it to assign tasks to your team members!

This app introduces a new gesture—the rotation gesture— which is a two-finger twist. It also simulates inertia (and friction), so the bottle keeps spinning once the fingers have left the screen and then gradually slows to a halt. The faster the fingers twist before releasing, the longer the bottle will continue to spin. This simulation of what happens in the real world is essential for this kind of app, otherwise the user could control exactly where the bottle would point!

The User Interface

This app contains only one page. Its user interface, shown to the right, is just an image of a bottle surrounded by two text blocks. Listing 43.1 contains the XAML.

LISTING 43.1 MainPage.xaml—The User Interface for Spin the Bottle!’s Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
SupportedOrientations=”Portrait”>
<!– Add two storyboards to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– The initial spin to smoothly match the angle when the two fingers first
make contact with the screen –>
<Storyboard x:Name=”SpinStoryboard” Storyboard.TargetName=”BottleTransform”
Storyboard.TargetProperty=”Angle”>
<DoubleAnimation x:Name=”SpinAnimation” Duration=”0:0:.2”/>
</Storyboard>
<!– The inertia-simulating storyboard, whose strength is based on the
velocity of the fingers –>
<Storyboard x:Name=”InertiaStoryboard” Storyboard.TargetName=”BottleTransform”
Storyboard.TargetProperty=”Angle”>
<DoubleAnimation x:Name=”InertiaAnimation”>
<DoubleAnimation.EasingFunction>
<!– Simulates drag –>
<PowerEase Power=”7”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– Allow the rotate gesture anywhere on the page –>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener PinchStarted=”GestureListener_PinchStarted”
PinchDelta=”GestureListener_PinchDelta”
PinchCompleted=”GestureListener_PinchCompleted”/>
</toolkit:GestureService.GestureListener>
<!– The explicit background is important for detecting the gesture anywhere –>
<Canvas Background=”Transparent”>
<!– The title –>
<TextBlock Canvas.Left=”-3” Canvas.Top=”74” FontFamily=”Segoe WP Black”
FontSize=”80” Foreground=”#2EA538” LineHeight=”58”
LineStackingStrategy=”BlockLineHeight”>
<TextBlock.RenderTransform>
<RotateTransform Angle=”-10”/>
</TextBlock.RenderTransform>
SPIN THE<LineBreak/>BOTTLE!
</TextBlock>
<!– Instructions –>
<TextBlock Canvas.Left=”-5” Canvas.Top=”780” FontFamily=”Segoe WP Black”
FontSize=”34” Foreground=”#2EA538” Width=”480” TextWrapping=”Wrap”
TextAlignment=”Right” LineHeight=”26”
LineStackingStrategy=”BlockLineHeight”>
<TextBlock.RenderTransform>
<RotateTransform Angle=”-10”/>
</TextBlock.RenderTransform>
DO A TWO-FINGERED SPIN AS FORCEFULLY AS POSSIBLE!
</TextBlock>
<!– The bottle –>
<Image Canvas.Left=”166” Canvas.Top=”160” Source=”Images/bottle.png”
Width=”148” Height=”480” RenderTransformOrigin=”.5,.5”>
<Image.RenderTransform>
<!– The transform’s angle is the target of both storyboards
plus direct manipulation from code-behind –>
<RotateTransform x:Name=”BottleTransform”/>
</Image.RenderTransform>
</Image>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

  • The two storyboards are customized from code-behind before they are started. The inertia storyboard uses a PowerEase easing function to simulate friction by gradually slowing down the animation toward the end.
  • To detect the rotate gesture, this app uses a gesture listener to detect all three pinch/stretch events. After all, a two-finger pinch/stretch gesture is very similar to the desired rotate gesture. The code must simply pay attention to the angle formed by the two fingers rather than the distance between them.
  • This is the perfect kind of app for the gesture listener, because there are no controls on the page that could be impacted by its use.
  • The root canvas is given a transparent background, which is important for ensuring that the entire screen is hit-testable.

The Code-Behind

Listing 43.2 contains the code-behind for the main page.

LISTING 43.2 MainPage.xaml.cs—The Code-Behind for Spin the Bottle!’s Main Page

[code]

using System;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
double startingAngle;
double previousDelta;
DateTime previousTime;
public MainPage()
{
InitializeComponent();
}
void GestureListener_PinchStarted(object sender,
PinchStartedGestureEventArgs e)
{
// Normalize the current angle, which can get quite large
// or small after the inertia animation
this.BottleTransform.Angle %= 360;
// Reset the velocity-tracking variables
this.previousDelta = 0;
this.previousTime = DateTime.Now;
// Rather than instantly jump to the angle of the fingers, smoothly
// animate to that angle
this.SpinAnimation.To = startingAngle = e.Angle + 90;
this.SpinStoryboard.Begin();
}
void GestureListener_PinchDelta(object sender, PinchGestureEventArgs e)
{
// Directly update the angle of the bottle, as this should be a small
// incremental change
this.BottleTransform.Angle = startingAngle + e.TotalAngleDelta;
// Every 1/10th of a second, record the current delta and time
if ((DateTime.Now – this.previousTime).TotalSeconds > .1)
{
this.previousDelta = e.TotalAngleDelta;
this.previousTime = DateTime.Now;
}
}
void GestureListener_PinchCompleted(object sender, PinchGestureEventArgs e)
{
// Now compare the values from ~.1 second ago to the current values to
// get the rotation velocity at the moment the fingers release the bottle
double distance = e.TotalAngleDelta – this.previousDelta;
double time = (DateTime.Now – this.previousTime).TotalSeconds;
if (distance == 0 || time == 0)
return;
double velocity = distance / time;
// Adjust the inertia animation so the length of the remaining spin
// animation is proportional to the velocity
this.InertiaAnimation.Duration =
TimeSpan.FromMilliseconds(Math.Abs(velocity));
// Choose a number of spins proportional to the length of the animation
this.InertiaAnimation.By = 360 *
Math.Pow(this.InertiaAnimation.Duration.TimeSpan.TotalSeconds, 5);
// Make sure the bottle spins in the appropriate direction
if (velocity < 0)
this.InertiaAnimation.By *= -1;
this.InertiaStoryboard.Begin();
}
}
}

[/code]

  • Other than the constructor, the code-behind simply consists of the three pinch/stretch event handlers.
  • In GestureListener_PinchStarted, the handy Angle property on PinchStartedGestureEventArgs is used instead of Distance. To make the angle of the bottle match the angle represented by the two fingers, the code could have simply set BottleTransform’s Angle to this value. However, to avoid a jarring experience, this method uses SpinAnimation to quickly animate from the bottle’s current angle to the new angle.
  • GestureListener_PinchDelta ignores the familiar DistanceRatio property passed via PinchGestureEventArgs and instead uses its TotalAngleDelta property. Adding this to the angle reported in the PinchStarted event gives an angle that matches the current position of the fingers. (The bottle ends up pointing toward the second of the two fingers.) Note that this method does directly set BottleTransform’s Angle to this value rather than animate it, but this is perfectly acceptable because the angle only changes by a small amount between PinchStarted, PinchDelta, and subsequent PinchDelta events.
  • This app’s inertia simulation is different from what was done in the preceding chapter because this app manually calculates the final velocity. Every tenth of a second, GestureListener_PinchDelta records the current time and the current angle delta. When the PinchCompleted event is raised, GestureListener_PinchCompleted again captures the current time and angle delta and then compares them to the previously recorded values to determine the velocity at the point of release. (GestureListener_PinchDelta doesn’t record the values every time because the last angle delta passed to PinchDelta matches the angle delta passed to PinchCompleted, so it would always give a velocity of zero!)
  • The absolute velocity value is not very meaningful, but its relative size and direction are. The mapping of the velocity value to InertiaAnimation’s duration and number of spins was derived by trial and error, looking for a realistic final effect.

The Finished Product

Spin Bottle, Rotate Gesture, Simulating Inertia

Musical Robot (Multi-Touch)

Musical Robot is a quirky musical instrument app that can play two-octaves-worth of robotic sounds based on where you place your fingers. Touching toward the left produces lower notes, and touching toward the right produces higher notes. You can slide your fingers around to produce interesting effects. You can use multiple fingers— as many as your phone supports simultaneously—to play chords (multiple notes at once). You’re more likely to use this app to annoy your friends rather than play actual compositions, but it’s fun nevertheless!

The User Interface

Musical Robot’s main page, pictured in Figure 38.1 in its initial state, contains a few visual elements that have nothing to do with the core functionality of this app, but provide some visual flair and simple instructions. Listing 38.1 contains the XAML.

The main page contains a robot image and instructions.
FIGURE 38.1 The main page contains a robot image and instructions.

LISTING 38.1 MainPage.xaml—The User Interface for Musical Robot’s Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
SupportedOrientations=”Landscape” Orientation=”Landscape”>
<Canvas>
<!– The dynamic mouth that is visible through the image’s mouth hole –>
<Rectangle Canvas.Left=”168” Canvas.Top=”127” Width=”114” Height=”23”
RadiusX=”10” RadiusY=”10” RenderTransformOrigin=”.5,.5”
Fill=”{StaticResource PhoneForegroundBrush}”>
<Rectangle.RenderTransform>
<!– The scale is continually changed from code-behind –>
<ScaleTransform x:Name=”MouthScale” ScaleX=”0”/>
</Rectangle.RenderTransform>
</Rectangle>
<!– 5 lights representing up to 5 simultaneous fingers –>
<Ellipse x:Name=”Light1” Visibility=”Collapsed” Canvas.Left=”137”
Canvas.Top=”284” Width=”23” Height=”23” Fill=”Red”/>
<Ellipse x:Name=”Light2” Visibility=”Collapsed” Canvas.Left=”174”
Canvas.Top=”294” Width=”23” Height=”23” Fill=”Red”/>
<Ellipse x:Name=”Light3” Visibility=”Collapsed” Canvas.Left=”213”
Canvas.Top=”298” Width=”23” Height=”23” Fill=”Red”/>
<Ellipse x:Name=”Light4” Visibility=”Collapsed” Canvas.Left=”252”
Canvas.Top=”294” Width=”23” Height=”23” Fill=”Red”/>
<Ellipse x:Name=”Light5” Visibility=”Collapsed” Canvas.Left=”290”
Canvas.Top=”284” Width=”23” Height=”23” Fill=”Red”/>
<!– The accent-colored robot –>
<Rectangle Width=”453” Height=”480” Fill=”{StaticResource PhoneAccentBrush}”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/robot.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<!– Instructions –>
<TextBlock Canvas.Left=”350” Canvas.Top=”40” FontFamily=”Segoe WP Black”
FontSize=”40” Foreground=”{StaticResource PhoneAccentBrush}”>
<TextBlock.RenderTransform>
<RotateTransform Angle=”-10”/>
</TextBlock.RenderTransform>
TAP &amp; DRAG.
<LineBreak/>
USE MANY FINGERS!
</TextBlock>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

  • MouthScale’s ScaleX value is randomly set anywhere from 0 to 1 whenever a finger makes contact with or moves across the screen. This provides the illusion that the robot is singing as it makes its noises.
  • The circles are filled with red to indicate how many fingers are simultaneously in contact with the screen (up to 5). The limit of 5 is simply due to space constraints in the artwork. It does not reflect on multi-touch limitations of the operating system or any particular device.

How many simultaneous touch points does Windows Phone support?

All Windows phones are guaranteed to support at least four simultaneous touch points. (Current models support exactly four.) The operating system can support up to 10, in case an ambitious device wants to support it.

The Code-Behind

Listing 38.2 contains the code-behind for the main page.

LISTING 38.2 MainPage.xaml.cs—The Code-Behind for Musical Robot’s Main Page

[code]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Xna.Framework.Audio;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Store a separate sound effect instance for each unique finger
Dictionary<int, SoundEffectInstance> fingerSounds =
new Dictionary<int, SoundEffectInstance>();
// For the random mouth movement
Random random = new Random();
public MainPage()
{
InitializeComponent();
SoundEffects.Initialize();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Subscribe to the touch/multi-touch event.
// This is application-wide, so only do this when on this page.
Touch.FrameReported += Touch_FrameReported;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Unsubscribe from this application-wide event
Touch.FrameReported -= Touch_FrameReported;
}
void Touch_FrameReported(object sender, TouchFrameEventArgs e)

{
// Get all touch points
TouchPointCollection points = e.GetTouchPoints(this);
// Filter out the “up” touch points because those fingers are
// no longer in contact with the screen
int numPoints =
(from p in points where p.Action != TouchAction.Up select p).Count();
// Update up to 5 robot lights to indicate how many fingers are in contact
this.Light1.Visibility =
(numPoints >= 1 ? Visibility.Visible : Visibility.Collapsed);
this.Light2.Visibility =
(numPoints >= 2 ? Visibility.Visible : Visibility.Collapsed);
this.Light3.Visibility =
(numPoints >= 3 ? Visibility.Visible : Visibility.Collapsed);
this.Light4.Visibility =
(numPoints >= 4 ? Visibility.Visible : Visibility.Collapsed);
this.Light5.Visibility =
(numPoints >= 5 ? Visibility.Visible : Visibility.Collapsed);
// If any fingers are in contact, stretch the inner mouth anywhere from
// 0 to 100%
if (numPoints == 0)
this.MouthScale.ScaleX = 0;
else
this.MouthScale.ScaleX = this.random.NextDouble(); // Returns a # from 0-1
// Process each touch point individually
foreach (TouchPoint point in points)
{
// The “touch device” is each finger, and it has a unique ID
int fingerId = point.TouchDevice.Id;
if (point.Action == TouchAction.Up)
{
// Stop the sound corresponding to this just-lifted finger
if (this.fingerSounds.ContainsKey(fingerId))
this.fingerSounds[fingerId].Stop();
// Remove the sound from the dictionary
this.fingerSounds.Remove(fingerId);
}
else
{

// Turn the horizontal position into a pitch from -1 to 1.
// -1 represents 1 octave lower, 1 represents 1 octave higher.
float pitch = (float)(2 * point.Position.X / this.ActualWidth) – 1;
if (!this.fingerSounds.ContainsKey(fingerId))
{
// We haven’t yet created the sound effect for this finger, so do it
this.fingerSounds.Add(fingerId, SoundEffects.Sound.CreateInstance());
this.fingerSounds[fingerId].IsLooped = true;
}
// Start playing the looped sound at the correct pitch
this.fingerSounds[fingerId].Pitch = pitch;
this.fingerSounds[fingerId].Play();
}
}
// Work around the fact that we sometimes don’t get Up actions reported
CheckForStuckSounds(points);
}
void CheckForStuckSounds(TouchPointCollection points)
{
List<int> soundsToRemove = new List<int>();
// Inspect each active sound
foreach (var sound in this.fingerSounds)
{
bool found = false;
// See if this sound corresponds to an active finger
foreach (TouchPoint point in points)
{
if (point.TouchDevice.Id == sound.Key)
{
found = true;
break;
}
}
// It doesn’t, so stop the sound and mark it for removal
if (!found)
{
sound.Value.Stop();
soundsToRemove.Add(sound.Key);
}
}
// Remove each orphaned sound
foreach (int id in soundsToRemove)
this.fingerSounds.Remove(id);
}
}
}

[/code]

  • Because this app contains only a single page, unsubscribing from the FrameReported event in OnNavigatedFrom is not necessary, but this is done to keep good hygiene in case another page is ever added (or this code is used in a different app).
  • Inside the FrameReported event handler (Touch_FrameReported), GetTouchPoints is called to get the entire collection of touch points. On most devices, this collection will always contain 1–4 items. It never is zero-length or null.
  • Because each touch point can be in the returned collection for one of three reasons (making initial contact with the screen, moving on the screen, or being released from the screen), simply counting its items does not tell us how many fingers are currently in contact with the screen. To do this, we must filter out any touch points whose Action property is set to Up. This could be done by manually enumerating the points collection, but this code instead opts for a LINQ query to set the value of numPoints.
  • The code responsible for starting and stopping each sound makes use of a property on TouchPoint not mentioned in the preceding chapter: TouchDevice. This oddsounding property represents the user’s finger responsible for the touch point. Each finger is assigned an integer ID, exposed as a property on TouchDevice, which can be used to track each finger individually. This would otherwise be impossible when multiple fingers are triggering events simultaneously. The ID assigned to any finger is guaranteed to remain unique during the lifetime of a down/move/up action cycle.
  • The fingerSounds dictionary leverages the unique finger IDs for its keys. This listing starts playing a looped sound as each new finger makes contact with the screen, it adjusts the pitch of the sound as the finger is moved, and it stops the sound as the finger is released.
  • The SoundEffects class used by this listing is just like the same-named class used by many of this book’s apps, but customized to expose a single sound through its Sound property. The included sound is so short that it is barely audible when played by itself, but the IsLooped property on SoundEffectInstance is leveraged to produce a smooth and very audible sound. The pitch of each sound is varied based on the horizontal position of each touch point.

Occasionally, a finger-up action might not be reported!

Due to a bug in Silverlight (or perhaps in some touch drivers), a finger that has reported touching down and moving around might never report that it has been lifted up. Instead, the corresponding touch point simply vanishes from the collection returned by GetTouchPoints. In Musical Robot, this would manifest as sounds that never stop playing.

To prevent such “stuck” sounds, the CheckForStuckSounds method in Listing 38.2 looks at every active sound and attempts to find a current touch point that corresponds to it. If a sound is not associated with an active touch point, it is stopped and removed from the dictionary, just like what happens when a finger properly reports being lifted up.

Can a finger ID continue to identity a specific finger even if it temporarily leaves the screen?

No, the phone cannot continue to track a specific finger once it has broken contact with the screen.The unique ID, assigned during each new initial contact, is only valid until the FrameReported event reports an Up action for that touch point.

The Finished Product

Musical Robot (Multi-Touch)

Reflex Test (Single Touch)

Reflex Test is a simple game in which you see how quickly you can tap the screen when a target appears. (You can tap anywhere on the screen; the target is just a visual gimmick.) The app keeps track of your fastest time as well as your average time.

The tap detection done by this app could easily be done with a simple MouseLeftButtonDown event handler, but instead this app serves as an introduction to multi-touch functionality. You can easily use the multi-touch infrastructure for single touches, as this app does.

The User Interface

Reflex Test has a main page, an instructions page, and an about page. The latter two pages aren’t interesting and therefore aren’t shown in this chapter, but Listing 37.1 contains the XAML for the main page.

Figure 37.1 illustrates main page’s user interface during the four stages of the reflex-testing process.

The main page goes through four stages as the app is used.
FIGURE 37.1 The main page goes through four stages as the app is used.

LISTING 37.1 MainPage.xaml—The User Interface for Reflex Test’s Main Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape”>
<!– Add three animations to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– Slide the best time out then back in –>
<Storyboard x:Name=”SlideBestTimeStoryboard”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName=”BestTimeTransform”
Storyboard.TargetProperty=”TranslateX”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”0”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.4” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:.4” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.8” Value=”0”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName=”BestTimeTextBlock”
Storyboard.TargetProperty=”Visibility”>
<!– Ensure the time is visible on the way in,
even if collapsed on the way out –>
<DiscreteObjectKeyFrame KeyTime=”0:0:.4” Value=”Visible”/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<!– Slide the average time out then back in –>
<Storyboard x:Name=”SlideAvgTimeStoryboard”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName=”AvgTimeTransform”
Storyboard.TargetProperty=”TranslateX”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”0”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.4” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:.4” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.8” Value=”0”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName=”AvgTimeTextBlock”
Storyboard.TargetProperty=”Visibility”>
<!– Ensure the time is visible on the way in,
even if collapsed on the way out –>
<DiscreteObjectKeyFrame KeyTime=”0:0:.4” Value=”Visible”/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<!– Animate in (then out) a message, which will either say
“CONGRATULATIONS!” or “TOO EARLY!” –>
<Storyboard x:Name=”ShowMessageStoryboard”
Storyboard.TargetName=”MessageTransform”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=”TranslateY”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.5” Value=”50”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:2.5” Value=”50”/>
<EasingDoubleKeyFrame KeyTime=”0:0:3” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– The application bar, with two buttons and one menu item –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBarIconButton Text=”delete”
IconUri=”/Shared/Images/appbar.delete.png” Click=”DeleteButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid Background=”Transparent”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– A target vector graphic and ellipse to mark where the screen
was touched, collapsed until appropriate to show –>
<Grid Grid.RowSpan=”2”>
<Grid x:Name=”TargetGrid” Visibility=”Collapsed”>
<Ellipse Width=”440” Height=”440” StrokeThickness=”35”
Stroke=”{StaticResource PhoneAccentBrush}”/>
<Ellipse Width=”300” Height=”300” StrokeThickness=”35”
Stroke=”{StaticResource PhoneAccentBrush}”/>
<Ellipse Width=”160” Height=”160” StrokeThickness=”35”
Stroke=”{StaticResource PhoneAccentBrush}”/>
<Ellipse x:Name=”TouchEllipse” Visibility=”Collapsed” Opacity=”.9”
Fill=”{StaticResource PhoneForegroundBrush}” Width=”100”
Height=”100” HorizontalAlignment=”Left” VerticalAlignment=”Top”/>
</Grid>
</Grid>
<!– Show indeterminate progress (dancing dots) while waiting for the target
to appear (IsIndeterminate is set from code-behind for perf reasons) –>
<ProgressBar x:Name=”ProgressBar” Visibility=”Collapsed”
VerticalAlignment=”Top”/>
<!– The standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”REFLEX TEST”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock x:Name=”PageTitle” Text=”tap to begin”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<Grid Grid.Row=”1” Margin=”{StaticResource PhoneHorizontalMargin}”>
<!– A display for the best time, average time, and # of tries –>
<StackPanel x:Name=”TimesPanel” VerticalAlignment=”Bottom”
HorizontalAlignment=”Right”>
<TextBlock Text=”BEST TIME” Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”BestTimeTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-15,0,30”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”BestTimeTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock x:Name=”AvgTimeHeaderTextBlock” Text=”AVG TIME”
Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”AvgTimeTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-15,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”AvgTimeTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
<!– A “CONGRATULATIONS!” or “TOO EARLY!” message –>
<TextBlock x:Name=”MessageTextBlock” RenderTransformOrigin=”.5,.5”
FontWeight=”Bold” HorizontalAlignment=”Center”
FontSize=”{StaticResource PhoneFontSizeExtraLarge}”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”MessageTransform” TranslateY=”800”/>
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
</Grid>
</phone:PhoneApplicationPage>

[/code]

The first two animations are used to slide in the best time and average time text blocks the first time they are displayed, and to slide them out and then in when they are updated. The last animation slides a message onto and then off the screen: “CONGRATULATIONS!” when the user gets a new best time, or “TOO EARLY!” when the user taps the screen before the target appears.

The Code-Behind

Listing 37.2 contains the code-behind for the main page.

LISTING 37.2 MainPage.xaml.cs—The Code-Behind for Reflex Test’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Navigation;
using System.Windows.Threading;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Persistent settings
Setting<TimeSpan> bestTime = new Setting<TimeSpan>(“BestTime”,
TimeSpan.MaxValue);
Setting<TimeSpan> avgTime = new Setting<TimeSpan>(“AvgTime”,
TimeSpan.MaxValue);
Setting<int> numTries = new Setting<int>(“NumTries”, 0);
DispatcherTimer timer = new DispatcherTimer();
Random random = new Random();
DateTime beginTime;
DateTime targetShownTime;
bool tapToBegin;
public MainPage()
{
InitializeComponent();
this.timer.Tick += Timer_Tick;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Subscribe to the touch/multi-touch event.
// This is application-wide, so only do this when on this page.
Touch.FrameReported += Touch_FrameReported;
// Respect the persisted values
UpdateLabels(true);
// Reset
this.tapToBegin = true;
this.PageTitle.Text = “tap to begin”;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Unsubscribe from this application-wide event
Touch.FrameReported -= Touch_FrameReported;
}
void Touch_FrameReported(object sender, TouchFrameEventArgs e)
{
TouchPoint point = e.GetPrimaryTouchPoint(this);
if (point != null && point.Action == TouchAction.Down)
{
if (this.tapToBegin)
{
// Get started
this.tapToBegin = false;
this.PageTitle.Text = “”;
this.TargetGrid.Visibility = Visibility.Collapsed;
this.TouchEllipse.Visibility = Visibility.Collapsed;
// Show the indeterminate progress bar
this.ProgressBar.IsIndeterminate = true;
this.ProgressBar.Visibility = Visibility.Visible;
this.beginTime = DateTime.Now;
// Make the target appear between .5 sec and 7 sec from now
timer.Interval = TimeSpan.FromMilliseconds(random.Next(500, 7000));
timer.Start();
}
else if (this.TargetGrid.Visibility == Visibility.Visible)
{
// The target has been tapped
DateTime endTime = DateTime.Now;
// Position and show the ellipse where the screen was touched
this.TouchEllipse.Margin = new Thickness(
point.Position.X – this.TouchEllipse.Height / 2,
point.Position.Y – this.TouchEllipse.Height / 2, 0, 0);
this.TouchEllipse.Visibility = Visibility.Visible;
// Show the elapsed time
TimeSpan newTime = endTime – this.targetShownTime;
this.PageTitle.Text = newTime.TotalSeconds + “ sec”;
this.tapToBegin = true;
// Record this attempt and update the UI
double oldTotal = this.avgTime.Value.TotalSeconds * this.numTries.Value;
// New average
this.avgTime.Value = TimeSpan.FromSeconds(
(oldTotal + newTime.TotalSeconds) / (this.numTries.Value + 1));
// New total number of tries
this.numTries.Value++;
if (newTime < this.bestTime.Value)
{
// New best time
this.bestTime.Value = newTime;
UpdateLabels(true);
// Animate in a congratulations message
this.MessageTextBlock.Text = “CONGRATULATIONS!”;
this.ShowMessageStoryboard.Begin();
}
else
{
UpdateLabels(false);
}
}
else
{
// The screen has been tapped too early
// Cancel the timer that would show the target
this.timer.Stop();
// Hide the progress bar and turn off the indeterminate
// animation to avoid poor performance
this.ProgressBar.Visibility = Visibility.Collapsed;
this.ProgressBar.IsIndeterminate = false;
// Show exactly how early the tap was
DateTime endTime = this.beginTime + this.timer.Interval;
this.PageTitle.Text = (DateTime.Now – endTime).TotalSeconds + “ sec”;
this.tapToBegin = true;
// Animate in an explanatory message
this.MessageTextBlock.Text = “TOO EARLY!”;
this.ShowMessageStoryboard.Begin();
}
}
}
void Timer_Tick(object sender, EventArgs e)
{
// Show the target
this.TargetGrid.Visibility = Visibility.Visible;
// Hide the progress bar and turn off the indeterminate
// animation to avoid poor performance
this.ProgressBar.Visibility = Visibility.Collapsed;
this.ProgressBar.IsIndeterminate = false;
this.targetShownTime = DateTime.Now;
// We only want the Tick once
this.timer.Stop();
}
void UpdateLabels(bool animateBestTime)
{
if (this.numTries.Value > 0)
{
// Ensure the panel is visible and update the text blocks
this.TimesPanel.Visibility = Visibility.Visible;
this.BestTimeTextBlock.Text = this.bestTime.Value.TotalSeconds + “ sec”;
this.AvgTimeTextBlock.Text = this.avgTime.Value.TotalSeconds + “ sec”;
if (this.numTries.Value == 1)
this.AvgTimeHeaderTextBlock.Text = “AVG TIME (1 TRY)”;
else
this.AvgTimeHeaderTextBlock.Text = “AVG TIME (“ + this.numTries.Value
+ “ TRIES)”;
// Animate the textblocks out then in. The animations take care of
// showing the textblocks if they are collapsed.
this.SlideAvgTimeStoryboard.Begin();
if (animateBestTime)
this.SlideBestTimeStoryboard.Begin();
else
this.BestTimeTextBlock.Visibility = Visibility.Visible;
}
else
{
// Hide everything
this.TimesPanel.Visibility = Visibility.Collapsed;
this.BestTimeTextBlock.Visibility = Visibility.Collapsed;
this.AvgTimeTextBlock.Visibility = Visibility.Collapsed;
}
}
// Application bar handlers
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void DeleteButton_Click(object sender, EventArgs e)
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Reflex Text”, UriKind.Relative));
}
}
}

[/code]

  • The event for capturing touch and multi-touch activity has the odd name FrameReported and is exposed on a static class called Touch (from the System.Windows.Input namespace). This event gets raised for touch activity across the entire application, so Listing 37.1 attaches a handler to this event in OnNavigatedTo but removes the handler in OnNavigatedFrom.
  • Inside the FrameReported event handler (Touch_FrameReported), GetPrimaryTouchPoint is called to get single-touch data, which is all this app cares about. If multiple fingers are pressed on the screen, GetPrimaryTouchPoint returns data about the first finger that makes contact with the screen.
  • FrameReported gets raised for three types of actions: a finger making contact with the screen, a finger moving on the screen, and a finger being released from the screen. These actions are analogous to mouse down, mouse move, and mouse up events, although here they apply per finger. This app only cares about taps, so the logic inside Touch_FrameReported only runs when the primary touch point is a touching-down action.

Be sure to detach any FrameReported handlers as soon as possible!

In addition to the performance implication of FrameReported handlers being invoked when they don’t need to be, forgetting to detach from the event can cause other problems if the page containing the handler is no longer active.

  • In addition to the Action property, the TouchPoint class returned by GetPrimaryTouchPoint exposes Position and Size properties. This app only makes use of the position of the touch point in order to place TouchEllipse in the spot that was tapped.
  • Notice that the progress bar’s IsIndeterminate property is only set to true while it is visible.

You should ignore TouchPoint’s Size property!

The current version of Windows Phone doesn’t actually support the discovery of a touch point’s size, so the reported Size.Width and Size.Height are always 1. This is unfortunate, as it would be a nice touch (no pun intended) for Reflex Test to make TouchEllipse the size of the fingertip that touched the screen.

The Finished Product

Reflex Test (Single Touch)

Sound Recorder (Saving Audio Files & Playing Sound Backward)

Sound Recorder enables you to record, manage, and play audio clips. It is named after the Sound Recorder program on Windows and reminiscent of the Voice Memos app on the iPhone. It can come in quite handy when you’re away from a computer and have some thoughts that you don’t want to forget, especially because it enables you to pause in the midst of a single recording.

Control your recording with simple (and large!) record, pause, and stop buttons. Rename or delete previous recordings one-by-one, or bulk-delete unwanted recordings with a check box mechanism matching the one used by the builtin Mail app. When playing a recording, you can adjust the playback speed, pause it, adjust the playback position on an interactive slider, and even reverse the sound!

The idea of adjusting the speed is that you can listen to recorded thoughts or a lecture much faster than the words were originally spoken. Playing the audio back at a faster rate can help you be more productive.

But why would you want to play recorded words backward? Laura Foy from Microsoft’s Channel 9 has theorized that it’s to “find out if you’re secretly sending satanic messages” (see http://bit.ly/laurafoy), but my real motivation is to enable people to play the nerdy game my brother and I used to play as kids. Here’s how you play:

  1. Record yourself saying a word or phrase.
  2. Play it backwards many times, so you can try to memorize what it sounds like backward.
  3. Make a new recording with you mimicking the backward audio.
  4. Play this new recording backward to see how close you can come to replicating the original word or phrase.

We used to play this game with Sound Recorder on Windows (the good version of the program, prior to Windows Vista). Now you can play it anytime and anywhere with Sound Recorder on your Windows Phone! You’ll be surprised by the sounds you have to make to produce a good result!

As far as interaction with the microphone is concerned, Sound Recorder is simpler than Talking Parrot because it doesn’t need to automatically determine when to start and stop collecting data. This app requires a lot more code, however, for managing the audio that it does capture.

Sound Recorder contains three pages in addition to its about page: the main page, which does all the recording; the list page, which shows past recordings; and the details page, which handles playback and editing.

The Main Page

The main page, shown at the beginning of this chapter, has three basic states: stopped, recording, and paused. Figure 36.1 demonstrates all three.

The three possible states of the main page.
FIGURE 36.1 The three possible states of the main page.

The four buttons (shown two at a time) mimic application bar buttons but are significantly bigger. Using real application bar buttons would be fine (and easier to implement), but this makes the buttons a little easier to press when the user is in a hurry.

This page’s user interface, with its photograph and Volume Units (VU) meter, does a bad job of following the design guideline that Windows Phone apps should be “authentically digital.” Showing a digital display similar to the phone’s voice recognition overlay would fit in better. However, sometimes violating guidelines can help your app stand out in a positive way.

The User Interface

Listing 36.1 contains the XAML for the main page. It consists of two images, four custom buttons, a line for the VU meter needle, and a text block for displaying the elapsed time.

LISTING 36.1 MainPage.xaml—The User Interface for Sound Recorder’s Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Portrait” shell:SystemTray.IsVisible=”True”>
<Canvas>
<!– The on-air image –>
<Image Source=”Images/background.png”/>
<!– The off-air image –>
<Image x:Name=”OffAirImage” Source=”Images/offAir.png”/>
<!– The large buttons: 2 in the same left spot, 2 in the same right spot –>
<local:ImageButton x:Name=”RecordButton” Click=”RecordButton_Click”
Text=”record” Canvas.Left=”16” Canvas.Top=”586”
Source=”../../Images/RecordButton.png”
PressedSource=”../../Images/RecordButtonPressed.png”/>
<local:ImageButton x:Name=”PauseButton” Click=”PauseButton_Click”
Text=”pause” Canvas.Left=”16” Canvas.Top=”586”
Source=”../../Images/PauseButton.png”
PressedSource=”../../Images/PauseButtonPressed.png”
Visibility=”Collapsed”/>
<local:ImageButton x:Name=”ListButton” Click=”ListButton_Click”
Text=”list” Canvas.Left=”371” Canvas.Top=”586”
Source=”../../Images/ListButton.png”
PressedSource=”../../Images/ListButtonPressed.png”/>
<local:ImageButton x:Name=”StopButton” Click=”StopButton_Click”
Text=”stop” Canvas.Left=”371” Canvas.Top=”586”
Source=”../../Images/StopButton.png”
PressedSource=”../../Images/StopButtonPressed.png”
Visibility=”Collapsed”/>
<!– The needle for the sound meter –>
<Line Canvas.Left=”240” Canvas.Top=”590” Width=”3” Height=”110” Y2=”110”
Stroke=”Black” StrokeThickness=”3” StrokeStartLineCap=”Triangle”
RenderTransformOrigin=”.5,1”>
<Line.RenderTransform>
<RotateTransform x:Name=”NeedleTransform” Angle=”-55”/>
</Line.RenderTransform>
</Line>
<!– The elapsed time –>
<TextBlock x:Name=”TimerTextBlock” Canvas.Top=”512” Width=”480”
TextAlignment=”Center” Style=”{StaticResource PhoneTextExtraLargeStyle}”
Foreground=”White” Visibility=”Collapsed”/>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

  • The page is portrait-only due to the dimensions of the artwork and the exact layout of the controls surrounding it.
  • The second image, OffAirImage, is shown during the stopped and paused states. It covers the illuminated “on air” sign with one that is off, and it also dims the VU meter (whose needle still moves in response to sound). It accomplishes the dimming with a translucent region in the image, as demonstrated in Figure 36.2.
  • The four buttons are instances of a simple user control called ImageButton that is included with this app’s source code. Rather than doing tricks with opacity masks to get the inverted-colors-when-pressed effect, this control simply asks for two separate image files. It displays PressedSource when pressed; otherwise it displays Source.
  • Because background.png (with fixed colors) fills the page, the text and images in this page all use a hard-coded white color.
  • The VU meter needle is implemented as a Line element whose RotateTransform is manipulated from code behind. Its RenderTransformOrigin of .5,1 rotates it around its bottom edge. It is positioned with Canvas.Left and Canvas.Top rather than solely with its X1 and Y1 properties—and it is given an explicit width and height—to make the transform work more understandably.
The overlay image replaces the photo and dims the sound meter.
FIGURE 36.2 The overlay image replaces the photo and dims the sound meter.

The Code-Behind

Listing 36.2 contains the code-behind for the main page.

LISTING 36.2 MainPage.xaml.cs—The Code-Behind for Sound Recorder’s Main Page

[code]

using System;
using System.IO;
using System.Windows;
using System.Windows.Media;
using Microsoft.Phone.Controls;
using Microsoft.Xna.Framework.Audio;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Used for capturing audio from the microphone
byte[] buffer;
MemoryStream stream;
// Needle management
double targetNeedleAngle;
const int MIN_ANGLE = -55;
const int MAX_ANGLE = 55;
const int VELOCITY_FACTOR = 10;
const int DOWNWARD_VELOCITY = -6;
const int SMALL_ANGLE_DELTA = 6;
const int RANGE_FACTOR = 20;
// The current state (Stopped, Recording, or Paused)
AudioState currentState = AudioState.Stopped;
public MainPage()
{
InitializeComponent();
CompositionTarget.Rendering += CompositionTarget_Rendering;
// Required for XNA Microphone API to work
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
// Configure the microphone with the smallest supported BufferDuration (.1)
Microphone.Default.BufferDuration = TimeSpan.FromSeconds(.1);
Microphone.Default.BufferReady += Microphone_BufferReady;
// Initialize the buffer for holding microphone data
int size = Microphone.Default.GetSampleSizeInBytes(
Microphone.Default.BufferDuration);
this.buffer = new byte[size];
// Initialize the stream used to record microphone data
this.stream = new MemoryStream();
// Listen the whole time so the needle moves even when not recording
Microphone.Default.Start();
}
void Microphone_BufferReady(object sender, EventArgs e)
{
int size = Microphone.Default.GetData(this.buffer);
if (size == 0)
return;
// Calculate the target angle for the volume meter needle
long volume = GetAverageVolume(size);
double range = Math.Min(MAX_ANGLE – MIN_ANGLE, volume / RANGE_FACTOR);
this.targetNeedleAngle = MIN_ANGLE + range;
if (CurrentState == AudioState.Recording)
{
// If recording, write the current buffer to the stream and
// refresh the elapsed time
this.stream.Write(this.buffer, 0, size);
TimeSpan recordingLength = Microphone.Default.GetSampleDuration(
(int)this.stream.Position);
this.TimerTextBlock.Text = String.Format(“{0:00}:{1:00}”,
recordingLength.Minutes, recordingLength.Seconds);
}
}
void CompositionTarget_Rendering(object sender, EventArgs e)
{
// Required for XNA Microphone API to work
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
double newAngle = this.targetNeedleAngle;
double delta = this.targetNeedleAngle – this.NeedleTransform.Angle;
// If the difference is larger than SMALL_ANGLE_DELTA°, gradually move the
// needle rather than directly setting its angle to the target angle
if (Math.Abs(delta) > SMALL_ANGLE_DELTA)
{
// Limit the downward velocity, so it returns to the
// resting position at a constant rate (DOWNWARD_VELOCITY)
newAngle = this.NeedleTransform.Angle +
Math.Max(delta / VELOCITY_FACTOR, DOWNWARD_VELOCITY);
}
// Update the needle’s angle, restricting it
// to a range of MIN_ANGLE° to MAX_ANGLE°
this.NeedleTransform.Angle =
Math.Max(MIN_ANGLE, Math.Min(MAX_ANGLE, newAngle));
}
// Returns the average value among all the values in the buffer
int GetAverageVolume(int numBytes)
{
long total = 0;
// Buffer is an array of bytes, but we want to examine each 2-byte value
for (int i = 0; i < numBytes; i += 2)
{
// Cast from short to int to prevent -32768 from overflowing Math.Abs
int value = Math.Abs((int)BitConverter.ToInt16(this.buffer, i));
total += value;
}
return (int)(total / (numBytes / 2));
}
AudioState CurrentState
{
get { return this.currentState; }
set
{
this.currentState = value;
// Not pretty code, but shorter than the alternatives
switch (this.currentState)
{
case AudioState.Recording:
RecordButton.Visibility = Visibility.Collapsed;
ListButton.Visibility = Visibility.Collapsed;
OffAirImage.Visibility = Visibility.Collapsed;
PauseButton.Visibility = Visibility.Visible;
StopButton.Visibility = Visibility.Visible;
TimerTextBlock.Text = “”;
TimerTextBlock.Visibility = Visibility.Visible;
break;
case AudioState.Paused:
RecordButton.Visibility = Visibility.Visible;
OffAirImage.Visibility = Visibility.Visible;
PauseButton.Visibility = Visibility.Collapsed;
TimerTextBlock.Text += “ (paused)”;
break;
case AudioState.Stopped:
RecordButton.Visibility = Visibility.Visible;
ListButton.Visibility = Visibility.Visible;
OffAirImage.Visibility = Visibility.Visible;
PauseButton.Visibility = Visibility.Collapsed;
StopButton.Visibility = Visibility.Collapsed;
TimerTextBlock.Visibility = Visibility.Collapsed;
break;
}
}
}
// Button click handlers
void RecordButton_Click(object sender, EventArgs e)
{
CurrentState = AudioState.Recording;
}
void ListButton_Click(object sender, EventArgs e)
{
CurrentState = AudioState.Stopped;
this.NavigationService.Navigate(
new Uri(“/ListPage.xaml”, UriKind.Relative));
}
void PauseButton_Click(object sender, EventArgs e)
{
CurrentState = AudioState.Paused;
}
void StopButton_Click(object sender, EventArgs e)
{
CurrentState = AudioState.Stopped;
// Create a new recording with a unique filename
Recording r = new Recording { Filename = Guid.NewGuid().ToString(),
TimeStamp = DateTimeOffset.Now };
// Save the recording
r.SaveContent(this.stream);
// Ready the stream for another recording
this.stream.Position = 0;
// Add the recording to the persisted list
Settings.RecordingsList.Value.Add(r);
}
}
}

[/code]

  • The structure of this code is pretty similar to the preceding two chapters. In this app, the microphone is started from the constructor to enable the VU meter needle to move at all times, not just while recording is in progress. During development, ensure that your application manifest contains the ID_CAP_MICROPHONE capability, otherwise the call to Microsoft.Default.Start will fail due to Microphone.Default being null.
  • In the microphone’s BufferReady event handler, the buffer is written to the stream, but only if we’re recording. (The three states of the page are indicated by the threevalue AudioState enumeration.) The average volume of the sample is used to determine where the needle should be placed, transforming the value to a range from MIN_ANGLE (-55°) to MAX_ANGLE (55°). The GetAverageVolume method is identical to the one from the preceding two chapters.
  • CompositionTarget_Rendering not only performs the requisite call to FrameworkDispatcher.Update, but it also takes the opportunity to adjust the needle based on the value of targetNeedleAngle calculated in Microphone_BufferReady. If the difference between the current angle and the target angle is small enough, the needle is directly moved to the target angle. Otherwise, the needle is moved a fraction of the necessary distance each time CompositionTarget_Rendering is called to provide a smooth animation. The speed of “downward” motion (decreasing the angle due to softer audio) is limited to DOWNWARD_VELOCITY to provide a more realistic effect of a needle that can jump to a louder volume but always smoothly returns to its resting position. This isn’t meant to be an accurate simulation of VU meter ballistics, but it should look good enough to most people.
  • CurrentState’s property setter updates the user interface the old-fashioned way; by touching several properties on several elements. It would look more satisfactory if the relevant elements were data-bound to a version of CurrentState that is either a dependency property or a property that manually raises change notifications. However, several value converters would be needed to morph the enumeration value into the variety of Visibility values and strings needed. The end result would involve much more code.
  • The code that actually saves the audio data is at the end of the listing in StopButton_Click. To do this, the memory stream holding the data is passed to SaveContent on a custom Recording class shown in the next listing.
  • This app uses two persisted settings, defined as follows in a separate Settings.cs file:

    [code]

    public static class Settings
    {
    // The user’s data
    public static readonly Setting<ObservableCollection<Recording>>
    RecordingsList =
    new Setting<ObservableCollection<Recording>>(“RecordingsList”,
    new ObservableCollection<Recording>());
    // Communicate the selection on the list page to the details page
    public static readonly Setting<int> SelectedRecordingIndex =
    new Setting<int>(“SelectedRecordingIndex”, -1);
    }
    [/code]

LISTING 36.3 Recording.cs—The Object Representing Each Sound File Stored in Isolated Storage

[code]

using System;
using System.ComponentModel;
using System.IO;
using System.IO.IsolatedStorage;
using Microsoft.Xna.Framework.Audio;
namespace WindowsPhoneApp
{
public class Recording : INotifyPropertyChanged
{
// The backing fields
string filename;
string label;
DateTimeOffset timeStamp;
TimeSpan duration;
// The properties, which raise change notifications
public string Filename
{
get { return this.filename; }
set { this.filename = value; OnPropertyChanged(“Filename”); }
}
public string Label
{
get { return this.label; }
set { this.label = value; OnPropertyChanged(“Label”);
// Raise notifications for the readonly properties based on Label
OnPropertyChanged(“Title”); OnPropertyChanged(“ShortTitle”);
OnPropertyChanged(“Subtitle”); }
}
public DateTimeOffset TimeStamp
{
get { return this.timeStamp; }
set { this.timeStamp = value; OnPropertyChanged(“TimeStamp”);
// Raise notifications for the readonly properties based on TimeStamp
OnPropertyChanged(“Title”); OnPropertyChanged(“ShortTitle”);
OnPropertyChanged(“Subtitle”); }
}
public TimeSpan Duration
{
get { return this.duration; }
set { this.duration = value; OnPropertyChanged(“Duration”);
// Raise notifications for the readonly properties based on Duration
OnPropertyChanged(“Title”); OnPropertyChanged(“ShortTitle”);
OnPropertyChanged(“Subtitle”); }
}
// A few computed properties for display purposes
public string Title
{
get {
return String.Format(“{0} ({1:00}:{2:00})”,
this.label ?? this.TimeStamp.LocalDateTime.ToShortTimeString(),
this.Duration.Minutes, Math.Floor(this.Duration.Seconds));
}
}
public string ShortTitle
{
get {
return this.label ?? this.TimeStamp.LocalDateTime.ToShortTimeString();
}
}
public string Subtitle
{
get {
if (this.label != null)
return String.Format(“{0} {1}”,
this.TimeStamp.LocalDateTime.ToShortDateString(),
this.TimeStamp.LocalDateTime.ToShortTimeString());
else
return this.TimeStamp.LocalDateTime.ToShortDateString();
}
}
// Save the stream to isolated storage
public void SaveContent(MemoryStream memoryStream)
{
// Store the duration of the content, used for display purposes
this.Duration = Microphone.Default.GetSampleDuration(
(int)memoryStream.Position);
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream stream =
userStore.CreateFile(this.Filename))
{
stream.Write(memoryStream.GetBuffer(), 0, (int)memoryStream.Position);
}
}
// Get the raw bytes from the file in isolated storage
byte[] GetBuffer()
{
byte[] buffer =
new byte[Microphone.Default.GetSampleSizeInBytes(this.Duration)];
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream stream =
userStore.OpenFile(this.Filename, FileMode.Open))
{
stream.Read(buffer, 0, buffer.Length);
}
return buffer;
}
// Create and return a sound effect based on the raw bytes in the file
public SoundEffect GetContent()
{
return new SoundEffect(this.GetBuffer(), Microphone.Default.SampleRate,
AudioChannels.Mono);
}
// Delete the file
public void DeleteContent()
{
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
userStore.DeleteFile(this.Filename);
}
// Overwrite the file’s contents with the audio data reversed
public void Reverse()
{
byte[] buffer = this.GetBuffer();
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream stream =
userStore.OpenFile(this.Filename, FileMode.Open, FileAccess.Write))
{
// Reverse each 2-byte chunk (each 16-bit audio sample)
for (int i = buffer.Length – 2; i >= 0; i -= 2)
stream.Write(buffer, i, 2);
}
}
void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
}

[/code]

  • Any changes to Label, TimeStamp, or Duration also raise property-changed notifications for Title, Subtitle, and ShortTitle, three readonly properties whose value is based on these read/write properties. The list page leverages Title and Subtitle in the display for each recording, and the details page leverages ShortTitle.
  • The implementation of SaveContent is a straightforward writing of the passed-in stream’s bytes to the isolated storage file indicated by the Filename property. This method also automatically sets Duration to the length of the recording (revealed by the microphone) so this information can be leveraged in other parts of the app without having to load the audio file. This is especially important for the list page, which displays the duration for every recording simultaneously.
  • When GetBuffer reads in the data from isolated storage, it knows how big the buffer needs to be ahead of time, thanks to the stored duration (and thanks to the microphone’s GetSampleSizeInBytes method). GetBuffer is not public; consumers instead call GetContent, which returns the audio data as a familiar SoundEffect object.
  • DeleteContent deletes the file containing the audio data, just like the same-named method from the Notepad app.
  • The Reverse method does the trick of reversing the audio file. It’s simply a matter of reversing the bytes, except that this needs to be done in 2-byte chunks to keep each 16-bit audio sample intact.

The List Page

The list page, shown in Figure 36.3, contains a list box with recordings that link to the details page. However, this is not a regular list box—it is a custom subclass called CheckableListBox that mimics the Mail app’s mechanism for bulk-selecting items. To enter bulk-selection mode, you can either tap the application bar button or tap the leftmost edge of any item in the list. The latter approach has the advantage of automatically selecting the tapped item. The code to CheckableListBox is not covered in this chapter, but it is included with this chapter’s source code.

The CheckableListBox supports multi-select interaction the same way as the phone’s built-in Mail app.
FIGURE 36.3 The CheckableListBox supports multi-select interaction the same way as the phone’s built-in Mail app.

The only thing you can do with bulk-selected items is delete them. Notice in Figure 36.3 that the application bar changes to show a delete button at the same time that the check boxes appear.

The XAML for the list page is shown in Listing 36.4, and the code-behind is shown in Listing 36.5.

LISTING 36.4 ListPage.xaml—The User Interface for Sound Recorder’s List of Recordings

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.ListPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<!– The application bar, with one button and one menu item –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”select” Click=”SelectButton_Click”
IconUri=”/Shared/Images/appbar.select.png”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”SOUND RECORDER”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”recordings” Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<TextBlock x:Name=”NoItemsTextBlock” Grid.Row=”1” Text=”No recordings”
Visibility=”Collapsed” Margin=”22,17,0,0”
Style=”{StaticResource PhoneTextGroupHeaderStyle}”/>
<!– A list box supporting check boxes for bulk selection –>
<local:CheckableListBox x:Name=”CheckableListBox” Grid.Row=”1”
Margin=”0,18,0,0”
SelectionMode=”Multiple” ItemsSource=”{Binding}”
SelectionChanged=”ListBox_SelectionChanged”>
<local:CheckableListBox.ItemTemplate>
<DataTemplate>
<!– Give each recording two lines: a title and a subtitle –>
<StackPanel>
<TextBlock Text=”{Binding Title}” Margin=”-2,-13,0,0”
Style=”{StaticResource PhoneTextExtraLargeStyle}”/>
<TextBlock Text=”{Binding Subtitle}” Margin=”0,-5,0,28”
Style=”{StaticResource PhoneTextSubtleStyle}”/>
</StackPanel>
</DataTemplate>
</local:CheckableListBox.ItemTemplate>
</local:CheckableListBox>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 36.5 ListPage.xaml.cs—The Code-Behind for Sound Recorder’s List of Recordings

[code]

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class ListPage : PhoneApplicationPage
{
bool inSelectMode;
public ListPage()
{
InitializeComponent();
// Assign the data source for the list box
this.DataContext = Settings.RecordingsList.Value;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (Settings.RecordingsList.Value.Count == 0)
ShowListAsEmpty();
}
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
// The Back button should exit select mode
if (this.inSelectMode)
{
e.Cancel = true;
LeaveSelectMode();
}
}
void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (this.CheckableListBox.SelectedItems.Count == 1 &&
!this.CheckableListBox.AreCheckBoxesShowing)
{
// This is a normal, single selection, so navigate to the details page
Settings.SelectedRecordingIndex.Value =
this.CheckableListBox.SelectedIndex;
this.NavigationService.Navigate(
new Uri(“/DetailsPage.xaml”, UriKind.Relative));
// Clear the selection for next time
this.CheckableListBox.SelectedIndex = -1;
}
else if (this.CheckableListBox.AreCheckBoxesShowing && !this.inSelectMode)
this.EnterSelectMode();
else if (!this.CheckableListBox.AreCheckBoxesShowing && this.inSelectMode)
this.LeaveSelectMode();
if (this.inSelectMode)
(this.ApplicationBar.Buttons[0] as IApplicationBarIconButton).IsEnabled =
(this.CheckableListBox.SelectedItems.Count > 0);
}
void ShowListAsEmpty()
{
NoItemsTextBlock.Visibility = Visibility.Visible;
this.ApplicationBar.IsVisible = false;
}
void EnterSelectMode()
{
// Show the check boxes
this.CheckableListBox.ShowCheckBoxes();
// Clear the application bar and show a delete button
this.ApplicationBar.Buttons.Clear();
ApplicationBarIconButton deleteButton = new ApplicationBarIconButton(
new Uri(“/Shared/Images/appbar.delete.png”, UriKind.Relative));
deleteButton.Text = “delete”;
deleteButton.IsEnabled = false; // Will be enabled when >=1 item selected
deleteButton.Click += DeleteButton_Click;
this.ApplicationBar.Buttons.Add(deleteButton);
this.inSelectMode = true;
}
void LeaveSelectMode()
{
// Hide the check boxes
if (this.CheckableListBox.AreCheckBoxesShowing)
this.CheckableListBox.HideCheckBoxes();
// Clear the application bar and show a select button
this.ApplicationBar.Buttons.Clear();
ApplicationBarIconButton button = new ApplicationBarIconButton(
new Uri(“/Shared/Images/appbar.select.png”, UriKind.Relative));
button.Text = “select”;
button.Click += SelectButton_Click;
this.ApplicationBar.Buttons.Add(button);
this.inSelectMode = false;
}
// Application bar handlers
void SelectButton_Click(object sender, EventArgs e)
{
EnterSelectMode();
}
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to delete “ +
(this.CheckableListBox.SelectedItems.Count > 1 ? “these recordings” :
“this recording”) + “?”, “Delete recording” +
(this.CheckableListBox.SelectedItems.Count > 1 ? “s” : “”) + “?”,
MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
Recording[] itemsToDelete =
new Recording[this.CheckableListBox.SelectedItems.Count];
this.CheckableListBox.SelectedItems.CopyTo(itemsToDelete, 0);
this.CheckableListBox.SelectedIndex = -1;
this.LeaveSelectMode();
for (int i = 0; i < itemsToDelete.Length; i++)
{
// Remove it from the list
Settings.RecordingsList.Value.Remove(itemsToDelete[i]);
// Delete the audio file in isolated storage
itemsToDelete[i].DeleteContent();
}
if (Settings.RecordingsList.Value.Count == 0)
ShowListAsEmpty();
}
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Sound Recorder”, UriKind.Relative));
}
}
}

[/code]

  • The checkable list box data-binds to the RecordingsList setting and stays up-todate thanks to the observable collection and property-changed notifications from each item. Its SelectionMode property, inherited from the base list box control, is set to Multiple to enable bulk-selection to work.
  • Tapping an item navigates to the details page filled out for that item. This is communicated via the SelectedRecordingIndex setting that is set appropriately before navigating.
  • When an item is deleted, the code makes sure to call DeleteContent in addition to removing the Recording instance from the list. Otherwise, the audio file would be left behind in isolated storage.
  • The rest of the code manages the CheckableListBox. Bulk-selection mode (just called select mode in the listing) can be triggered either by tapping an item’s left margin (handled internally in code for CheckableListBoxItem) or by tapping the “select” button on the application bar. As in the Mail app, bulk-selection mode can be exited either by pressing the hardware Back button or by unchecking all of the check boxes.

The Details Page

The details page contains several features in addition to playing the selected recording.
FIGURE 36.4 The details page contains several features in addition to playing the selected recording.

The details page, shown in Figure 36.4 with its application bar expanded, enables playback, editing, and deletion of the selected sound.

The XAML for this page is shown in Listing 36.6, and the code-behind is shown in Listing 36.7.

LISTING 36.6 DetailsPage.xaml—The User Interface for Sound Recorder’s Details Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.DetailsPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<!– The application bar, with 3 buttons and 2 menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”pause”
IconUri=”/Shared/Images/appbar.pause.png” Click=”PlayPauseButton_Click”/>
<shell:ApplicationBarIconButton Text=”edit name”
IconUri=”/Shared/Images/appbar.edit.png” Click=”EditButton_Click”/>
<shell:ApplicationBarIconButton Text=”delete”
IconUri=”/Shared/Images/appbar.delete.png” Click=”DeleteButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”reverse”
Click=”ReverseMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock x:Name=”ApplicationTitle” Text=”SOUND RECORDER”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”{Binding ShortTitle}”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<!– The playback slider –>
<TextBlock x:Name=”PlaybackDurationTextBlock” Grid.Row=”1”
Foreground=”{StaticResource PhoneSubtleBrush}” Margin=”12,58,0,0”/>
<Slider x:Name=”PlaybackSlider” SmallChange=”.1” Grid.Row=”1”
Margin=”0,24,0,84”/>
<!– The playback speed slider with its reset button –>
<Grid Grid.Row=”2”>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width=”Auto”/>
</Grid.ColumnDefinitions>
<TextBlock Text=”Playback Speed” Grid.ColumnSpan=”2” Margin=”12,0,0,0”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<Slider x:Name=”SpeedSlider” Grid.Row=”1” SmallChange=”.1” LargeChange=”.1”
Minimum=”-1” Maximum=”1” Margin=”0,18,0,0”
ValueChanged=”SpeedSlider_ValueChanged”/>
<Button Grid.Row=”1” Grid.Column=”1” Content=”reset” Margin=”0,0,0,16”
VerticalAlignment=”Center” local:Tilt.IsEnabled=”True”
Click=”SpeedResetButton_Click”/>
</Grid>
<!– The “edit name” dialog –>
<local:Dialog x:Name=”EditDialog” Grid.RowSpan=”3” Closed=”EditDialog_Closed”>
<local:Dialog.InnerContent>
<StackPanel>
<TextBlock Text=”Choose a name” Margin=”11,5,0,-5”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBox Text=”{Binding Result, Mode=TwoWay}” InputScope=”Text”/>
</StackPanel>
</local:Dialog.InnerContent>
</local:Dialog>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 36.7 DetailsPage.xaml.cs—The Code-Behind for Sound Recorder’s Details Page

[code]

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using Microsoft.Xna.Framework.Audio;
namespace WindowsPhoneApp
{
public partial class DetailsPage : PhoneApplicationPage
{
Recording selectedRecording;
SoundEffectInstance soundInstance;
double durationScale = 1;
double elapsedSeconds;
DateTime lastPlayFrame;
SoundState lastSoundState = SoundState.Stopped;
IApplicationBarIconButton playPauseButton;
public DetailsPage()
{
InitializeComponent();
this.playPauseButton = this.ApplicationBar.Buttons[0]
as IApplicationBarIconButton;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// The recording chosen from the list page
this.selectedRecording =
Settings.RecordingsList.Value[Settings.SelectedRecordingIndex.Value];
// The actual sound effect instance to play
this.soundInstance = this.selectedRecording.GetContent().CreateInstance();
// The page title data-binds to the ShortTitle property
this.DataContext = this.selectedRecording;
// Adjust the playback slider based on the recording’s length
PlaybackSlider.Maximum = this.selectedRecording.Duration.TotalSeconds;
// Start playing automatically
Play();
}
void Play()
{
CompositionTarget.Rendering += CompositionTarget_Rendering;
this.playPauseButton.Text = “pause”;
this.playPauseButton.IconUri = new Uri(“/Shared/Images/appbar.pause.png”,
UriKind.Relative);
this.lastPlayFrame = DateTime.Now;
if (this.soundInstance.State == SoundState.Paused)
this.soundInstance.Resume();
else
{
// Play from the beginning
this.soundInstance.Play();
this.elapsedSeconds = 0;
}
}
void Pause()
{
this.playPauseButton.Text = “play”;
this.playPauseButton.IconUri = new Uri(“/Shared/Images/appbar.play.png”,
UriKind.Relative);
this.soundInstance.Pause();
}
void CompositionTarget_Rendering(object sender, EventArgs e)
{
if (this.soundInstance != null)
{
// Keep the playback slider up-to-date with the playing audio
if (this.soundInstance.State == SoundState.Playing ||
this.lastSoundState == SoundState.Playing
/* So remaining time after pausing/stopping is accounted for */)
{
this.elapsedSeconds +=
(DateTime.Now – lastPlayFrame).TotalSeconds / this.durationScale;
this.lastPlayFrame = DateTime.Now;
this.PlaybackSlider.Value = this.elapsedSeconds;
if (this.soundInstance.State == SoundState.Stopped)
this.PlaybackSlider.Value = this.PlaybackSlider.Maximum;
UpdatePlaybackLabel();
}
// Automatically turn the pause button back into a play button when the
// recording has finished playing
if (this.soundInstance.State != SoundState.Playing &&
this.playPauseButton.Text != “play”)
{
this.playPauseButton.Text = “play”;
this.playPauseButton.IconUri =
new Uri(“/Shared/Images/appbar.play.png”, UriKind.Relative);
// Unhook this event since it gets hooked on each play
CompositionTarget.Rendering -= CompositionTarget_Rendering;
}
this.lastSoundState = this.soundInstance.State;
// Required for XNA sound effect to work
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
}
}
void UpdatePlaybackLabel()
{
TimeSpan elapsedTime =
TimeSpan.FromSeconds(elapsedSeconds * this.durationScale);
TimeSpan scaledDuration =
TimeSpan.FromSeconds(PlaybackSlider.Maximum * this.durationScale);
PlaybackDurationTextBlock.Text = String.Format(“{0:00}:{1:00}”,
elapsedTime.Minutes, Math.Floor(elapsedTime.Seconds)) + “ / “ +
String.Format(“{0:00}:{1:00}”,
scaledDuration.Minutes, Math.Floor(scaledDuration.Seconds));
}
// Speed slider handlers
void SpeedSlider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
// Directly apply the -1 to 1 slider value as the pitch
this.soundInstance.Pitch = (float)SpeedSlider.Value;
// The duration scale used by other calculations ranges from
// .5 for double-speed/half-length (+1 pitch) to
// 2 for half-speed/double-length (-1 pitch)
this.durationScale = 1 + Math.Abs(this.soundInstance.Pitch);
if (this.soundInstance.Pitch > 0)
this.durationScale = 1 / this.durationScale;
UpdatePlaybackLabel();
}
void SpeedResetButton_Click(object sender, RoutedEventArgs e)
{
SpeedSlider.Value = 0;
}
// Handlers related to the “edit name” dialog
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
if (EditDialog.Visibility == Visibility.Visible)
{
e.Cancel = true;
EditDialog.Hide(MessageBoxResult.Cancel);
}
}
void EditDialog_Closed(object sender, MessageBoxResultEventArgs e)
{
this.ApplicationBar.IsVisible = true;
if (e.Result == MessageBoxResult.OK)
{
this.selectedRecording.Label = EditDialog.Result.ToString();
}
}
// Application bar handlers
void PlayPauseButton_Click(object sender, EventArgs e)
{
if (this.soundInstance.State == SoundState.Playing)
this.Pause();
else
this.Play();
}
void EditButton_Click(object sender, EventArgs e)
{
EditDialog.Result = this.selectedRecording.Label;
EditDialog.Show();
this.ApplicationBar.IsVisible = false;
}
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to delete this recording?”,
“Delete recording?”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
// Remove it from the list
Settings.RecordingsList.Value.Remove(this.selectedRecording);
// Delete the audio file in isolated storage
this.selectedRecording.DeleteContent();
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
}
void ReverseMenuItem_Click(object sender, EventArgs e)
{
this.selectedRecording.Reverse();
// We must get the new, reversed sound effect instance
this.soundInstance = this.selectedRecording.GetContent().CreateInstance();
// Re-apply the chosen pitch
this.soundInstance.Pitch = (float)SpeedSlider.Value;
Play();
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Sound Recorder”, UriKind.Relative));
}
}
}

[/code]

  • All three rows in this page’s root grid are given a height of Auto, so the content doesn’t shift when the application bar is hidden during the display of the “edit name” dialog.
  • The Dialog user control used by several apps in this book is leveraged here to enable the user to rename the recording. This name is applied as the Label property on the Recording instance, which impacts the Title, ShortTitle, and Subtitle properties as shown in Listing 36.3.
  • The CompositionTarget_Rendering handler, used only while the audio is playing, keeps the slider and pause/play button in sync with the audio.
  • The speed slider uses the familiar Pitch property on SoundEffectInstance to adjust the audio as it plays. This affects speed and pitch, as there’s unfortunately no builtin way to adjust the speed while maintaining the pitch.
  • The audio reversal is done inside ReverseMenuItem_Click. Because the reversal is done to the file in isolated storage, the sound effect instance must be retrieved again. Invoking the reversal a second time restores the audio file to its original data.

The Finished Product

Sound Recorder (Saving Audio Files & Playing Sound Backward)