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)

 

 

Noise Maker (Shake)

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

The Main Page

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

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

[code]

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

[/code]

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

[code]

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

[/code]

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

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

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

LISTING 46.3 ShakeDetection.cs—The Shake Detection Algorithm

[code]

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

[/code]

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

The Settings Page

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

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

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

[code]

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

[/code]

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

[code]

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

[/code]

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

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

The Finished Product

Noise Maker (Shake)

Boxing Glove (Accelerometer Basics)

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

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

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

The Accelerometer

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

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

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

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

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

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

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

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

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

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

The User Interface

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

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

[code]

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

[/code]

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

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

The Code-Behind

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

[code]

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

[/code]

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

[code]

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

[/code]

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

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

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

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

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

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

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

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

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

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

[code]

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

[/code]

The Settings Page

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

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

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

[code]

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

[/code]

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

[code]

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

[/code]

The Finished Product

Boxing Glove (Accelerometer Basics)

Notepad (Reading & Writing Files)

The Notepad app enables fast, efficient note-taking. It boasts the following features:

  • Auto-save, which makes jotting down notes fast and easy
  • Quick previews of each note
  • The ability to customize each note’s background/foreground colors and text size
  • The ability to email your notes

Does this sound familiar? It should, because to a user this app behaves exactly like the preceding chapter’s Passwords & Secrets app, but without the master password and associated encryption. There is one important difference in its implementation, however, that makes it interesting for this chapter. Because the notes stored by Notepad are expected to be longer than the notes stored by Passwords & Secrets, each note is persisted as a separate file in isolated storage. This enables the app to load a note’s contents on-demand rather than loading everything each time the app launches/activates (which is what happens with application settings).

The Main Page

Notepad’s main page works just like the previous chapter’s main page, but without the LoginControl user control and associated logic. Listing 22.1 contains the XAML for the main page, with differences emphasized.

LISTING 22.1 MainPage.xaml—The User Interface for Notepad’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=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<phone:PhoneApplicationPage.Resources>
<local:DateConverter x:Key=”DateConverter”/>
</phone:PhoneApplicationPage.Resources>
<!– The application bar, with one button and one menu item –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”new”
IconUri=”/Shared/Images/appbar.add.png” Click=”NewButton_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>
<!– The standard header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”NOTEPAD” Style=”{StaticResource PhoneTextTitle0Style}”/>
</StackPanel>
<!– Show this when there are no notes –>
<TextBlock Name=”NoItemsTextBlock” Grid.Row=”1” Text=”No notes”
Visibility=”Collapsed” Margin=”22,17,0,0”
Style=”{StaticResource PhoneTextGroupHeaderStyle}”/>
<!– The list box containing notes –>
<ListBox x:Name=”ListBox” Grid.Row=”1” ItemsSource=”{Binding}”
SelectionChanged=”ListBox_SelectionChanged”>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<!– The title, in a style matching the note –>
<Border Background=”{Binding ScreenBrush}” Margin=”24,0” Width=”800”
MinHeight=”60” local:Tilt.IsEnabled=”True”>
<TextBlock Text=”{Binding Title}” FontSize=”{Binding TextSize}”
Foreground=”{Binding TextBrush}” Margin=”12”
VerticalAlignment=”Center”/>
</Border>
<!– The modified date –>
<TextBlock Foreground=”{StaticResource PhoneSubtleBrush}”
Text=”{Binding Modified, Converter={StaticResource DateConverter}}”
Margin=”24,0,0,12”/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</phone:PhoneApplicationPage>

[/code]

The application bar now has the “new” button from the start because there is no mode in which adding new notes is forbidden.

The code-behind for the main page is shown in Listing 22.2.

LISTING 22.2 MainPage.xaml.cs—The Code-Behind for Notepad’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Bind the notes list as the data source for the list box
if (this.DataContext == null)
this.DataContext = Settings.NotesList.Value;
// Clear the selection so selecting the same item twice in a row will
// still raise the SelectionChanged event
Settings.CurrentNoteIndex.Value = -1;
this.ListBox.SelectedIndex = -1;
if (Settings.NotesList.Value.Count == 0)
NoItemsTextBlock.Visibility = Visibility.Visible;
else
NoItemsTextBlock.Visibility = Visibility.Collapsed;
}
void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ListBox.SelectedIndex >= 0)
{
// Navigate to the details page for the selected item
Settings.CurrentNoteIndex.Value = ListBox.SelectedIndex;
this.NavigationService.Navigate(new Uri(“/DetailsPage.xaml”,
UriKind.Relative));
}
}
// Application bar handlers
void NewButton_Click(object sender, EventArgs e)
{
// Create a new note and add it to the top of the list
Note note = new Note();
note.Filename = Guid.NewGuid().ToString();
note.Modified = DateTimeOffset.Now;
note.ScreenColor = Settings.ScreenColor.Value;
note.TextColor = Settings.TextColor.Value;
note.TextSize = Settings.TextSize.Value;
Settings.NotesList.Value.Insert(0, note);
// “Select” the new note
Settings.CurrentNoteIndex.Value = 0;
// Navigate to the details page for the newly created note
this.NavigationService.Navigate(new Uri(“/DetailsPage.xaml”,
UriKind.Relative));
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Notepad”, UriKind.Relative));
}
}
}

[/code]

  • The list box is now filled immediately inside OnNavigatedTo by setting the page’s data context to the list of notes.
  • The Note class, shown at the end of this chapter, is slightly different from the preceding chapter in order to accommodate its file-based storage. Inside NewButton_Click, you can see that it now has a Filename property that points to the file containing its contents. The filename is never shown in this app’s user interface; internally, each note just needs to know where to fetch its content. Therefore, when a new note is created, it is given a unique filename thanks to the Guid.NewGuid method. This returns a Globally Unique Identifier (GUID) that is unique for all practical purposes.

The Details Page

The details page, just like in Passwords & Secrets, displays the entire contents of the note and enables the user to edit it, delete it, change its settings, or email its contents. The page’s XAML is identical to DetailsPage.xaml in the preceding chapter, except the application bar is not marked with IsVisible=”False” because it doesn’t need to be hidden while the LoginControl is shown. Listing 22.3 contains the code-behind for this page with differences emphasized.

LISTING 22.3 DetailsPage.xaml.cs—The Code-Behind for Passwords & Secrets’Details Page

[code]

using System;
using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Tasks;
namespace WindowsPhoneApp
{
public partial class DetailsPage : PhoneApplicationPage
{
bool navigatingFrom;
string initialText = “”;
public DetailsPage()
{
InitializeComponent();
this.Loaded += DetailsPage_Loaded;
}
void DetailsPage_Loaded(object sender, RoutedEventArgs e)
{
// Automatically show the keyboard for new notes.
// This also gets called when navigating away, hence the extra check
// to make sure we’re only doing this when navigating to the page
if (this.TextBox.Text.Length == 0 && !this.navigatingFrom)
this.TextBox.Focus();
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
this.navigatingFrom = true;
base.OnNavigatedFrom(e);
if (this.initialText != this.TextBox.Text)
{
// Automatically save the new content
Note n = Settings.NotesList.Value[Settings.CurrentNoteIndex.Value];
n.SaveContent(this.TextBox.Text);
// Update the title now, so each one can be accessed
// later without reading the file’s contents
string title = this.TextBox.Text.TrimStart();
// Don’t include more than the first 100 characters, which should be long
// enough, even in landscape with a small font
if (title.Length > 100)
title = title.Substring(0, 100);
// Fold the remaining content into a single line. We can’t use
// Environment.NewLine because it’s rn, whereas newlines inserted from
// a text box are just r
n.Title = title.Replace(‘r’, ‘ ‘);
n.Modified = DateTimeOffset.Now;
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Show the note’s contents
Note n = Settings.NotesList.Value[Settings.CurrentNoteIndex.Value];
if (n != null)
{
this.initialText = this.TextBox.Text = n.GetContent();
this.TextBox.Background = n.ScreenBrush;
this.TextBox.Foreground = n.TextBrush;
this.TextBox.FontSize = n.TextSize;
}
}
void TextBox_GotFocus(object sender, RoutedEventArgs e)
{
this.ApplicationBar.IsVisible = false;
}
void TextBox_LostFocus(object sender, RoutedEventArgs e)
{
this.ApplicationBar.IsVisible = true;
}
// Application bar handlers:
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to delete this note?”,
“Delete note?”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
Note n = Settings.NotesList.Value[Settings.CurrentNoteIndex.Value];
n.DeleteContent();
Settings.NotesList.Value.Remove(n);
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
}
void EmailButton_Click(object sender, EventArgs e)
{
EmailComposeTask launcher = new EmailComposeTask();
launcher.Body = this.TextBox.Text;
launcher.Subject = “Note”;
launcher.Show();
}
void SettingsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
}
}

