This chapter
is taken from book "Programming Windows Phone 7" by Charles Petzold published by
Microsoft press.
http://www.charlespetzold.com/phone/index.html
Animations in Silverlight work by changing a particular
property of a particular object, the animation classes are distinguished by the
type of the
property that they animate. Silverlight animations can target properties of type
double,
Color,
Point, and
Object.
Frame-Based vs. Time-Based
Suppose you want to write a little program that rotates some text using the
CompositionTarget.Rendering event. You can pace this animation either by the
rate that video hardware refreshes the display, or by clock time. Because each
refresh of the video display is called a frame, these two methods of pacing
animation are referred to as frame-based and time-based.
Here's a little program that shows the difference. The
content area of the XAML file has two
TextBlock elements with
RotateTransform
objects set to their RenderTransform
properties, and a
Button:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<Grid.RowDefinitions>
<RowDefinition
Height="*" />
<RowDefinition
Height="*" />
<RowDefinition
Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Text="Frame-Based"
FontSize="{StaticResource
PhoneFontSizeLarge}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RenderTransformOrigin="0.5
0.5">
<TextBlock.RenderTransform>
<RotateTransform
x:Name="rotate1"
/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock
Grid.Row="1"
Text="Time-Based"
FontSize="{StaticResource
PhoneFontSizeLarge}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RenderTransformOrigin="0.5
0.5">
<TextBlock.RenderTransform>
<RotateTransform
x:Name="rotate2"
/>
</TextBlock.RenderTransform>
</TextBlock>
<Button
Grid.Row="2"
Content="Hang for 5
seconds"
HorizontalAlignment="Center"
Click="OnButtonClick" />
</Grid>
The code-behind file saves the current time in a field and then attaches a
handler for the CompositionTarget.Rendering event. This event handler is then
called in synchronization with the video frame rate.
namespace
FrameBasedVsTimeBased
{
public partial
class MainPage
: PhoneApplicationPage
{
DateTime startTime;
public MainPage()
{
InitializeComponent();
startTime = DateTime.Now;
CompositionTarget.Rendering +=
OnCompositionTargetRendering;
}
void OnCompositionTargetRendering(object
sender, EventArgs args)
{
// Frame-based
rotate1.Angle = (rotate1.Angle + 0.2) % 360;
// Time-based
TimeSpan elapsedTime =
DateTime.Now - startTime;
rotate2.Angle = (elapsedTime.TotalMinutes * 360) % 360;
}
void OnButtonClick(object
sender, RoutedEventArgs args)
{
Thread.Sleep(5000);
}
}
}
The rotation angle for the first
TextBlock is increased
by 0.2degree every frame. I calculated this by knowing that the phone display is
refreshed at 30 frames per second. Multiply 30 frames per second by 60 seconds
per minute by 0.2degree and you get 360degree.
The rotation angle for the second
TextBlock is
calculated based on the elapsed time. The
TimeSpan structure has a convenient
TotalMinutes property
and this is multiplied by 360 for the total number of degrees to rotate the
text.
Both work, and they work approximately the same:
Click and Spin
Suppose you want to enhance a button to give some extra visual feedback to the user. You decide you actually want a lot of visual feedback to wake up a drowsy user, and therefore you choose to spin the button around in a circle every time it's clicked.
Here are a few buttons in a XAML file:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<Grid.RowDefinitions>
<RowDefinition
Height="*" />
<RowDefinition
Height="*" />
<RowDefinition
Height="*" />
</Grid.RowDefinitions>
<Button
Content="Button No. 1"
Grid.Row="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RenderTransformOrigin="0.5
0.5"
Click="OnButtonClick">
<Button.RenderTransform>
<RotateTransform
/>
</Button.RenderTransform>
</Button>
<Button
Content="Button No. 2"
Grid.Row="1"
HorizontalAlignment="Center
VerticalAlignment="Center"
RenderTransformOrigin="0.5
0.5"
Click="OnButtonClick">
<Button.RenderTransform>
<RotateTransform
/>
</Button.RenderTransform>
</Button>
<Button
Content="Button No. 3"
Grid.Row="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RenderTransformOrigin="0.5
0.5"
Click="OnButtonClick">
<Button.RenderTransform>
<RotateTransform
/>
</Button.RenderTransform>
</Button>
</Grid>
Each of the buttons has its RenderTransform property set to a RotateTransform,
and its RenderTransformOrigin set for
the element center.
The Click
event handler is responsible for defining and initiating the animation that
spins the clicked button. (Of course, in a real application, the Click handler
would also perform something important to the program!) The handler begins by
obtaining the Button that the user touched, and the RotateTransform associated
with that particular Button:
namespace
ClickAndSpin
{
public partial
class MainPage
: PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
}
void OnButtonClick(object
sender, RoutedEventArgs args)
{
Button btn = sender
as Button;
RotateTransform rotateTransform =
btn.RenderTransform as
RotateTransform;
// Create and define animation
DoubleAnimation anima =
new DoubleAnimation();
anima.From = 0;
anima.To = 360;
anima.Duration = new
Duration(TimeSpan.FromSeconds(0.5));
// Set attached properties
Storyboard.SetTarget(anima,
rotateTransform);
Storyboard.SetTargetProperty(anima,
new PropertyPath(RotateTransform.AngleProperty));
// Create storyboard, add animation, and
fire it up!
Storyboard storyboard =
new Storyboard();
storyboard.Children.Add(anima);
storyboard.Begin();
}
}
}
Getting the animation going requires three steps:
Define the animation itself. The animation needed here will target the Angleproperty of aRotateTransform, and the Angle property is of typedouble, so that suggests a DoubleAnimation:
DoubleAnimation anima = new DoubleAnimation();
anima.From = 0;
anima.To = 360;
anima.Duration = new Duration(TimeSpan.FromSeconds(0.5));
This DoubleAnimation will animate a property of typedouble from a value of 0 to a value of 360 in 1/2 second. TheDuration property of DoubleAnimation is of type Duration, and in code it is very common to set it from aTimeSpan object. But the Duration property is not itself of type TimeSpan primarily due to legacy issues. You can alternatively set the Duration property to the static Duration.Automatic value, which is the same as not settingDuration at all (or setting it to null), and which creates an animation with a duration of 1 second.
Set the attached properties. TheDoubleAnimationmust be associated with a particular object and property of that object. You specify these using two attached properties defined by theStoryboard class:
Storyboard.SetTarget(anima, rotateTransform);
Storyboard.SetTargetProperty(anima, new PropertyPath(RotateTransform.AngleProperty));
The attached properties areTarget and TargetProperty. As you'll recall, when you set attached properties in code, you use static methods that begin with the wordSet.
In both cases, the first argument is theDoubleAnimation just created. The SetTargetcall indicates the object being animated (in this caseRotateTransform), and the SetTargetProperty call indicates a property of that object. The second argument of theSetTargetProperty method is of type PropertyPath, and you'll note that I've specified the fully-qualified dependency property for the Angle property of RotateTransform.
- Define, set, and start the
Storyboard.
At this point, everything seems to be ready. But there's still another step. In Silverlight, animations are always enclosed in Storyboard objects. A particular Storyboard can have multiple children, so it is very useful for synchronizing multiple animations. But even if you have just one animation running by itself, you still need a Storyboard:
Storyboard storyboard = new Storyboard();
storyboard.Children.Add(anima);
storyboard.Begin();
As soon as you call Begin on theStoryboard object, the clicked button spins around in half a second, giving the user perhaps a little too much visual feedback.
Key Frame Animations
If you like the idea of giving the user some visual
feedback from a button, but the 360 degree spin is just a bit too ostentatious,
perhaps jiggling the button a little might be more polite. So you open a new
project named JiggleButtonTryout and begin experimenting.
Let's start with just one
Button with a
TranslateTransform set
to the RenderTransform
property:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<Button
Content="Jiggle Button"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Click="OnButtonClick">
<Button.RenderTransform>
<TranslateTransform
x:Name="translate"
/>
</Button.RenderTransform>
</Button>
</Grid>
The code-behind file starts the animation when the
button is clicked:
void
OnButtonClick(object
sender, RoutedEventArgs
args)
{
jiggleStoryboard.Begin();
}
To bring the time values down to something reasonable, I
want to show you a little trick. Often when you're developing animations you
want to do run them very slowly to get them working correctly, and then you want
to speed them up for the final version. Of course, you could go through and
adjust all the KeyTime
values, or you could simply specify a
SpeedRatio on the
animation, as in the version of the animation in the JiggleButtonTryout project:
<phone:PhoneApplicationPage.Resources>
<Storyboard
x:Name="jiggleStoryboard">
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="translate"
Storyboard.TargetProperty="X"
RepeatBehavior="3x"
SpeedRatio="40">
<DiscreteDoubleKeyFrame
KeyTime="0:0:0"
Value="0" />
<LinearDoubleKeyFrame
KeyTime="0:0:01"
Value="-10" />
<LinearDoubleKeyFrame
KeyTime="0:0:03"
Value="10" />
<LinearDoubleKeyFrame
KeyTime="0:0:04"
Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
Each cycle of the key frames requires 4 seconds; this is
repeated 3 times for a total of 12 seconds, but the
SpeedRatio value of 40 effectively speeds up the
animation by a factor of 40 so it's only 0.3 seconds total.
Trigger on Loaded
The Windows Presentation Foundation has somewhat more flexibility than Silverlight in defining and using animations. WPF includes objects called triggers, which respond to event firings or to changes in properties and which can start animations going entirely in XAML, eliminating the need for the code-behind file to start the Storyboard. Triggers are largely gone from Silverlight-mostly replaced by the Visual State Manager that I'll discuss in the next chapter.
However, one trigger remains in Silverlight. This is a trigger that responds to the Loaded event. This allows you to define an animation entirely in XAML that automatically starts up when the page (or another element) is loaded.
The FadeInOnLoaded project contains the following XAML near the bottom of the
page, right above the PhoneApplicationPage end tag. This is the traditional
spot for event triggers:
<phone:PhoneApplicationPage.Triggers>
<EventTrigger>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="TitlePanel"
Storyboard.TargetProperty="Opacity"
From="0"
To="1"
Duration="0:0:10" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</phone:PhoneApplicationPage.Triggers>
The markup begins with a property-element tag for the Triggers property
defined by FrameworkElement. The Triggers property is of type TriggerCollection,
which sounds quite extensive and versatile, but in Silverlight the only thing
you can put in there is an EventTrigger tag that is always associated with the
Loaded event. This next tag is BeginStoryboard. This is the only place you'll
see a BeginStoryboard tag in Silverlight. And now we get to something familiar:
A Storyboard
with one or more animations that can target
any dependency object of any object on the page.
This one targets the
Opacity property of the TitlePanel, which is the
StackPanel containing the two titles
at the top of the page. I made the animation 10 seconds long so you don't miss
it. As the page is loaded, the titles fade into view: