Pick a Card Magic Trick (3D Transforms)

The Pick a Card Magic Trick app enables you to amaze your friends with a slick magic trick that’s likely to keep them guessing how it’s done even after multiple performances. In this trick, you ask someone in your audience to name any card while the deck of cards is shuffling. After he or she names a card, you press “tap here when ready” to stop the shuffling, shown in Figure 17.1. You then tap the screen to flip over the card, and the card they just named is shown! You can tap the card again to start over.

FIGURE 17.1 The deck of cards shuffles in the background while the “tap here when ready” button is showing.
FIGURE 17.1 The deck of cards shuffles in the background while the “tap here when ready” button is showing.

The magician’s secret is not revealed in this book; you’ll have to run the app (or look at the full source code) that comes with this book in order to see how it is done! (My wife still can’t figure it out; she thinks the app is using speech recognition in order to know what card to show.) What is shown is the main lesson of this chapter—using 3D transforms to flip the playing cards.

3D Transforms

Unlike XNA, Silverlight does not provide a full 3D graphics engine. However, Silverlight enables you to perform the most common 3D effects with perspective transforms. These transforms escape the limitations of the 2D transforms by enabling you to rotate and translate an element in any or all of the three dimensions.

Perspective transforms are normally done with a class called PlaneProjection, which defines RotationX, RotationY, and RotationZ properties. The X and Y dimensions are defined as usual, and the Z dimension extends into and out of the screen, as illustrated in Figure 17.2. X increases from left-to-right, Y increases from topto- bottom, and Z increases from backto- front.

FIGURE 17.2 The three dimensions, relative to the phone screen.
FIGURE 17.2 The three dimensions, relative to the phone screen.

Although plane projections act like render transforms, they are not assigned to an element via the RenderTransform property, but rather a separate property called Projection. The following plane projections are marked on playing card images, producing the result in Figure 17.3:

[code]

<phone:PhoneApplicationPage …>
<StackPanel Orientation=”Horizontal”>
<Image Source=”Images/CardHA.png” Width=”150” Margin=”12”>
<Image.Projection>
<PlaneProjection RotationX=”55”/>
</Image.Projection>
</Image>
<Image Source=”Images/CardH2.png” Width=”150”>
<Image.Projection>
<PlaneProjection RotationY=”55”/>
</Image.Projection>
</Image>
<Image Source=”Images/CardH3.png” Width=”150” Margin=”36”>
<Image.Projection>
<PlaneProjection RotationZ=”55”/>
</Image.Projection>
</Image>
<Image Source=”Images/CardH4.png” Width=”150” Margin=”48”>
<Image.Projection>
<PlaneProjection RotationX=”30” RotationY=”30” RotationZ=”30”/>
</Image.Projection>
</Image>
</StackPanel>
</phone:PhoneApplicationPage>

[/code]

FIGURE 17.3 Using a plane projection to rotate the card around the X,Y, and Z axes and then all three axes.
FIGURE 17.3 Using a plane projection to rotate the card around the X,Y, and Z axes and then all three axes.

Notice that rotating around only the Z axis is like using a 2D RotateTransform, although the direction is reversed.

Although having permanently rotated elements might be interesting for some apps, normally plane projections are used as the target of an animation. Pick a Card leverages a plane projection for its card-flip animation, as well as its card-shuffling animation. Figure 17.4 demonstrates the 3D card flip. After the card back is rotated 90° (to be perpendicular to the screen and therefore temporarily invisible), the image is hidden to reveal the 9 of diamonds card front for the remaining 90° of the animation.

FIGURE 17.4 The 3D card flip is enabled with a plane projection and an animated RotationY property.
FIGURE 17.4 The 3D card flip is enabled with a plane projection and an animated RotationY property.

Much like the 2D transform classes, PlaneProjection defines additional properties for changing the center of rotation: CenterOfRotationX, CenterOfRotationY, and CenterOfRotationZ. The first two properties are relative to the size of the element, on a scale from 0 to 1. The CenterOfRotationZ property is always in terms of absolute pixels, as elements never have any size in the Z dimension to enable a relative specification. Pick a Card leverages CenterOfRotationX in its shuffle animation to make cards flip in from either the left edge of the screen or the right edge of the screen, as demonstrated in Figure 17.5 for the following XAML:

[code]

