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)

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)

Coin Toss (Throw)

Coin Toss helps you with any heads-or-tails type of decision. Make an upward tossing motion with the phone in your hand, and watch the coin of your choice flip to give you an answer. You can choose between a penny, nickel, dime, or quarter, and see a neat visual history of your coin tosses.

The lesson of this chapter is detecting a tossing/throwing motion with the phone. And surprise, surprise, it’s the same algorithm used in the preceding chapter for detecting punches! (I guess they call it “throwing punches” for a reason!) The only difference is that the Z axis is tracked rather than the X axis.

The Main User Interface

Coin Toss has a main page, a history page, a settings page, and an instructions page. The code for the latter two pages isn’t shown in this chapter. The settings page is actually identical to the settings page from the preceding chapter, except “Required punching strength” is renamed to “Required tossing strength,” and the tip textbox at the bottom is removed.

Listing 45.1 contains the XAML for the main page. It is much more complicated than the preceding chapter’s main page in order to support all the animations done by this app.

LISTING 45.1 MainPage.xaml—The User Interface for Coin Toss’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”>
<!– Add three storyboards to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– The 3D flip, 90° at a time –>
<Storyboard x:Name=”FlipStoryboard” Storyboard.TargetName=”CoinProjection”
Storyboard.TargetProperty=”RotationX”
Completed=”FlipStoryboard_Completed”>
<DoubleAnimation By=”90” Duration=”0:0:.06”/>
</Storyboard>
<!– The movement up and off the screen –>
<Storyboard x:Name=”RiseAndFallStoryboard”
Storyboard.TargetName=”CoinTransform”>
<!– Moving up then back down –>
<DoubleAnimation x:Name=”RiseAndFallAnimation1” AutoReverse=”True”
Storyboard.TargetProperty=”TranslateY” By=”-300”>
<DoubleAnimation.EasingFunction>
<QuadraticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Growing then shrinking (X) –>
<DoubleAnimation x:Name=”RiseAndFallAnimation2” AutoReverse=”True”
Storyboard.TargetProperty=”ScaleX” By=”1.05”>
<DoubleAnimation.EasingFunction>
<QuadraticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Growing then shrinking (Y) –>
<DoubleAnimation x:Name=”RiseAndFallAnimation3” AutoReverse=”True”
Storyboard.TargetProperty=”ScaleY” By=”1.05”>
<DoubleAnimation.EasingFunction>
<QuadraticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Flip in the “HEADS!” or “TAILS!” text in 3D –>
<Storyboard x:Name=”ShowResultStoryboard”
Storyboard.TargetName=”ResultProjection”
Storyboard.TargetProperty=”RotationX”>
<DoubleAnimation To=”0” Duration=”0:0:.4”>
<DoubleAnimation.EasingFunction>
<BackEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– The application bar, with three buttons and four menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.8”>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click” />
<shell:ApplicationBarIconButton Text=”history”
IconUri=”/Images/appbar.history.png”
Click=”HistoryButton_Click” />
<shell:ApplicationBarIconButton Text=”settings”
IconUri=”/Shared/Images/appbar.settings.png”
Click=”SettingsButton_Click” />
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”penny” Click=”CoinMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”nickel” Click=”CoinMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”dime” Click=”CoinMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”quarter” Click=”CoinMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<!– The grid containing the coin –>
<Grid RenderTransformOrigin=”.5,.5”>
<Grid.RenderTransform>
<!– Used to move and scale the coin –>
<CompositeTransform x:Name=”CoinTransform”/>
</Grid.RenderTransform>
<Grid.Projection>
<!– Used to flip the coin in 3D –>
<PlaneProjection x:Name=”CoinProjection”/>
</Grid.Projection>
<!– The tails side of the coin –>
<Image x:Name=”TailsImage” RenderTransformOrigin=”.5,.5”
Stretch=”None”>
<!– Reverse, so it looks correct when flipped over –>
<Image.RenderTransform>
<ScaleTransform ScaleY=”-1”/>
</Image.RenderTransform>
</Image>
<!– The heads side of the coin –>
<Image x:Name=”HeadsImage” Stretch=”None”/>
</Grid>
<!– The “HEADS!” or “TAILS!” text block, which animates independently –>
<TextBlock x:Name=”ResultTextBlock” FontFamily=”Segoe WP Black” FontSize=”120”
Foreground=”{StaticResource PhoneAccentBrush}” HorizontalAlignment=”Center”>
<TextBlock.Projection>
<!– Used to flip the text in 3D –>
<PlaneProjection x:Name=”ResultProjection” RotationX=”90”/>
</TextBlock.Projection>
</TextBlock>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • The first two storyboards create the 3D coin-flip effect, pictured in Figure 45.1. The first storyboard does the 3D part, and the second one makes it more realistic by animating the scale and position of the coin while it flips in 3D.
  • The final storyboard performs a 3D flip-in animation on the “HEADS!” or “TAILS!” result text, as shown in Figure 45.2.
The coin appears to leap out from the screen as it spins in 3D.
FIGURE 45.1 The coin appears to leap out from the screen as it spins in 3D.
The 3D text animation in action.
FIGURE 45.2 The 3D text animation in action.
  • The application bar contains buttons for going to each of the other pages, and the application bar menu contains an entry for each of the four available coins, as shown in Figure 45.3. Although the settings page isn’t really important enough to have a prominent spot on the application bar, a button is used rather than a menu item because mixing a settings menu item with the four coin menu items wouldn’t be the cleanest presentation.
The application bar menu enables the choice between four coins.
FIGURE 45.3 The application bar menu enables the choice between four coins.

The Main Code-Behind

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

[code]

public static class Settings
{
public static readonly Setting<string> ChosenCoin =
new Setting<string>(“ChosenCoin”, “penny”);
public static readonly Setting<List<string>> HistoryList =
new Setting<List<string>>(“HistoryList”, new List<string>());
public static readonly Setting<double> Threshold =
new Setting<double>(“Threshold”, 1);
}

[/code]

The Threshold setting is like the one from the preceding chapter, but with a default value of 1 rather than 1.5, so the default strength required for performing a coin toss is less than the default strength required for punching in the Boxing Glove app.

LISTING 45.2 MainPage.xaml.cs—The Code-Behind for Coin Toss’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Media.Imaging;
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;
int animationStep;
int animationTotalSteps;
bool isTails = false;
bool isTossing = false;
DateTimeOffset acceleratingQuicklyForwardTime = DateTime.MinValue;
Random random = new Random();
public MainPage()
{
InitializeComponent();
// Initialize the accelerometer
this.accelerometer = new Accelerometer();
this.accelerometer.ReadingChanged += Accelerometer_ReadingChanged;
}
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);
}
// Restore the chosen coin
this.HeadsImage.Source = new BitmapImage(new Uri(“Images/” +
Settings.ChosenCoin.Value + “Heads.png”, UriKind.Relative));
this.TailsImage.Source = new BitmapImage(new Uri(“Images/” +
Settings.ChosenCoin.Value + “Tails.png”, UriKind.Relative));
}
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)
{
// We want the threshold to be negative, so
// forward motion is up and out of the screen
double threshold = -Settings.Threshold.Value;
// Only pay attention to large-enough magnitudes in the Z dimension
if (Math.Abs(e.Z) < Math.Abs(threshold))
return;
// See if the force is in the same direction as the threshold
// (forward throwing motion)
if (e.Z * threshold > 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 flip the coin!
this.acceleratingQuicklyForwardTime = DateTimeOffset.MinValue;
// We’re on a different thread, so transition to the UI thread
this.Dispatcher.BeginInvoke(delegate() { BeginToss(); });
}
}
void BeginToss()
{
if (this.isTossing)
return;
this.isTossing = true;
this.ShowResultStoryboard.Stop();
// Choose heads or tails
bool willBeTails = this.random.Next(0, 2) == 0;
// First randomly choose number of complete 360° flips
// (multiples of 4 because the animation is done 90° at a time)
this.animationTotalSteps = this.random.Next(3, 6) * 4;
if (this.isTails != willBeTails)
{
// It needs to land on the opposite side,
// so add two more animation steps (180°)
this.animationTotalSteps += 2;
}
// Make the duration of the rise-and-fall animations match the length
// of time that the coin will be spinning (+ a .1 second buffer)
this.RiseAndFallAnimation1.Duration =
this.RiseAndFallAnimation2.Duration =
this.RiseAndFallAnimation3.Duration =
TimeSpan.FromSeconds(this.animationTotalSteps * .03 + .1);
this.isTails = willBeTails;
this.animationStep = 0;
// Perform the first 90° of the flip and start the rise-and-fall in sync
this.FlipStoryboard.Begin();
this.RiseAndFallStoryboard.Begin();
}
void FlipStoryboard_Completed(object sender, EventArgs e)
{
this.animationStep++;
if (this.animationStep == this.animationTotalSteps)
{
// We’re done
if (this.isTails)
{
this.ResultTextBlock.Text = “TAILS!”;
Settings.HistoryList.Value.Add(“Images/” +
Settings.ChosenCoin.Value + “Tails.png”);
}
else
{
this.ResultTextBlock.Text = “HEADS!”;
Settings.HistoryList.Value.Add(“Images/” +
Settings.ChosenCoin.Value + “Heads.png”);
}
this.isTossing = false;
this.ShowResultStoryboard.Begin();
return;
}
// Each complete rotation has 4 phases (0°, 90°, 180°, 270°)
int phase = this.animationStep % 4;
// Check for 90° or 270°, the two points where the coin is
// perpendicular to the screen (and therefore invisible)
if (phase == 1 || phase == 3)
{
// This is when we toggle the visible image between heads and tails
// by showing/hiding the heads image on top of the tails image
if (this.HeadsImage.Visibility == Visibility.Collapsed)
this.HeadsImage.Visibility = Visibility.Visible;
else
this.HeadsImage.Visibility = Visibility.Collapsed;
}
this.FlipStoryboard.Begin();
}
// Application bar handlers
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void HistoryButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/HistoryPage.xaml”,
UriKind.Relative));
}
void SettingsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
void CoinMenuItem_Click(object sender, EventArgs e)
{
IApplicationBarMenuItem menuItem = sender as IApplicationBarMenuItem;
Settings.ChosenCoin.Value = menuItem.Text;
this.HeadsImage.Source = new BitmapImage(new Uri(“Images/” + menuItem.Text
+ “Heads.png”, UriKind.Relative));
this.TailsImage.Source = new BitmapImage(new Uri(“Images/” + menuItem.Text
+ “Tails.png”, UriKind.Relative));
}
}
}

[/code]

  • The interaction with the accelerometer is identical to the preceding chapter, except that e.Z is used instead of e.X. Notice that the threshold is made negative, which seems to contradict the description of the Z axis in Figure 44.1 from the preceding chapter. That’s because when you hold a phone flat and accelerate it upward, it “feels heavier” in the opposite direction, causing the value of e.Z to decrease. You experience the same sensation when going up in a fast-moving elevator. The same behavior applies to either direction in any dimension. This is the difference between measuring g-forces (what accelerometers do) versus measuring acceleration.
  • The project includes image files named pennyHeads.png, pennyTails.png, nickelHeads.png, nickelTails.png, and so on. This is why the code is able to take the text from the menu item (or the string from the ChosenCoin setting) and simply prepend it to Heads.png or Tails.png.
  • The URI string from the chosen heads or tails coin image is added to the history list each time. Although this is an odd and inefficient way to store the data, it’s an easy way to get the history page’s experience, shown next.

The History Page

The history page, shown in Figure 45.4, uses a wrap panel from the Silverlight for Windows Phone Toolkit to display miniature versions of past coin results.

The XAML for the history page is shown in Listing 45.3 and its code-behind is in Listing 45.4. The wrap panel is placed inside a scroll viewer so the entire history can be accessed when the coins are wrapped to off-screen rows.

The history page uses a wrap panel that reflows the coins based on the current orientation.
FIGURE 45.4 The history page uses a wrap panel that reflows the coins based on the current orientation.