[/code]

  • In OnNavigatedFrom, the content of the text box is saved to a file via the SaveContent method shown later in this chapter. The Title property for each note is set at this point rather than dynamically when the property is accessed because it enables each title to be shown without reading each file. Otherwise, rendering the list on the main page would end up reading the contents of every file and take away the advantage of storing the notes in files!
  • The note’s contents are immediately shown inside OnNavigatedTo with the help of a GetContent method defined on Note.
  • Inside DeleteButton_Click, the note’s DeleteContent method ensures that the backing file doesn’t get left behind when a note is deleted.

The Note Class

Listing 22.4 shows the implementation of the modified Note class used by this app, with differences from the preceding chapter emphasized.

LISTING 22.4 Note.cs—The Code-Behind for Passwords & Secrets’Details Page

[code]

using System;
using System.ComponentModel;
using System.IO;
using System.IO.IsolatedStorage;
using System.Windows.Media;
namespace WindowsPhoneApp
{
public class Note : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
// A helper method used by the properties
void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
DateTimeOffset modified;
public DateTimeOffset Modified
{
get { return this.modified; }
set { this.modified = value; OnPropertyChanged(“Modified”); }
}
int textSize;
public int TextSize
{
get { return this.textSize; }
set { this.textSize = value; OnPropertyChanged(“TextSize”); }
}
Color screenColor;
public Color ScreenColor
{
get { return this.screenColor; }
set { this.screenColor = value;
OnPropertyChanged(“ScreenColor”); OnPropertyChanged(“ScreenBrush”); }
}
Color textColor;
public Color TextColor
{
get { return this.textColor; }
set { this.textColor = value;
OnPropertyChanged(“TextColor”); OnPropertyChanged(“TextBrush”); }
}
// Three readonly properties whose value is computed from other properties:
public Brush ScreenBrush
{
get { return new SolidColorBrush(this.ScreenColor); }
}
public Brush TextBrush
{
get { return new SolidColorBrush(this.TextColor); }
}
string title;
public string Title
{
get { return this.title; }
set { this.title = value; OnPropertyChanged(“Title”); }
}
public string Filename { get; set; }
public void SaveContent(string content)
{
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream stream =
userStore.CreateFile(this.Filename))
using (StreamWriter writer = new StreamWriter(stream))
{
writer.Write(content);
}
}
public string GetContent()
{
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
{
if (!userStore.FileExists(this.Filename))
return “”;
else
{
using (IsolatedStorageFileStream stream =
userStore.OpenFile(this.Filename, FileMode.Open))
using (StreamReader reader = new StreamReader(stream))
return reader.ReadToEnd();
}
}
}
public void DeleteContent()
{
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
userStore.DeleteFile(this.Filename);
}
}
}

[/code]

  • As implied earlier, the Title property is now a normal read-write property rather than a read-only property whose value is determined dynamically.
  • To save a new file, SaveContent first calls IsolatedStorageFile. GetUserStoreForApplication. This is the first step in any code that interacts directly with the isolated storage file system. The IsolatedStoreFile instance returned contains several methods for creating, enumerating, opening, and deleting files and folders. Once CreateFile is called, SaveContent uses a StreamWriter to easily write the passed-in string to the stream.
  • GetContent and DeleteContent work similarly to SaveContent, making use of three more methods on IsolatedStorageFile: FileExists, OpenFile, and DeleteFile. To keep the UI responsive while interacting with large files, this would be a good place to use BackgroundWorker.

When managing files, it’s tempting to simply use the IsolatedStorageFile. GetFileNames method to enumerate and perhaps display the files.This approach has problems, however. For example:

  • The isolated storage APIs don’t expose any way to discover the created/modified dates of files.Therefore, sorting files by such properties rather than the default alphabetical order requires you to store extra information (stored in the Note class in this app).
  • The list includes an extra file if you use any isolated storage application settings.These get persisted in an XML file called _ _ApplicationSettings in the root of your app’s isolated storage folder. Although you could manually filter this out, there’s no guarantee that there won’t be other special files in the future.
  • As in Windows, filenames have restrictions on their characters (such as no colons or question marks). If you use filenames as user-visible and potentially editable labels, you need to make sure you don’t introduce invalid characters.

The Settings Page

The settings page, shown in Figure 22.1, enables the customization of any note’s foreground color, background color, and text size. Although these settings are only applied to the current note (stored as properties on the Note instance), the user can check a check box to automatically apply the chosen settings to any new notes created in the future. Listing 22.5 contains the XAML for this page, and Listing 22.6 contains the code-behind.

FIGURE 22.1 The settings page exposes per-note settings and enables you to apply them to all future notes.
FIGURE 22.1 The settings page exposes per-note settings and enables you to apply them to all future notes.

LISTING 22.5 SettingsPage.xaml—The User Interface for Notepad’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>
<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=”notepad” Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<Grid Margin=”{StaticResource PhoneMargin}”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<CheckBox x:Name=”MakeDefaultCheckBox” Grid.ColumnSpan=”2”
Content=”Make these the default settings” Margin=”0,-4,0,0”
Checked=”MakeDefaultCheckBox_IsCheckedChanged”
Unchecked=”MakeDefaultCheckBox_IsCheckedChanged”
local:Tilt.IsEnabled=”True”/>
<!– The two colors –>
<TextBlock Grid.Row=”1” Text=”Screen color”
Foreground=”{StaticResource PhoneSubtleBrush}” Margin=”12,8”/>
<Rectangle Grid.Row=”2” x:Name=”ScreenColorRectangle”
Margin=”{StaticResource PhoneHorizontalMargin}” Height=”90”
Stroke=”{StaticResource PhoneForegroundBrush}”
StrokeThickness=”3” local:Tilt.IsEnabled=”True”
MouseLeftButtonUp=”ScreenColorRectangle_MouseLeftButtonUp”/>
<TextBlock Grid.Row=”1” Grid.Column=”1” Text=”Text color”
Foreground=”{StaticResource PhoneSubtleBrush}” Margin=”12,8”/>
<Rectangle Grid.Row=”2” Grid.Column=”1” x:Name=”TextColorRectangle”
Height=”90” StrokeThickness=”3” local:Tilt.IsEnabled=”True”
Margin=”{StaticResource PhoneHorizontalMargin}”
Stroke=”{StaticResource PhoneForegroundBrush}”
MouseLeftButtonUp=”TextColorRectangle_MouseLeftButtonUp”/>
<!– Text size –>
<TextBlock Grid.Row=”3” Grid.ColumnSpan=”2” Text=”Text size”
Foreground=”{StaticResource PhoneSubtleBrush}”
Margin=”12,20,12,-14”/>
<Grid Grid.Row=”4” Grid.ColumnSpan=”2”>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width=”Auto”/>
</Grid.ColumnDefinitions>
<Slider x:Name=”TextSizeSlider” Minimum=”12” Maximum=”100”
ValueChanged=”TextSizeSlider_ValueChanged”/>
<Button x:Name=”ResetButton” Grid.Column=”1” Content=”reset”
VerticalAlignment=”Top” Click=”ResetButton_Click”
local:Tilt.IsEnabled=”True”/>
</Grid>
<!– Sample text –>
<Rectangle x:Name=”SampleBackground” Grid.Row=”5” Grid.ColumnSpan=”2”
Margin=”-12,0,-12,-12”/>
<TextBlock x:Name=”SampleTextBlock” Grid.Row=”5” Grid.ColumnSpan=”2”
Text=”Sample text.” Padding=”12”/>
</Grid>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 22.6 SettingsPage.xaml.cs—The Code-Behind for Notepad’s Settings Page

[code]

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class SettingsPage : PhoneApplicationPage
{
public SettingsPage()
{
InitializeComponent();
}
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
// Doing this here instead of OnNavigatedFrom, so it’s not
// applied when navigating forward to color picker pages
if (Settings.MakeDefault.Value)
{
// Apply everything as defaults, too
Note n = Settings.NotesList.Value[Settings.CurrentNoteIndex.Value];
Settings.ScreenColor.Value = n.ScreenColor;
Settings.TextColor.Value = n.TextColor;
Settings.TextSize.Value = n.TextSize;
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
Note n = Settings.NotesList.Value[Settings.CurrentNoteIndex.Value];
// Apply any color just selected from the color picker
if (Settings.TempScreenColor.Value != null)
{
n.ScreenColor = Settings.TempScreenColor.Value.Value;
Settings.TempScreenColor.Value = null;
}
if (Settings.TempTextColor.Value != null)
{
n.TextColor = Settings.TempTextColor.Value.Value;
Settings.TempTextColor.Value = null;
}
// Respect the saved settings
this.MakeDefaultCheckBox.IsChecked = Settings.MakeDefault.Value;
this.ScreenColorRectangle.Fill = new SolidColorBrush(n.ScreenColor);
this.TextColorRectangle.Fill = new SolidColorBrush(n.TextColor);
this.SampleBackground.Fill = this.ScreenColorRectangle.Fill;
this.SampleTextBlock.Foreground = this.TextColorRectangle.Fill;
this.TextSizeSlider.Value = n.TextSize;
}
void ScreenColorRectangle_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.NotesList.Value
[Settings.CurrentNoteIndex.Value].ScreenColor.ToString().Substring(1);
string defaultColorString =
Settings.ScreenColor.Value.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.TempScreenColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=TempScreenColor”, UriKind.Relative));
}
void TextColorRectangle_MouseLeftButtonUp(object sender,
MouseButtonEventArgs e)
{
// Get a string representation of the colors, without the leading #
string currentColorString = Settings.NotesList.Value
[Settings.CurrentNoteIndex.Value].TextColor.ToString().Substring(1);
string defaultColorString =
Settings.TextColor.Value.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.TempTextColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “showOpacity=false”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=TempTextColor”, UriKind.Relative));
}
void TextSizeSlider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
// Gets called during InitializeComponent
if (this.TextSizeSlider != null)
{
int textSize = (int)Math.Round(this.TextSizeSlider.Value);
Settings.NotesList.Value[Settings.CurrentNoteIndex.Value].TextSize =
textSize;
this.SampleTextBlock.FontSize = textSize;
}
}
void MakeDefaultCheckBox_IsCheckedChanged(object sender, RoutedEventArgs e)
{
Settings.MakeDefault.Value = this.MakeDefaultCheckBox.IsChecked.Value;
}
void ResetButton_Click(object sender, RoutedEventArgs e)
{
int textSize = Settings.TextSize.DefaultValue;
this.TextSizeSlider.Value = textSize;
Settings.NotesList.Value[Settings.CurrentNoteIndex.Value].TextSize =
textSize;
this.SampleTextBlock.FontSize = textSize;
}
}
}

[/code]

To work with the color picker that writes directly to a key in the isolated storage application settings, TempScreenColor and TempTextColor settings are used. These values are then applied to the current note’s properties inside OnNavigatedTo.

The Finished Product

Notepad (Reading & Writing Files)

Lottery Numbers Picker (Sharing Animations)

Lottery Numbers Picker helps you choose potentially winning numbers to use when you buy a lottery ticket. First, you can tell it how many numbers it needs to select, what the range of the numbers should be, and whether duplicate numbers are allowed. Then, Lottery Numbers Picker selects the numbers with a fun animation that mimics a machine filled with percolating lottery balls (the kind you see on TV).

The Main Page

Lottery Numbers Picker has a main page, a settings page, and the standard about page.

The User Interface

Listing 16.1 contains the XAML for the main page.

LISTING 16.1 MainPage.xaml—The Main User Interface for Lottery Numbers Picker

[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 –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”pick”
IconUri=”/Shared/Images/appbar.play.png”
Click=”PickButton_Click”/>
<shell:ApplicationBarIconButton Text=”settings”
IconUri=”/Shared/Images/appbar.settings.png”
Click=”SettingsButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<!– Add one storyboard to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– This storyboard is applied to each chosen ball, one at a time –>
<Storyboard x:Name=”ChosenBallStoryboard”
Completed=”ChosenBallStoryboard_Completed”>
<!– Makes the chosen ball “blow” upward from the bottom –>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=”(Canvas.Top)”>
<LinearDoubleKeyFrame KeyTime=”0:0:0” Value=”728”/>
<EasingDoubleKeyFrame KeyTime=”0:0:1.5” Value=”100”>
<EasingDoubleKeyFrame.EasingFunction>
<ElasticEase Oscillations=”1” Springiness=”6” EasingMode=”EaseOut”/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<!– Makes the chosen ball slide to the left, once at the top –>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=”(Canvas.Left)”>
<LinearDoubleKeyFrame KeyTime=”0:0:0” Value=”424”/>
<LinearDoubleKeyFrame KeyTime=”0:0:1.2” Value=”424”/>
<EasingDoubleKeyFrame x:Name=”FinalLeftKeyFrame” KeyTime=”0:0:2”>
<EasingDoubleKeyFrame.EasingFunction>
<BounceEase EasingMode=”EaseOut” Bounces=”1” Bounciness=”8”/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<!– Spins the chosen ball while it moves up and left, and end upright –>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=
“(local:LotteryBall.RenderTransform).(RotateTransform.Angle)”>
<LinearDoubleKeyFrame KeyTime=”0:0:0” Value=”0”/>
<EasingDoubleKeyFrame KeyTime=”0:0:1.2” Value=”360”>
<EasingDoubleKeyFrame.EasingFunction>
<BounceEase Bounces=”10”/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime=”0:0:2” Value=”0”>
<EasingDoubleKeyFrame.EasingFunction>
<BounceEase EasingMode=”EaseOut” Bounces=”1” Bounciness=”8”/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– Prevent off-screen visuals from appearing during a page transition –>
<phone:PhoneApplicationPage.Clip>
<RectangleGeometry Rect=”0,0,480,728”/>
</phone:PhoneApplicationPage.Clip>
<Canvas>
<!– Mini-header –>
<TextBlock Text=”LOTTERY NUMBERS PICKER” Margin=”24,16,0,12”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<!– Chosen balls get dynamically added to this canvas –>
<Canvas x:Name=”ChosenBallsCanvas”/>
<!– A canvas filled with percolating plain balls –>
<Canvas>
<local:LotteryBall Percolating=”True” Canvas.Top=”630”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”40” Canvas.Top=”635”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”80” Canvas.Top=”630”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”120” Canvas.Top=”635”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”160” Canvas.Top=”630”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”200” Canvas.Top=”635”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”240” Canvas.Top=”630”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”280” Canvas.Top=”635”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”320” Canvas.Top=”630”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”360” Canvas.Top=”635”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”5” Canvas.Top=”650”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”45” Canvas.Top=”655”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”85” Canvas.Top=”650”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”125” Canvas.Top=”655”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”165” Canvas.Top=”650”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”205” Canvas.Top=”655”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”245” Canvas.Top=”650”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”285” Canvas.Top=”655”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”325” Canvas.Top=”650”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”365” Canvas.Top=”655”/>
<local:LotteryBall Percolating=”True” Canvas.Top=”670”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”40” Canvas.Top=”675”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”80” Canvas.Top=”670”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”120” Canvas.Top=”675”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”160” Canvas.Top=”670”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”200” Canvas.Top=”675”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”240” Canvas.Top=”670”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”280” Canvas.Top=”675”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”320” Canvas.Top=”670”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”360” Canvas.Top=”675”/>
</Canvas>
<!– The container of balls –>
<Rectangle Fill=”{StaticResource PhoneAccentBrush}” Canvas.Top=”153”
Width=”421” Height=”600” Opacity=”.5”/>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

Notes:

  • The single storyboard in Listing 16.1 doesn’t have Storyboard.TargetName assigned. That’s because this storyboard is dynamically assigned to each new ball that reveals a lottery number, as you’ll see in the code-behind. It contains three animations: one to move the ball upward along the right side of the screen, one to move it over to the left, and one to spin it the whole time. The animations are given various easing functions with various properties set to give a lifelike appearance of each ball being blown into place.
  • Although the chosen lottery balls are dynamically added to ChosenBallsCanvas, the percolating balls in the ball machine have been statically added to the next canvas in hardcoded locations. Each ball is represented by a LotteryBall user control, shown in the next section. Because their custom Percolating property is set to true, these balls bounce erratically.

The Code-Behind

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

LISTING 16.2 MainPage.xaml.cs—The Code-Behind for Lottery Numbers Picker’s Main Page

[code]

using System;
using System.Collections.Generic;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
Random random = new Random();
List<int> chosenNumbers = new List<int>();
LotteryBall mostRecentBall;
IApplicationBarIconButton pickButton;
public MainPage()
{
InitializeComponent();
this.pickButton = this.ApplicationBar.Buttons[0]
as IApplicationBarIconButton;
}
void ShowNewBall()
{
// Create a new ball with the correct chosen number.
// The RotateTransform is there so the spinning animation works.
this.mostRecentBall = new LotteryBall {
RenderTransform = new RotateTransform(),
Number = this.chosenNumbers[this.ChosenBallsCanvas.Children.Count]
};
// Assign the storyboard to this new ball
Storyboard.SetTarget(this.ChosenBallStoryboard, this.mostRecentBall);
// Adjust the final horizontal position of the ball based on which one it is
this.FinalLeftKeyFrame.Value = this.mostRecentBall.Width *
this.ChosenBallsCanvas.Children.Count;
// Add the new ball to the canvas
this.ChosenBallsCanvas.Children.Add(this.mostRecentBall);
// Start animating
this.ChosenBallStoryboard.Begin();
}
void ChosenBallStoryboard_Completed(object sender, EventArgs e)
{
// The storyboard must be stopped before its
// target is changed again inside ShowNewBall
this.ChosenBallStoryboard.Stop();
// Manually position the ball in the same spot where the animation left it
Canvas.SetTop(this.mostRecentBall, 100);
Canvas.SetLeft(this.mostRecentBall,
this.mostRecentBall.Width * (this.ChosenBallsCanvas.Children.Count – 1));
// Keep going until enough balls have been chosen
if (this.ChosenBallsCanvas.Children.Count < Settings.NumBalls.Value)
ShowNewBall();
else
this.pickButton.IsEnabled = true;
}
// Application bar handlers
void PickButton_Click(object sender, EventArgs e)
{
this.pickButton.IsEnabled = false;
this.chosenNumbers.Clear();
this.ChosenBallsCanvas.Children.Clear();
// Pick all the numbers
for (int i = 0; i < Settings.NumBalls.Value; i++)
{
// If no duplicate numbers are allowed, keep
// picking until the number is unique
int num;
do
{
num = this.random.Next(Settings.MinNumber.Value,
Settings.MaxNumber.Value + 1);
}
while (!Settings.AllowDuplicates.Value &&
this.chosenNumbers.Contains(num));
this.chosenNumbers.Add(num);
}
// Sort the chosen numbers in increasing numeric order
this.chosenNumbers.Sort();
// Reveal the first ball
ShowNewBall();
}
void SettingsButton_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(
“/Shared/About/AboutPage.xaml?appName=Lottery Numbers Picker”,
UriKind.Relative));
}
}
}