<phone:PhoneApplicationPage …>
<Grid>
<!– The card on the left –>
<Image Source=”Images/CardBack.png”>
<Image.Projection>
<PlaneProjection RotationY=”62” CenterOfRotationX=”0”/>
</Image.Projection>
</Image>
<!– The card on the right –>
<Image Source=”Images/CardBack.png”>
<Image.Projection>
<PlaneProjection RotationY=”-62” CenterOfRotationX=”1”/>
</Image.Projection>
</Image>
</Grid>
</phone:PhoneApplicationPage>

[/code]

FIGURE 17.5 Two playing cards that would normally overlap are given different centers of rotation, so they appear to flip in from opposite edges of the screen.
FIGURE 17.5 Two playing cards that would normally overlap are given different centers of rotation, so they appear to flip in from opposite edges of the screen.

PlaneProjection defines six properties for translating an element in any or all dimensions. GlobalOffsetX, GlobalOffsetY, and GlobalOffsetZ apply the translation after the rotation, so the offsets are relative to the global screen coordinates. LocalOffsetX, LocalOffsetY, and LocalOffsetZ apply the translation before the rotation, causing the rotation to be relative to the rotated coordinate space.

The Main Page

Pick a Card’s main page doesn’t do much; it’s a main menu that has two modes—an initial one for teaching you how to use the app, and one that hides the secrets once you have learned how to perform the trick. Both modes are shown in Figure 17.6.

FIGURE 17.6 The main page in two different modes.
FIGURE 17.6 The main page in two different modes.

The initial main menu has three buttons: one for instructions, one for performing the trick in a special practice mode, and one for the standard about page. Once the instructions have self-destructed (which can be done by tapping a button on the instructions page), the main menu has a button for performing the trick in its normal mode, and the same about button.

The User Interface

Listing 17.1 contains the XAML for the main page.

LISTING 17.1 MainPage.xaml—The User Interface for Pick a Card’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”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”5*”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<Rectangle Fill=”{StaticResource PhoneForegroundBrush}” Margin=”0,0,0,40”
Width=”158” Height=”200” VerticalAlignment=”Bottom”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/logo.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<TextBlock Text=”pick a card” Margin=”21,16,0,0”
Style=”{StaticResource PhoneTextTitle1Style}”/>
<Button x:Name=”InstructionsButton” Grid.Row=”1”
Content=”instructions for the new magician” Height=”100”
local:Tilt.IsEnabled=”True” Click=”InstructionsButton_Click”/>
<Button x:Name=”BeginButton” Grid.Row=”2”
Content=”practice (FOR YOUR EYES ONLY!)” Height=”100”
local:Tilt.IsEnabled=”True” Click=”BeginButton_Click”/>
<Button Content=”about” Grid.Row=”3” Height=”100” Click=”AboutButton_Click”
local:Tilt.IsEnabled=”True”/>
</Grid>
</phone:PhoneApplicationPage>

[/code]

Notes:

  • The page is set up for its initial mode. Code-behind transforms it to the other mode.
  • Rather than using an Image element to display the logo, Listing 17.1 uses the logo.png file as an opacity mask for a rectangle filled with the phone theme foreground color. This is done to enable the otherwise-white image to appear black under the light theme, as shown in Figure 17.7.
FIGURE 17.7 The opacity mask enables the image to remain the phone theme’s foreground color, regardless of the theme.
FIGURE 17.7 The opacity mask enables the image to remain the phone theme’s foreground color, regardless of the theme.

Opacity masks are often harmful for performance!

Using an opacity mask with an image brush is a neat trick for enabling nonvector content to respect the phone’s current theme.However, be aware that their use can severely hamper the performance of your app, especially when animations are involved.Opacity masks cause animations to be rasterized on the UI thread, even if they otherwise would have been able to completely run on the compositor thread.Therefore, use extreme caution when applying an opacity mask.You can check whether it is impacting your app by examining the frame rate counter with and without the opacity mask applied.

The Code-Behind

Listing 17.2 contains the code-behind for the main page, which consists of straightforward Click event handlers for each button and code to morph the main menu after the instructions have been hidden. This is based off of a single setting defined in Settings.cs:

[code]

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

LISTING 17.2 MainPage.xaml.cs—The Code-Behind for Pick a Card’s Main Page

[ocde]

using System;
using System.Windows;
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);
if (!Settings.PracticeMode.Value)
{
BeginButton.Content = “begin”;
InstructionsButton.Visibility = Visibility.Collapsed;
}
}
void BeginButton_Click(object sender, RoutedEventArgs e)
{
this.NavigationService.Navigate(new Uri(“/TrickPage.xaml”,
UriKind.Relative));
}
void InstructionsButton_Click(object sender, RoutedEventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void AboutButton_Click(object sender, RoutedEventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Pick a Card”, UriKind.Relative));
}
}
}

[/code]

The Trick Page

The “trick page” is used for both phases of the trick—the shuffling and the final card reveal. This same page is used whether the trick is running in “practice mode” or for real.

The User Interface

Listing 17.3 contains the XAML for the trick page.

LISTING 17.3 TrickPage.xaml—The User Interface for Pick a Card’s Trick Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.TrickPage”
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”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Portrait”>
<!– Prevent off-screen visuals from appearing during a page transition –>
<phone:PhoneApplicationPage.Clip>
<RectangleGeometry Rect=”0,0,480,800”/>
</phone:PhoneApplicationPage.Clip>
<!– Add two storyboards to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– The flip –>
<Storyboard x:Name=”FlipStoryboard”
Storyboard.TargetName=”ChosenCardProjection”
Storyboard.TargetProperty=”RotationY”
Completed=”FlipStoryboard_Completed”>
<DoubleAnimation By=”90” Duration=”0:0:.25”/>
</Storyboard>
<!– The shuffle, with separate left and right animations –>
<Storyboard x:Name=”ShuffleStoryboard”
Storyboard.TargetProperty=”RotationY”>
<DoubleAnimation Storyboard.TargetName=”NextCardLeftProjection” From=”120”
To=”0” Duration=”0:0:.2” RepeatBehavior=”Forever”
BeginTime=”0:0:.1”/>
<DoubleAnimation Storyboard.TargetName=”NextCardRightProjection”
From=”-120” To=”0” Duration=”0:0:.2”
RepeatBehavior=”Forever”/>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<Grid Background=”Black”>
<!– The card that flips over –>
<Grid>
<Grid.Projection>
<PlaneProjection x:Name=”ChosenCardProjection”/>
</Grid.Projection>
<Image x:Name=”CardFrontImage” RenderTransformOrigin=”.5,.5”>
<!– Reverse, so it looks correct when flipped over –>
<Image.RenderTransform>
<ScaleTransform ScaleX=”-1”/>
</Image.RenderTransform>
</Image>
<Image x:Name=”CardBackImage” Source=”Images/CardBack.png”/>
</Grid>
<!– More cards, for shuffling –>
<Image x:Name=”NextCardRightImage” Source=”Images/CardBack.png”>
<Image.Projection>
<PlaneProjection x:Name=”NextCardRightProjection” CenterOfRotationX=”1”/>
</Image.Projection>
</Image>
<Image x:Name=”NextCardLeftImage” Source=”Images/CardBack.png”>
<Image.Projection>
<PlaneProjection x:Name=”NextCardLeftProjection” CenterOfRotationX=”-1”/>
</Image.Projection>
</Image>
<!– The “tap here when ready” button and a translucent background–>
<Grid x:Name=”ReadyPanel” Background=”#7000”>
<Button Background=”{StaticResource PhoneBackgroundBrush}”
Content=”tap here when ready”
VerticalAlignment=”Center”/>
</Grid>
<!– Images for practice mode –>
<Image x:Name=”PracticeImage1” Visibility=”Collapsed”
Source=”Images/practice1.png”/>
<Image x:Name=”PracticeImage2” Visibility=”Collapsed”
Source=”Images/practice2.png”/>
</Grid>
</phone:PhoneApplicationPage>

[/code]

Notes:

  • The grid containing the chosen card has a plane projection (ChosenCardProjection) that is animated by FlipStoryboard to perform the 3D flip. This grid contains the image for the card front (chosen by code-behind) and the image for the card back. The card front image is reversed (with a ScaleTransform) so it appears correctly once the grid is flipped around. The animation only rotates the card 90°, because at that point the card back needs to be hidden so the card front can be seen for the remaining 90°. This is handled by the FlipStoryboard_Completed method in codebehind.
  • ShuffleStoryboard performs the shuffling by animating the plane projections on NextCardRightImage and NextCardLeftImage. These are given centers of rotation that make them flip from the outer edges of the screen, as seen back in Figure 17.1. The left image is given a center of –1 rather than 0 to give a more realistic, asymmetric effect.

The Code-Behind

Listing 17.4 contains the code-behind for the trick page, with 61 lines of code omitted that “magically” set the chosenSuit string to C, D, H, or S and the chosenRank string to A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, or K.

LISTING 17.4 TrickPage.xaml.cs—The Code-Behind for Pick a Card’s Trick Page

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class TrickPage : PhoneApplicationPage
{
string chosenSuit;
string chosenRank;
bool flipPart2;
bool finalPhase;
public TrickPage()
{
InitializeComponent();
this.AddHandler(Page.MouseLeftButtonUpEvent,
new MouseButtonEventHandler(MainPage_MouseLeftButtonUp),
true /* handledEventsToo, so we get the button click */);
InitializeTrick();
}
void InitializeTrick()
{
if (Settings.PracticeMode.Value)
this.PracticeImage1.Visibility = Visibility.Visible;
// Reset everything
this.ReadyPanel.Visibility = Visibility.Visible;
this.CardBackImage.Visibility = Visibility.Visible;
this.NextCardLeftImage.Visibility = Visibility.Visible;
this.NextCardRightImage.Visibility = Visibility.Visible;
this.CardFrontImage.Source = null;
this.flipPart2 = false;
this.ChosenCardProjection.RotationY = 0;
// Start shuffling
this.ShuffleStoryboard.Begin();
}
void MainPage_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (this.ReadyPanel.Visibility == Visibility.Visible)
{
// This is a tap on the “tap here when ready” button
if (Settings.PracticeMode.Value)
{
this.PracticeImage1.Visibility = Visibility.Collapsed;
this.PracticeImage2.Visibility = Visibility.Visible;
}
// Hide ReadyPanel and the shuffling deck,
// leaving the single card back exposed
this.ReadyPanel.Visibility = Visibility.Collapsed;
this.NextCardLeftImage.Visibility = Visibility.Collapsed;
this.NextCardRightImage.Visibility = Visibility.Collapsed;
this.ShuffleStoryboard.Stop();
this.finalPhase = true;
}
else if (this.finalPhase)
{
// This is a tap on the card back to flip it over
if (Settings.PracticeMode.Value)
this.PracticeImage2.Visibility = Visibility.Collapsed;
// Show the chosen card image
this.CardFrontImage.Source = new BitmapImage(new Uri(“Images/Card” +
this.chosenSuit + this.chosenRank + “.png”, UriKind.Relative));
// Perform the first 90° of the flip
this.FlipStoryboard.Begin();
this.finalPhase = false;
}
else if (this.FlipStoryboard.GetCurrentState() != ClockState.Active)
{
// Do it again. (Don’t allow this until the flip animation is finished.)
InitializeTrick();
}
}
void FlipStoryboard_Completed(object sender, EventArgs e)
{
if (!this.flipPart2)
{
// The card is now perpendicular to the screen. It’s time to hide the
// back and run the animation again so the remaining 90° shows the front
this.CardBackImage.Visibility = Visibility.Collapsed;
this.flipPart2 = true;
this.FlipStoryboard.Begin();
}
}
#region Magician’s Secret

#endregion
}
}

[/code]

Notes:

  • A single handler—MainPage_MouseLeftButtonUp—handles the first tap on the “tap here when ready” button, which can actually be anywhere on the screen, the tap on the card back to flip it over, and the tap on the card front to start the trick again. The handler is attached with true passed for handledEventsToo, so the event is received when the button is tapped.
  • When the card back is tapped (indicated by finalPhase being true inside MainPage_MouseLeftButtonUp), the card front image is set to one of 52 images included in the project. These 52 images are shown in Figure 17.8.
  • Inside FlipStoryboard_Completed, the card back is hidden and FlipStoryboad is run again to complete the 180° flip. This works because the animation is marked with By=”90”, so the first run takes it from 0° to 90°, and the second run takes it from 90° to 180°. The card back must be manually hidden because flipping elements over in 3D does not change their Z-order. In other words, unlike in the physical world, the card back remains on top of the card front regardless of the angle of rotation.
FIGURE 17.8 The 52 card images cover every choice except a Joker.
FIGURE 17.8 The 52 card images cover every choice except a Joker.

The Instructions Page

The instructions page, shown in Figure 17.9, contains a button that makes them selfdestruct (turning off practice mode and changing the main menu). Once this is done, the instructions never come back unless the app is uninstalled and reinstalled. This is done to prevent nosy audience members from figuring out the secret to the trick. The XAML for this page isn’t very interesting (other than the fact that its text reveals the secret to the trick), but Listing 17.5 shows the code-behind, which implements the self-destructing behavior.