LISTING 45.3 HistoryPage.xaml—The User Interface for Coin Toss’s History Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.HistoryPage”
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:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
SupportedOrientations=”PortraitOrLandscape”
shell:SystemTray.IsVisible=”True”>
<!– The application bar with a delete button –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.8”>
<shell:ApplicationBarIconButton Text=”delete”
IconUri=”/Shared/Images/appbar.delete.png”
Click=”DeleteButton_Click” />
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid Background=”{StaticResource PhoneBackgroundBrush}”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”COIN TOSS” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”history”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<TextBlock Name=”NoItemsTextBlock” Text=”No history. Start tossing!”
Visibility=”Collapsed”
Margin=”22 17 0 0”
Style=”{StaticResource PhoneTextGroupHeaderStyle}”
Grid.Row=”1”/>
<ScrollViewer Grid.Row=”1”>
<toolkit:WrapPanel x:Name=”WrapPanel” Margin=”12,0,0,0”/>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 45.4 HistoryPage.xaml.cs—The Code-Behind for Coin Toss’s History Page

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class HistoryPage : PhoneApplicationPage
{
public HistoryPage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
foreach (string uri in Settings.HistoryList.Value)
{
this.WrapPanel.Children.Add(new Image {
Source = new BitmapImage(new Uri(uri, UriKind.Relative)),
Height = 90, Margin = new Thickness(12) });
}
if (Settings.HistoryList.Value.Count == 0)
ShowListAsEmpty();
}
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(
“Are you sure you want to clear your coin toss history?”,
“Delete history”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
Settings.HistoryList.Value.Clear();
this.WrapPanel.Children.Clear();
ShowListAsEmpty();
}
}
void ShowListAsEmpty()
{
this.NoItemsTextBlock.Visibility = Visibility.Visible;
this.ApplicationBar.IsVisible = false;
}
}
}

[/code]

The Finished Product

Coin Toss (Throw)

Jigsaw Puzzle (Drag Gesture & WriteableBitmap)

Jigsaw Puzzle enables you to turn any picture into a challenging 30-piece jigsaw puzzle. You can use one of the included pictures, or choose a photo from your camera or pictures library. You can even zoom and crop the photo to get it just right. Drag pieces up from a scrollable tray at the bottom of the screen and place them where you think they belong. As you drag a piece, it snaps to each of the 30 possible correct positions to reduce your frustration when arranging pieces. Jigsaw Puzzle also can solve the puzzle for you, or reshuffle the pieces, both with fun animations.

Other than the instructions page, Jigsaw Puzzle contains a main page and a page for cropping an imported picture. This app leverages gesture listener’s drag events for a few different reasons. On the main page, dragging is used for moving puzzle pieces and for scrolling the tray of unused pieces at the bottom of the screen. On the page for cropping imported pictures, dragging is used to pan the picture.

Are you thinking of ways to increase the difficulty of the puzzles in this app? Although the puzzle pieces would become too difficult to drag if you make them much smaller (without also enabling zooming), you could enable pieces to be rotated.The next chapter demonstrates how to implement a rotation gesture.

The Main Page

Jigsaw Puzzle’s main page contains the 30 pieces arranged in 6 rows of 5. Each piece is a 96×96 canvas that contains a vector drawing represented as a Path element. 14 distinct shapes are used (4 if you consider rotated/flipped versions as equivalent), shown in Figure 42.1.

The 14 shapes consist of 4 corner pieces, 8 edge pieces, and 2 middle pieces.
FIGURE 42.1 The 14 shapes consist of 4 corner pieces, 8 edge pieces, and 2 middle pieces.

Each piece is actually larger than 96 pixels in at least one dimension, which is fine because each Path can render outside the bounds of its parent 96×96 canvas. Each Path is given an appropriate offset inside its parent canvas to produce the appropriate interlocking pattern, as illustrated in Figure 42.2. Every puzzle presented by this app uses these exact 30 pieces in the exact same spots; only the image on the pieces changes.

The choice of a vector-based path to represent each piece is important because it enables the nonrectangular shapes to interlock and retain precise hit-testing. If puzzle-pieceshaped images were instead used as an opacity mask on rectangular elements, the bounding box of each piece would respond to gestures on the entire area that overlaps the bounding box of any pieces underneath. This would cause the wrong piece to move in many areas of the puzzle. The use of paths also enables us to apply a custom stroke to each piece to highlight its edges.

The User Interface

Listing 42.1 contains the XAML for the main page.

The 30 vector-based shapes, each shown with its parent canvas represented as a yellow square outline.
FIGURE 42.2 The 30 vector-based shapes, each shown with its parent canvas represented as a yellow square outline.

LISTING 42.1 MainPage.xaml—The User Interface for Jigsaw Puzzle’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:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
SupportedOrientations=”Portrait”>
<!– Listen for drag events anywhere on the page –>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener DragStarted=”GestureListener_DragStarted”
DragDelta=”GestureListener_DragDelta”
DragCompleted=”GestureListener_DragCompleted”/>
</toolkit:GestureService.GestureListener>
<!– The application bar, with 4 buttons and 5 menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.5” ForegroundColor=”White”
BackgroundColor=”#443225”>
<shell:ApplicationBarIconButton Text=”picture”
IconUri=”/Shared/Images/appbar.picture.png” Click=”PictureButton_Click”/>
<shell:ApplicationBarIconButton Text=”start over”
IconUri=”/Shared/Images/appbar.delete.png” Click=”StartOverButton_Click”/>
<shell:ApplicationBarIconButton Text=”solve” IsEnabled=”False”
IconUri=”/Images/appbar.solve.png” Click=”SolveButton_Click”/>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”cat and fish”
Click=”ApplicationBarMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”city”
Click=”ApplicationBarMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”statue of liberty”
Click=”ApplicationBarMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”traffic”
Click=”ApplicationBarMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”under water”
Click=”ApplicationBarMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<!– Prevent off-screen pieces from appearing during a page transition –>
<phone:PhoneApplicationPage.Clip>
<RectangleGeometry Rect=”0,0,480,800”/>
</phone:PhoneApplicationPage.Clip>
<Canvas Background=”#655”>
<!– The tray at the bottom –>
<Rectangle x:Name=”Tray” Fill=”#443225” Width=”480” Height=”224”
Canvas.Top=”576”/>
<!– All 30 pieces placed where they belong –>
<Canvas x:Name=”PiecesCanvas”>
<!– Row 1 –>
<Canvas Width=”96” Height=”96”>
<Path Data=”F1M312.63,0L385.7,0C385.48,…” Height=”129” Stretch=”Fill”
Width=”96” Stroke=”#2000”>
<Path.Fill>
<ImageBrush Stretch=”None” AlignmentX=”Left” AlignmentY=”Top”/>
</Path.Fill>
</Path>
<Canvas.RenderTransform><CompositeTransform/></Canvas.RenderTransform>
</Canvas>
<Canvas Canvas.Left=”96” Width=”96” Height=”96”>
<Path Data=”F1M25.12,909.28C49.47,909.27,…” Height=”96”
Canvas.Left=”-33” Stretch=”Fill” Width=”162” Stroke=”#2000”>
<Path.Fill>
<ImageBrush Stretch=”None” AlignmentX=”Left” AlignmentY=”Top”>
<ImageBrush.Transform>
<TranslateTransform X=”-63”/>
</ImageBrush.Transform>
</ImageBrush>
</Path.Fill>
</Path>
<Canvas.RenderTransform><CompositeTransform/></Canvas.RenderTransform>
</Canvas>
… 27 pieces omitted …
<Canvas Canvas.Left=”384” Canvas.Top=”480” Width=”96” Height=”96”>
<Path Data=”F1M777.45,0L800.25,0C802.63,…” Canvas.Left=”-33”
Height=”96” Stretch=”Fill” Width=”129” Stroke=”#2000”>
<Path.Fill>
<ImageBrush Stretch=”None” AlignmentX=”Left” AlignmentY=”Top”>
<ImageBrush.Transform>
<TranslateTransform X=”-351” Y=”-480”/>
</ImageBrush.Transform>
</ImageBrush>
</Path.Fill>
</Path>
<Canvas.RenderTransform><CompositeTransform/></Canvas.RenderTransform>
</Canvas>
</Canvas>
<!– The image without visible piece boundaries, shown when solved –>
<Image x:Name=”CompleteImage” Visibility=”Collapsed” IsHitTestVisible=”False”
Stretch=”None”/>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

  • A gesture listener is attached to the entire page to listen for the three drag events: DragStarted, DragDelta, and DragCompleted.

