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.
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.
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.
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.
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).
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.
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.
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?”
+ “¤tColor=” + 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”
+ “¤tColor=” + 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.
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.
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