FIGURE 17.9 The instructions page contains a button that permanently hides the instructions and turns off practice mode.
FIGURE 17.9 The instructions page contains a button that permanently hides the instructions and turns off practice mode.

LISTING 17.5 InstructionsPage.xaml.cs—The Code-Behind for Pick a Card’s Instructions Page

[code]

using System.Windows;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class InstructionsPage : PhoneApplicationPage
{
public InstructionsPage()
{
InitializeComponent();
}
void SelfDestructButton_Click(object sender, RoutedEventArgs e)
{
if (MessageBox.Show(“To protect the secret of this trick, these “ +
“instructions will disappear forever once you turn off practice mode.” +
“ The only way to get them back is to uninstall then reinstall this “ +
“app. Are you ready to destroy the instructions?”,
“These instructions will self-destruct!”, MessageBoxButton.OKCancel)
== MessageBoxResult.OK)
{
Settings.PracticeMode.Value = false;
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
}
}
}

[/code]

To make the instructions self-destruct, Listing 17.5 changes the PracticeMode persisted setting to false and then navigates back to the main page which hides the instructions button. This setting never changes unless the app is uninstalled because it doesn’t provide the user any way to change it back. Because uninstalling an app removes anything it puts in isolated storage, however, reinstalling it restores PracticeMode’s default value of true.

To implement a behavior that only happens the first time an app is run (or until the user makes some action to turn it off ), simply base it on a value persisted in isolated storage.The Setting object used throughout this book internally uses isolated storage to preserve each value until it is either changed by code or deleted by the app being uninstalled.

The Finished Product

Pick a Card Magic Trick (3D Transforms)

 

 

 

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

Vibration Composer

Vibration Composer is probably the strangest app in this part of the book. A cross between a musical instrument and a handheld massager, this app provides an interesting way to create your own custom vibrating patterns.

Users can do some fun things with this app:

  • Create a solid vibration to use it like a massager app.
  • Set the phone on a table while it vibrates and watch it move!
  • Play music from the Music + Videos hub and then use this app to accompany it with vibrations.

This app requires the running-while-the-phone-is-locked capability (automatically granted during the marketplace certification process).Therefore, this app’s marketplace listing will contain the awkward phrase,“This app makes use of your phone’s RunsUnderLock.” (The exact phrase depends on whether you look in Zune or the phone’s Marketplace app, and may change in the future.)

The Main Page

Vibration Composer’s main page represents time in square chunks. Each chunk lasts 1/10th of a second. You can tap any square to toggle it between an activated state and a deactivated state. When you press the app’s start button, it highlights each square in sequence and makes the phone vibrate when an activated square is highlighted. Once it reaches the last activated square, it repeats the sequence from the beginning. Therefore, although the page contains 90 squares (enabling the unique pattern to be up to 9 seconds long), the user is in control over the length of the segment to be repeated. Figure 8.1 demonstrates three simple vibration patterns.

FIGURE 8.1 The pattern to be repeated is automatically trimmed after the last activated square.
FIGURE 8.1 The pattern to be repeated is automatically trimmed after the last activated square.

The User Interface

Listing 8.1 contains the XAML for the main page.

LISTING 8.1 MainPage.xaml—The User Interface for Vibration Composer’s Main Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
SupportedOrientations=”PortraitOrLandscape”>
<!– The application bar, with three buttons and five menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”start”
IconUri=”/Shared/Images/appbar.play.png” Click=”StartStopButton_Click”/>
<shell:ApplicationBarIconButton Text=”delete”
IconUri=”/Shared/Images/appbar.delete.png” Click=”DeleteButton_Click”/>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”solid”/>
<shell:ApplicationBarMenuItem Text=”alternating fast”/>
<shell:ApplicationBarMenuItem Text=”alternating slow”/>
<shell:ApplicationBarMenuItem Text=”sos”/>
<shell:ApplicationBarMenuItem Text=”phone ring”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<ScrollViewer>
<Grid>
<!– A wrap panel for containing the 90 buttons –>
<toolkit:WrapPanel x:Name=”WrapPanel”/>
<!– A separate rectangle for tracking the current position –>
<Rectangle x:Name=”CurrentPositionRectangle” Visibility=”Collapsed”
Width=”56” Height=”56” Margin=”12” Opacity=”.8”
HorizontalAlignment=”Left” VerticalAlignment=”Top”
Fill=”{StaticResource PhoneForegroundBrush}”/>
</Grid>
</ScrollViewer>
</phone:PhoneApplicationPage>