My favorite way to create vector artwork based on an illustration is with Vector Magic (http://vectormagic.com). It’s not free, but it does a fantastic job of converting image files to a variety of vector formats. If you download the result as a PDF file and then rename the file extension to .ai, you can import it into Expression Blend, which converts it to XAML.

  • Although each piece is placed in its final “solved” position, the code-behind adjusts each position by modifying TranslateX and TranslateY properties on the CompositeTransform assigned to each piece. This gives us the nice property that no matter where a piece is moved, it can be returned to its solved position by setting both of these properties to zero.
  • The magic behind making each puzzle piece contain a portion of a photo is enabled by the image brush that fills each path. (The actual image is set in code-behind.) To make each piece contain the correct portion of the photo, each image brush (except the one used on the piece in the top left corner) is given a TranslateTransform. This shifts its rendering by the distance that the piece is from the top-left corner. (To make this work, each image brush is marked with top-left alignment, rather than its default center alignment.)

Fortunately for apps such as Jigsaw Puzzle, using many image brushes that point to the same image is efficient. Silverlight shares the underlying image rather than creating a separate copy for each brush.

  • The scrolling tray at the bottom isn’t an actual scroll viewer; it’s just a simple rectangle. The code-behind manually scrolls puzzle pieces when they sufficiently overlap this rectangle. This is done for two reasons: It’s convenient to keep the pieces on the same canvas at all times, and the gesture listener currently interferes with Silverlight elements such as scroll viewers.
  • The CompleteImage element at the bottom of the listing is used to show the complete image once the puzzle is solved, without the puzzle piece borders and tiny gaps between pieces obscuring it. Because it is aligned with the puzzle, showing this image simply makes it seem like the puzzle edges have faded away. Figure 42.3 shows what this looks like for the cat-and-fish puzzle shown at the beginning of this chapter. Because CompleteImage is not hit-testable, the user can still drag a piece while it is showing. As soon as any piece moves out of its correct position, the codebehind hides the image once again.
Once the puzzle is solved, the puzzle piece borders are no longer visible.
FIGURE 42.3 Once the puzzle is solved, the puzzle piece borders are no longer visible.

The Code-Behind

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

LISTING 42.2 MainPage.xaml.cs—The Code-Behind for Jigsaw Puzzle’s Main Page

[code]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
bool isDraggingTray;
bool isDraggingPiece;
double cumulativeDeltaX;
double cumulativeDeltaY;
int topmostZIndex;
List<FrameworkElement> piecesOnTray = new List<FrameworkElement>();
Random random = new Random();
IApplicationBarIconButton solveButton;
public MainPage()
{
InitializeComponent();
this.solveButton = this.ApplicationBar.Buttons[2]
as IApplicationBarIconButton;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Persist the offset currently being applied to each piece, so
// they can appear in the same locations next time
Settings.PieceOffsets.Value.Clear();
foreach (FrameworkElement piece in this.PiecesCanvas.Children)
{
Settings.PieceOffsets.Value.Add(new Point(
(piece.RenderTransform as CompositeTransform).TranslateX,
(piece.RenderTransform as CompositeTransform).TranslateY));
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
RefreshPuzzleImage();
bool arePiecesCorrect = false;
if (Settings.PieceOffsets.Value.Count == this.PiecesCanvas.Children.Count)
{
// Restore the persisted position of each piece
for (int i = 0; i < this.PiecesCanvas.Children.Count; i++)
{
UIElement piece = this.PiecesCanvas.Children[i];
CompositeTransform t = piece.RenderTransform as CompositeTransform;
t.TranslateX = Settings.PieceOffsets.Value[i].X;
t.TranslateY = Settings.PieceOffsets.Value[i].Y;
}
arePiecesCorrect = AreAllPiecesCorrect();
}
else
{
// This is the first run. After a 1-second delay, animate the pieces
// from their solved positions to random positions on the tray.
DispatcherTimer timer = new DispatcherTimer {
Interval = TimeSpan.FromSeconds(1) };
timer.Tick += delegate(object sender, EventArgs args)
{
StartOver();
timer.Stop();
};
timer.Start();
}
if (arePiecesCorrect)
ShowAsSolved();
else
ShowAsUnsolved();
}
// The three drag event handlers
void GestureListener_DragStarted(object sender, DragStartedGestureEventArgs e)
{
// Determine if we’re dragging the tray, a piece, or neither
FrameworkElement source = e.OriginalSource as FrameworkElement;
if (source == this.Tray)
{
// An empty spot on the tray is being dragged
if (e.Direction == System.Windows.Controls.Orientation.Horizontal)
this.isDraggingTray = true;
return;
}
FrameworkElement piece = GetPieceFromDraggedSource(source);
if (piece == null)
return;
if (e.Direction == System.Windows.Controls.Orientation.Horizontal &&
GetPieceTop(piece) > Constants.ON_TRAY_Y)
{
// Although a piece is being dragged, the piece is on the tray and the
// drag is horizontal, so consider this to be a tray drag instead
this.isDraggingTray = true;
}
else
{
this.isDraggingPiece = true;
// A piece is being dragged, so record its pre-drag position
CompositeTransform t = piece.RenderTransform as CompositeTransform;
this.cumulativeDeltaX = t.TranslateX;
this.cumulativeDeltaY = t.TranslateY;
}
}
void GestureListener_DragDelta(object sender, DragDeltaGestureEventArgs e)
{
if (this.isDraggingTray)
{
// Scroll the tray
ScrollTray(e.HorizontalChange);
}
else if (this.isDraggingPiece)
{
FrameworkElement piece = GetPieceFromDraggedSource(
e.OriginalSource as FrameworkElement);
if (piece == null)
return;
CompositeTransform t = piece.RenderTransform as CompositeTransform;
// Apply the position change caused by dragging.
// We’re keeping track of the total change from DragStarted so the piece
// remains in the right spot after repeated snapping and unsnapping.
this.cumulativeDeltaX += e.HorizontalChange;
this.cumulativeDeltaY += e.VerticalChange;
t.TranslateX = this.cumulativeDeltaX;
t.TranslateY = this.cumulativeDeltaY;
// Ensure that this piece is on top of all others
this.topmostZIndex++;
Canvas.SetZIndex(piece, this.topmostZIndex);
// Ensure that the puzzle is no longer in the solved state
ShowAsUnsolved();
// If the piece is not on the tray, snap it to a solved horizontal
// and/or vertical boundary if it’s close enough
double left = GetPieceLeft(piece);
double top = GetPieceTop(piece);
if (top > Constants.ON_TRAY_Y)
return; // The piece is on the tray, so never mind
// Snapping to a horizontal boundary
if (left % Constants.PIECE_WIDTH < Constants.SNAPPING_MARGIN)
t.TranslateX -= left % Constants.PIECE_WIDTH;
else if (left % Constants.PIECE_WIDTH >
Constants.PIECE_WIDTH – Constants.SNAPPING_MARGIN)
t.TranslateX += Constants.PIECE_WIDTH – left % Constants.PIECE_WIDTH;
// Snapping to a vertical boundary
if (top % Constants.PIECE_HEIGHT < Constants.SNAPPING_MARGIN)
t.TranslateY -= top % Constants.PIECE_HEIGHT;
else if (top % Constants.PIECE_HEIGHT >
Constants.PIECE_HEIGHT – Constants.SNAPPING_MARGIN)
t.TranslateY += Constants.PIECE_HEIGHT – top % Constants.PIECE_HEIGHT;
}
}
void GestureListener_DragCompleted(object sender,
DragCompletedGestureEventArgs e)
{
// Give the tray an extra push (simulating inertia) based on
// the final dragging horizontal velocity
if (this.isDraggingTray && e.HorizontalVelocity != 0)
ScrollTray(e.HorizontalVelocity / 10);
this.isDraggingTray = this.isDraggingPiece = false;
if (AreAllPiecesCorrect())
ShowAsSolved();
}
FrameworkElement GetPieceFromDraggedSource(FrameworkElement source)
{
// When a piece is dragged, the source is the path,
// but we want to return its parent canvas
if (source == null || source.Parent == null ||
(source.Parent as FrameworkElement).Parent == null ||
(source.Parent as FrameworkElement).Parent != this.PiecesCanvas)
return null;
else
return source.Parent as FrameworkElement;
}
double GetPieceTop(FrameworkElement piece)
{
return Canvas.GetTop(piece) +
(piece.RenderTransform as CompositeTransform).TranslateY;
}
double GetPieceLeft(FrameworkElement piece)
{
return Canvas.GetLeft(piece) +
(piece.RenderTransform as CompositeTransform).TranslateX;
}
void ScrollTray(double amount)
{
// Retrieve the minimum and maximum horizontal positions among all
// pieces in the tray, to provide bounds on how far it can scroll
double minX = double.MaxValue;
double maxX = double.MinValue;
this.piecesOnTray.Clear();
foreach (FrameworkElement piece in this.PiecesCanvas.Children)
{
if (GetPieceTop(piece) > Constants.ON_TRAY_Y)
{
this.piecesOnTray.Add(piece);
double left = GetPieceLeft(piece);
if (left < minX) minX = left;
if (left > maxX) maxX = left;
}
}
if (this.piecesOnTray.Count == 0)
return;
// Change the amount if it would make the tray scroll too far
if (amount < 0 && (maxX + amount < this.ActualWidth –
Constants.MAX_PIECE_WIDTH || minX < Constants.NEGATIVE_SCROLL_BOUNDARY))
amount = Math.Max(-maxX + this.ActualWidth – Constants.MAX_PIECE_WIDTH,
Constants.NEGATIVE_SCROLL_BOUNDARY – minX);
if (amount > 0 && minX + amount > Constants.TRAY_LEFT_MARGIN)
amount = Constants.TRAY_LEFT_MARGIN – minX;
// “Scroll” the tray by moving each piece on the tray the same amount
foreach (FrameworkElement piece in this.piecesOnTray)
(piece.RenderTransform as CompositeTransform).TranslateX += amount;
}
// Move each piece to the tray in a random order
void StartOver()
{
// Copy the children to an array so their order
// in the collection is preserved
UIElement[] pieces = this.PiecesCanvas.Children.ToArray();
// Shuffle the children in place
for (int i = pieces.Length – 1; i > 0; i–)
{
int r = this.random.Next(0, i);
// Swap the current child with the randomly-chosen one
UIElement temp = pieces[i]; pieces[i] = pieces[r]; pieces[r] = temp;
}
// Now move the pieces to the bottom in their random order
for (int i = 0; i < pieces.Length; i++)
{
UIElement piece = pieces[i];
// Alternate the pieces between two rows
CreatePieceMovingStoryboard(piece, TimeSpan.Zero, TimeSpan.FromSeconds(1),
(i % 2 * Constants.TRAY_2ND_ROW_HORIZONTAL_OFFSET) +
(i / 2) * Constants.TRAY_HORIZONTAL_SPACING – Canvas.GetLeft(piece),
(i % 2 * Constants.TRAY_VERTICAL_SPACING) + Constants.TRAY_TOP_MARGIN
– Canvas.GetTop(piece)).Begin();
// Reset the z-index of each piece
Canvas.SetZIndex(piece, 0);
}
this.topmostZIndex = 0;
ShowAsUnsolved();
The Main Page 925
}
// Create a storyboard that animates the piece to the specified position
Storyboard CreatePieceMovingStoryboard(UIElement piece, TimeSpan beginTime,
TimeSpan duration, double finalX, double finalY)
{
DoubleAnimation xAnimation = new DoubleAnimation { To = finalX,
Duration = duration, EasingFunction = new QuinticEase() };
DoubleAnimation yAnimation = new DoubleAnimation { To = finalY,
Duration = duration, EasingFunction = new QuinticEase() };
Storyboard.SetTargetProperty(xAnimation, new PropertyPath(“TranslateX”));
Storyboard.SetTargetProperty(yAnimation, new PropertyPath(“TranslateY”));
Storyboard storyboard = new Storyboard { BeginTime = beginTime };
Storyboard.SetTarget(storyboard, piece.RenderTransform);
storyboard.Children.Add(xAnimation);
storyboard.Children.Add(yAnimation);
return storyboard;
}
bool AreAllPiecesCorrect()
{
for (int i = 0; i < this.PiecesCanvas.Children.Count; i++)
{
UIElement piece = this.PiecesCanvas.Children[i];
CompositeTransform t = piece.RenderTransform as CompositeTransform;
if (t.TranslateX != 0 || t.TranslateY != 0)
return false; // This piece is in the wrong place
}
// All pieces are in the right place
return true;
}
void ShowAsSolved()
{
this.solveButton.IsEnabled = false;
int piecesToMove = 0;
Storyboard storyboard = null;
// For any piece that’s out of place, animate it to the solved position
for (int i = 0; i < this.PiecesCanvas.Children.Count; i++)
{
UIElement piece = this.PiecesCanvas.Children[i];
CompositeTransform t = piece.RenderTransform as CompositeTransform;
if (t.TranslateX == 0 && t.TranslateY == 0)
continue; // This piece is already in the right place
// Animate it to a (0,0) offset, which is its natural position
storyboard = CreatePieceMovingStoryboard(piece,
TimeSpan.FromSeconds(.3 * piecesToMove), // Spread out the animations
TimeSpan.FromSeconds(1), 0, 0);
storyboard.Begin();
// Ensure each piece moves on top of pieces already in the right place
this.topmostZIndex++;
Canvas.SetZIndex(piece, this.topmostZIndex);
piecesToMove++;
}
if (storyboard == null)
{
// Everything is in the right place
this.CompleteImage.Visibility = Visibility.Visible;
}
else
{
// Delay the showing of CompleteImage until the last storyboard
// has completed
storyboard.Completed += delegate(object sender, EventArgs e)
{
// Ensure that the user didn’t unsolve the puzzle during the animation
if (!this.solveButton.IsEnabled)
this.CompleteImage.Visibility = Visibility.Visible;
};
}
}
void ShowAsUnsolved()
{
this.solveButton.IsEnabled = true;
this.CompleteImage.Visibility = Visibility.Collapsed;
}
void RefreshPuzzleImage()
{
ImageSource imageSource = null;
// Choose the right image based on the setting
switch (Settings.PhotoIndex.Value)
{
// The first case is for a custom photo saved
// from CroppedPictureChooserPage
case -1:
try { imageSource = IsolatedStorageHelper.LoadFile(“custom.jpg”); }
catch { imageSource = new BitmapImage(new Uri(“Images/catAndFish.jpg”,
UriKind.Relative)); }
break;
// The remaining cases match the indices in the application bar menu
case 0:
imageSource = new BitmapImage(new Uri(“Images/catAndFish.jpg”,
UriKind.Relative));
break;
case 1:
imageSource = new BitmapImage(new Uri(“Images/city.jpg”,
UriKind.Relative));
break;
case 2:
imageSource = new BitmapImage(new Uri(“Images/statueOfLiberty.jpg”,
UriKind.Relative));
break;
case 3:
imageSource = new BitmapImage(new Uri(“Images/traffic.jpg”,
UriKind.Relative));
break;
case 4:
imageSource = new BitmapImage(new Uri(“Images/underWater.jpg”,
UriKind.Relative));
break;
}
if (imageSource != null)
{
this.CompleteImage.Source = imageSource;
// Each of the 30 pieces needs to be filled with the right image
foreach (Canvas piece in this.PiecesCanvas.Children)
((piece.Children[0] as Shape).Fill as ImageBrush).ImageSource =
imageSource;
}
}
// Application bar handlers
void PictureButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/CroppedPictureChooserPage.xaml”,
UriKind.Relative));
}
void StartOverButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to dismantle the puzzle and “ +
“start from scratch?”, “Start over”, MessageBoxButton.OKCancel)
== MessageBoxResult.OK)
StartOver();
}
void SolveButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Do you give up? Are you sure you want the puzzle to “
+ “be solved for you?”, “Solve”, MessageBoxButton.OKCancel)
!= MessageBoxResult.OK)
return;
ShowAsSolved();
}
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void ApplicationBarMenuItem_Click(object sender, EventArgs e)
{
for (int i = 0; i < this.ApplicationBar.MenuItems.Count; i++)
{
// Set the persisted photo index to match the menu item index
if (sender == this.ApplicationBar.MenuItems[i])
Settings.PhotoIndex.Value = i;
}
RefreshPuzzleImage();
}
}
}

[/code]

  • This app uses two settings defined in a separate Settings.cs file for remembering the user’s chosen photo and for remembering the position of every puzzle piece:

    [code]
    public static class Settings
    {
    public static Setting<int> PhotoIndex = new Setting<int>(“PhotoIndex”, 2);
    public static Setting<List<Point>> PieceOffsets =
    new Setting<List<Point>>(“PieceOffsets”, new List<Point>());
    }
    [/code]

  • This app also uses many constants, defined as follows in a Constants.cs file:

    [code]
    public static class Constants
    {
    public const int PUZZLE_WIDTH = 480;
    public const int PUZZLE_HEIGHT = 576;
    public const int PIECE_WIDTH = 96;
    public const int PIECE_HEIGHT = 96;
    public const int MAX_PIECE_WIDTH = 162;
    public const int SNAPPING_MARGIN = 15;
    public const int NEGATIVE_SCROLL_BOUNDARY = -1550;
    public const int TRAY_HORIZONTAL_SPACING = 110;
    public const int TRAY_VERTICAL_SPACING = 80;
    public const int TRAY_LEFT_MARGIN = 24;
    public const int TRAY_TOP_MARGIN = 590;
    public const int TRAY_2ND_ROW_HORIZONTAL_OFFSET = 50;
    public const int ON_TRAY_Y = 528;
    }
    [/code]

  • The first time Jigsaw Puzzle is run, the pieces animate from their solved positions to a random ordering on the tray. (This condition is detected in OnNavigatedTo because the PieceOffsets list does not initially contain the same number of elements as pieces in PiecesCanvas.) This ordering of pieces on the tray is shown in Figure 42.4. Every other time, the pieces are placed exactly where they were previously left by reapplying their persisted TranslateX and TranslateY values.
