Pedometer (Walking Motion)

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

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

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

The Main Page

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

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

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

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

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

[code]

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

[/code]

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

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

[code]

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

[/code]

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

The Settings Page

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

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

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

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

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

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

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

[code]

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

[/code]

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

[code]

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

[/code]

The Finished Product

Pedometer (Walking Motion)

 

 

Balance Test (2D)

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

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

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

The Main Page

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

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

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

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

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

[code]

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

[/code]

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

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

[code]

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

[/code]

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

The Finished Product

Balance Test (2D)

Level (Determining Angle)

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

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

The BubbleWindow User Control

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

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

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

[code]

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

[/code]

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

[code]

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

[/code]

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

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

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

The Main Page

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

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

[code]

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

[/code]

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

[code]

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

[/code]

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

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

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

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

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

The Finished Product

Level (Determining Angle)

Moo Can (Turn Over)

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

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

The Main Page

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

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

[code]

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

[/code]

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

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

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

[code]

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

[/code]

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

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

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

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

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

The Calibration Page

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

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

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

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

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

[code]

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

[/code]

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

[code]

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

[/code]

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

Moo Can (Turn Over)

Noise Maker (Shake)

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

The Main Page

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

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

[code]

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

[/code]

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

[code]

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

[/code]

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

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

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

LISTING 46.3 ShakeDetection.cs—The Shake Detection Algorithm

[code]

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

[/code]

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

The Settings Page

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

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

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

[code]

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

[/code]

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

[code]

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

[/code]

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

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

The Finished Product

Noise Maker (Shake)

Spin the Bottle! (Rotate Gesture & Simulating Inertia)

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

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

The User Interface

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

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

[code]

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

[/code]

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

The Code-Behind

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

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

[code]

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

[/code]

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

The Finished Product

Spin Bottle, Rotate Gesture, Simulating Inertia

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)

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)

Paint (Ink Presenter)

Paint is a classic finger-painting app, but with several powerful options:

  • You can paint on top of any color canvas or a photo from your pictures library and paint with multiple fingers simultaneously (naturally).
  • In addition to using the rich color picker shared by many apps in this book, Paint provides many options for customizing the brush strokes.
  • You can undo and redo your strokes to get them just right.
  • A “stroke straightening” feature can help you create more precise artwork, either by straightening your diagonal lines or by snapping your lines to be completely vertical/horizontal.
  • Save your masterpieces to your phone’s pictures library.

Paint uses multi-touch the same way as the preceding chapter’s Musical Robot app, but it applies the data to an interesting element worth knowing about—ink presenter.

An ink presenter holds a collection of objects known as strokes that are meant to represent handwriting. Each stroke contains a collection of points that are connected to form each one. Each stroke also exposes a DrawingAttributes object with four properties: Color, OutlineColor, Width, and Height. Therefore, this app’s main drawing surface is simply an ink presenter whose strokes are added based on the touch data and settings chosen for the DrawingAttributes object.

Paint has two pages (in addition to standard instructions and about pages not shown in this chapter)—the main page containing the drawing surface and a palette page for adjusting the brush settings. This chapter begins by examining the palette page first, as it uses an ink presenter in a simpler fashion.

The Palette Page

 The palette page exposes a way to change each of the four properties on DrawingAttributes.
FIGURE 39.1 The palette page exposes a way to change each of the four properties on DrawingAttributes.

Paint’s palette page, pictured in Figure 39.1, enables changing each of the properties on the DrawingAttributes object. It links to this book’s shared color picker for the main color as well as the optional outline color, and exposes two sliders for independently controlling a stroke’s width and height.

The page has a hard-coded stroke that demonstrates how the different settings affect the resulting strokes as the user changes them. This is especially helpful for visualizing width and height changes, as shown in Figure 39.2.

Demonstrating every combination of the minimum and maximum brush sizes.
FIGURE 39.2 Demonstrating every combination of the minimum and maximum brush sizes.

The User Interface

Listing 39.1 contains the XAML for the palette page.