[/code]

Notes:

  • Inside ShowNewBall, a number is assigned to the new ball from the chosenNumbers list that is filled inside PickButton_Click. The number of children in ChosenBallsCanvas can be used as the list’s index before the new ball is added to the canvas because the number is 0 for the first ball, 1 for the second ball, and so on.
  • Rather than calling Storyboard.SetTargetName (which effectively sets the Storyboard.TargetName attachable property), the code calls Storyboard.SetTarget to assign the target element to the storyboard. This is easier to use in C#, because you can simply pass it the instance of the element to animate even when it doesn’t have a name.
  • The storyboard’s Completed event handler (ChosenBallStoryboad_ Completed) calls ShowNewBall to repeat the creation and animation of a new ball until the correct number of balls have been shown. It must stop the storyboard, rather than having it remain in a filling state, because otherwise the next call to Storyboard.SetTarget inside ShowNewBall would fail. You cannot change a storyboard’s target unless it is completely stopped. Because the storyboard is stopped, the just-animated ball is manually given its ending position. Without this code, the ball would snap back to its pre-animation position. An alternative approach would be to get the ball’s position before stopping the storyboard and then setting it to that position after stopping it.

The LotteryBall User Control

The LotteryBall user control used by the main page represents each circle with an optional number centered on it. The XAML for this user control is shown in Listing 16.3. The most interesting part of this XAML is that the control uses a TransformGroup for its translation and rotation rather than a CompositeTransform. This is because a CompositeTransform performs rotation before translation, but the functionality of this control requires that the rotation happens after the translation. Therefore, TransformGroup is used with its TranslateTransform child placed before the RotateTransform child.

LISTING 16.3 LotteryBall.xaml—The User Interface for the LotteryBall User Control

[code]

<UserControl x:Class=”WindowsPhoneApp.LotteryBall”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
FontFamily=”Segoe WP Black” FontSize=”{StaticResource PhoneFontSizeLarge}”
Width=”53” Height=”53” RenderTransformOrigin=”.5,.5”>
<UserControl.RenderTransform>
<TransformGroup>
<TranslateTransform x:Name=”TranslateTransform”/>
<RotateTransform x:Name=”RotateTransform”/>
</TransformGroup>
</UserControl.RenderTransform>
<Grid>
<Ellipse Fill=”{StaticResource PhoneForegroundBrush}”/>
<TextBlock x:Name=”NumberTextBlock” Margin=”1,0,0,3”
Foreground=”{StaticResource PhoneBackgroundBrush}”
HorizontalAlignment=”Center” VerticalAlignment=”Center”/>
</Grid>
</UserControl>

[/code]

Listing 16.4 contains the code-behind for this user control.

LISTING 16.4 LotteryBall.xaml.cs—The Code-Behind for the LotteryBall User Control

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
namespace WindowsPhoneApp
{
public partial class LotteryBall : UserControl
{
int number;
bool percolating;
Storyboard percolatingStoryboard;
DoubleAnimation percolatingAnimation;
Random random = new Random();
public LotteryBall()
{
InitializeComponent();
}
public int Number
{
get { return this.number; }
set
{
this.NumberTextBlock.Text = value.ToString();
this.number = value;
}
}
public bool Percolating
{
get { return this.percolating; }
set
{
this.percolating = value;
if (this.percolating)
{
// Create a new single-animation storyboard
this.percolatingStoryboard = new Storyboard();
this.percolatingAnimation = new DoubleAnimation { AutoReverse = true };
this.percolatingAnimation.EasingFunction = new QuadraticEase {
EasingMode = EasingMode.EaseInOut };
this.percolatingStoryboard.Children.Add(this.percolatingAnimation);
// Assign the storyboard to this instance’s TranslateTransform and
// animate its Y property to create a “bounce”
Storyboard.SetTarget(this.percolatingStoryboard,
this.TranslateTransform);
Storyboard.SetTargetProperty(this.percolatingStoryboard,
new PropertyPath(“Y”));
// When the “bounce” completes, choose new random values and start
// it again. Repeat indefinitely.
this.percolatingStoryboard.Completed += delegate(object s, EventArgs e)
{
Randomize();
this.percolatingStoryboard.Begin();
};
// Choose random values related to the animation
// and start it for the first time
Randomize();
percolatingStoryboard.Begin();
}
}
}
void Randomize()
{
// Vary the distance and duration of the bounce
this.percolatingAnimation.To = this.random.Next(20, 60) * -1;
this.percolatingAnimation.Duration = TimeSpan.FromMilliseconds(
this.random.Next(50, 200));
// Very the angle of the bounce
this.RotateTransform.Angle = this.random.Next(0, 90) * -1;
}
}
}

[/code]

Notes:

  • This user control exposes two properties: Number, which displays the input number on top of the ellipse, and Percolating, which makes the ball bounce erratically when set to true. In this app, only Number is set for the chosen balls and only Percolating is set for the balls inside the machine.
  • The erratic bouncing of a percolating ball is enabled by a short random bounce storyboard that, when completed, is given new random values before beginning again. This storyboard and animation is created entirely in C#, which has not previously been seen in this book. The code mirrors what you would write in XAML, except that the adding of the animation to the storyboard is more explicit (with a call to Children.Add), and the property path used for specifying the target property is also more explicit. As with Listing 16.2, SetTarget is used to directly associate the storyboard with the target object.
  • Inside Randomize, the length of each bounce is allowed to vary from 20 to 59 pixels upward (in the negative direction), and the duration of the upward motion is allowed to vary from 50 to 199 milliseconds. To prevent each ball from bouncing straight up, the angle of the user control is randomly set to a range of 0 to –90°. Although the angle is not part of the animation, it affects the animation because it changes the meaning of translating upward. This is why it’s important that the RotateTransform appears after the TranslateTransform in Listing 16.3. Although the ball is translated upward from its origin, the whole coordinate space is then rotated, causing the ball to translate at whatever angle has been chosen. (If the rotation happened first, it would have no effect on the symmetric plain circle.) The range of 0 to –90° is chosen so the balls don’t ever bounce to the right and break the illusion that they are contained inside the machine.

The Settings Page

The Lottery Numbers Picker app uses the following settings defined in Settings.cs:

[code]

public static class Settings
{
public static readonly Setting<int> NumBalls =
new Setting<int>(“NumBalls”, 5);
public static readonly Setting<int> MinNumber =
new Setting<int>(“MinNumber”, 1);
public static readonly Setting<int> MaxNumber =
new Setting<int>(“MaxNumber”, 56);
public static readonly Setting<bool> AllowDuplicates =
new Setting<bool>(“AllowDuplicates”, false);
}