[/code]

Notes:

  • The first application bar button starts and stops the vibration sequence playback, the second button clears the sequence, and the third button navigates to an instructions page. Each of the menu items, shown in Figure 8.2, fills the page with a predefined pattern to help familiarize users with the app and give them ideas for getting interesting results.
  • This page uses a panel from the Silverlight for Windows Phone Toolkit known as a wrap panel to contain each of the squares. (The squares are added in code-behind.) A wrap panel stacks its children from left to right, and then wraps them from top to bottom when it runs out of room. (Or you can change the wrap panel’s Orientation property to Vertical to make it stack its children from top to bottom and then wrap them from left to right.) Figure 8.3 demonstrates how the wrap panel changes the layout of the squares based on the current orientation. In the portrait orientation, 6 squares fit in each row before wrapping to the next row. In the landscape orientations, 9 squares fit before wrapping. The reason this app uses a total of 90 squares is that it happens to be divisible by 6 and 9, avoiding a partial row of squares at the end.
    FIGURE 8.2 The application bar menu provides access to five predefined patterns.
    FIGURE 8.2 The application bar menu provides access to five predefined patterns.

    FIGURE 8.3 The wrap panel reflows the squares based on the current orientation.
    FIGURE 8.3 The wrap panel reflows the squares based on the current orientation.
  • The wrap panel is placed in a grid so a rectangle tracking the current position can be moved by code-behind to overlap each square when appropriate. This grid is placed inside a scroll viewer, so the user can access all 90 squares.

The Code-Behind

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

LISTING 8.2 MainPage.xaml.cs—The Code-Behind for Vibration Composer’s Main Page