The puzzle pieces are arranged on the tray once they animate away from their solved positions.
FIGURE 42.4 The puzzle pieces are arranged on the tray once they animate away from their solved positions.
  • The three drag event handlers act differently depending on whether a puzzle piece is being dragged or the tray is being dragged. When the tray is dragged horizontally, we want it to scroll and reveal off-screen pieces. When a piece is dragged, it should move wherever the user’s finger takes it.
  • The DragStarted event handler (GestureListener_DragStarted) determines which type of dragging is occurring and sets either isDraggingTray or isDraggingPiece. (This handler can be called in cases where neither is true, such as dragging on an empty upper part of the screen, because these handlers are attached to the whole page.)

    DragStarted isn’t raised as soon as a finger touches the screen and starts moving; the gesture listener waits for the finger to move more than 12 pixels away to ensure that the gesture is a drag rather than a tap, and to determine the primary direction of the drag. DragStartedGestureEventArgs exposes this primary direction as a Direction property that is either Horizontal or Vertical.

    GestureListener_DragStarted leverages the Direction property to determine which kind of drag is happening. If the element reporting the event is the tray and the direction is horizontal, then it considers the gesture to be a tray drag. If a piece is being dragged horizontally and the vertical position of the piece visually makes it look like it’s on the tray, it also considers the gesture to be a tray drag. This is important to avoid the requirement that the tray can only be dragged on an empty spot. If a piece is being dragged vertically, or if it’s dragged in any direction far enough from the tray, then it’s considered to be a piece drag.

    Although this scheme is easy to implement, users might find the requirement to drag pieces off the tray in a mostly vertical fashion to be confusing and/or inconvenient. A more flexible approach would be to perform your own math and use a wider angle range.

The Direction property passed to drag events never changes until a new drag is initiated!

Although the Direction property exposed to DragStarted handlers is also exposed to DragDelta and DragCompleted handlers, its value never changes until the drag has completed and a new drag has started.This is true even if the actual direction of the finger motion changes to be completely vertical instead of horizontal, or vice versa.This makes it easy to implement panning or other motion that is locked to one axis, although it also means that detecting more flexible motion requires you to interpret the finger motion manually.

In Jigsaw Puzzle, this fact can cause frustration if a user tries to drag a piece from the tray directly to its final position, yet the straight-line path is more horizontal than it is vertical.To help combat this, the instructions page explains that pieces must be dragged upward to leave the tray.

  • The DragDelta event exposes two more properties than DragStarted: HorizontalChange and VerticalChange. For tray dragging, the HorizontalChange value is passed to a ScrollTray helper method. This method provides the illusion of scrolling by manually updating the horizontal position of every piece whose vertical position makes it appear to be on the tray. Keeping all the pieces in the same canvas at all times (instead of moving pieces on the tray to a separate panel inside an actual scroll viewer) makes the logic throughout this page easier.

    For piece dragging, both HorizontalChange and VerticalChange are applied to the current piece’s transform, and then the piece is snapped to one or two solved-piece boundary locations if it’s close enough to a horizontal and/or vertical boundary. Ordinarily, the values of HorizontalChange and VerticalChange would be directly added to TranslateX and TranslateY, respectively, but this doesn’t work well when snapping is done. Because each snap moves the piece by as much as 14 pixels away from its natural position, continued snapping would cause the piece to drift further away from the user’s finger if we continued to add the HorizontalChange and VerticalChange values. Instead, by manually tracking the total cumulative distance from the beginning of the drag, the piece is returned to its natural position after it breaks free of a snapping boundary.

The HorizontalChange and VerticalChange properties exposed by DragDelta are relative to the previous raising of DragDelta!

Unlike PinchGestureEventArgs, whose DistanceRatio and TotalAngleDelta properties are relative to the values when pinching or stretching started, the HorizontalChange and VerticalChange properties exposed by DragDeltaGestureEventArgs and DragCompletedGestureEventArgs do not accumulate as dragging proceeds.

  • The DragCompleted event exposes all the properties from DragDelta plus two more: HorizontalVelocity and VerticalVelocity. These values are the same ones exposed to the Flick event, and enable inertial flicking motion at the end of a drag. Just like in Chapter 40, “Darts,” the velocity is scaled down and then used to continue the dragging motion a bit. This is done for tray dragging only, to make it better mimic a real scroll viewer. Therefore, only the horizontal component of the velocity is used. At the end of every drag action, the location of each piece is checked to see whether the puzzle has been solved. We know that the pieces are all in the correct spots if their transforms all have TranslateX and TranslateY values of zero.
  • The “picture” and “instructions” button click handlers navigate to other pages, and the “start over” and “solve” button click handlers trigger animations that either move the pieces to random spots on the tray or to their solved positions. The solve animation is performed by the ShowAsSolved method, which animates each outof- place piece to its correct position over the course of one second, spaced .3 seconds apart. The resulting effect smoothly fills in the pieces in row major order, as pictured in Figure 42.5.
The automatic solving animation makes the pieces float into place according to their order in the canvas.
FIGURE 42.5 The automatic solving animation makes the pieces float into place according to their order in the canvas.

The Cropped Picture Chooser Page

Because the photo chooser can sometimes be slow to launch, and because decoding the chosen picture can be slow, this page shows a “loading” message during these actions. Figure 42.6 demonstrates the user flow through this page.

 The sequence of events when navigating to the cropped photo chooser page.
FIGURE 42.6 The sequence of events when navigating to the cropped photo chooser page.

The User Interface

Listing 42.3 contains the XAML for this page.

LISTING 42.3 CroppedPictureChooserPage.xaml—The User Interface for Jigsaw Puzzle’s Cropped Picture Chooser Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.CroppedPictureChooserPage”
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:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Portrait”>
<!– The 2-button application bar, shown on return from the PhotoChooserTask –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”0” IsVisible=”False” ForegroundColor=”White”>
<shell:ApplicationBarIconButton Text=”done”
IconUri=”/Shared/Images/appbar.done.png” Click=”DoneButton_Click”/>
<shell:ApplicationBarIconButton Text=”cancel”
IconUri=”/Shared/Images/appbar.cancel.png” Click=”CancelButton_Click”/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<!– Listen for drag events anywhere on the page –>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener DragDelta=”GestureListener_DragDelta”
PinchStarted=”GestureListener_PinchStarted”
PinchDelta=”GestureListener_PinchDelta”/>
</toolkit:GestureService.GestureListener>
<!– Prevent a zoomed-in photo from making the screen go blank –>
<phone:PhoneApplicationPage.Clip>
<RectangleGeometry Rect=”0,0,480,800”/>
</phone:PhoneApplicationPage.Clip>
<Canvas Background=”#443225”>
<!– Shown on return from the PhotoChooserTask –>
<Canvas x:Name=”CropPanel” Visibility=”Collapsed”>
<!– Designate the puzzle boundary –>
<Rectangle Fill=”White” Canvas.Top=”112” Width=”480” Height=”576”/>
<!– The canvas provides screen-centered zooming –>
<Canvas Canvas.Top=”112” Width=”480” Height=”576”
RenderTransformOrigin=”.5,.5”>
<Canvas.RenderTransform>
<!– For zooming –>
<CompositeTransform x:Name=”CanvasTransform”/>
</Canvas.RenderTransform>
<Image x:Name=”Image” Stretch=”None” CacheMode=”BitmapCache”>
<Image.RenderTransform>
<!– For panning –>
<CompositeTransform x:Name=”ImageTransform”/>
</Image.RenderTransform>
</Image>
</Canvas>
<!– Top and bottom borders that let the image show through slightly –>
<Rectangle Opacity=”.8” Fill=”#443225” Width=”480” Height=”112”/>
<Rectangle Canvas.Top=”688” Opacity=”.8” Fill=”#443225” Width=”480”
Height=”112”/>
<!– The title and instructions –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”CROP PICTURE” Foreground=”White”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Foreground=”White” TextWrapping=”Wrap” Width=”432”>
Pinch &amp; stretch your fingers to zoom.<LineBreak/>
Drag to move the picture.
</TextBlock>
</StackPanel>
</Canvas>
<!– Shown while launching the PhotoChooserTask –>
<Canvas x:Name=”LoadingPanel”>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock x:Name=”LoadingTextBlock” Text=”LOADING…” Foreground=”White”
Style=”{StaticResource PhoneTextTitle0Style}” Width=”432”
TextWrapping=”Wrap”/>
</StackPanel>
</Canvas>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

  • Drag gestures (and pinch/stretch gestures) are detected with a page-level gesture listener, just like on the main page.
  • The white rectangle not only reveals the puzzle’s dimensions if the picture doesn’t completely cover the area, but it also ends up giving the puzzle a white background when this happens. This is demonstrated in Figure 42.7.
  • Two different transforms are used to make the user’s gestures feel natural. Whereas drag gestures adjust ImageTransform much like the dragging of puzzle pieces on the main page, pinch and stretch gestures are applied to CanvasTransform. Because the canvas represents the puzzle area and it’s marked with a centered render transform origin of (.5,.5), the user’s gesture always zooms the image centered around the middle of the puzzle. If this were applied to the image instead, zooming would occur around the image’s middle, which might be far off-screen as the image gets zoomed and panned.
  • Two subtle things on this page prevent its performance from being disastrous. The bitmap caching on the image makes the panning and zooming much smoother than it would be otherwise, as does the clip applied to the page. (The page’s clip also prevents the entire screen from going blank if the photo is zoomed in to an extreme amount, caused by Silverlight failing to render a surface that is too big.)
A white rectangle serves as the puzzle’s background if the picture doesn’t completely cover it.
FIGURE 42.7 A white rectangle serves as the puzzle’s background if the picture doesn’t completely cover it.

If an image has transparent regions, those regions are not hit-testable when used to fill a shape!

This app ensures that the picture used to fill the puzzle pieces never contains any transparency. Those transparent regions would not respond to any touch events,making it difficult or impossible to drag the affected pieces.Note that this is different behavior compared to giving an otherwise- rectangular element transparent regions with an opacity mask. With an opacity mask, the transparent regions are still hit-testable (just like an element marked with an opacity of 0).

The Code-Behind

Listing 42.4 contains the code-behind for this page.

LISTING 42.4 CroppedPictureChooserPage.xaml.cs—The Code-Behind for Jigsaw Puzzle’s Cropped Picture Chooser Page

[code]

using System;
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Microsoft.Phone;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Tasks;
namespace WindowsPhoneApp
{
public partial class CroppedPictureChooserPage : PhoneApplicationPage
{
bool loaded;
double scaleWhenPinchStarted;
public CroppedPictureChooserPage()
{
InitializeComponent();
this.Loaded += CroppedPictureChooserPage_Loaded;
}
void CroppedPictureChooserPage_Loaded(object sender, RoutedEventArgs e)
{
if (this.loaded)
return;
this.loaded = true;
// When navigating to this page in the forward direction only (from the
// main page or when reactiving the app), launch the photo chooser task
Microsoft.Phone.Tasks.PhotoChooserTask task = new PhotoChooserTask();
task.ShowCamera = true;
task.Completed += delegate(object s, PhotoResult args)
{
if (args.TaskResult == TaskResult.OK)
{
WriteableBitmap imageSource = PictureDecoder.DecodeJpeg(
args.ChosenPhoto);
// Perform manual “uniform to fill” scaling by choosing the larger
// of the two scales that make the image just fit in one dimension
double scale = Math.Max(
(double)Constants.PUZZLE_WIDTH / imageSource.PixelWidth,
(double)Constants.PUZZLE_HEIGHT / imageSource.PixelHeight);
this.CanvasTransform.ScaleX = this.CanvasTransform.ScaleY = scale;
// Center the image in the puzzle
this.ImageTransform.TranslateY =
-(imageSource.PixelHeight – Constants.PUZZLE_HEIGHT) / 2;
this.ImageTransform.TranslateX =
-(imageSource.PixelWidth – Constants.PUZZLE_WIDTH) / 2;
// Show the cropping user interface
this.Image.Source = imageSource;
this.LoadingPanel.Visibility = Visibility.Collapsed;
this.CropPanel.Visibility = Visibility.Visible;
this.ApplicationBar.IsVisible = true;
}
else
{
// The user cancelled from the photo chooser, but we can’t automatically
// navigate back right here, so update “LOADING…” with instructions
this.LoadingTextBlock.Text =
“Press the Back button again to return to the puzzle.”;
}
};
task.Show();
}
// Raised for single-finger dragging
void GestureListener_DragDelta(object sender, DragDeltaGestureEventArgs e)
{
// Pan the image based on the drag
this.ImageTransform.TranslateX +=
e.HorizontalChange / this.CanvasTransform.ScaleX;
this.ImageTransform.TranslateY +=
e.VerticalChange / this.CanvasTransform.ScaleY;
}
// Raised when two fingers touch the screen (likely to begin a pinch/stretch)
void GestureListener_PinchStarted(object sender,
PinchStartedGestureEventArgs e)
{
this.scaleWhenPinchStarted = this.CanvasTransform.ScaleX;
}
// Raised continually as either or both fingers move
void GestureListener_PinchDelta(object sender, PinchGestureEventArgs e)
{
// The distance ratio is always relative to when the pinch/stretch started,
// so be sure to apply it to the ORIGINAL zoom level, not the CURRENT
double scale = this.scaleWhenPinchStarted * e.DistanceRatio;
this.CanvasTransform.ScaleX = this.CanvasTransform.ScaleY = scale;
}
// Application bar handlers
void DoneButton_Click(object sender, EventArgs e)
{
// Create a new bitmap with the puzzle’s dimensions
WriteableBitmap wb = new WriteableBitmap(Constants.PUZZLE_WIDTH,
Constants.PUZZLE_HEIGHT);
// Render the page’s contents to the puzzle, but shift it upward
// so only the region intended to be for the puzzle is used
wb.Render(this, new TranslateTransform { Y = -112 });
// We must explicitly tell the bitmap to draw its new contents
wb.Invalidate();
using (MemoryStream stream = new MemoryStream())
{
// Fill the stream with a JPEG representation of this bitmap
wb.SaveJpeg(stream, Constants.PUZZLE_WIDTH, Constants.PUZZLE_HEIGHT,
0 /* orientation */, 100 /* quality */);
// Seek back to the beginning of the stream
stream.Seek(0, SeekOrigin.Begin);
// Save the file to isolated storage.
// This overwrites the file if it already exists.
IsolatedStorageHelper.SaveFile(“custom.jpg”, stream);
}
// Indicate that the user has chosen to use a custom image
Settings.PhotoIndex.Value = -1;
// Return to the puzzle
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
void CancelButton_Click(object sender, EventArgs e)
{
// Don’t do anything, just return to the puzzle
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
}
}

[/code]

  • Inside DragDelta, the HorizontalChange and VerticalChange values are directly added to the image transform each time; although they must be divided by any scale applied to the parent canvas so the distance that the image travels remains consistent. This direct application works well because the TranslateX and TranslateY are not changed through any other means, such as the snapping logic used on the main page.

    The PinchStarted handler records the scale when two fingers touch the screen, arbitrarily choosing ScaleX because both ScaleX and ScaleY are always set to the same value. The PinchDelta handler multiplies the initial scale by the finger distance ratio and then applies it to the canvas transform.

  • The handler for the done button’s click event, DoneButton_Click, leverages WriteableBitmap’s killer feature—the ability to capture the contents of any element and write it to a JPEG file. A new WriteableBitmap is created with the puzzle’s dimensions, and then this (the entire page) is rendered into it with a transform that shifts it 112 pixels upward. This is necessary to avoid rendering the page’s header into the captured image. Figure 42.8 demonstrates what happens if null is passed for the second parameter of Render.

    After refreshing the bitmap with a call to Invalidate, SaveJpeg writes the contents in JPEG format to a memory stream which can then be written to isolated storage.

If the page is rendered to the puzzle image from its top-left corner, the page header becomes part of the puzzle!
FIGURE 42.8 If the page is rendered to the puzzle image from its top-left corner, the page header becomes part of the puzzle!

If you want to take a screenshot of your app for your marketplace submission on a real phone rather than the emulator, you can temporarily put code in your page that captures the screen and saves it to your pictures library as follows:

[code]

void CaptureScreen()
{
// Create a new bitmap with the page’s dimensions
WriteableBitmap wb = new WriteableBitmap((int)this.ActualWidth,
(int)this.ActualHeight);
// Render the page’s contents with no transform applied
wb.Render(this, null);
// We must explicitly tell the bitmap to draw its new contents
wb.Invalidate();
using (MemoryStream stream = new MemoryStream())
{
// Fill the stream with a JPEG representation of this bitmap
wb.SaveJpeg(stream, (int)this.ActualWidth, (int)this.ActualHeight,
0 /* orientation */, 100 /* quality */);
// Seek back to the beginning of the stream
stream.Seek(0, SeekOrigin.Begin);
// Requires referencing Microsoft.Xna.Framework.dll
// and the ID_CAP_MEDIALIB capability, and only works
// when the phone is not connected to Zune
new Microsoft.Xna.Framework.Media.MediaLibrary().SavePicture(
“screenshot.jpg”, stream);
}
}

[/code]

Once there, you can sync it to your desktop with Zune to retrieve the photo in its full resolution. This has a few important limitations, however:

  • It doesn’t capture parts of the user interface outside of the Silverlight visual tree—the application bar, status bar, and message boxes.
  • It doesn’t capture any popups, even ones that are attached to an element on the page.
  • It doesn’t capture any WebBrowser instances.
  • It doesn’t capture any MediaElement instances.

Also, you need to determine a way to invoke this code without impacting what you’re capturing.

Listing 42.4 doesn’t make any attempt to preserve the page’s state in the face of deactivation and reactivation; it simply relaunches the photo chooser.There are a few strategies for preserving the state of this page.The most natural would be to persist the image to a separate temporary file in isolated storage, along with values in page state that remember the current zoom and panning values.

What’s the difference between detecting dragging with gesture listener drag events versus using mouse down, mouse move, and mouse up events?

One major difference is that the drag events are only raised when one finger is in contact with the screen. With the mouse events (or with the multi-touch FrameReported event), you can base dragging on the primary finger and simply ignore additional touch points.This may or may not be a good thing, depending on your app. Because the preceding chapter uses the mouse events for panning, it gives the user the ability to do zooming and panning as a combined gesture, which mimics the behavior of the built-in Maps app. In Jigsaw Puzzle’s cropped photo chooser page, on the other hand, the user must lift their second finger if they wish to pan right after zooming the picture.

Another difference is that the drag events are not raised until the gesture listener is sure that a drag is occurring, e.g. one finger has made contact with the screen and has already moved a little bit. In contrast, the mouse move event is raised as soon as the finger moves at all. For Jigsaw Puzzle, the delayed behavior of the drag events is beneficial for helping to avoid accidental dragging.

A clear benefit of the drag events, if applicable to your app, is that the finger velocity at the end of the gesture is exposed to your code.However, you could still get this information when using the mouse approach if you also attach a handler to gesture listener’s Flick event.The Direction property exposed to the drag events, discussed earlier, also enables interesting behavior that is tedious to replicate on your own.

The Finished Product

Windows Phone Jigsaw Puzzle (Drag Gesture & WriteableBitmap)
Jigsaw Puzzle (Drag Gesture & WriteableBitmap)

Deep Zoom Viewer (Pinch, Stretch, & Double Tap Gestures)

Deep Zoom is a slick technology for creating, viewing, and manipulating huge images or collections of images. It can be used to create experiences much like Bing Maps or Google Maps, but applied to any domain. With the samples available from this app, you can explore large panoramic photographs, scanned-in artwork, a computer-generated data visualization, an example of what a deep zoom advertisement might look like, and, yes, Earth.

To maximize performance, Deep Zoom images are multiresolution; the image file format includes many separate subimages—called tiles—at multiple zoom levels. Tiles are downloaded on-demand and rendered in a fairly seamless fashion with smooth transitions. For end users, the result is a huge image that can be loaded, zoomed, and panned extremely quickly.

Deep Zoom Viewer enables viewing and interacting with any online Deep Zoom image right on your Windows phone. You can enter a URL that points to any Deep Zoom image (or image collection), or you can browse any of the seven interesting samples that are already provided.

To render a Deep Zoom image, this app leverages Silverlight’s MultiScaleImage control, which does all the hard work. To view a file, you just need to place a MultiScaleImage on a page and then set its Source property to an appropriate URL. However, the control does not provide any built-in gestures for manipulating the image. Therefore, this app provides a perfect opportunity to demonstrate how to implement pinch-&-stretch zooming and double-tap gestures—practically a requirement for any respectable Deep Zoom viewer.

Pinching is the standard zoom-out gesture that involves placing two fingers on the screen and then sliding them toward each other. Stretching is the standard zoom-in gesture that involves placing two fingers on the screen and then sliding them away from each other. In this app, double tapping is used to quickly zoom in, centered on the point that was tapped.

Windows Phone style guidelines dictate that touch gestures should only be used for their intended purposes. As in Deep Zoom Viewer, a pinch should always zoom out, a stretch should always zoom in, and a double tap should always perform some kind of zoom in and/or zoom out.

How do I create my own Deep Zoom images?

Currently, the quickest and easiest way is to use Microsoft’s free Zoom.it service (http://zoom.it).This turns any JPG, PNG, or TIFF image into a Deep Zoom image.The service also supports SVG files, PDF files, and even web pages as input! You just enter an appropriate URL, and it does the conversion. It even hosts the file for you! Alternatively, the most powerful option is to use Microsoft’s Deep Zoom Composer, a free program that can be downloaded at http://bit.ly/deepzoomdownload.

Deep Zoom versus Seadragon

Although Deep Zoom refers to a Silverlight-specific feature, the underlying Seadragon technology (which Microsoft originally acquired from a company called Seadragon Software) has been exposed in other forms. For example,Microsoft has released an open-source JavaScript version called “Seadragon Ajax” in its Ajax Control Toolkit. It can view the same file types as Deep Zoom.

The User Interface

Deep Zoom Viewer is a single-page app (except for an instructions page) that dedicates all of its screen real estate to the MultiScaleImage control. On top of this, it layers a translucent application bar and a dialog that enables the user to type arbitrary Deep Zoom image URLs. Figure 41.1 shows the main page with its application bar menu expanded, and Figure 41.2 shows the main page with its dialog showing. The XAML for this page is in Listing 41.1.

The application bar menu is expanded on top of the Carina Nebula.
FIGURE 41.1 The application bar menu is expanded on top of the Carina Nebula.
Entering a custom URL is done via a dialog that appears on top of the current Deep Zoom image.
FIGURE 41.2 Entering a custom URL is done via a dialog that appears on top of the current Deep Zoom image.

LISTING 41.1 MainPage.xaml—The User Interface for Deep Zoom Viewers’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:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape”>
<!– The application bar –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.5”>
<shell:ApplicationBarIconButton Text=”fit to screen”
IconUri=”/Images/appbar.fitToScreen.png”
Click=”FitToScreenButton_Click”/>
<shell:ApplicationBarIconButton Text=”zoom in”
IconUri=”/Shared/Images/appbar.plus.png”
Click=”ZoomInButton_Click”/>
<shell:ApplicationBarIconButton Text=”zoom out”
IconUri=”/Shared/Images/appbar.minus.png”
Click=”ZoomOutButton_Click”/>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”[enter url]”
Click=”CustomUrlMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<!– The Deep Zoom image –>
<MultiScaleImage x:Name=”DeepZoomImage”>
<!– Attach the gesture listener to this element –>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener DoubleTap=”GestureListener_DoubleTap”
PinchStarted=”GestureListener_PinchStarted”
PinchDelta=”GestureListener_PinchDelta”/>
</toolkit:GestureService.GestureListener>
</MultiScaleImage>
<!– Show a progress bar while loading an image –>
<ProgressBar x:Name=”ProgressBar” Visibility=”Collapsed”/>
<!– A dialog for entering a URL –>
<local:Dialog x:Name=”CustomFileDialog” Closed=”CustomFileDialog_Closed”>
<local:Dialog.InnerContent>
<StackPanel>
<TextBlock Text=”Enter the URL of a Deep Zoom file” Margin=”11,5,0,-5”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBox InputScope=”Url” Text=”{Binding Result, Mode=TwoWay}”/>
</StackPanel>
</local:Dialog.InnerContent>
</local:Dialog>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • A gesture listener from the Silverlight for Windows Phone Toolkit is attached to the MultiScaleImage control, so we can very easily detect double taps and pinch/stretch gestures.
  • The MultiScaleImage control has a lot of automatic functionality to make the viewing experience as smooth as possible. For example, as tiles are downloaded, they are smoothly blended in with a blurry-to-crisp transition, captured in Figure 41.3.
You can occasionally catch pieces of the view starting out blurry and then seamlessly becoming crisp.
FIGURE 41.3 You can occasionally catch pieces of the view starting out blurry and then seamlessly becoming crisp.

The Code-Behind

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

LISTING 41.2 MainPage.xaml.cs—The Code-Behind for Deep Zoom Viewers’Main Page

[code]

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Persistent settings
Setting<Uri> savedImageUri = new Setting<Uri>(“ImageUri”,
new Uri(Data.BaseUri, “last-fm.dzi”));
Setting<Point> savedViewportOrigin = new Setting<Point>(“ViewportOrigin”,
new Point(0, -.2));
Setting<double> savedZoom = new Setting<double>(“Zoom”, 1);
// Used by pinch and stretch
double zoomWhenPinchStarted;
// Used by panning and double-tapping
Point mouseDownPoint = new Point();
Point mouseDownViewportOrigin = new Point();
public MainPage()
{
InitializeComponent();
// Fill the application bar menu with the sample images
foreach (File f in Data.Files)
{
ApplicationBarMenuItem item = new ApplicationBarMenuItem(f.Title);
// This assignment is needed so each anonymous method gets the right value
string filename = f.Filename;
item.Click += delegate(object sender, EventArgs e)
{
OpenFile(new Uri(Data.BaseUri, filename), true);
};
this.ApplicationBar.MenuItems.Add(item);
}
// Handle success for any attempt to open a Deep Zoom image
this.DeepZoomImage.ImageOpenSucceeded +=
delegate(object sender, RoutedEventArgs e)
{
// Hide the progress bar
this.ProgressBar.Visibility = Visibility.Collapsed;
this.ProgressBar.IsIndeterminate = false; // Avoid a perf issue
// Initialize the view
this.DeepZoomImage.ViewportWidth = this.savedZoom.Value;
this.DeepZoomImage.ViewportOrigin = this.savedViewportOrigin.Value;
};
// Handle failure for any attempt to open a Deep Zoom image
this.DeepZoomImage.ImageOpenFailed +=
delegate(object sender, ExceptionRoutedEventArgs e)
{
// Hide the progress bar
this.ProgressBar.Visibility = Visibility.Collapsed;
this.ProgressBar.IsIndeterminate = false; // Avoid a perf issue
MessageBox.Show(“Unable to open “ + this.savedImageUri.Value + “.”,
“Error”, MessageBoxButton.OK);
};
// Load the previously-viewed (or default) image
OpenFile(this.savedImageUri.Value, false);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Remember settings for next time
this.savedViewportOrigin.Value = this.DeepZoomImage.ViewportOrigin;
this.savedZoom.Value = this.DeepZoomImage.ViewportWidth;
}
// Attempt to open the Deep Zoom image at the specified URI
void OpenFile(Uri uri, bool resetPosition)
{
if (resetPosition)
{
// Restore these settings to their default values
this.savedZoom.Value = this.savedZoom.DefaultValue;
this.savedViewportOrigin.Value = this.savedViewportOrigin.DefaultValue;
}
this.savedImageUri.Value = uri;
// Assign the image
this.DeepZoomImage.Source = new DeepZoomImageTileSource(uri);
// Show a temporary progress bar
this.ProgressBar.IsIndeterminate = true;
this.ProgressBar.Visibility = Visibility.Visible;
}
// Three handlers (mouse down/move/up) to implement panning
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
// Ignore if the dialog is visible
if (this.CustomFileDialog.Visibility == Visibility.Visible)
return;
this.mouseDownPoint = e.GetPosition(this.DeepZoomImage);
this.mouseDownViewportOrigin = this.DeepZoomImage.ViewportOrigin;
this.DeepZoomImage.CaptureMouse();
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
// Ignore if the dialog is visible
if (this.CustomFileDialog.Visibility == Visibility.Visible)
return;
Point p = e.GetPosition(this.DeepZoomImage);
// ViewportWidth is the absolute zoom (2 == half size, .5 == double size)
double scale = this.DeepZoomImage.ActualWidth /
this.DeepZoomImage.ViewportWidth;
// Pan the image by setting a new viewport origin based on the mouse-down
// location and the distance the primary finger has moved
this.DeepZoomImage.ViewportOrigin = new Point(
this.mouseDownViewportOrigin.X + (this.mouseDownPoint.X – p.X) / scale,
this.mouseDownViewportOrigin.Y + (this.mouseDownPoint.Y – p.Y) / scale);
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
// Stop panning
this.DeepZoomImage.ReleaseMouseCapture();
}
// The three gesture handlers for double tap, pinch, and stretch
void GestureListener_DoubleTap(object sender, GestureEventArgs e)
{
// Ignore if the dialog is visible
if (this.CustomFileDialog.Visibility == Visibility.Visible)
return;
// Zoom in by a factor of 2 centered at the place where the double tap
// occurred (the same place as the most recent MouseLeftButtonDown event)
ZoomBy(2, this.mouseDownPoint);
}
// Raised when two fingers touch the screen (likely to begin a pinch/stretch)
void GestureListener_PinchStarted(object sender,
PinchStartedGestureEventArgs e)
{
this.zoomWhenPinchStarted = this.DeepZoomImage.ViewportWidth;
}
// Raised continually as either or both fingers move
void GestureListener_PinchDelta(object sender, PinchGestureEventArgs e)
{
// Ignore if the dialog is visible
if (this.CustomFileDialog.Visibility == Visibility.Visible)
return;
// The distance ratio is always relative to when the pinch/stretch started,
// so be sure to apply it to the ORIGINAL zoom level, not the CURRENT
double zoom = this.zoomWhenPinchStarted / e.DistanceRatio;
this.DeepZoomImage.ViewportWidth = zoom;
}
void ZoomBy(double zoomFactor, Point centerPoint)
{
// Restrict how small the image can get (don’t get smaller than half size)
if (this.DeepZoomImage.ViewportWidth >= 2 && zoomFactor < 1)
return;
// Convert the on-screen point to the image’s coordinate system, which
// is (0,0) in the top-left corner and (1,1) in the bottom right corner
Point logicalCenterPoint =
this.DeepZoomImage.ElementToLogicalPoint(centerPoint);
// Perform the zoom
this.DeepZoomImage.ZoomAboutLogicalPoint(
zoomFactor, logicalCenterPoint.X, logicalCenterPoint.Y);
}
// Code for the custom file dialog
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
// If the dialog is open, close it instead of leaving the page
if (this.CustomFileDialog.Visibility == Visibility.Visible)
{
e.Cancel = true;
this.CustomFileDialog.Hide(MessageBoxResult.Cancel);
}
}
void CustomFileDialog_Closed(object sender, MessageBoxResultEventArgs e)
{
// Try to open the typed-in URL
if (e.Result == MessageBoxResult.OK && this.CustomFileDialog.Result != null)
OpenFile(new Uri(this.CustomFileDialog.Result.ToString()), true);
}
// Application bar handlers
void FitToScreenButton_Click(object sender, EventArgs e)
{
this.DeepZoomImage.ViewportWidth = 1; // Un-zoom
this.DeepZoomImage.ViewportOrigin = new Point(0, -.4); // Give a top margin
}
void ZoomInButton_Click(object sender, EventArgs e)
{
// Zoom in by 50%, keeping the current center point
ZoomBy(1.5, new Point(this.DeepZoomImage.ActualWidth / 2,
this.DeepZoomImage.ActualHeight / 2));
}
void ZoomOutButton_Click(object sender, EventArgs e)
{
// Zoom out by 50%, keeping the current center point
ZoomBy(1 / 1.5, new Point(this.DeepZoomImage.ActualWidth / 2,
this.DeepZoomImage.ActualHeight / 2));
}
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(
new Uri(“/InstructionsPage.xaml”, UriKind.Relative));
}
void CustomUrlMenuItem_Click(object sender, EventArgs e)
{
// Show the custom file dialog, initialized with the current URI
if (this.savedImageUri.Value != null)
this.CustomFileDialog.Result = this.savedImageUri.Value;
this.CustomFileDialog.Show();
}
}
}