[/code]

The settings page, shown in Figure 16.1, enables the user to adjust all these settings.

FIGURE 16.1 The settings page contains a check box and three sliders.
FIGURE 16.1 The settings page contains a check box and three sliders.

The User Interface

Listing 16.5 contains the XAML for the settings page.

LISTING 16.5 SettingsPage.xaml—The User Interface for Lottery Numbers Picker’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>
<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=”lottery numbers picker”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<Grid Margin=”{StaticResource PhoneMargin}”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
</Grid.RowDefinitions>
<!– Allow duplicate numbers –>
<CheckBox x:Name=”AllowDuplicatesCheckBox” HorizontalAlignment=”Left”
Content=”Allow duplicate numbers” local:Tilt.IsEnabled=”True”/>
<!– Number of balls –>
<TextBlock Grid.Row=”1” Text=”Number of balls” Margin=”12,16,0,8”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBlock x:Name=”NumBallsTextBlock” Grid.Row=”1” Margin=”0,16,23,0”
HorizontalAlignment=”Right”
Text=”{Binding Value, ElementName=NumBallsSlider}”/>
<Slider x:Name=”NumBallsSlider” Grid.Row=”2” Minimum=”1” Maximum=”8”
Tag=”{Binding ElementName=NumBallsTextBlock}”
ValueChanged=”Slider_ValueChanged” />
<!– Smallest possible number –>
<TextBlock Grid.Row=”3” Text=”Smallest possible number” Margin=”12,7,0,8”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBlock x:Name=”MinNumberTextBlock” Grid.Row=”3” Margin=”0,7,23,0”
HorizontalAlignment=”Right”
Text=”{Binding Value, ElementName=MinNumberSlider}”/>
<Slider x:Name=”MinNumberSlider” Grid.Row=”4” Minimum=”0” Maximum=”98”
Tag=”{Binding ElementName=MinNumberTextBlock}”
ValueChanged=”Slider_ValueChanged” />
<!– Largest possible number –>
<TextBlock Grid.Row=”5” Text=”Largest possible number” Margin=”12,7,0,8”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBlock x:Name=”MaxNumberTextBlock” Grid.Row=”5” Margin=”0,7,23,0”
HorizontalAlignment=”Right”
Text=”{Binding Value, ElementName=MaxNumberSlider}”/>
<Slider x:Name=”MaxNumberSlider” Grid.Row=”6” Minimum=”1” Maximum=”99”
Tag=”{Binding ElementName=MaxNumberTextBlock}”
ValueChanged=”Slider_ValueChanged” />
</Grid>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

Notes:

  • AllowDuplicatesCheckBox is aligned to the left to prevent accidental tapping and over-tilting if the right side of the screen is tapped.
  • NumBallsSlider enables a range from 1 to 8, MinNumberSlider enables a range from 0 to 98, and MaxNumberSlider enables a range from 1 to 99. Each slider has a corresponding text block that binds its text to the slider’s current value. This only works appropriately due to logic in code-behind in an event handler for each slider’s ValueChanged event.

The Code-Behind

Listing 16.6 contains the code-behind for the settings page.

LISTING 16.6 SettingsPage.xaml.cs—The Code-Behind for Lottery Numbers Picker’s Settings Page

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class SettingsPage : PhoneApplicationPage
{
bool initialized = false;
public SettingsPage()
{
InitializeComponent();
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Save the chosen settings
Settings.AllowDuplicates.Value =
this.AllowDuplicatesCheckBox.IsChecked.Value;
Settings.NumBalls.Value = (int)this.NumBallsSlider.Value;
Settings.MinNumber.Value = (int)this.MinNumberSlider.Value;
Settings.MaxNumber.Value = (int)this.MaxNumberSlider.Value;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the saved settings
this.AllowDuplicatesCheckBox.IsChecked = Settings.AllowDuplicates.Value;
this.NumBallsSlider.Value = Settings.NumBalls.Value;
this.MinNumberSlider.Value = Settings.MinNumber.Value;
this.MaxNumberSlider.Value = Settings.MaxNumber.Value;
this.initialized = true;
}
void Slider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
Slider slider = sender as Slider;
// Round the value so it’s a whole number even when the slider is dragged
slider.Value = Math.Round(slider.Value);
// Don’t do anything until all initial values have been set
if (this.initialized)
{
// Don’t allow min to be higher than max
if (this.MinNumberSlider.Value > this.MaxNumberSlider.Value)
this.MaxNumberSlider.Value = this.MinNumberSlider.Value;
// If the range is too small, auto-allow duplicates
if (this.MinNumberSlider.Value >=
this.MaxNumberSlider.Value – this.NumBallsSlider.Value)
{
this.AllowDuplicatesCheckBox.IsChecked = true;
this.AllowDuplicatesCheckBox.IsEnabled = false;
}
else
{
this.AllowDuplicatesCheckBox.IsEnabled = true;
}
}
}
bool IsMatchingOrientation(PageOrientation orientation)
{
return ((this.Orientation & orientation) == orientation);
}
}
}

[/code]

Slider_ValueChanged forces each slider’s value to an integral value, because it could otherwise become fractional when the user drags the slider. (When this Math.Round assignment actually changes the value, it causes Slider_ValueChanged to be called again. When the value is already a whole number, this does not happen, which is important for preventing an infinite loop.) This method also guards against conditions that would cause the app to crash or hang, enforcing a valid relationship between the minimum number and the maximum number.

The Finished Product

Lottery Numbers Picker (Sharing Animations)

Silly Eye (Intro to Animation)

Silly Eye is a crowd-pleaser, especially when the crowd contains children. This app displays a large cartoonish eye that animates in a funny, frantic way that can’t be conveyed on paper. Simply hold it up to your right eye and pretend it’s your own silly eye! Figure 12.1 demonstrates how to use this app.

Introducing Animation

When most people think about animation, they think of a cartoon-like mechanism, where movement is simulated by displaying images in rapid succession. In Silverlight, animation has a more specific definition: varying the value of a property over time. This could be related to motion, such as making an element grow by increasing its width, or it could be something completely different like varying an element’s opacity.

There are many ways to change a property’s value over time. The classic approach is to use a timer, much like the DispatcherTimer used in previous chapters, and use a method that is periodically called back based on the frequency of the timer (the Tick event handler). Inside this method, you can manually update the target property (doing a little math to determine the current value based on the elapsed time) until it reaches the final value. At that point, you can stop the timer and/or remove the event handler.

FIGURE 12.1 Give yourself a silly eye by holding the phone up to your right eye.
FIGURE 12.1 Give yourself a silly eye by holding the phone up to your right eye.

However, Silverlight provides an animation mechanism that is much easier to use, more powerful, and performs better than a timer-based approach. It is centered around an object known as a storyboard. Storyboards contain one or more special animation objects and apply them to specific properties on specific elements.

Silly Eye uses three storyboards to perform its animations. To understand what storyboards are and how they work, we’ll examine each one:

  • The pupil storyboard
  • The iris storyboard
  • The eyelid storyboard

The Pupil Storyboard

Here is the storyboard that Silly Eye applies to the pupil to make it appear to repeatedly grow and shrink:

[code]

<Storyboard x:Name=”PupilStoryboard”
Storyboard.TargetName=”Pupil”
Storyboard.TargetProperty=”StrokeThickness”>
<DoubleAnimation From=”100” To=”70” Duration=”0:0:.5”
AutoReverse=”True” RepeatBehavior=”Forever”>
<DoubleAnimation.EasingFunction>
<ElasticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>

[/code]

Notes:

  • The Storyboard.TargetName attachable property indicates that this animation is being applied to an element on the page named Pupil. Pupil is an ellipse defined as follows:

    [code]<Ellipse x:Name=”Pupil” Width=”238” Height=”237” StrokeThickness=”100”
    Fill=”Black”/>[/code]

    (The brush for its stroke is set in code-behind.)

  • The Storyboard.TargetProperty attachable property indicates that Pupil’s StrokeThickness property is being animated.
  • The DoubleAnimation inside the storyboard indicates that StrokeThickness will be animated from 100 to 70 over a duration of half a second. The “Double” in DoubleAnimation represents the type of the target property being animated. (StrokeThickness is a double.)
  • Because AutoReverse is set to true, StrokeThickness will automatically animate back to 100 after reaching the end value of 70. Because RepeatBehavior is set to Forever, this cycle from 100 to 70 to 100 will repeat indefinitely once the animation has started.
  • The EasingFunction property (set to an instance of an ElasticEase) controls how the value of StrokeThickness is interpolated over time. This is discussed in the upcoming “Interpolation” section.