[code]

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using Microsoft.Devices;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// The squares
Button[] buttons = new Button[90];
// Remember the current sequence
Setting<IList<bool>> savedSequence = new Setting<IList<bool>>(“Sequence”,
new List<bool>());
DispatcherTimer timer = new DispatcherTimer();
int sequencePosition;
int sequenceEndPosition = -1;
bool isRunning = false;
IApplicationBarIconButton startStopButton;
public MainPage()
{
InitializeComponent();
this.startStopButton = this.ApplicationBar.Buttons[0]
as IApplicationBarIconButton;
// Initialize the timer
this.timer.Interval = TimeSpan.FromSeconds(.1);
this.timer.Tick += Timer_Tick;
// Attach the same Click handler to all menu items in the application bar
foreach (IApplicationBarMenuItem menuItem in this.ApplicationBar.MenuItems)
menuItem.Click += MenuItem_Click;
// Fill the wrap panel with the 90 buttons
for (int i = 0; i < this.buttons.Length; i++)
{
this.buttons[i] = new Button {
// Each button contains a square, invisible (Fill=null) when off and
// accent-colored when on
Content = new Rectangle { Margin = new Thickness(0, 7, 0, 5),
Width = 30, Height = 30 }
};
this.buttons[i].Click += Button_Click;
this.WrapPanel.Children.Add(this.buttons[i]);
}
TrimSequence();
// Allow the app to run (and vibrate) even when the phone is locked.
// Once disabled, you cannot re-enable the default behavior!
PhoneApplicationService.Current.ApplicationIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Persist the current sequence
this.savedSequence.Value.Clear();
for (int i = 0; i <= this.sequenceEndPosition; i++)
this.savedSequence.Value.Add(this.buttons[i].Tag != null);
// Prevent this from running while instructions are shown
Stop();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Restore the saved sequence, if any
for (int i = 0; i < this.savedSequence.Value.Count; i++)
{
if (this.savedSequence.Value[i])
TurnOn(this.buttons[i]);
}
TrimSequence();
}
// Click handler for each of the 90 buttons
void Button_Click(object sender, RoutedEventArgs e)
{
// Toggle the state of this button
Button b = sender as Button;
if (b.Tag != null)
TurnOff(b);
else
TurnOn(b);
TrimSequence();
}
void TurnOn(Button b)
{
b.Tag = true; // This button is “on”
(b.Content as Rectangle).Fill =
Application.Current.Resources[“PhoneAccentBrush”] as Brush;
}
void TurnOff(Button b)
{
b.Tag = null; // This button is “off”
(b.Content as Rectangle).Fill = null;
}
void TrimSequence()
{
// Find the end of the sequence (the last “on” button)
// and make the remaining buttons dim (opacity of .2)
this.sequenceEndPosition = -1;
for (int i = this.buttons.Length – 1; i >= 0; i–)
{
this.buttons[i].Opacity = 1;
if (this.sequenceEndPosition == -1)
{
if (this.buttons[i].Tag == null)
this.buttons[i].Opacity = .2;
else
this.sequenceEndPosition = i;
}
}
if (this.isRunning && this.sequenceEndPosition == -1)
{
// Force the playback to stop, because a sequenceEndPosition of -1
// means that the sequence is empty (all buttons are off)
StartStopButton_Click(this, EventArgs.Empty);
}
}
void Timer_Tick(object sender, EventArgs e)
{
// Find the wrap-panel-relative location of the current button
Point buttonLocation = this.buttons[this.sequencePosition]
.TransformToVisual(this.WrapPanel).Transform(new Point(0, 0));
// Move the current position rectangle to overlap the current button
this.CurrentPositionRectangle.Margin = new Thickness(
buttonLocation.X + 12, buttonLocation.Y + 12, 0, 0);
// Either start or stop vibrating, based on the state of the current button
if (this.buttons[this.sequencePosition].Tag != null)
VibrateController.Default.Start(TimeSpan.FromSeconds(.5));
else
VibrateController.Default.Stop();
// Advance the current position, and make it loop indefinitely
this.sequencePosition = (this.sequencePosition + 1)
% (this.sequenceEndPosition + 1);
}
void Stop()
{
this.timer.Stop();
VibrateController.Default.Stop();
this.startStopButton.IconUri = new Uri(“/Shared/Images/appbar.play.png”,
UriKind.Relative);
this.startStopButton.Text = “start”;
this.CurrentPositionRectangle.Visibility = Visibility.Collapsed;
this.isRunning = false;
}
void Clear()
{
for (int i = 0; i < this.buttons.Length; i++)
TurnOff(this.buttons[i]);
}
// Application bar handlers
void StartStopButton_Click(object sender, EventArgs e)
{
if (this.isRunning)
{
// Stop and restore all state
Stop();
return;
}
if (this.sequenceEndPosition == -1)
{
MessageBox.Show(“The vibration pattern is empty! You must turn at least “
+ “one square on.”, “Empty Sequence”, MessageBoxButton.OK);
return;
}
// Start
this.startStopButton.IconUri =
new Uri(“/Shared/Images/appbar.stop.png”, UriKind.Relative);
this.startStopButton.Text = “stop”;
this.CurrentPositionRectangle.Visibility = Visibility.Visible;
this.sequencePosition = 0;
this.isRunning = true;
this.timer.Start();
}
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to clear the whole sequence?”,
“Delete Sequence”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
Clear();
TrimSequence();
}
}
void MenuItem_Click(object sender, EventArgs e)
{
Clear();
// Grab the text from the menu item to determine the chosen sequence
switch ((sender as IApplicationBarMenuItem).Text)
{
case “solid”:
TurnOn(this.buttons[0]);
break;
case “alternating fast”:
TurnOn(this.buttons[1]);
break;
case “alternating slow”:
TurnOn(this.buttons[6]);
TurnOn(this.buttons[7]);
break;
case “sos”:
TurnOn(this.buttons[10]);
TurnOn(this.buttons[11]);

break;
case “phone ring”:
TurnOn(this.buttons[17]);
TurnOn(this.buttons[21]);

break;
}
TrimSequence();
}
}
}

[/code]

Notes:

  • The squares used by this app are actually normal buttons whose content is a rectangle element. These are created in the page’s constructor and added to the wrap panel.
  • The setting of ApplicationIdleDetectionMode to Disabled allows the app to continue running even if the user locks their screen. (This is the only kind of background running your app can do in version 7.0 of Windows Phone. Actual multitasking is coming by the end of 2011.) This enables the phone to be used as, say, a massager, without needing to keep the screen active and worrying about accidentally tapping things.

When you make your app run under the lock screen, consider making it optional by providing a setting that toggles the value of ApplicationIdleDetectionMode.Unfortunately, although setting it to Disabled takes effect immediately, setting it to Enabled once it’s in this state does not work.Therefore, you must explain to your users that toggling the run-underlock feature requires restarting your app, at least in one direction.

