Amood ring is a ring with an element that changes color based on the body temperature of the person wearing it. Some people claim that such temperature changes are able to reveal the wearer’s mood, making these rings fun novelty items.
The Mood Ring app captures the essence of a mood ring. Users can press their finger on the screen, and the spot they touch slowly changes color to reveal their mood. In one way, the Mood Ring app is even better that a real mood ring—it explains what the resulting color means. (For example, orange means restless.) In another way, the Mood Ring app is worse than a real mood ring—much like the preceding chapter, it must randomly choose what mood to display rather than base it on your finger’s temperature. Windows phones have many sensors, but a thermometer is not one of them!
The preceding three chapters have only used DoubleAnimations, but this app makes use of color and object animations. It also provides an excuse to use a radial gradient brush, which rarely has an appropriate use in a Windows Phone app.
Color Animations
The idea of a color animation might sound strange at first. For example, what exactly does it mean to animate a color from red to blue? Internally, each color has floating-point values representing its alpha, red, green, and blue channels.
Therefore, color animations can interpolate those values the same way that DoubleAnimation does for its single value. As a result of this interpolation, an animation from red to blue appears to change the color from red to deep pink to purple to blue.
There aren’t many color properties to be directly animated by a color animation. Mood Ring happens to animate one such property (Color on GradientStop). Most of the properties that appear to be set to colors in XAML (Foreground, Background, and so on) are actually brush properties. But because solid color brushes have a color property (called Color), color animations can be used on brushes with subproperty syntax like the following:
[code]
<ColorAnimation From=”Red” To=”Blue” Storyboard.TargetName=”TextBlock”
Storyboard.TargetProperty=”(TextBlock.Foreground).(SolidColorBrush.Color)”/>
[/code]
This assumes the presence of a text block named TextBlock as follows:
[code]
<Grid x:Name=”Grid”>
<TextBlock x:Name=”TextBlock” Foreground=”Red” …/>
</Grid>
[/code]
The value used for Storyboard.TargetProperty is called a property path. With it, you can specify a chain of properties and subproperties, where each property is wrapped in parentheses and qualified with its class name. You can even use array syntax, so the following property path works when the animation target is changed to the parent grid:
[code]
<ColorAnimation From=”Red” To=”Blue” Storyboard.TargetName=”Grid”
Storyboard.TargetProperty=
“(Grid.Children)[0].(TextBlock.Foreground).(SolidColorBrush.Color)”/>
[/code]
Color animations don’t perform as well as other animations!
When animations do not alter the underlying render surface (also called a texture), Silverlight is able to perform the animation on the Graphics Processing Unit (GPU) rather than the Central Processing Unit (CPU), using a thread known as the compositor thread. Preventing work from occurring on the UI thread is important for high-performing apps, because the UI thread is needed to handle input, data binding, your app’s logic, and more.
Changing an element’s color requires changing the underlying surface, so color animations involve a lot of work for the UI thread. Animating an element’s opacity (or rotating it, translating it, or scaling it), on the other hand, does not require changing the underlying surface.
Gradient Brushes
Silverlight includes two types of gradient brushes, one for filling an area with a linear gradient and one for filling an area with a radial gradient. Mood Ring uses a radial gradient brush to make the appropriate color radiate from the point where the user’s finger touches the screen.
The following radial gradient brush fills a grid’s background with the multi-color pattern shown in Figure 15.1:
[code]
<phone:PhoneApplicationPage …>
<Grid>
<Grid.Background>
<RadialGradientBrush>
<GradientStop Offset=”0” Color=”Red”/>
<GradientStop Offset=”.2” Color=”Blue”/>
<GradientStop Offset=”.4” Color=”Orange”/>
<GradientStop Offset=”.6” Color=”Green”/>
<GradientStop Offset=”.8” Color=”Purple”/>
<GradientStop Offset=”1” Color=”Yellow”/>
</RadialGradientBrush>
</Grid.Background>
</Grid>
</phone:PhoneApplicationPage>
[/code]
The brush contains a collection of gradient stops, each of which contains a color and an offset. The offset is a double value relative to the bounding box of the area being filled, where 0 is the beginning and 1 is the end. The brush performs linear interpolation between each gradient stop to fill the area smoothly. Both radial gradient brushes and linear gradient brushes define several properties for tweaking their appearance.
Gradients do not render smoothly on Windows Phone 7!
Although gradients render nicely in the emulator (and the figures in this book), the screens on current Windows phones are set to only use 16-bit color depth. Although good for performance, 16-bit color depth is not good enough to show gradients nicely.The result is a “banding” phenomenon where sharp lines are seen between individual colors in the gradient.The closer the colors in the gradient are (and the larger the gradient-filled area is), the more noticeable this problem becomes.
This normally isn’t a problem, as using a gradient looks out of place in the flat Metro style of Windows Phone apps.You certainly won’t find any gradients in the phone’s built-in apps. (By the way, this does not normally affect the display of photos and video, whose inherent noise usually counteracts any noticeable banding.)
Therefore, you should use gradients extremely sparingly. In this book, only the color picker page seen in the preceding chapter uses linear gradients, and only Mood Ring uses a radial gradient. If you insist on using a gradient (which can be appropriate for games), you can get better results by using a gradient-filled image with dithering or other noise built in.
Gradient brushes can be used wherever solid color brushes can be used, such as the stroke of a shape, the border or background of a button, or the foreground for text, such as the following text block shown in Figure 15.2:
[code]
<phone:PhoneApplicationPage …>
<Grid>
<TextBlock Text=”psychedelic” FontSize=”150”>
<TextBlock.Foreground>
<RadialGradientBrush>
<GradientStop Offset=”0” Color=”Red”/>
<GradientStop Offset=”.2” Color=”Blue”/>
<GradientStop Offset=”.4” Color=”Orange”/>
<GradientStop Offset=”.6” Color=”Green”/>
<GradientStop Offset=”.8” Color=”Purple”/>
<GradientStop Offset=”1” Color=”Yellow”/>
</RadialGradientBrush>
</TextBlock.Foreground>
</TextBlock>
</Grid>
</phone:PhoneApplicationPage>
[/code]
The gradient stretches to fill the text block’s bounding box.
To get crisp lines inside a gradient brush, you can simply add two gradient stops at the same offset with different colors.The following linear gradient brush does this at offsets .2 and .6 to get two distinct lines defining the PhoneAccentColor region, shown in Figure 15.3 in blue:
[code]
<phone:PhoneApplicationPage …>
<Grid>
<Grid.Background>
<LinearGradientBrush>
<GradientStop Offset=”0”
Color=”{StaticResource PhoneBackgroundColor}”/>
<GradientStop Offset=”.2”
Color=”{StaticResource PhoneForegroundColor}”/>
<GradientStop Offset=”.2” Color=”{StaticResource PhoneAccentColor}”/>
<GradientStop Offset=”.4” Color=”{StaticResource PhoneAccentColor}”/>
<GradientStop Offset=”.6” Color=”{StaticResource PhoneAccentColor}”/>
<GradientStop Offset=”.6”
Color=”{StaticResource PhoneBackgroundColor}”/>
<GradientStop Offset=”.8”
Color=”{StaticResource PhoneBackgroundColor}”/>
<GradientStop Offset=”1”
Color=”{StaticResource PhoneForegroundColor}”/>
</LinearGradientBrush>
</Grid.Background>
</Grid>
</phone:PhoneApplicationPage>
[/code]
When it comes to gradients, not all transparent colors are equal!
Because all colors have an alpha channel, you can incorporate transparency and translucency into any gradient by changing the alpha channel on any gradient stop’s color.The following linear gradient brush uses two blue colors, one fully opaque and one fully transparent:
[code]
<LinearGradientBrush>
<GradientStop Offset=”0” Color=”#FF0000FF”/>
<GradientStop Offset=”1” Color=”#000000FF”/>
</LinearGradientBrush>
[/code]
Notice that the second gradient stop uses a “transparent blue” color rather than simply specifying Transparent as the color.That’s because Transparent is defined as white with a 0 alpha channel (#00FFFFFF). Although both colors are completely invisible, the interpolation to each color does not behave the same way. If Transparent were used for the second gradient stop, you would not only see the alpha value gradually change from 0xFF to 0, you would also see the red and green values gradually change from 0 to 0xFF, giving the brush more of a gray look.This is demonstrated in Figure 15.4 on top of a black background.
The User Interface
Mood Ring only has one page other than its instructions and about pages, which aren’t shown in this chapter. Listing 15.1 contains its XAML.
LISTING 15.1 MainPage.xaml—The Main User Interface for Mood Ring
[code]
<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
SupportedOrientations=”PortraitOrLandscape”>
<!– The application bar, for instructions and about,
forced to be white on the black background –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”0” ForegroundColor=”White”>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<phone:PhoneApplicationPage.Resources>
<!– The storyboard that reveals the current mood color –>
<Storyboard x:Name=”ShowColorStoryboard” Storyboard.TargetName=”GradientStop”>
<!– Change the gradient stop color to gray and then the mood color
(set in code-behind) –>
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty=”Color”>
<LinearColorKeyFrame KeyTime=”0:0:4” Value=”Gray”/>
<LinearColorKeyFrame x:Name=”MoodColorKeyFrame” KeyTime=”0:0:8”/>
</ColorAnimationUsingKeyFrames>
<!– Move the gradient stop’s initial offset to .5 during the
second half of the storyboard –>
<DoubleAnimation BeginTime=”0:0:4” To=”.5”
Storyboard.TargetProperty=”Offset” Duration=”0:0:4”/>
</Storyboard>
<!– The storyboard that removes the current mood color –>
<Storyboard x:Name=”HideColorStoryboard” Storyboard.TargetName=”GradientStop”>
<!– Fade the gradient back to black –>
<ColorAnimation To=”Black” Duration=”0:0:1”
Storyboard.TargetProperty=”Color”/>
<!– Move the gradient stop’s initial offset back to 0 –>
<DoubleAnimation To=”0” Duration=”0:0:1”
Storyboard.TargetProperty=”Offset”/>
</Storyboard>
<!– The storyboard that animates the progress bar –>
<Storyboard x:Name=”ProgressStoryboard”
Completed=”ProgressStoryboard_Completed”
Storyboard.TargetName=”ProgressBar”>
<!– Show the progress bar at the beginning and hide it at the end –>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty=”Visibility”>
<DiscreteObjectKeyFrame KeyTime=”0:0:0” Value=”Visible”/>
<DiscreteObjectKeyFrame KeyTime=”0:0:8” Value=”Collapsed”/>
</ObjectAnimationUsingKeyFrames>
<!– Animate its value to 100% –>
<DoubleAnimation To=”100” Duration=”0:0:8”
Storyboard.TargetProperty=”Value”/>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<Grid>
<!– Set the background to a gradient (that initially looks solid black) –>
<Grid.Background>
<RadialGradientBrush x:Name=”GradientBrush”>
<GradientStop x:Name=”GradientStop” Offset=”0” Color=”Black”/>
<GradientStop Offset=”1” Color=”Black”/>
</RadialGradientBrush>
</Grid.Background>
<Grid>
<ProgressBar x:Name=”ProgressBar” Visibility=”Collapsed”
VerticalAlignment=”Top” Margin=”12,24”/>
<!– A drop shadow for MoodTextBlock, created by duplicating the text in
black and offsetting it 4 pixels to the right and 4 pixels down –>
<TextBlock x:Name=”MoodTextBlockDropShadow” Foreground=”Black”
HorizontalAlignment=”Center” VerticalAlignment=”Center”
Visibility=”Collapsed” FontSize=”65” Margin=”4,4,0,0”/>
<TextBlock x:Name=”MoodTextBlock” Opacity=”.8”
HorizontalAlignment=”Center” VerticalAlignment=”Center”
Visibility=”Collapsed” FontSize=”65”/>
</Grid>
</Grid>
</phone:PhoneApplicationPage>
[/code]
Notes:
- The application bar is given an opacity of 0 so it doesn’t interfere with the color gradient.
- The color animation in ShowColorStoryboard has two keyframes in order to keep users in suspense as they wait for their mood to be revealed. During the first four seconds, the first gradient stop in the page’s grid’s background (named GradientStop) is animated from its initial color of black to gray. For the remaining four seconds, the color is animated from gray to a color chosen in code-behind. The second animation in this storyboard also animates the gradient stop’s offset from 0 to .5 to make the chosen color grow into a solid ellipse that ends up occupying half the size of the screen.
- In HideColorStoryboard, the gradient stop’s color is quickly restored to black and its offset is restored to 0.
- ProgressStoryboard works much like ProgressStoryboard from the preceding chapter. It smoothly animates the value of a progress bar to 100 (completely filled) over a fixed duration (this time, exactly 8 seconds). However, rather than showing and hiding the progress bar with a DoubleAnimation operating on Opacity, it uses an object animation to animate the progress bar’s Visibility property from Visible to Collapsed.
Because animations operating on arbitrary objects cannot possibly perform any kind of interpolation, object animations must be keyframe animations (using the ObjectAnimationUsingKeyFrames class) and only discrete keyframes are supported (using the DiscreteObjectKeyFrame class). Therefore, object animations enable you to simply set arbitrary properties to arbitrary values at specific times.
Setting Visibility to Collapsed Versus Setting Opacity to 0
Rather than toggling the progress bar’s visibility, the first animation inside ProgressStoryboard could have been changed to the following and it would produce the same result (assuming the progress bar is initially marked with Opacity=”0” rather than Visibility=”Collapsed”):
[code]
<!– Show the progress bar at the beginning and hide it at the end –>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=”Opacity”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”1”/>
<DiscreteDoubleKeyFrame KeyTime=”0:0:8” Value=”0”/>
</DoubleAnimationUsingKeyFrames>
[/code]
Although an element is invisible whether its Opacity is set to 0 or its Visibility is set to Collapsed, the choice you make has subtle differences. Setting an element’s Visibility to Collapsed makes the layout system ignore it, which conserves memory and stops it from receiving input events.When an element’s Opacity is set to 0, it still receives input events and, if marked appropriately, its render surface is cached in video memory so it can be later shown again much more efficiently than by toggling Visibility.
- The text block that displays the chosen mood (MoodTextBlock) is given a drop shadow (simulated with MoodTextBlockDropShadow) and an opacity of .8 so it is always readable on top of the gradient. The code-behind gives MoodTextBlock a foreground color matching the chosen mood color.
Point Animations
Other than classes for animating double, Color, and Object data types, the only remaining animation classes are for the Point data type.There aren’t many reasons to use a point animation, as very few dependency properties of type Point exist. One such property is GradientOrigin on a gradient brush, so you could add the following animation to ShowColorStoryboard to create an interesting effect:
[code]
<PointAnimation Storyboard.TargetName=”GradientBrush”
Storyboard.TargetProperty=”GradientOrigin”
From=”0,0” To=”1,1” AutoReverse=”True”
RepeatBehavior=”Forever”/>
[/code]
The Code-Behind
Listing 15.2 contains the code-behind for the main page.
LISTING 15.2 MainPage.xaml.cs—The Code-Behind for Mood Ring’s Main Page
[code]
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
Random random = new Random();
DateTime fingerReleaseTime;
int moodIndex;
public MainPage()
{
InitializeComponent();
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
// Move the gradient origin to where the screen was tapped,
// and begin mood detection
MoveGradientOrigin(e);
StartMoodDetection();
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
// Move the gradient origin to wherever the finger goes
MoveGradientOrigin(e);
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
// Stopping the storyboard undoes everything done by it, returning
// the progress bar’s value to 0 and hiding it
this.ProgressStoryboard.Stop();
// ShowColorStoryboard doesn’t need to be stopped, because
// HideColorStoryboard animates the same properties back to their
// initial values
this.HideColorStoryboard.Begin();
// Remember when this happened
this.fingerReleaseTime = DateTime.Now;
}
void ProgressStoryboard_Completed(object sender, EventArgs e)
{
// We’ve made it to the end, so show the text block and its drop shadow
this.MoodTextBlock.Visibility = Visibility.Visible;
this.MoodTextBlockDropShadow.Visibility = Visibility.Visible;
}
void MoveGradientOrigin(MouseEventArgs e)
{
// Get the finger’s point but scale each dimension from 0 to 1
Point point = e.GetPosition(this);
point.X /= this.ActualWidth;
point.Y /= this.ActualHeight;
// Move both the gradient origin and center to this point
this.GradientBrush.GradientOrigin = point;
this.GradientBrush.Center = point;
}
void StartMoodDetection()
{
// Only change the mood if it has been at least 3 seconds from the last tap
// (A simple attempt to give the same result when the same user tries
// tapping many times in a row.)
if (DateTime.Now – this.fingerReleaseTime > TimeSpan.FromSeconds(3))
this.moodIndex = random.Next(0, 12); // Randomly choose a mood
Color currentColor = Colors.Black;
string currentMood = null;
switch (this.moodIndex)
{
case 0:
currentMood = “tense”; currentColor = Colors.DarkGray;
break;
case 1:
currentMood = “unsettled”; currentColor = Colors.Brown;
break;
case 2:
currentMood = “active”;
currentColor = Color.FromArgb(0xFF, 0, 0xFF, 0); // Lime
break;
case 3:
currentMood = “relaxed”; currentColor = Colors.Cyan;
break;
case 4:
currentMood = “happy”; currentColor = Colors.Blue;
break;
case 5:
currentMood = “frustrated”; currentColor = Colors.White;
break;
case 6:
currentMood = “restless”; currentColor = Colors.Orange;
break;
case 7:
currentMood = “fearful”; currentColor = Colors.Magenta;
break;
case 8:
currentMood = “imaginative”; currentColor = Colors.Yellow;
break;
case 9:
currentMood = “stimulated”; currentColor = Colors.Orange;
break;
case 10:
currentMood = “excited”; currentColor = Colors.Red;
break;
case 11:
currentMood = “romantic”; currentColor = Colors.Purple;
break;
}
// Apply the chosen color to the animation and the text block
this.MoodColorKeyFrame.Value = currentColor;
this.MoodTextBlock.Foreground = new SolidColorBrush(currentColor);
// Apply the name of the mood to the text block and its drop shadow
this.MoodTextBlock.Text = currentMood;
this.MoodTextBlockDropShadow.Text = currentMood;
// Hide the text block and its shadow, so it doesn’t reveal
// the current mood until the Completed event is raised
this.MoodTextBlock.Visibility = Visibility.Collapsed;
this.MoodTextBlockDropShadow.Visibility = Visibility.Collapsed;
// Begin the storyboards
this.ShowColorStoryboard.Begin();
this.ProgressStoryboard.Begin();
}
// Application bar handlers
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Mood Ring”, UriKind.Relative));
}
}
}
[/code]
Notes:
- Inside OnMouseLeftButtonUp, ProgressStoryboard is stopped, so the progress bar’s value is restored to its pre-animation value of 0 and it is hidden (in case the storyboard is stopped in the middle). This is why the animation doesn’t have (or need) an explicit To value of 0 in XAML. Rather than simply stopping ShowColorStoryboard, however, HideColorStoryboard is started to smoothly return the gradient stop’s color and offset to its pre-animation values. The time of this event is remembered, because this app does a little trick of reporting the same mood when the screen is pressed fewer than three seconds after a finger is released.
- The text block and its drop shadow are manually shown when ProgressStoryboard completes (inside ProgressStoryboard_Completed) and manually hidden inside StartMoodDetection (which is only called inside OnMouseLeftButtonDown). That way, the chosen mood remains visible after the user removes their finger from the screen.
- MoveGradientOrigin adjusts two properties of the gradient brush—GradientOrigin and Center. When these two properties are not in-sync, interesting effects occur, such as a sharp conical shape. The finger’s position is divided by the dimensions of the page so it is scaled from 0 to 1, as the gradient brush’s mapping mode is RelativeToBoundingBox by default. You can change the mapping mode to Absolute with the brush’s MappingMode property, but this mode doesn’t work correctly in Windows Phone 7.
- StartMoodDetection chooses a random mood (a number from 0 to 11) or reuses the previous one and then adjusts the relevant pieces of UI accordingly before starting the two relevant storyboards.
The Finished Product