To begin the animation, the storyboard’s Begin method is called as follows:

[code]this.PupilStoryboard.Begin();[/code]

The result of this animation is shown in Figure 12.2 in the context of the entire app. The Pupil ellipse has been given a light blue stroke via code-behind.

FIGURE 12.2 PupilStoryboard makes Pupil’s stroke thickness (seen in blue) shrink from 100 down to 70, causing its black fill to appear to grow.
FIGURE 12.2 PupilStoryboard makes Pupil’s stroke thickness (seen in blue) shrink from 100 down to 70, causing its black fill to appear to grow.

There is a way to trigger a storyboard entirely in XAML so there’s no need for a call to its Begin method in code-behind.You can add an event trigger to an element’s Triggers property.This can look as follows:

[code]

<Grid>
<Grid.Triggers>
<EventTrigger RoutedEvent=”Grid.Loaded”>
<BeginStoryboard>
<Storyboard Storyboard.TargetName=”Pupil”
Storyboard.TargetProperty=”StrokeThickness”>
<DoubleAnimation To=”70” Duration=”0:0:.5” AutoReverse=”True”
RepeatBehavior=”Forever”>
<DoubleAnimation.EasingFunction>
<ElasticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>

</Grid>

[/code]

Thanks to the special BeginStoryboard element, this internally calls Begin on the storyboard in response to the grid’s Loaded event.The Loaded event is the only event supported by event triggers in Silverlight.

Types of Animations

Silverlight provides animation classes to animate four different data types: double, Color, Point, and object. Only double properties are animated in Silly Eye.

If you want to vary the value of an element’s double property over time (such as Width, Height, Opacity, Canvas.Left, and so on), you can use an instance of DoubleAnimation. If you instead want to vary the value of an element’s Point property over time (such as a linear gradient brush’s StartPoint or EndPoint property), you could use an instance of PointAnimation. DoubleAnimation is by far the most commonly used animation class due to large the number of properties of type double that make sense to animate.

Interpolation

It’s important to note that, by default, DoubleAnimation takes care of smoothly changing the double value over time via linear interpolation. In other words, for a one-second animation from 50 to 100, the value is 55 when 0.1 seconds have elapsed (10% progress in both the value and time elapsed), 75 when 0.5 seconds have elapsed (50% progress in both the value and time elapsed), and so on. This is why StrokeThickness is shown with a value of 85 halfway through the animation in Figure 12.2.

Most animations used in Windows Phone apps are not linear, however. Instead, they tend to “spring” from one value to another with a bit of acceleration or deceleration. This makes the animations more lifelike and interesting. You can produce such nonlinear animations by applying an easing function.

An easing function is responsible for doing custom interpolation from the starting value to the ending value. The pupil storyboard uses an easing function called ElasticEase to make its behavior much more “silly” than linear. Figure 12.3 graphs how the interpolation from 100 to 70 differs between the default linear behavior and the elastic ease behavior. In this case, the midpoint value of 85 actually isn’t reached half-way through the animation, but rather right toward the end.

FIGURE 12.3 The ElasticEase easing function drastically alters how a double value changes from 100 to 70.
FIGURE 12.3 The ElasticEase easing function drastically alters how a double value changes from 100 to 70.

Silverlight provides eleven different easing functions, each with three different modes, and several with properties to further customize their behavior. For example, ElasticEase has Oscillations and Springiness properties, both set to 3 by default. Combined with the fact that you can write your own easing functions to plug into animations, the possibilities for custom behaviors are endless. The easing functions used in this app give a wildly different experience than the default linear behavior.

The Iris Storyboard

Silly Eye applies the following storyboard to a canvas called Iris to make the eyeball appear to move left and right:

[code]

<Storyboard x:Name=”IrisStoryboard”
Storyboard.TargetName=”Iris”
Storyboard.TargetProperty=”(Canvas.Left)”>
<DoubleAnimation To=”543” Duration=”0:0:2”
AutoReverse=”True” RepeatBehavior=”Forever”>
<DoubleAnimation.EasingFunction>
<BounceEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>

[/code]

Notes:

  • The syntax for TargetProperty is sometimes more complex than just a property name. When set to an attachable property such as Canvas.Left, it must be surrounded in parentheses.
  • The animation has a different easing function applied that gives the movement a noticeable bounciness. See Appendix D for a graph of BounceEase behavior.
  • The animation is missing a From value! This is okay and often recommended. When no From is specified, the animation starts with the target property’s current value, whatever it may be. Similarly, an animation can specify a From but no To! This animates the property from the value specified in From to whatever its current (pre-animation) value is.

You must specify both From and To if the current property can’t be interpolated!

If you try to animate the width or height of an auto-sized element with a From-less or To-less animation, nothing happens. Elements are auto-sized when their width and height are set to Double.NaN (not-a-number), and the DoubleAnimation can’t interpolate between two values when one of them isn’t even a number. Furthermore, applying the animation to ActualWidth or ActualHeight (which is set to the true width/height rather than NaN) isn’t an option because these properties are read-only and they are not dependency properties. Instead, you must explicitly set the width/height of the target element for such an animation to work.

As with the pupil storyboard, this storyboard’s Begin method is called to make it start:

[code]this.IrisStoryboard.Begin();[/code]

The result of this animation is shown in Figure 12.4. The Iris canvas contains the Pupil ellipse (whose stroke is actually the iris) along with two other ellipses that give the iris its “shine.” Because the position of the parent canvas is animated, all these contents move together.

FIGURE 12.4 IrisStoryboard moves the Iris canvas horizontally from 287 (its initial Canvas.Left value) to 543.
FIGURE 12.4 IrisStoryboard moves the Iris canvas horizontally from 287 (its initial Canvas.Left value) to 543.

Animations also have a By field that can be set instead of the To field.The following animation means “animate the value by adding 256 to its current value”:

[code]<DoubleAnimation By=”256” Duration=”0:0:2”/>[/code]

Negative values are supported for shrinking the current value.

The Eyelid Animation

The final storyboard used by Silly Eye animates two properties on a skin-colored Eyelid ellipse to simulate blinking:

[code]

<Storyboard x:Name=”EyelidStoryboard”
Storyboard.TargetName=”Eyelid”
RepeatBehavior=”Forever” Duration=”0:0:3”>
<DoubleAnimation Storyboard.TargetProperty=”Height”
To=”380” Duration=”0:0:.1” AutoReverse=”True”/>
<DoubleAnimation Storyboard.TargetProperty=”(Canvas.Top)”
To=”50” Duration=”0:0:.1” AutoReverse=”True”/>
</Storyboard>

[/code]

The Eyelid ellipse is defined as follows:

[code]

<Ellipse x:Name=”Eyelid” Canvas.Left=”73” Canvas.Top=”-145”
Width=”897” Height=”770” StrokeThickness=”200”/>

[/code]

As with the Pupil ellipse, the skin-colored brush for its stroke is set in code-behind.

Notes:

  • There’s a reason that Storyboard.TargetName and Storyboard.TargetProperty are attachable properties: They can be set on individual animations to override any storyboard-wide settings. This storyboard targets both the Height and Canvas.Top properties on the target Eyelid ellipse. Therefore, a single target name is marked on the storyboard but separate target properties are marked for each animation.
  • Canvas.Top is animated in sync with Height so the ellipse stays centered as it shrinks vertically.
  • The two animations both use the default linear interpolation behavior. Their motion is so quick that it’s not necessary to try anything more lifelike.
  • A storyboard is more than just a simple container that associates animations with target objects and their properties. This storyboard has its own duration and repeat behavior! The two animations only last .2 seconds (.1 seconds to animate from the current value to 380 and 50, and another .1 seconds to animate back to the original values due to the auto-reverse setting). However, because the storyboard is given a duration of 3 seconds, and because it has the auto-reverse setting rather than its children, the animation remains stationery until the 3 seconds are up. At that point, the .2-second long movement occurs again, and the animation will then be still for another 2.8 seconds. Therefore, this storyboard makes the eyelid blink very quickly, but only once every 3 seconds.