LISTING 39.1 PalettePage.xaml—The User Interface for Paint’s Palette Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.PalettePage”
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 standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”PAINT” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”palette” Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<!– The translucent foreground-colored palette image –>
<Rectangle Canvas.Left=”6” Width=”474” Height=”632” Opacity=”.6”
Fill=”{StaticResource PhoneForegroundBrush}”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/paletteBackground.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<!– The InkPresenter with a single 5-point stroke –>
<InkPresenter x:Name=”PreviewInkPresenter” Canvas.Left=”236” Canvas.Top=”220”>
<InkPresenter.Strokes>
<Stroke>
<Stroke.StylusPoints>
<StylusPoint X=”100” Y=”0”/>
<StylusPoint X=”0” Y=”0”/>
<StylusPoint X=”80” Y=”80”/>
<StylusPoint X=”0” Y=”120”/>
<StylusPoint X=”0” Y=”170”/>
</Stroke.StylusPoints>
</Stroke>
</InkPresenter.Strokes>
</InkPresenter>
<!– Paint color –>
<TextBlock Text=”Paint color” Canvas.Left=”84” Canvas.Top=”431” FontSize=”23”
Foreground=”{StaticResource PhoneBackgroundBrush}”/>
<Ellipse x:Name=”PaintColorEllipse” Canvas.Left=”78” Canvas.Top=”305”
Width=”120” Height=”120” Stroke=”{StaticResource PhoneBackgroundBrush}”
StrokeThickness=”10” local:Tilt.IsEnabled=”True”
MouseLeftButtonUp=”PaintColorEllipse_MouseLeftButtonUp”/>
<!– Outline color –>
<CheckBox x:Name=”OutlineCheckBox” Canvas.Left=”210” Canvas.Top=”521”
Foreground=”{StaticResource PhoneBackgroundBrush}”
local:Tilt.IsEnabled=”True” Checked=”OutlineCheckBox_IsCheckedChanged”
Unchecked=”OutlineCheckBox_IsCheckedChanged”>
<TextBlock FontSize=”23” Foreground=”{StaticResource PhoneBackgroundBrush}”>
Outline<LineBreak/>color
</TextBlock>
</CheckBox>
<Ellipse x:Name=”OutlineColorEllipse” Canvas.Left=”213” Canvas.Top=”414”
Width=”120” Height=”120” Stroke=”{StaticResource PhoneBackgroundBrush}”
StrokeThickness=”10” local:Tilt.IsEnabled=”True”
MouseLeftButtonUp=”OutlineColorEllipse_MouseLeftButtonUp”/>
<!– Brush width –>
<TextBlock Text=”Brush width” Canvas.Left=”35” Canvas.Top=”660”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<Slider x:Name=”BrushWidthSlider” Canvas.Left=”24” Canvas.Top=”680”
Minimum=”2” Maximum=”55” Width=”203”
ValueChanged=”BrushWidthSlider_ValueChanged”/>
<!– Brush height –>
<TextBlock Text=”Brush height” Canvas.Left=”263” Canvas.Top=”660”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<Slider x:Name=”BrushHeightSlider” Canvas.Left=”252” Canvas.Top=”680”
Minimum=”2” Maximum=”55” Width=”203”
ValueChanged=”BrushHeightSlider_ValueChanged”/>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

  • The ink presenter’s collection of strokes contains just one 5-point stroke. The stroke doesn’t specify any explicit drawing attributes because those are set in code-behind. By default, a stroke is given a width and height of 3, a color of black, and an outline color of transparent.
  • The StylusPoint objects that must be used to define a stroke are just like Point objects, but they have one additional property—PressureFactor—that unfortunately has no effect on Windows Phone.
  • The slider-enforced minimum and maximum width/height values of 2 and 55, respectively, are arbitrary. The corresponding properties on DrawingAttributes can be set to any nonnegative double.

Can I change the thickness of a stroke’s outline?

No, it is always a thin, approximately 1-pixel border.However, you could mimic a thicker border by rendering a duplicate stroke with a larger width and height behind each “real” stroke. It turns out that this is exactly how the outline color is rendered anyway, which you can see by giving the stroke a translucent or transparent color.This is demonstrated in Figure 39.3, which uses a translucent white stroke color and a green outline color.

A stroke outline is really just a slightly larger stroke underneath, which can be seen when a translucent paint color is used.
FIGURE 39.3 A stroke outline is really just a slightly larger stroke underneath, which can be seen when a translucent paint color is used.

What’s the difference between an ink presenter with strokes and a path with polylines?

Either of these two elements can be used for an app like Paint, as the differences between them are subtle.The main reason to prefer an ink presenter is that it performs better for the large number of points that are generated by finger movement. It’s also slightly easier to serialize its strokes so they can be saved and then later restored.

A path is more powerful because it can express mathematical curves between any two points, whereas each stroke’s stylus points are connected with straight lines. (There are just so many points when plotting finger movement that you don’t normally notice the connections.) It also supports arbitrary brushes (like gradient or image brushes instead of a solid color), and you can leverage its fill and stroke properties to either fill in a closed shape or to provide a true border of any thickness.

An ink presenter is more powerful because it can contain arbitrary UI elements in addition to strokes. (That’s because InkPresenter derives from Canvas.) It also holds the promise of easily enabling pressure-sensitive painting, as each stylus point exposes a PressureFactor property that can be set to a value from 0 to 1. However, given that setting this property currently has no effect on Windows Phone, and touch points never report how hard a finger is pressing the screen, this advantage is only a theoretical one for the future.

The Code-Behind

Listing 39.2 contains the code-behind for the palette page.

LISTING 39.2 PalettePage.xaml.cs—The Code-Behind for Paint’s Palette Page

[code]