[/code]

  • The application bar menu is filled with a list of sample files based on the following two classes defined in a separate Data.cs file:

    [code]
    public struct File
    {
    public string Title { get; set; }
    public string Filename { get; set; }
    }
    public static class Data
    {
    public static readonly Uri BaseUri =
    new Uri(“http://static.seadragon.com/content/misc/”);
    public static File[] Files = {
    new File { Title = “World-Wide Music Scene”, Filename = “last-fm.dzi” },
    new File { Title = “Carina Nebula”, Filename = “carina-nebula.dzi” },
    new File { Title = “Blue Marble”, Filename = “blue-marble.dzi” },
    new File { Title = “Contoso Fixster”, Filename = “contoso-fixster.dzi” },
    new File { Title = “Milwaukee, 1898”, Filename = “milwaukee.dzi” },
    new File { Title = “Yosemite Panorama”, Filename=“yosemite-panorama.dzi” },
    new File { Title = “Angkor Wat Temple”, Filename = “angkor-wat.dzi” }
    };
    }
    [/code]

  • When constructing the URI for each filename, BaseUri is prepended to the filename using an overloaded constructor of Uri that accepts two arguments.
  • Much like the Image element, the MultiScaleImage element is told what to render by setting its Source property. This is done inside OpenFile. Note that the type of Source is MultiScaleTileSource, an abstract class with one concrete subclass: DeepZoomImageTileSource.
  • After setting Source, the image download is asynchronous and either results in an ImageOpenSucceeded or ImageOpenFailed event being raised. This listing leverages this fact to temporarily show an indeterminate progress bar while the initial download is occurring, although this is usually extremely fast.

Can MultiScaleImage work with a local image included with the app?

Surprisingly, no! Only online files are supported.

  • The current zoom level and visible region of the image are represented by two properties: ViewportWidth and ViewportOrigin.
    • ViewportWidth is actually the inverse of the zoom level. A value of .5 means that half the width is visible. (So the zoom level is 2.) A value of 2 means that the width of the viewport is double that of the image, so the image width occupies half of the visible area.
    • ViewportOrigin is the point in the image that is currently at the top-left corner of the visible area. The point is expressed in what Deep Zoom calls logical coordinates. In this system, (0,0) is the top-left corner of the image, and (1,1) is the bottom-right corner of the image.
  • This app’s panning functionality is supported with traditional MouseLeftButtonDown, MouseMove, and MouseLeftButtonUp handlers that implement a typical drag-and-drop scheme. In MouseMove, the amount that the finger has moved since MouseLeftButtonDown is applied to the ViewportOrigin, but this value is scaled appropriately based on the control’s width (480 or 800, depending on the phone orientation) and the zoom level. This is necessary because ViewportOrigin must be set to a logical point, and it also ensures that the panning gesture doesn’t get magnified as the user zooms in.

Be sure to use a logical point when setting ViewportOrigin!

Otherwise, the image will likely pan far offscreen. Luckily, MultiScaleImage provides two handy methods—ElementToLogicalPoint and LogicalToElementPoint—for converting between logical points and element-relative points. (When the MultiScaleImage control fills the screen and has no transforms applied, as in this app, element-relative points are equivalent to points on the screen.)

  • After the three handlers that implement panning, this listing contains the three handlers for gesture listener events. The first handler (GestureListener_ DoubleTap) performs a 2x zoom each time a double tap is detected.

MultiScaleImage has built-in inertia effects whenever you change the zoom level or viewport origin, so the panning and zooming done by this app exhibit smooth and inertial transitions without any extra work. If you do not want these effects, simply set MultiScaleImage’s UseSprings property to false.

  • The next two handlers (GestureListener_PinchStarted and GestureListener_PinchDelta) handle pinching and stretching gestures. The DistanceRatio property reveals how much further apart (>1) or closer together (<1) the two fingers are, compared to when they made contact with the screen. The key to getting the appropriate effect is to apply this ratio to the original zoom level captured in the PinchStarted event handler. Normally, as with a ScaleTransform or CompositeTransform, you would multiply the original value by the ratio. Because ViewportWidth is the inverse of the zoom level, however, this listing instead divides its value by the ratio.
  • GestureListener_PinchDelta directly updates ViewportWidth rather than calling the ZoomBy method used elsewhere. ZoomBy centers the zoom around a passedin point, but MultiScaleImage doesn’t work well when the viewport is continually and rapidly moved.

The same three gesture listener events—PinchStarted, PinchDelta, and PinchCompleted—can be used to detect both pinching and stretching.The key piece of data is the DistanceRatio property on PinchGestureEventArgs, which indicates how far apart or close together the two fingers are compared to when they first touched the screen. Be careful how you use this value, however. A value greater than 1 does not necessarily mean stretching is occurring, and a value less than one does not necessarily mean pinching is occurring. For example, users could stretch their fingers until the ratio is 5 and then pinch them until the ratio goes back down to 2. As long as the ratio is continually applied to the zooming element’s original zoom level when the pinch/stretch started rather than the current zoom level, pinching and stretching will work as intended.

  • ZoomBy, used by the double-tap handler and the zooming application bar button handlers, zooms the viewport by an amount relative to the current zoom level with MultiScaleImage’s ZoomAboutLogicalPoint method.

How do I determine the center point of a pinch or stretch gesture, so I can center my zoom on that point?

Although it’s not done by this app (due to flakiness in constantly recentering the viewport), it’s common practice to center the zoom of a pinch or stretch gesture based on the midpoint between the two fingers. Although this point is not directly exposed by the gesture listener, you can calculate it as follows:

[code]

void GestureListener_PinchDelta(object sender, PinchGestureEventArgs e)
{
Point firstPoint = e.GetPosition(this, 0); // Finger #1
Point secondPoint = e.GetPosition(this, 1); // Finger #2
// Calculate the midpoint
Point pinchOrigin = new Point(
(firstPoint.X + secondPoint.X) / 2,
(firstPoint.Y + secondPoint.Y) / 2);

}

[/code]

Both PinchGestureEventArgs and PinchStartedGestureEventArgs expose an overload of GetPosition that enables passing 0 or 1 to get the point for either of the two relevant fingers. (The regular GetPosition overload always gives the data for the first, primary finger.) By continually calculating the midpoint in a PinchDelta event handler rather than once in a PinchStarted event handler, the center is continually updated as the two fingers move,which gives the best experience.

The Finished Product

Deep Zoom Viewer (Pinch, Stretch, & Double Tap Gestures)

Darts (Gesture Listener & Flick Gesture)

Darts is an addictive and slick game that provides a great reason to demonstrate support for the flick gesture (a single-finger, rapid swipe). To make detecting flicks easy, this app leverages a feature in the Silverlight for Windows Phone Toolkit called the gesture listener.

In this one-player game, you throw a dart by flicking the screen. The direction and strength of your flick determines the angle and distance of the throw. Depending on where the dart lands, you could earn 0–60 points with each throw. Try to get the highest score possible with 20 darts!

This game has a lot of potential for easy-to-add enhancements. For some examples, check out my “Hooked on Darts” version of this app in the Windows Phone Marketplace.

Detecting Gestures

The three preceding apps in this part of the book leverage touch points in a straightforward fashion. Often, however, you want to support standard gestures such as flicks, pinch-and-stretch zooming, and rotation. Detecting such gestures based on raw touch point data from the FrameReported event would be a major undertaking.

Fortunately, two options exist that make detecting gestures much easier: Silverlight’s manipulation events and the Silverlight for Windows Phone Toolkit’s gesture listener.

Manipulation Events

Silverlight defines three manipulation events on every UI element:

  • ManipulationStarted
  • ManipulationDelta
  • ManipulationCompleted

These events combine the information from all active touch points and package the data in an easy-to-consume form. ManipulationStarted gets raised when the TouchDown action happens for the first finger. ManipulationDelta gets raised for each TouchMove. ManipulationCompleted gets raised after TouchUp is reported for all fingers.

The ManipulationDelta event gives you information about how the relevant element is expected to be translated or scaled based on a one-finger panning gesture or a two-finger pinch or stretch. The ManipulationDelta and ManipulationCompleted events also provide translation and scale velocities in both X and Y dimensions.

Gesture Listener

Although the manipulation events are helpful for detecting gestures, the Silverlight for Windows Phone Toolkit’s gesture listener raises the bar for simplicity and ease-of-use. In fact, all the remaining apps in this part of the book use the gesture listener instead of the manipulation events.

The GestureListener class exposes nothing but 12 events for detecting 6 types of gestures:

  • Tap (with the Tap event)
  • Double Tap (with the DoubleTap event)
  • Touch & Hold (with the Hold event)
  • Flick (with the Flick event)
  • Pinch & Stretch (with the PinchStarted, PinchDelta, and PinchCompleted events)
  • Drag (with the DragStarted, DragDelta, and DragCompleted events)

The remaining two events— GestureBegin and GestureCompleted— are generic events raised for any gestures, analogous to the ManipulationStarted and ManipulationCompleted events.

The Tap event is similar to MouseLeftButtonUp, except that it only gets raised if there’s no movement between the finger press and release.

A gesture listener can be attached to any element with the GestureService class, which exposes a GestureListener attachable property. For example, the following XAML detects a touch & hold gesture (pressing an element for one second) on a text block:

[code]

<TextBlock …>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener Hold=”GestureListener_Hold”/>
</toolkit:GestureService.GestureListener>
</TextBlock>

[/code]

The gesture listener can cause performance problems!

As of the February 2011 version of the Silverlight for Windows Phone Toolkit, the gesture listener internally uses XNA’s TouchPanel class to detect gestures.However, this can cause performance problems and can even interfere with the input received by other Silverlight controls.Therefore, until these issues are resolved in a future version of the toolkit, you should avoid using the gesture listener (or the XNA TouchPanel) in traditional apps with typical controls.Darts and the other apps in this book that use the gesture listener do not have such problems.

The User Interface

Darts 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 40.1 contains the XAML for the main page. The main page has an “intro panel” canvas that does triple duty as a welcome screen, a game-over screen, as well as a paused screen. Underneath the intro panel is a canvas with all the game elements. Figure 40.1 shows the intro panel in its initial state and then the appearance of the page once the user flicks the screen to start the game.

The main page overlays an intro panel on top of the game canvas.
FIGURE 40.1 The main page overlays an intro panel on top of the game canvas.

LISTING 40.1 MainPage.xaml—The User Interface for Darts’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:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
FontFamily=”Segoe WP Black” FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”White” SupportedOrientations=”Portrait”>
<!– Allow the gesture anywhere on the page –>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener Flick=”GestureListener_Flick”/>
</toolkit:GestureService.GestureListener>
<!– Add several animations to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– The dart-throwing animation, adjusted from code-behind based on
the angle and velocity of the flick –>
<Storyboard x:Name=”DartThrowStoryboard”
Completed=”DartThrowStoryboard_Completed”>
<!– Animate the horizontal position –>
<DoubleAnimation x:Name=”DartXAnimation”
Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateX” Duration=”0:0:.5”>
<DoubleAnimation.EasingFunction>
<CircleEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Animate the vertical position –>
<DoubleAnimation x:Name=”DartYAnimation”
Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateY” Duration=”0:0:.5”>
<DoubleAnimation.EasingFunction>
<CircleEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Animate the horizontal scale –>
<DoubleAnimation x:Name=”DartScaleXAnimation”
Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”ScaleX” By=”1.2”
Duration=”0:0:.25” AutoReverse=”True”>
<DoubleAnimation.EasingFunction>
<CircleEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Animate the vertical scale –>
<DoubleAnimation x:Name=”DartScaleYAnimation”
Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”ScaleY” By=”4”
Duration=”0:0:.25” AutoReverse=”True”>
<DoubleAnimation.EasingFunction>
<CircleEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Leave the dart in its ending position for half a second before returning
it to the bottom of the page for another throw –>
<Storyboard x:Name=”DartDelayStoryboard” Duration=”0:0:.5”
Completed=”DartDelayStoryboard_Completed”/>
<!– Move the dart back to the starting position –>
<Storyboard x:Name=”DartReturnStoryboard”>
<DoubleAnimation Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateX” To=”0”
Duration=”0:0:.2”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateY” To=”0”
Duration=”0:0:.2”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Slide the dart completely off the bottom of the screen –>
<Storyboard x:Name=”DartOffScreenStoryboard”>
<DoubleAnimation Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateY” To=”130”
Duration=”0:0:.6”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Slide the intro panel off the top of the screen –>
<Storyboard x:Name=”IntroOffStoryboard”
Completed=”IntroOffStoryboard_Completed”>
<DoubleAnimation Storyboard.TargetName=”IntroPanelTransform”
Storyboard.TargetProperty=”TranslateY” To=”-800”
Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Slide the intro panel back onto the screen from up above –>
<Storyboard x:Name=”IntroOnStoryboard”>
<DoubleAnimation Storyboard.TargetName=”IntroPanelTransform”
Storyboard.TargetProperty=”TranslateY” To=”0”
Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
… more storyboards …
<!– Animate in (then out) a message, such as the # of points just earned –>
<Storyboard x:Name=”ShowMessageStoryboard”
Storyboard.TargetName=”MessageTransform”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=”TranslateY”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.5” Value=”430”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:2.5” Value=”430”/>
<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 BackgroundColor=”#5F3000” ForegroundColor=”White”
Opacity=”.8”>
<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=”#5F3000”>
<Grid.Clip>
<RectangleGeometry Rect=”0,0,480,800”/>
</Grid.Clip>
<!– The game canvas –>
<Canvas>
<!– The dartboard background –>
<Ellipse Width=”480” Height=”480” Fill=”#8000”/>
<!– The dartboard segments –>
<Canvas x:Name=”DartboardSegments”>
<!– Vector art created in Expression Blend. Each region with a different
point value is a distinct Path element (82 total) –>
<Path x:Name=”D1” Data=”M268.269483095,61.50827908C287.000166325,
64.476988515,305.148564115,70.37132529,322.042831735,
78.980320525L240.000027675,240.000437465z” Fill=”Green”
Canvas.Left=”239.417” Canvas.Top=”60.925” Stretch=”Fill”
Width=”83.209” Height=”179.658”/>
… 81 more paths …
</Canvas>
<!– The dartboard numbers –>
<Canvas Opacity=”.5”>
<TextBlock Text=”20” Canvas.Left=”218” Canvas.Top=”1”
FontFamily=”Segoe WP” FontSize=”40”/>
… 19 more text blocks …
</Canvas>
<!– Each time a dart lands, a little black “hole” is placed in this
canvas to mark the position –>
<Canvas x:Name=”HolesCanvas”/>
<!– The dart –>
<Image x:Name=”DartImage” Canvas.Top=”675” Canvas.Left=”202”
Width=”100”>
<Image.RenderTransform>
<CompositeTransform x:Name=”DartTransform” TranslateY=”130”/>
</Image.RenderTransform>
</Image>
</Canvas>
<!– A display for the current score and # of darts remaining –>
<StackPanel x:Name=”ScorePanel” Visibility=”Collapsed”
VerticalAlignment=”Bottom” HorizontalAlignment=”Right”
Margin=”18,64”>
<TextBlock Text=”SCORE” Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”ScoreTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-20,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”ScoreTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock Text=”DARTS REMAINING”
Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”DartsRemainingTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-20,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”DartsRemainingTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
<!– The welcome/paused/game-over screen –>
<Grid x:Name=”IntroPanel”>
<Grid.RenderTransform>
<CompositeTransform x:Name=”IntroPanelTransform”/>
</Grid.RenderTransform>
<Rectangle Fill=”#5F3000” Opacity=”.6”/>
<!– The title and subtitle –>
<TextBlock Text=”DARTS” Margin=”-19,200,0,0” FontSize=”140”>
<TextBlock.RenderTransform>
<RotateTransform Angle=”-20”/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock x:Name=”Subtitle” Text=”DO A PRACTICE FLICK TO BEGIN”
Margin=”40,340,-40,0” FontSize=”27”>
<TextBlock.RenderTransform>
<RotateTransform Angle=”-20”/>
</TextBlock.RenderTransform>
</TextBlock>
<!– A display for the best score, average score, and # of games –>
<StackPanel x:Name=”StatsPanel” VerticalAlignment=”Bottom”
HorizontalAlignment=”Right” Margin=”18,64”>
<TextBlock Text=”BEST SCORE” Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”BestScoreTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-20,0,0”>
<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,-20,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”AvgScoreTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
</Grid>
<!– An animated message –>
<TextBlock x:Name=”MessageTextBlock” RenderTransformOrigin=”.5,.5”
FontWeight=”Bold” HorizontalAlignment=”Center” FontSize=”120”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”MessageTransform” TranslateY=”800”/>
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • To detect the flick gesture, this app handles the Flick event of a gesture listener attached to the whole page.
  • This page contains lots of storyboards for smoothly transitioning elements on and off the screen. The most important one is DartThrowStoryboard, which makes the dart travel based on the flick data. Its first two animations, which alter the dart transform’s TranslateX and TranslateY values, have their By values set in code-behind. The second two animations, which alter ScaleX and ScaleY, make the dart appear to jump out of and then back into the screen in a pseudo-3D arc motion. ScaleY is increased much more than ScaleX to make the image appear to flatten out, because the image already has Y-rotation perspective applied to it. The next section shows how the in-flight dart appears.
  • This app’s dartboard is one of the more interesting chunks of XAML in the whole book. It consists of 82 path elements and 19 text blocks on top of an ellipse. (Although I usually type all my XAML by hand, I created this part in Expression Blend!) Representing each region with a unique score value as a distinct element makes it easy to figure out how many points to award the user when the dart lands. The code-behind performs hit testing to find the topmost element hit by the dart and then awards points accordingly.

    Why are there 82 paths? Because there are 4 for each of the 20 wedges (the outer ring that awards double points, the outer “normal” wedge, the inner ring that awards triple points, and then the inner “normal” wedge), plus the outer and inner bull’s-eyes. Figure 40.2 illustrates how these 82 elements stack to form the final dartboard, as if the elements were separated and viewed at a 3D perspective.

  • The DartImage element doesn’t have its Source assigned. This is done in codebehind, because the source is continually changed between three different images.
  • The root grid has a clip applied to avoid a far-flung dart from causing the screen to go blank. This happens whenever Silverlight tries to render an element that is too far away.
A visualization of the 82 distinct dartboard segments.
FIGURE 40.2 A visualization of the 82 distinct dartboard segments.

The Code-Behind

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

LISTING 40.2 MainPage.xaml.cs—The Code-Behind for Darts’Main Page

[code]

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
The Code-Behind 885
{
// Persistent settings
Setting<int> bestScore = new Setting<int>(“BestScore”, 0);
Setting<double> avgScore = new Setting<double>(“AvgScore”, 0);
Setting<int> numGames = new Setting<int>(“NumGames”, 0);
// Current game state
int dartsRemaining;
int score;
// Three different dart images
BitmapImage dartStartImageSource =
new BitmapImage(new Uri(“Images/dartStart.png”, UriKind.Relative));
BitmapImage dartBigImageSource =
new BitmapImage(new Uri(“Images/dartBig.png”, UriKind.Relative));
BitmapImage dartSmallImageSource =
new BitmapImage(new Uri(“Images/dartSmall.png”, UriKind.Relative));
public MainPage()
{
InitializeComponent();
// Use the starting dart image
this.DartImage.Source = this.dartStartImageSource;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the persisted values
UpdateStatsLabels();
}
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
// If the game is in progress, pause it rather than exiting the app
if (this.IntroPanel.Visibility == Visibility.Collapsed)
{
this.Subtitle.Text =
“PAUSED.nFLICK TO RESUME THE GAME,nOR PRESS BACK TO EXIT.”;
ShowIntroPanel();
e.Cancel = true;
}
}
// The event handler for flicks
void GestureListener_Flick(object sender, FlickGestureEventArgs e)
{
if (this.IntroPanel.Visibility == Visibility.Visible)
{
// Start a new game if we’re out of darts
if (this.dartsRemaining == 0)
{
// Remove all holes and clear the tracking variables
this.HolesCanvas.Children.Clear();
this.dartsRemaining = 20;
this.score = 0;
// Animate the off-screen dart to the starting position
this.DartReturnStoryboard.Begin();
// Animate a message
this.MessageTextBlock.Text = “GO!”;
this.ShowMessageStoryboard.Begin();
}
// Remove the intro panel (for both new-game and unpause cases)
HideIntroPanel();
}
else
{
// First ensure that a dart throw is not in progress
if (this.DartThrowStoryboard.GetCurrentState() == ClockState.Active ||
this.DartDelayStoryboard.GetCurrentState() == ClockState.Active)
return;
// Throw the dart!
this.DartTransform.TranslateX = 0;
this.DartTransform.TranslateY = 0;
// Here is where the flick data is used
this.DartXAnimation.By = e.HorizontalVelocity / 10;
this.DartYAnimation.By = e.VerticalVelocity / 10;
// The animation scales the image up by as much as 4 vertically, so
// replace the image with a higher-resolution one
this.DartImage.Source = this.dartBigImageSource;
this.DartThrowStoryboard.Begin();
}
}
void IntroOffStoryboard_Completed(object sender, EventArgs e)
{
// The intro panel is now off-screen, so collapse it for better performance
this.IntroPanel.Visibility = Visibility.Collapsed;
}
void DartThrowStoryboard_Completed(object sender, EventArgs e)
{
// The dart has landed, so change the image to a small one
// that looks better that this scale (and is angled like it’s sticking into
// the wall)
this.DartImage.Source = this.dartSmallImageSource;
// Determine the exact point where the tip of the dart has landed
Point point = new Point(
// X: The tip isn’t quite centered in the image, hence the slight offset
Canvas.GetLeft(this.DartImage) + this.DartTransform.TranslateX
+ this.DartImage.Width / 2 – 11.5,
// Y: The tip is at the top edge of the image
Canvas.GetTop(this.DartImage) + this.DartTransform.TranslateY
);
// Place a “hole” where the tip of the dart landed
Ellipse hole = new Ellipse { Fill = new SolidColorBrush(Colors.Black),
Width = 5, Height = 5 };
Canvas.SetLeft(hole, point.X – hole.Width / 2);
Canvas.SetTop(hole, point.Y – hole.Height / 2);
this.HolesCanvas.Children.Add(hole);
// Calculate the score for this throw
int pointsEarned = DetermineScoreAtPoint(point);
// Update the game state and display
this.score += pointsEarned;
this.dartsRemaining–;
UpdateScoreLabels(pointsEarned > 0);
// Let the dart sit for a while before it animates back to the start
this.DartDelayStoryboard.Begin();
if (this.dartsRemaining > 0)
{
// Animate in the score message
this.MessageTextBlock.Text = pointsEarned.ToString();
this.ShowMessageStoryboard.Begin();
}
else
{
// Game over!
// Update the stats
double oldTotal = this.avgScore.Value * this.numGames.Value;
// New average
this.avgScore.Value = (oldTotal + score) / (this.numGames.Value + 1);
// New total number of games
this.numGames.Value++;
this.Subtitle.Text = “FINAL SCORE: “ + this.score + “. FLICK AGAIN!”;
if (this.score > this.bestScore.Value)
{
// New best score
this.bestScore.Value = this.score;
// Animate a best-score message
this.MessageTextBlock.Text = “BEST!”;
this.ShowMessageStoryboard.Begin();
}
ShowIntroPanel();
}
}
void DartDelayStoryboard_Completed(object sender, EventArgs e)
{
// Restore the image
this.DartImage.Source = this.dartStartImageSource;
// Move the dart to the starting position for the next throw,
// or off-screen if the game is over
if (this.dartsRemaining > 0)
this.DartReturnStoryboard.Begin();
else
this.DartOffScreenStoryboard.Begin();
}
int DetermineScoreAtPoint(Point p)
{
// Retrieve all elements inside DartboardSegments that intersect with p
foreach (UIElement element in
VisualTreeHelper.FindElementsInHostCoordinates(p,
this.DartboardSegments))
{
// Fortunately, the list of elements is ordered from top-to-bottom.
// We only care about the top-most element, so we can directly return a
// value based on the first item in this collection.
if (element == this.InnerBull)
return 50;
else if (element == this.OuterBull)
return 25;
else if (element is Path)
{
// The elements are named with a letter and a number.
// The letter is D for double score, T for triple score, or something
// else (A or B) for a normal region.
// The number is the score.
string name = (element as FrameworkElement).Name;
// Retrieve the score from the name
int score = int.Parse(name.Substring(1));
// Apply a multiplier, if applicable
if (name[0] == ‘D’)
score *= 2;
else if (name[0] == ‘T’)
score *= 3;
return score;
}
}
// No relevant element was hit
return 0;
}
void ShowIntroPanel()
{
UpdateStatsLabels();
this.IntroOnStoryboard.Begin();
this.IntroPanel.Visibility = Visibility.Visible;
this.ScorePanel.Visibility = Visibility.Collapsed;
this.ApplicationBar.IsVisible = true;
}
void HideIntroPanel()
{
UpdateScoreLabels(true);
this.IntroOffStoryboard.Begin();
this.ScorePanel.Visibility = Visibility.Visible;
this.ApplicationBar.IsVisible = false;
}
void UpdateStatsLabels()
{
if (this.numGames.Value > 0)
{
this.BestScoreTextBlock.Text = this.bestScore.Value.ToString();
this.AvgScoreTextBlock.Text = this.avgScore.Value.ToString(“0.#”);
if (this.numGames.Value == 1)
this.AvgScoreHeaderTextBlock.Text = “AVG SCORE (1 GAME)”;
else
this.AvgScoreHeaderTextBlock.Text = “AVG SCORE (“ + this.numGames.Value
+ “ GAMES)”;
}
else
{
this.BestScoreTextBlock.Text = “0”;
this.AvgScoreTextBlock.Text = “0”;
this.AvgScoreHeaderTextBlock.Text = “AVG SCORE”;
}
// Animate the textblocks out then in
this.SlideAvgScoreStoryboard.Begin();
this.SlideBestScoreStoryboard.Begin();
}
void UpdateScoreLabels(bool animateScore)
{
this.ScoreTextBlock.Text = this.score.ToString();
this.DartsRemainingTextBlock.Text = this.dartsRemaining.ToString();
// Animate the textblocks out then in
this.SlideDartsRemainingStoryboard.Begin();
if (animateScore)
this.SlideScoreStoryboard.Begin();
}
// 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)
{
if (MessageBox.Show(“Are you sure you want to clear your scores?”,
“Delete history”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
this.numGames.Value = 0;
this.bestScore.Value = 0;
UpdateStatsLabels();
}
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Darts”, UriKind.Relative));
}
}
}