The result of this animation is shown in Figure 12.5 (after calling Begin in C#). Because the Eyelid ellipse is the same color as the background (and intentionally covered on its left side by the black area), you can’t see the ellipse itself. Instead, you see the empty space inside it shrinking to nothing once the height of the ellipse (380) is less than two times its stroke thickness (400).

FIGURE 12.5 EyelidStoryboard compresses the height of the Eyelid ellipse and moves it downward to keep it centered.
FIGURE 12.5 EyelidStoryboard compresses the height of the Eyelid ellipse and moves it downward to keep it centered.

Storyboard and Animation Properties

You’ve already seen the Duration, AutoReverse, and RepeatBehavior properties, which can apply to individual animations or an entire storyboard. In total, there are six properties that can be applied to both storyboards and animations:

  • Duration—The length of the animation or storyboard, set to 1 second by default.
  • BeginTime—A timespan that delays the start of the animation or storyboard by the specified amount of time, set to 0 by default. A storyboard can use custom BeginTime values on its child animations to make them occur in sequence rather than simultaneously.
  • SpeedRatio—A multiplier applied to duration, set to 1 by default. You can set it to any double value greater than 0. A value less than 1 slows down the animation, and a value greater than 1 speeds it up. SpeedRatio does not impact BeginTime.
  • AutoReverse—Can be set to true to make an animation or storyboard “play backward” once it completes. The reversal takes the same amount of time as the forward progress, so SpeedRatio affects the reversal as well. Note that any delay specified via BeginTime does not delay the reversal; it always happens immediately after the normal part of the animation completes.
  • RepeatBehavior—Can be set to a timespan, or a string like “2x” or “3x”, or “Forever”. Therefore, you can use RepeatBehavior to make animations repeat themselves (or cut themselves short) based on a time cutoff, to make animations repeat themselves a certain number of times (even a fractional number of times like “2.5x”), or to make animations repeat themselves forever (as done in this chapter). If AutoReverse is true, the reversal is repeated as well.
  • FillBehavior—Can be set to Stop rather than its default value of HoldEnd, to make the animated properties jump back to their pre-animation values once the relevant animations are complete.

The Main Page

Silly Eye’s main page, whose XAML is in Listing 12.1, contains some vector graphics, an application bar, and the three storyboards just discussed. It also contains an “intro pane” that tells the user to tap the screen to begin, as shown in Figure 12.6. This is done so we can initially show the application bar but then hide it while the app is in use, as the buttons on the screen interfere with the effect. The intro pane informs the user that they can bring the application bar back at any time by tapping the screen.

FIGURE 12.6 The application bar is only visible when the “intro pane” is visible.
FIGURE 12.6 The application bar is only visible when the “intro pane” is visible.

LISTING 12.1 MainPage.xaml—The Main User Interface for Silly Eye

[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}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Landscape” Orientation=”Landscape”>
<!– The application bar, with 2 buttons and 1 menu item –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBarIconButton Text=”settings” Click=”SettingsButton_Click”
IconUri=”/Shared/Images/appbar.settings.png”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<!– Three storyboard resources –>
<phone:PhoneApplicationPage.Resources>
<!– Animate the stroke thickness surrounding the pupil –>
<Storyboard x:Name=”PupilStoryboard” Storyboard.TargetName=”Pupil”
Storyboard.TargetProperty=”StrokeThickness”>
<DoubleAnimation To=”70” Duration=”0:0:.5” AutoReverse=”True”
RepeatBehavior=”Forever”>
<DoubleAnimation.EasingFunction>
<ElasticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Animate the iris so it moves left and right –>
<Storyboard x:Name=”IrisStoryboard” Storyboard.TargetName=”Iris”
Storyboard.TargetProperty=”(Canvas.Left)”>
<DoubleAnimation To=”543” Duration=”0:0:2” AutoReverse=”True”
RepeatBehavior=”Forever”>
<DoubleAnimation.EasingFunction>
<BounceEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Animate the eyelid so it blinks –>
<Storyboard x:Name=”EyelidStoryboard” Storyboard.TargetName=”Eyelid”
RepeatBehavior=”Forever” Duration=”0:0:3”>
<DoubleAnimation Storyboard.TargetProperty=”Height”
To=”380” Duration=”0:0:.1” AutoReverse=”True”/>
<DoubleAnimation Storyboard.TargetProperty=”(Canvas.Top)”
To=”50” Duration=”0:0:.1” AutoReverse=”True”/>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– A 1×1 grid with IntroPanel on top of EyeCanvas –>
<Grid>
<Canvas x:Name=”EyeCanvas”
MouseLeftButtonDown=”EyeCanvas_MouseLeftButtonDown”>
<!– The eyeball –>
<Ellipse Canvas.Left=”270” Canvas.Top=”55” Width=”503” Height=”370”
Fill=”White”/>
<!– Four “bloodshot” curvy/angled paths –>
<Path Data=”M782,252 C648,224 666,270 666,270 L622,212 L604,230” Width=”190”
Height=”70” Canvas.Left=”588” Canvas.Top=”206” Stroke=”Red”
StrokeThickness=”8” Stretch=”Fill” StrokeEndLineCap=”Triangle”/>
<Path Data=”M658,122 C604,176 582,136 582,136 L586,190 L526,204” Width=”144”
Height=”94” Canvas.Left=”541” Canvas.Top=”91” Stretch=”Fill”
Stroke=”Red” StrokeThickness=”8” StrokeEndLineCap=”Triangle”/>
<Path Data=”M348,334 C414,296 386,296 428,314 C470,332 464,302 476,292
C488,282 498,314 500,306” Width=”164” Height=”56” Canvas.Left=”316”
Canvas.Top=”303” Stretch=”Fill” Stroke=”Red” StrokeThickness=”8”/>
<Path Data=”M324,164 C388,210 434,130 444,178 C454,226 464,226 470,224”
Width=”154” Height=”70” Canvas.Left=”322” Canvas.Top=”115”
Stretch=”Fill” Stroke=”Red” StrokeThickness=”8”/>
<!– The complete iris canvas –>
<Canvas x:Name=”Iris” Canvas.Left=”287” Canvas.Top=”124”>
<!– The pupil, whose stroke is the iris –>
<Ellipse x:Name=”Pupil” Width=”238” Height=”237” StrokeThickness=”100”
Fill=”Black”/>
<!– Two “shine” circles –>
<Ellipse Width=”73” Height=”72” Canvas.Left=”134” Canvas.Top=”28”
Fill=”#8DFFFFFF”/>
<Ellipse Width=”110” Height=”107” Canvas.Left=”20” Canvas.Top=”86”
Fill=”#5FFFFFFF”/>
</Canvas>
<!– The skin-colored eyelid –>
<Ellipse x:Name=”Eyelid” StrokeThickness=”200” Width=”897” Height=”770”
Canvas.Left=”73” Canvas.Top=”-145”/>
<!– The black area on the left side that defines the edge of the face –>
<Ellipse Stroke=”Black” StrokeThickness=”300” Width=”1270” Height=”2380”
Canvas.Left=”-105” Canvas.Top=”-1140”/>
</Canvas>
<!– Quick instructions shown at the beginning –>
<Grid x:Name=”IntroPanel” Opacity=”.8”
Background=”{StaticResource PhoneBackgroundBrush}”>
<!– Enable tapping anywhere except very close to the application bar –>
<TextBlock x:Name=”IntroTextBlock” Width=”700” Padding=”170”
MouseLeftButtonDown=”IntroTextBlock_MouseLeftButtonDown”
HorizontalAlignment=”Left” VerticalAlignment=”Stretch”
FontSize=”{StaticResource PhoneFontSizeExtraLarge}”>
Tap to begin.<LineBreak/>Later, tap to return.
</TextBlock>
</Grid>
</Grid>
</phone:PhoneApplicationPage>

[/code]

Notes:

  • The application bar contains links to a settings page, an instructions page, and an about page. The first two pages are shown in the next two sections.
  • Notice that the three storyboard resources are given names with x:Name rather than keys with x:Key! This is a handy trick that makes using resources from codebehind much more convenient. When you give a resource a name, it is used as the key in the dictionary and a field with that name is generated for access from C#!
  • The explicit From value has been removed from PupilStoryboard’s animation because it’s not necessary. It was included earlier in the chapter simply to help explain how animations work.
  • IntroTextBlock is the element that listens for taps and hides IntroPanel. It is given a width of 700 rather than the entire width of the page because if it gets too close to the application bar, users might accidentally tap it (and hide the application bar) when actually trying to tap the bar—especially its ellipsis.

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

LISTING 12.2 MainPage.xaml.cs—The Code-Behind for Silly Eye’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
// Start all the storyboards, which animate indefinitely
this.IrisStoryboard.Begin();
this.PupilStoryboard.Begin();
this.EyelidStoryboard.Begin();
// Prevent off-screen parts from being seen when animating to other pages
this.Clip = new RectangleGeometry { Rect = new Rect(0, 0,
Constants.SCREEN_WIDTH, Constants.SCREEN_HEIGHT) };
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Remember the intro panel’s visibility for deactivation/activation
this.State[“IntroPanelVisibility”] = this.IntroPanel.Visibility;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the saved settings for the skin and eye colors
SolidColorBrush skinBrush = new SolidColorBrush(Settings.SkinColor.Value);
this.Eyelid.Stroke = skinBrush;
this.EyeCanvas.Background = skinBrush;
this.Pupil.Stroke = new SolidColorBrush(Settings.EyeColor.Value);
// Restore the intro panel’s visibility if we’re being activated
if (this.State.ContainsKey(“IntroPanelVisibility”))
{
this.IntroPanel.Visibility =
(Visibility)this.State[“IntroPanelVisibility”];
this.ApplicationBar.IsVisible =
(this.IntroPanel.Visibility == Visibility.Visible);
}
}
protected override void OnOrientationChanged(OrientationChangedEventArgs e)
{
base.OnOrientationChanged(e);
// Keep the text block aligned to the opposite side as the application bar,
// to preserve the “dead zone” where tapping doesn’t hide the bar
if (e.Orientation == PageOrientation.LandscapeRight)
this.IntroTextBlock.HorizontalAlignment = HorizontalAlignment.Right;
else
this.IntroTextBlock.HorizontalAlignment = HorizontalAlignment.Left;
}
void IntroTextBlock_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// Hide IntroPanel and application bar when the text block is tapped
this.IntroPanel.Visibility = Visibility.Collapsed;
this.ApplicationBar.IsVisible = false;
}
void EyeCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// Show IntroPanel and application bar when the canvas is tapped
this.IntroPanel.Visibility = Visibility.Visible;
this.ApplicationBar.IsVisible = true;
}
// Application bar handlers
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void SettingsButton_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(
“/Shared/About/AboutPage.xaml?appName=Silly Eye”, UriKind.Relative));
}
}
}