using System;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class PalettePage : PhoneApplicationPage
{
public PalettePage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the saved settings
this.PaintColorEllipse.Fill =
new SolidColorBrush(Settings.PaintColor.Value);
this.OutlineColorEllipse.Fill =
new SolidColorBrush(Settings.OutlineColor.Value);
this.OutlineCheckBox.IsChecked = Settings.HasOutline.Value;
this.BrushWidthSlider.Value = Settings.BrushWidth.Value;
this.BrushHeightSlider.Value = Settings.BrushHeight.Value;
// Update the ink presenter with the current settings
DrawingAttributes attributes =
this.PreviewInkPresenter.Strokes[0].DrawingAttributes;
attributes.Color = Settings.PaintColor.Value;
attributes.Width = Settings.BrushWidth.Value;
attributes.Height = Settings.BrushHeight.Value;
if (Settings.HasOutline.Value)
attributes.OutlineColor = Settings.OutlineColor.Value;
else
attributes.OutlineColor = Colors.Transparent; // Hide the outline
}
void PaintColorEllipse_MouseLeftButtonUp(object sender,
MouseButtonEventArgs e)
{
// Get a string representation of the colors we need to pass to the color
// picker, without the leading #
string currentColorString =
Settings.PaintColor.Value.ToString().Substring(1);
string defaultColorString =
Settings.PaintColor.DefaultValue.ToString().Substring(1);
// The color picker works with the same isolated storage value that the
// Setting works with, but we have to clear its cached value to pick up
// the value chosen in the color picker
Settings.PaintColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=PaintColor”, UriKind.Relative));
}
void OutlineColorEllipse_MouseLeftButtonUp(object sender,
MouseButtonEventArgs e)
{
// Get a string representation of the colors, without the leading #
string currentColorString =
Settings.OutlineColor.Value.ToString().Substring(1);
string defaultColorString =
Settings.OutlineColor.DefaultValue.ToString().Substring(1);
// The color picker works with the same isolated storage value that the
// Setting works with, but we have to clear its cached value to pick up
// the value chosen in the color picker
Settings.OutlineColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “showOpacity=true”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=OutlineColor”, UriKind.Relative));
}
void OutlineCheckBox_IsCheckedChanged(object sender, RoutedEventArgs e)
{
// Toggle the outline
Settings.HasOutline.Value = this.OutlineCheckBox.IsChecked.Value;
if (Settings.HasOutline.Value)
this.PreviewInkPresenter.Strokes[0].DrawingAttributes.OutlineColor =
Settings.OutlineColor.Value;
else
this.PreviewInkPresenter.Strokes[0].DrawingAttributes.OutlineColor =
Colors.Transparent;
}
void BrushWidthSlider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
if (this.BrushWidthSlider != null) // Ignore during XAML parsing
{
Settings.BrushWidth.Value = (int)this.BrushWidthSlider.Value;
this.PreviewInkPresenter.Strokes[0].DrawingAttributes.Width =
Settings.BrushWidth.Value;
}
}
void BrushHeightSlider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
if (this.BrushHeightSlider != null) // Ignore during XAML parsing
{
Settings.BrushHeight.Value = (int)this.BrushHeightSlider.Value;
this.PreviewInkPresenter.Strokes[0].DrawingAttributes.Height =
Settings.BrushHeight.Value;
}
}
}
}

[/code]

  • This app uses the following settings defined in a separate Settings.cs file:
    [code]
    public static class Settings
    {
    // Drawing attributes for strokes
    public static readonly Setting<Color> PaintColor = new Setting<Color>(
    “PaintColor”, (Color)Application.Current.Resources[“PhoneAccentColor”]);
    public static readonly Setting<Color> OutlineColor =
    new Setting<Color>(“OutlineColor”, Colors.Black);
    public static readonly Setting<bool> HasOutline =
    new Setting<bool>(“HasOutline”, false);
    public static readonly Setting<int> BrushWidth =
    new Setting<int>(“BrushWidth”, 10);
    public static readonly Setting<int> BrushHeight =
    new Setting<int>(“BrushHeight”, 10);
    // Background color
    public static readonly Setting<Color> PageColor =
    new Setting<Color>(“PageColor”, Colors.White);
    }
    [/code]
    All but the last one are modified by this page.
  • To update the stroke with all the current values, this code simply retrieves the 0th element of the ink presenter’s Strokes collection.
  • When the user turns off the outline color (by unchecking the check box), the outline color is set to transparent. This is the only way to prevent the outline color from interfering with the size of the stroke and even the color of the stroke if the main color is translucent.

The Main Page

Paint’s main page is nothing more than a drawing surface and an application bar with several available actions. As demonstrated in Figure 39.4, although the application bar adjusts for the current orientation, the artwork remains fixed relative to the screen. Having the artwork rotate would be problematic, as the page size would effectively change. Having the application bar rotate, however, is a nice touch when doing landscape- oriented artwork.

The application bar rotates according to the current orientation, but the artwork does not (relative to the physical screen).
FIGURE 39.4 The application bar rotates according to the current orientation, but the artwork does not (relative to the physical screen).

When designing this app, I wanted the palette button on the application bar to be colored with the current paint color as a helpful visual aid. However, it’s not currently possible to emit dynamic images to be used by application bar buttons. Therefore, I decided to update the application bar’s background color with the current paint color as the next best thing. In Figure 39.4, the current paint color is a light, translucent blue.

The User Interface