[/code]

  • Three dart image sources are created (once) and then assigned as DartImage’s Source value at various stages of the dart-throwing procedure. Figure 40.3 shows all three. The start image looks more vertical, whereas the big and small images have a lot more perspective. The small image is used while the dart is sticking into the dartboard (or wall). The big image is used only while the dart is in flight, because otherwise the up-scaling of the small image would produce a pixelated result. The combination of DartThrowStoryboard’s motion and the image-swapping produces a pretty realistic pseudo-3D dart motion demonstrated in Figure 40.4.
The three dart images are swapped in and out to provide the best-looking result.
FIGURE 40.3 The three dart images are swapped in and out to provide the best-looking result.
The dart’s flight path when it hits a double 3 (6 points).
FIGURE 40.4 The dart’s flight path when it hits a double 3 (6 points).
  • OnBackKeyPress is overridden to show a paused screen when the back key is pressed during a game, canceling the exiting of the app. When the intro panel is shown as a paused screen or game-over screen, the text under the “DARTS” title is updated to explain what is going on, as shown in Figure 40.5.
The intro screen subtitle updates to show that the game is paused or to show the final score.
FIGURE 40.5 The intro screen subtitle updates to show that the game is paused or to show the final score.

 

  • GestureListener_Flick is the flick handler that processes the data from FlickGestureEventArgs and adjusts the two relevant dartthrowing animations appropriately. The Flick event is described in the next section.
  • DetermineScoreAtPoint uses a static method called FindElementsInHostCoordinates on System.Windows.Media. VisualTreeHelper to find the topmost element underneath the passed-in point. (FindElementsInHostCoordinates also exposes an overload that accepts a rectangular region rather than a point.) Because of the naming scheme used by the dartboard elements, the hit element’s name can be converted into the correct point value.