[/code]

Notes:

  • The three storyboards are initiated from the constructor by name, thanks to the x:Name markings in XAML.
  • The page’s Clip property is set to a screen-size rectangular region. This is done to prevent the off-screen portions of the vector graphics from being rendered during the animated page-flip transition when navigating to another page. This not only prevents strange visual artifacts, but can be good for performance as well. All UI elements have this Clip property that can be set to an arbitrary geometry.
  • Two persisted settings are used for the skin and eye color, and they are respected in OnNavigatedTo. They do not need to be saved in OnNavigatedFrom because the settings page takes care of this. The settings are defined in a separate Settings.cs file as follows:

    [code]
    public static class Settings
    {
    public static readonly Setting<Color> EyeColor = new Setting<Color>(
    “EyeColor”, (Color)Application.Current.Resources[“PhoneAccentColor”]);
    public static readonly Setting<Color> SkinColor = new Setting<Color>(
    “SkinColor”, /* “Tan” */ Color.FromArgb(0xFF, 0xD2, 0xB4, 0x8C));
    }
    [/code]

  • The visibility of IntroPanel (and the application bar) is placed in page state so the page looks the same if deactivated and later activated.
  • The alignment of IntroTextBlock is adjusted in OnOrientationChanged to keep it on the opposite side of the application bar. Recall that the application bar appears on the left side of the screen for the landscape right orientation, and the right side of the screen for the landscape left orientation.

The Settings Page

Listing 12.3 contains the XAML for this app’s settings page, shown in Figure 12.7. It enables the user to choose different colors for the eye and the skin.

FIGURE 12.7 The settings page enables the user to change both of Silly Eye’s color settings.
FIGURE 12.7 The settings page enables the user to change both of Silly Eye’s color settings.

LISTING 12.3 SettingsPage.xaml—The User Interface for the 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>
<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=”silly eye” Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<!– A rectangle (and text block) for each of the two settings –>
<ScrollViewer Grid.Row=”1”>
<StackPanel Margin=”{StaticResource PhoneMargin}”>
<TextBlock Text=”Eye color” Foreground=”{StaticResource PhoneSubtleBrush}”
Margin=”12,7,12,8”/>
<Rectangle x:Name=”EyeColorRectangle” Margin=”12,0,12,18” Height=”90”
Stroke=”{StaticResource PhoneForegroundBrush}”
StrokeThickness=”3” local:Tilt.IsEnabled=”True”
MouseLeftButtonUp=”EyeColorRectangle_MouseLeftButtonUp”/>
<TextBlock Text=”Skin color”
Foreground=”{StaticResource PhoneSubtleBrush}”
Margin=”12,12,12,8”/>
<Rectangle x:Name=”SkinColorRectangle” Height=”90”
Margin=”{StaticResource PhoneHorizontalMargin}”
Stroke=”{StaticResource PhoneForegroundBrush}”
StrokeThickness=”3” local:Tilt.IsEnabled=”True”
MouseLeftButtonUp=”SkinColorRectangle_MouseLeftButtonUp”/>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • This page leverages the custom header styles from in App.xaml.
  • The two clickable regions that display the current colors look like buttons, but they are just rectangles. Their MouseLeftButtonUp event handlers take care of invoking the user interface that enables the user to change each color.
  • The main stack panel is placed in a scroll viewer even though the content completely fits on the screen in all orientations. This is a nice extra touch for users, as they are able to swipe the screen and easily convince themselves that there is no more content.

Listing 12.4 contains the code-behind for this settings page.

LISTING 12.4 SettingsPage.xaml.cs—The Code-Behind for the Settings Page

[code]

using System;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class SettingsPage : PhoneApplicationPage
{
public SettingsPage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the saved settings
this.EyeColorRectangle.Fill = new SolidColorBrush(Settings.EyeColor.Value);
this.SkinColorRectangle.Fill = new SolidColorBrush(Settings.SkinColor.Value);
}
void EyeColorRectangle_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.EyeColor.Value.ToString().Substring(1);
string defaultColorString =
Settings.EyeColor.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.EyeColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=EyeColor”, UriKind.Relative));
}
void SkinColorRectangle_MouseLeftButtonUp(object sender, MouseButtonEventArgs
e)
{
// Get a string representation of the colors, without the leading #
string currentColorString = Settings.SkinColor.Value.ToString().Substring(1);
string defaultColorString =
Settings.SkinColor.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.SkinColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “showOpacity=false”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=SkinColor”, UriKind.Relative));
}
}
}

[/code]

To enable the user to change each color, this page navigates to a color picker page pictured in Figure 12.8. This feature-filled page, shared by many apps, is included with this book’s source code. It provides a palette of standard colors but it also enables the user to finely customize the hue, saturation, and lightness of the color whether through interactive UI or by simply typing in a hex value (or any string recognized by XAML, such as “red”, “tan”, or “lemonchiffon”). It optionally enables adjusting the color’s opacity.

FIGURE 12.8 The color picker page provides a slick way to select a color.
FIGURE 12.8 The color picker page provides a slick way to select a color.

The color picker page accepts four parameters via its query string:

  • showOpacity—true by default, but can be set to false to hide the opacity slider. This also removes transparent from the palette of colors at the top, and it prevents users from typing in nonopaque colors. Therefore, when you set this to false, you can be sure that an opaque color will be chosen.
  • currentColor—The initial color selected when the page appears. It must be passed as a string that would be valid for XAML. If specified as a hex value, the # must be removed to avoid interfering with the URI.
  • defaultColor—The color that the user gets when they press the reset button on the color picker page. It must be specified in the same string format as currentColor.
  • settingName—A named slot in isolated storage where the chosen color can be found on return from the page. This is the same name used when constructing a Setting instance. The code in Listing 12.4’s OnNavigatedTo method automatically picks up the new value chosen when navigating back from the color picker page, but only because of the ForceRefresh call made before navigating to the color picker.

The Instructions Page

Listing 12.5 contains the XAML for the simple instructions page shown in Figure 12.9. Later chapters won’t bother showing the XAML for their instructions pages unless there’s something noteworthy inside.

FIGURE 12.9 The instructions page used by Silly Eye.
FIGURE 12.9 The instructions page used by Silly Eye.

LISTING 12.5 InstructionsPage.xaml—The User Interface for the Instructions Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.InstructionsPage”
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” shell:SystemTray.IsVisible=”True”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”SILLY EYE” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”instructions”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<TextBlock Margin=”24 12” TextWrapping=”Wrap”>
Hold up to your right eye, and watch the hilarity ensue!
<LineBreak/><LineBreak/>
Tapping the screen shows/hides the application bar on the side.
<LineBreak/><LineBreak/>
You can customize the eye color and/or skin color on the settings page.
</TextBlock>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • As with the settings page, the main content is placed in a scroll viewer simply to give the user feedback that there is no more content.
  • As with the intro pane on the main page, a single text block makes use of LineBreak elements to format its text.
  • The code-behind file, InstructionsPage.xaml.cs, has nothing more than the call to InitializeComponent in its constructor.

The Finished Product

Silly Eye