Listing 39.3 contains the XAML for the main page.

LISTING 39.3 MainPage.xaml—The User Interface for Paint’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=”PortraitOrLandscape”>
<!– The application bar, and that’s it! –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”palette”
IconUri=”/Images/appbar.palette.png” Click=”PaletteButton_Click”/>
<shell:ApplicationBarIconButton Text=”undo”
IconUri=”/Shared/Images/appbar.undo.png” Click=”UndoButton_Click”/>
<shell:ApplicationBarIconButton Text=”redo”
IconUri=”/Shared/Images/appbar.redo.png” Click=”RedoButton_Click”/>
<shell:ApplicationBarIconButton Text=”straighten”
IconUri=”/Images/appbar.straighten1.png” Click=”StraightenButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”set background color”
Click=”SetBackgroundColorMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”set background picture”
Click=”SetBackgroundPictureMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”erase all strokes”
Click=”EraseMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”save to pictures library”
Click=”SaveToPicturesLibraryMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”instructions”
Click=”InstructionsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”about”
Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
</phone:PhoneApplicationPage>

[/code]

This XAML file has the distinction of being the only one in this book where the page has no content! It only sets the values of its SupportedOrientations and ApplicationBar properties. That’s because the content shown on main page is created from code-behind and placed in a frame-rooted popup. This is what enables the behavior demonstrated 39.4, in which the application bar rotates but the content does not.

The Code-Behind

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

LISTING 39.4 MainPage.xaml.cs—The Code-Behind for Paint’s Main Page