Your game might fail marketplace certification if pressing the Back hardware button doesn’t pause the game!

The Windows Phone 7 Certification Requirements (http://go.microsoft.com/ ?linkid=9730558) state:

For games, when the Back button is pressed during gameplay, the game can choose to present a pause context menu or dialog or navigate the user to the prior menu screen. Pressing the Back button again while in a paused context menu or dialog closes the menu or dialog.

Although this makes the pausing behavior seem optional, I’ve had a game fail certification because it chose not to provide a pausing experience. (And this was a pool game in which pausing is meaningless!) Even if you can get away without doing this, it’s probably a good idea to have your app include this pausing behavior for consistency with user expectations for Windows Phone games.

VisualTreeHelper.FindElementsInHostCoordinates is the best way to determine what element or elements are underneath a specific point or region. Unlike approaches from previous chapters that involve mapping a point into an element’s coordinate space one element at a time, FindElementsInHostCoordinates examines all elements that are children of the passed-in element. (It also examines the passed-in element itself.)

The Flick Event

The FlickGestureEventArgs instance passed to the Flick event exposes the following properties:

  • Angle—A double value expressed in degrees
  • Direction—Either Horizontal or Vertical, revealing the primary direction of the flick
  • HorizontalVelocity—A double value expressing the horizontal magnitude of the 2D velocity vector in pixels per second
  • VerticalVelocity—A double value expressing the vertical magnitude of the 2D velocity vector in pixels per second

This app has no use for the Direction property, which is meant for elements whose motion should be locked to a single direction when the flick occurs. Instead, this app’s dart freely moves in whatever angle the flick reports, even if it’s completely horizontal or downward instead of upward.

Because Listing 40.2 applies the horizontal and vertical velocities as offsets to the dart’s position, it doesn’t even need to check the Angle property. The ratio between the two velocities effectively gives the appropriate angle. When set as offsets to the dart’s position, the velocity values are divided by 10 to give a distance range that works well for this app. This factor was arrived at by trial and error.

A flick event is not raised until the finger has broken contact with the screen. Before then, the finger motion raises drag (and other) events.

If you want to detect flicks without using the gesture listener (perhaps to avoid the problems discussed at the beginning of this chapter), you can use the ManipulationCompleted event defined on all UI elements.The data passed to its handlers includes a FinalVelocities property that is equivalent to the pair of HorizontalVelocity and VerticalVelocity properties exposed to Flick event handlers.

The Finished Product

Darts (Gesture Listener & Flick Gesture)

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)