For a brief period of time, the Windows Phone Marketplace required that any app that sets ApplicationIdleDetectionMode to Disabled must provide a way for users to set it to Enabled. For now, this is no longer a requirement.

  • The state of each button is stored in its Tag property. In addition to updating the fill of the rectangle inside the relevant button, TurnOn sets the button’s Tag to true, whereas TurnOff sets it to null. Tag can be set to any object, and it’s up to you how you want to use the values. It’s just a handy spot to store your own data. Using Tag is considered to be a bit of a hack, but it’s a simple solution. Instead of using it in this case, many Silverlight developers might use the Visual State Manager, covered in Chapter 19, “Animation Lab,” to define two distinct states for each button.
  • A timer keeps track of where we are when playing the sequence of vibrations, much like the timers used in Chapter 2, “Flashlight.” The Tick event handler, Timer_Tick, either starts or stops the phone’s vibration with VibrateController.Default.Start and VibrateController.Default.Stop (from the Microsoft.Devices namespace). You can call Start with a time span of up to 5 seconds. Although longer time spans aren’t supported, using a timer enables you to combine shorter vibrations into an arbitrarily-long one.

    Given that the timer interval is 1/10th of a second, a more straightforward implementation of Timer_Tick would make the phone vibrate for 1/10th of a second if the current square is on, otherwise do nothing:

    [code]if (this.buttons[this.sequencePosition].Tag != null)
    VibrateController.Default.Start(TimeSpan.FromSeconds(.1));
    else { /* Nothing to do! */ }[/code]

    However, this can cause small gaps of silence when consecutive squares are on. Because I wanted to enable long stretches of vibration with consecutive “on” squares, Timer_Tick instead starts the vibration for longer than necessary (half a second) or stops the vibration each tick. (Calling Start while the phone is already vibrating is fine; the vibration will simply continue for the length of time specified.)

How does the phone’s vibration on/off setting impact vibrations triggered by my app?

It has no impact.The phone’s vibration setting (found under “ringtones & sounds” in the Settings app) only controls whether incoming phone calls can vibrate.When you use VibrateController, it always makes the phone vibrate. Furthermore, you have no way to discover the phone’s vibration setting, so you can’t manually respect this setting even if you want to.

  • Inside Timer_Tick, the rectangle that highlights the current position is moved to completely overlap the appropriate button. This is done with the handy TransformToVisual method (available on all UI elements), which provides a way to map any point relative to one element into coordinates relative to another element. Previously, we have done this by calling GetPosition on a MouseEventArgs instance, but we have no such instance at this point in the code. The awkward thing about TransformToVisual (in addition to its name) is that it returns an instance of a GeneralTransform object. From this object, you must then call Transform to map one point to another.
  • The timer is stopped when the user navigates away from this page. This is done to avoid a problem when navigating to the instructions page while the timer is running. The Timer_Tick event would continue to be called, but the call to TransformToVisual fails when main page is no longer the active page.

The Instructions Page

Listing 8.3 contains the XAML for the simple instructions page shown in Figure 8.4.

FIGURE 8.4 The instructions page used by Vibration Composer.
FIGURE 8.4 The instructions page used by Vibration Composer.

LISTING 8.3 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, with some tweaks –>
<StackPanel Margin=”24,16,0,12”>
<TextBlock Text=”VIBRATION COMPOSER” Margin=”-1,0,0,0”
FontFamily=”{StaticResource PhoneFontFamilySemiBold}”
FontSize=”{StaticResource PhoneFontSizeMedium}”/>
<TextBlock Text=”instructions” Margin=”-3,-10,0,0”
FontFamily=”{StaticResource PhoneFontFamilySemiLight}”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<TextBlock Margin=”24,12” TextWrapping=”Wrap”>
Tap the squares to toggle them on and off. …
<LineBreak/><LineBreak/>
When you’re ready to start the vibration, tap the “play” button. …
<LineBreak/><LineBreak/>
The end of the sequence is always the last “on” square. …
<LineBreak/><LineBreak/>
Tap “…” to access a menu of sample sequences.
</TextBlock>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • This page also supports all orientations, and uses a scroll viewer to ensure that all the text can be read regardless of orientation.
  • The code-behind file, InstructionsPage.xaml.cs, has nothing more than the call to InitializeComponent in the class’s constructor.

Listing 8.3 has something new inside its text block: LineBreak elements! You can use LineBreak to insert a carriage return in the middle of a single text block’s text.This only works when the text is specified as the inner content of the element. It cannot be used when assigning the Text attribute to a string.

The Finished Product

Vibration Composer