[code]

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using Microsoft.Phone;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using Microsoft.Phone.Tasks;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Undo and redo stacks
Stack<HistoryEntry> undoStack = new Stack<HistoryEntry>();
Stack<HistoryEntry> redoStack = new Stack<HistoryEntry>();
// The in-progress strokes, tracked separately for each unique finger
Dictionary<int, Stroke> fingerStrokes = new Dictionary<int, Stroke>();
// The popup and its contents
Popup popup = new Popup { IsOpen = true };
Grid grid = new Grid { Width = 480, Height = 800 };
InkPresenter inkPresenter = new InkPresenter();
Image backgroundImage = new Image {
Stretch = Stretch.Uniform, RenderTransformOrigin = new Point(.5, .5),
RenderTransform = new CompositeTransform()
};
// Application bar buttons and a menu item that are changed by code-behind
IApplicationBarIconButton undoButton;
IApplicationBarIconButton redoButton;
IApplicationBarIconButton straightenButton;
IApplicationBarMenuItem backgroundPictureMenuItem;
public MainPage()
{
InitializeComponent();
// Assign the application bar items
this.undoButton = this.ApplicationBar.Buttons[1]
as IApplicationBarIconButton;
this.redoButton = this.ApplicationBar.Buttons[2]
as IApplicationBarIconButton;
this.straightenButton = this.ApplicationBar.Buttons[3]
as IApplicationBarIconButton;
this.backgroundPictureMenuItem = this.ApplicationBar.MenuItems[1]
as IApplicationBarMenuItem;
// Restore the background image, if persisted previously
if (IsolatedStorageHelper.FileExists(“background.jpg”))
SetBackgroundImage(IsolatedStorageHelper.LoadImageFile(“background.jpg”));
// Restore the strokes, if persisted previously.
// These are stored in a file rather than isolated storage settings due to
// a problem with the default serializer.
StrokeCollection strokes =
IsolatedStorageHelper.LoadSerializedObjectFromFile(“strokes.xml”,
typeof(StrokeCollection)) as StrokeCollection;
if (strokes != null)
this.inkPresenter.Strokes = strokes;
// Refresh the app bar based on the presence of a background image & strokes
RefreshAppBarMenu();
RefreshAppBarButtons();
// Attach the UI to the popup, which is already showing (IsOpen=true)
this.grid.Children.Add(this.backgroundImage);
this.grid.Children.Add(this.inkPresenter);
this.popup.Child = this.grid;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Need to hide the popup so the other page can be shown!
this.popup.IsOpen = false;
// Unsubscribe from this application-wide event
Touch.FrameReported -= Touch_FrameReported;
// Persist the current strokes.
// These are stored in a file rather than isolated storage settings due to
// a problem with the default serializer.
IsolatedStorageHelper.SaveFile(“strokes.xml”, this.inkPresenter.Strokes);
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Ensure the popup is shown, as it gets hidden when navigating away
this.popup.IsOpen = true;
// Reapply the background color, in case we just returned
// from the color picker page
this.grid.Background = new SolidColorBrush(Settings.PageColor.Value);
// Apply the current paint color as the app bar background color
Color paintColor = Settings.PaintColor.Value;
// Prevent the background from getting too transparent,
// potentialy making the buttons and menu items unreadable
if (paintColor.A < 60)
paintColor.A = 60;
this.ApplicationBar.BackgroundColor = paintColor;
// Choose a foreground color that will be visible over the background color
if (IsLight(Settings.PaintColor.Value))
this.ApplicationBar.ForegroundColor = Colors.Black;
else
this.ApplicationBar.ForegroundColor = Colors.White;
// Subscribe to the touch/multi-touch event.
// This is application-wide, so only do this when on this page.
Touch.FrameReported += Touch_FrameReported;
}
void Touch_FrameReported(object sender, TouchFrameEventArgs e)
{
// Get all touch points
TouchPointCollection points = e.GetTouchPoints(this.inkPresenter);
// Process each touch point individually
foreach (TouchPoint point in points)
{
// The “touch device” is each finger, and it has a unique ID
int fingerId = point.TouchDevice.Id;
if (point.Action == TouchAction.Down)
{
// Start a new stroke
Stroke stroke = new Stroke();
// Apply all the current settings
stroke.DrawingAttributes.Color = Settings.PaintColor.Value;
stroke.DrawingAttributes.Width = Settings.BrushWidth.Value;
stroke.DrawingAttributes.Height = Settings.BrushHeight.Value;
if (Settings.HasOutline.Value)
stroke.DrawingAttributes.OutlineColor = Settings.OutlineColor.Value;
else
stroke.DrawingAttributes.OutlineColor = Colors.Transparent;
// The first point of this stroke is the current finger position
stroke.StylusPoints.Add(
new StylusPoint(point.Position.X, point.Position.Y));
// Track which finger this stroke belongs to
this.fingerStrokes[fingerId] = stroke;
// Add it to the ink presenter’s collection of strokes
this.inkPresenter.Strokes.Add(stroke);
}
else if (point.Action == TouchAction.Move)
{
// Keep adding new points to the stroke
if (this.fingerStrokes.ContainsKey(fingerId))
this.fingerStrokes[fingerId].StylusPoints.Add(
new StylusPoint(point.Position.X, point.Position.Y));
}
else // TouchAction.Up
{
// The stroke is finished
if (this.fingerStrokes.ContainsKey(fingerId))
{
// Enable this action to be undone
this.undoStack.Push(
new HistoryEntry { StrokeAdded = this.fingerStrokes[fingerId] });
this.redoStack.Clear();
// Stop tracking this stroke
this.fingerStrokes.Remove(fingerId);
// Refresh the state of the undo/redo/straighten buttons
RefreshAppBarButtons();
}
}
}
}
bool IsLight(Color color)
{
return ((color.R + color.G + color.B) / 3 > 127.5);
}
void SetBackgroundImage(ImageSource source)
{
this.backgroundImage.Source = source;
// The ImageOpened event doesn’t get raised after this, but the values for
// ActualWidth and ActualHeight aren’t correct yet. The BeginInvoke enables
// us to retrieve the values.
this.Dispatcher.BeginInvoke(delegate()
{
// Rotate the image based on whether it’s landscape or portrait
if (this.backgroundImage.ActualWidth > this.backgroundImage.ActualHeight)
{
this.backgroundImage.Width = 800;
this.backgroundImage.Margin = new Thickness((480 – 800) / 2, 0, 0, 0);
(this.backgroundImage.RenderTransform as CompositeTransform).Rotation =
90;
}
else
{
this.backgroundImage.Width = 480;
this.backgroundImage.Margin = new Thickness(0, 0, 0, 0);
(this.backgroundImage.RenderTransform as CompositeTransform).Rotation =
0;
}
});
}
// Update the state of the application bar menu
void RefreshAppBarMenu()
{
if (IsolatedStorageHelper.FileExists(“background.jpg”))
this.backgroundPictureMenuItem.Text = “remove background picture”;
else
this.backgroundPictureMenuItem.Text = “set background picture”;
}
// Update the state of the application bar buttons
void RefreshAppBarButtons()
{
this.undoButton.IsEnabled = (this.undoStack.Count > 0);
this.redoButton.IsEnabled = (this.redoStack.Count > 0);
this.straightenButton.IsEnabled = (this.inkPresenter.Strokes.Count > 0);
// Customize the straighten button icon based on the last stroke’s shape
if (this.inkPresenter.Strokes.Count > 0)
{
Stroke lastStroke =
this.inkPresenter.Strokes[this.inkPresenter.Strokes.Count – 1];
if (lastStroke.StylusPoints.Count > 2)
this.straightenButton.IconUri =
new Uri(“/Images/appbar.straighten1.png”, UriKind.Relative);
else
this.straightenButton.IconUri =
new Uri(“/Images/appbar.straighten2.png”, UriKind.Relative);
}
}
// Application bar button handlers
void PaletteButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(
new Uri(“/PalettePage.xaml”, UriKind.Relative));
}
void UndoButton_Click(object sender, EventArgs e)
{
if (this.undoStack.Count == 0)
return;
// Get the previous action
HistoryEntry entry = this.undoStack.Pop();
// If a stroke was added, remove it
if (entry.StrokeAdded != null)
this.inkPresenter.Strokes.Remove(entry.StrokeAdded);
// If strokes were removed, add them back
if (entry.StrokesRemoved != null)
foreach (Stroke s in entry.StrokesRemoved)
this.inkPresenter.Strokes.Add(s);
// Enable the undo to be undone
this.redoStack.Push(entry);
// Update the state of the undo/redo/straighten buttons
RefreshAppBarButtons();
}
void RedoButton_Click(object sender, EventArgs e)
{
if (this.redoStack.Count == 0)
return;
// Get the action that was just undone
HistoryEntry entry = this.redoStack.Pop();
// If a stroke was added, add it back
if (entry.StrokeAdded != null)
this.inkPresenter.Strokes.Add(entry.StrokeAdded);
// If strokes were removed, remove them again
if (entry.StrokesRemoved != null)
foreach (Stroke s in entry.StrokesRemoved)
this.inkPresenter.Strokes.Remove(s);
// Enable this action to be undone
this.undoStack.Push(entry);
// Update the state of the undo/redo/straighten buttons
RefreshAppBarButtons();
}
void StraightenButton_Click(object sender, EventArgs e)
{
if (this.inkPresenter.Strokes.Count == 0)
return;
bool straightened = false;
Stroke lastStroke =
this.inkPresenter.Strokes[this.inkPresenter.Strokes.Count – 1];
// Clone the stroke before changing it, simply so the original stroke
// can be placed in the undo stack.
// The DrawingAttributes instance is shared by both, but we don’t change it.
Stroke newStroke = new Stroke { DrawingAttributes =
lastStroke.DrawingAttributes };
foreach (StylusPoint point in lastStroke.StylusPoints)
newStroke.StylusPoints.Add(point);
if (newStroke.StylusPoints.Count > 2)
{
// This is a raw stroke, so do the first round of straightening simply
// by removing every point except its two endpoints
while (newStroke.StylusPoints.Count > 2)
newStroke.StylusPoints.RemoveAt(1);
straightened = true;
}
else if (newStroke.StylusPoints.Count == 2)
{
// This is already a straight line, so make it completely horizontal or
// completely vertical depending on which is closer
double deltaX = newStroke.StylusPoints[0].X – newStroke.StylusPoints[1].X;
double deltaY = newStroke.StylusPoints[0].Y – newStroke.StylusPoints[1].Y;
if (Math.Abs(deltaX) > Math.Abs(deltaY))
{
// The line is more horizontal than vertical
if (newStroke.StylusPoints[0].Y != newStroke.StylusPoints[1].Y)
{
// Give the horizontal line the average Y value
double newY = (newStroke.StylusPoints[0].Y +
newStroke.StylusPoints[1].Y) / 2;
newStroke.StylusPoints[0] =
new StylusPoint(newStroke.StylusPoints[0].X, newY);
newStroke.StylusPoints[1] =
new StylusPoint(newStroke.StylusPoints[1].X, newY);
straightened = true;
}
}
else
{
// The line is more vertical than horizontal
if (newStroke.StylusPoints[0].X != newStroke.StylusPoints[1].X)
{
// Give the vertical line the average X value
double newX = (newStroke.StylusPoints[0].X +
newStroke.StylusPoints[1].X) / 2;
newStroke.StylusPoints[0] =
new StylusPoint(newX, newStroke.StylusPoints[0].Y);
newStroke.StylusPoints[1] =
new StylusPoint(newX, newStroke.StylusPoints[1].Y);
straightened = true;
}
}
}
if (straightened)
{
// Remove the old stroke and swap in the cloned and modified stroke
this.inkPresenter.Strokes.Remove(lastStroke);
this.inkPresenter.Strokes.Add(newStroke);
// Update the undo/redo stacks
HistoryEntry entry = new HistoryEntry { StrokeAdded = newStroke };
entry.StrokesRemoved = new Stroke[] { lastStroke };
this.undoStack.Push(entry);
this.redoStack.Clear();
// Update the state of the undo/redo/straighten buttons
RefreshAppBarButtons();
}
}
// Application bar menu handlers
void SetBackgroundColorMenuItem_Click(object sender, EventArgs e)
{
// Get a string representation of the colors, without the leading #
string currentColorString = Settings.PageColor.Value.ToString().Substring(1);
string defaultColorString =
Settings.PageColor.DefaultValue.ToString().Substring(1);
// The color picker works with the same isolated storage value that the
// Setting works with, but we have to clear its cached value to pick up
// the value chosen in the color picker
Settings.PageColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “showOpacity=false”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=PageColor”, UriKind.Relative));
}
void SetBackgroundPictureMenuItem_Click(object sender, EventArgs e)
{
if (IsolatedStorageHelper.FileExists(“background.jpg”))
{
// “remove background picture” was tapped
IsolatedStorageHelper.DeleteFile(“background.jpg”);
this.backgroundImage.Source = null;
RefreshAppBarMenu();
return;
}
// “set background picture” was tapped
PhotoChooserTask task = new PhotoChooserTask();
task.ShowCamera = true;
task.Completed += delegate(object s, PhotoResult args)
{
if (args.TaskResult == TaskResult.OK)
{
// Apply the image to the background
SetBackgroundImage(PictureDecoder.DecodeJpeg(args.ChosenPhoto));
// Seek back to the beginning of the stream again
args.ChosenPhoto.Seek(0, SeekOrigin.Begin);
// Save the file to isolated storage.
// This overwrites the file if it already exists.
IsolatedStorageHelper.SaveFile(“background.jpg”, args.ChosenPhoto);
RefreshAppBarMenu();
}
};
task.Show();
}
void EraseMenuItem_Click(object sender, EventArgs e)
{
// Allow this to be undone by storing all the current strokes
HistoryEntry entry = new HistoryEntry();
entry.StrokesRemoved = this.inkPresenter.Strokes.ToArray();
this.undoStack.Push(entry);
this.redoStack.Clear();
// Erase them all
this.inkPresenter.Strokes.Clear();
// Update the state of the undo/redo buttons
RefreshAppBarButtons();
}
void SaveToPicturesLibraryMenuItem_Click(object sender, EventArgs e)
{
// Create a new bitmap with the page’s dimensions
WriteableBitmap bitmap = new WriteableBitmap((int)this.grid.ActualWidth,
(int)this.grid.ActualHeight);
// Render the contents to the bitmap
bitmap.Render(grid, null);
// We must explicitly tell the bitmap to draw its new contents
bitmap.Invalidate();
using (MemoryStream stream = new MemoryStream())
{
// Fill the stream with a JPEG representation of this bitmap
bitmap.SaveJpeg(stream, (int)this.grid.ActualWidth,
(int)this.grid.ActualHeight,
0 /* orientation */, 100 /* quality */);
// Seek back to the beginning of the stream
stream.Seek(0, SeekOrigin.Begin);
// Save the image
try
{
new Microsoft.Xna.Framework.Media.MediaLibrary().SavePicture(
“paint.jpg”, stream);
}
catch
{
MessageBox.Show(“To do this, please disconnect your phone from Zune.”,
“Please Disconnect”, MessageBoxButton.OK);
return;
}
}
MessageBox.Show(
“Your artwork has been saved. Go to your phone’s Pictures hub to view it.”,
“Success”, MessageBoxButton.OK);
}
void InstructionsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(
new Uri(“/Shared/About/AboutPage.xaml?appName=Paint”, UriKind.Relative));
}
}
}

[/code]

  • The undo/redo feature is implemented as two stacks of the following simple data type:
    [code]
    public struct HistoryEntry
    {
    public Stroke StrokeAdded { get; set; }
    public IList<Stroke> StrokesRemoved { get; set; }
    }
    [/code]
    The undo feature supports not just the removal of newly added strokes, but undoingthe straightening of a stroke and undoing the erasure of all strokes simultaneously. If it weren’t for these two extra undo cases, this app wouldn’t need an undo stack at all—it could just treat the ink presenter’s Strokes collection as the stack and remove each stroke from the end of the list.
  • The fingerStrokes dictionary is used just like the fingerSounds dictionary from the preceding chapter, tracking each in-progress stroke while associating it with the correct finger. The hack to work around missing Up actions is not done here, however, because the only bad effect caused by this is extra entries left behind in the dictionary.
  • Although ink presenters can contain arbitrary elements with canvas-style layout, this page uses an image behind the ink presenter—placing both in a one-cell grid—to take advantage of grid’s automatic layout.
  • Because the application bar’s background is set to whatever the user has chosen as the paint color (inside OnNavigatedTo), we must ensure that the buttons and text are visible on top of this color no matter what. A simple IsLight method is used, defined toward the end of the listing, to make the foreground white if the background is dark or to make the foreground black if the background is light. The code in OnNavigatedTo also prevents the application bar background from becoming too transparent, as that could cause the buttons and text to become unreadable based on whatever the artwork happened to contain underneath.
  • Touch_FrameReported contains the code at the heart of this app. When a finger touches down, a new stroke is created and given drawing attributes that match all of the current settings. A stylus point is then added to its StylusPoints collection that matches the finger’s current location, and then it is added to the ink presenter’s Strokes collection. When a finger moves, the correct stroke is retrieved based on the finger ID, and then a new stylus point is added to it based on the current location. When a finger breaks contact with the screen, no further changes need to be made to the stroke, but the undo/redo stacks are adjusted appropriately and the application bar is refreshed.
  • The stroke-straightening feature works in two phases. The first time it is tapped, all stylus points on the most recent stroke are removed except for the starting and ending points. This causes it to form a straight but likely diagonal line. The second time it is tapped, the location of both points is adjusted to make the line horizontal or vertical, whichever is a closer match. The stroke is cloned and the copy is modified, but that’s only done so that the original stroke can be placed in the undo stack. If straightening did not need to be undone, the changes to the stroke’s points could be done directly to the instance already in the ink presenter.
  • The straightening process is demonstrated in Figure 39.5 for two different strokes. Notice that the straighten button’s icon changes to indicate which phase the most recent stroke is currently in.
The sequence of straightening two strokes.
FIGURE 39.5 The sequence of straightening two strokes.
  • XNA’s MediaLibrary.SavePicture method is called to save the artwork to the pictures library. Similar to the Local FM Radio and Subservient Cat apps, it checks for a failure case caused when the phone is connected to Zune on a PC. There’s one more failure case caused by the Zune connection: Calling Show on PhotoChooserTask causes the Completed event to be raised with the event-args TaskResult property set to TaskResult.Cancel. Because this isn’t easily distinguishable from the user cancelling the task, this case is left alone. It’s more likely to cause confusion for developers than users.

Manual Serialization and Deserialization

Although an ink presenter’s Strokes collection is serializable, attempting to assign such a collection to an isolated storage application setting (or a page state item) does not work. The automatic serialization process throws an exception. Therefore, rather than using a Setting object to persist and retrieve the ink presenter’s strokes, Listing 39.4 uses two methods in the project’s IsolatedStorageHelper class implemented as follows:

[code]

public static void SaveFile(string filename, object serializableObject)
{
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream stream = userStore.CreateFile(filename))
using (StreamWriter writer = new StreamWriter(stream))
{
// Serialize the object to XML and write it to the file
XmlSerializer serializer = new XmlSerializer(serializableObject.GetType());
serializer.Serialize(writer, serializableObject);
}
}
public static object LoadSerializedObjectFromFile(string filename, Type type)
{
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
{
if (userStore.FileExists(filename))
{
using (IsolatedStorageFileStream stream =
userStore.OpenFile(filename, FileMode.Open))
using (StreamReader reader = new StreamReader(stream))
{
// Deserialize the object from the XML in the file
XmlSerializer serializer = new XmlSerializer(type);
return serializer.Deserialize(reader);
}
}
}
return null;
}

[/code]

Manual serialization and deserialization is done with System.Runtime.Serialization.XmlSerializer from the System.Xml.Serialization assembly. The serialized XML it produces looks like the following for a one-point, onestroke collection:

[code]

<?xml version=”1.0” encoding=”utf-16”?>
<ArrayOfStroke xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
xmlns:xsd=”http://www.w3.org/2001/XMLSchema”>
<Stroke>
<StylusPoints>
<StylusPoint>
<X>100</X>
<Y>0</Y>
<PressureFactor>0.5</PressureFactor>
</StylusPoint>
</StylusPoints>
<DrawingAttributes>
<Color>
<A>255</A>
<R>27</R>
<G>161</G>
<B>226</B>
</Color>
<OutlineColor>
<A>0</A>
<R>255</R>
<G>255</G>
<B>255</B>
</OutlineColor>
<Width>10</Width>
<Height>10</Height>
</DrawingAttributes>
</Stroke>
</ArrayOfStroke>

[/code]

XmlSerializer isn’t the only available option, however. Silverlight for Windows Phone also ships with System.Runtime.Serialization.DataContractSerializer (in the System.Runtime.Serialization assembly) and System.Runtime.Serialization .Json.DataContractJsonSerializer (in the System.Servicemodel.Web assembly).

DataContractSerializer serializes objects to XML, but in a different way than XmlSerializer. It also happens to support the serialization of a broader set of types and properties. The serialized XML it produces looks like the following for the same stroke collection:

[code]

<ArrayOfStroke xmlns:i=”http://www.w3.org/2001/XMLSchema-instance” xmlns=”http://s
chemas.datacontract.org/2004/07/System.Windows.Ink”><Stroke><DrawingAttributes><Co
lor xmlns:d4p1=”http://schemas.datacontract.org/2004/07/System.Windows.Media”><d4p
1:A>255</d4p1:A><d4p1:B>226</d4p1:B><d4p1:G>161</d4p1:G><d4p1:R>27</d4p1:R></Color
><Height>10</Height><OutlineColor xmlns:d4p1=”http://schemas.datacontract.org/2004
/07/System.Windows.Media”><d4p1:A>0</d4p1:A><d4p1:B>255</d4p1:B><d4p1:G>255</d4p1:
G><d4p1:R>255</d4p1:R></OutlineColor><Width>10</Width></DrawingAttributes><StylusP
oints xmlns:d3p1=”http://schemas.datacontract.org/2004/07/System.Windows.Input”><d
3p1:StylusPoint><d3p1:PressureFactor>0.5</d3p1:PressureFactor><d3p1:X>100</d3p1:X>
<d3p1:Y>0</d3p1:Y></d3p1:StylusPoint></StylusPoints></Stroke></ArrayOfStroke>

[/code]

Rather than pretty-printing the XML, which is not needed in this case, it produces one big line.

DataContractJsonSerializer serializes objects to JavaScript Object Notation (JSON), the popular format that is usually much more compact than XML. Here is the serialized JSON for the same stroke collection, which again is produced as one big line:

[code]

[{“DrawingAttributes”:{“Color”:{“A”:255,”B”:226,”G”:161,”R”:27},”Height”:10,”Outli neColor”:{“A”:0,”B”:255,”G”:255,”R”:255},”Width”:10},”StylusPoints”:[{“PressureFac tor”:0.5,”X”:100,”Y”:0}]}]

[/code]

The Finished Product

Paint (Ink Presenter)

 

 

 

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)