WPF is big on animation and includes a vast array of objects to help you get your application looking and feeling as slick as possible. A great deal of work is done for you, the developer, saving you time and headaches and greatly speeding up the whole process. This means that more and more applications will be able to include savvy animations with ease, increasing the user experience quality and also looking cool. With careful design and planning, animation can really bring an application to life; although use too much animation, and the performance of your application may suffer.
The recipes in this chapter describe how to:
Animate the value of a property (recipe 11-1)
Animate data-bound properties (recipe 11-2)
Remove existing animations (recipe 11-3)
Overlap two animations (recipe 11-4)
Run several animations in parallel (recipe 11-5)
Create an animation that uses keyframes (recipe 11-6)
Interactively control the progress of an animation (recipe 11-7)
Animate the shape of a path object (recipe 11-8)
Loop and reverse animations (recipe 11-9)
Limit the frame rate of animations (recipes 11-10 and 11-11)
Animate an object along a path (recipe 11-12)
Play back audio or video files (recipe 11-13)
Synchronize animation playback with an audio or video file (recipe 11-14)
Receive notification when an animation completes (recipe 11-15)
Animate a property with indirect property targeting (recipe 11-16)
Control animations using triggers (recipe 11-17)
Animate text (recipe 11-18)
You need to change the value of a property on a control with respect to time, be it the opacity of a button, the color of a rectangle, or the height of an expander.
Animate the value of the property using one or more System.Windows.Media.Animation.Timeline
objects in a System.Windows.Media.Animation.Storyboard
.
Owing to the richness of WPF's animation framework, there are myriad options when it comes to animating something. In essence, you are able to animate just about any System.Windows.DependencyProperty
of an object that derives from System.Windows.Media.Animation.Animatable
. Couple that with the range of types for which Timeline
objects already exist, and you find yourself in a position of endless possibilities.
To animate the property of a control, you will generally declare one or more AnimationTimeline
objects that target the data type of the property being animated. These timelines are defined as children of a System.Windows.Media.Animation.Storyboard
, with the root Storyboard
being activated by a System.Windows.Media.Animation.BeginStoryboard
when used in markup. It is also possible to nest Storyboard
objects and ParallelTimeline
objects as children. Each AnimationTimeline
can target a different property of a different object, a different property of the same object, or the same property of the same object. The target object or target property can also be defined at the level of the parent ParallelTimeline
or Storyboard
.
For each data type that WPF supports, there exists an AnimationTimeline
. Each timeline will be named <Type>Animation, possibly with several variants for special types of Timeline
, where <Type> is the target data type of the Timeline
. With the exception of a few AnimationTimeline
objects, the animation's effect on a target property is defined by specifying values for one or more of the To
, From
, or By
properties. If the From
property of an AnimationTimeline
is not specified, the value of the property at the point the timeline's clock is applied will be used. This is useful because it means you do not need to worry about storing a property's initial value and then restore it at a later date. If a value for the From
property is specified, the property will be set with that value when the Timeline
is applied. Again, the original value of the property will be restored when the timeline's clock is removed.
The abstract Timeline
class, from which all AnimationTimeline
, Storyboard
, and ParallelTimeline
objects derive, defines several properties that allow you to define the characteristics of an animation. Table 11-1 describes these properties of the Timeline
class.
Table 11-1. Commonly Used Properties of the Timeline
Class
Property | Description |
---|---|
| Used to specify a percentage of the timeline's duration that should be used to accelerate the speed of the animation from 0 to the animation's maximum rate. The value should be a |
| A |
| A |
| Used to specify a percentage of the timeline's duration that should be used to reduce the speed of the animation from the maximum rate down to 0. The value should be a |
| A nullable |
| A value of the |
| A |
| A property of type |
The following example demonstrates some of the functionality available with animations. Properties of various controls are animated using different values for the previous properties to indicate their effect.
<Window x:Class="Recipe_11_01.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_01" Height="300" Width="300"> <Window.Resources> <Storyboard x:Key="ellipse1Storyboard" Storyboard.TargetName="ellipse1"> <ParallelTimeline> <DoubleAnimation To="50" Duration="0:0:5" AccelerationRatio="0.25" DecelerationRatio="0.25" Storyboard.TargetProperty="Width" RepeatBehavior="5x" /> <DoubleAnimation To="50" Duration="0:0:5" AccelerationRatio="0.5" DecelerationRatio="0.25" Storyboard.TargetProperty="Height" RepeatBehavior="5x" SpeedRatio="4" /> </ParallelTimeline> </Storyboard> <Storyboard x:Key="rect1Storyboard" Storyboard.TargetName="rect1"> <ParallelTimeline> <DoubleAnimation To="50" Duration="0:0:10" FillBehavior="Stop" Storyboard.TargetProperty="Width" /> <DoubleAnimation To="50" Duration="0:0:5"
FillBehavior="HoldEnd" AccelerationRatio="0.5" DecelerationRatio="0.25" Storyboard.TargetProperty="Height" /> </ParallelTimeline> </Storyboard> </Window.Resources> <Window.Triggers> <EventTrigger RoutedEvent="Ellipse.Loaded" SourceName="ellipse1"> <BeginStoryboard Storyboard="{DynamicResource ellipse1Storyboard}" /> </EventTrigger> <EventTrigger RoutedEvent="Rectangle.Loaded" SourceName="rect1"> <BeginStoryboard Storyboard="{StaticResource rect1Storyboard}" /> </EventTrigger> </Window.Triggers> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="0.5*" /> <ColumnDefinition Width="0.5*" /> </Grid.ColumnDefinitions> <Ellipse x:Name="ellipse1" Margin="10" Width="100" Height="100" Fill="CornflowerBlue" /> <Rectangle x:Name="rect1" Margin="10" Width="100" Height="100" Fill="Firebrick" Grid.Column="1" /> </Grid> </Window>
You need to animate the value of some property on a control, but that property is set using a data binding. When the value of the property changes, you need the animation to be updated to reflect the new value.
When the source value of a property changes, the animation needs to be restarted so that the new value can be used within the animation.
Data binding is commonplace in WPF applications, and you may find that you are animating properties of a control that are data bound to some object or you are using data bindings to set values of your System.Windows.Media.Animation.Timeline
objects. For example, you may be animating the Width
property of a System.Windows.Shapes.Ellipse
, where the Width
is bound to the value of a System.Windows.Controls.Slider
, or you have bound the AutoReverse
property of a System.Windows.Media.Animation.DoubleAnimation
to a System.Windows.Controls.CheckBox
. You would expect that when the source value of a data binding changes that the animation would update, but sadly this is one thing that doesn't come for free.
When a Storyboard
is activated, it and its child Timeline
objects are copied and frozen. A System.Windows.Media.Animation.Clock
object is then created for each Timeline
object, including the root Storyboard
that has a generated Clock
used to control any child Clock
objects. The Clock
objects that are created for the storyboard's children are then used to carry out the animation on the target properties. This means any changes to the root Storyboard
or its children will not have any effect on the Clock
objects that have been created. To reflect any changes to a property's data-bound value or a change to a property of a Timeline
object, the animation's Clock
objects need to be re-created with these new values.
In reapplying a Storyboard
, the current position of the existing Storyboard
will be lost, starting the animation over. To combat this, you need to record the current position in time of the active Storyboard
and use the Seek
method on the new root Storyboard
to advance the animation to where it was before the Storyboard
was reapplied. This means the animation can continue with the new values but also means you will have to write some code.
The following XAML demonstrates how to use data bindings in animations. The Duration
property of the Timeline
objects are bound to a dependency property defined in the window's code-behind and is set using the System.Windows.Controls.Slider
. The AutoReverse
property of the Timeline
objects is also bound but this time to a System.Windows.Controls.CheckBox
.
<Window x:Class="Recipe_11_02.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_02" Height="350" Width="350"> <Window.Triggers> <EventTrigger RoutedEvent="Window.Loaded"> <BeginStoryboard Name="ellipseBeginStoryboard"> <Storyboard x:Name="ellipseStoryboard"> <ParallelTimeline x:Name="ellipseTimeline" RepeatBehavior="Forever"> <DoubleAnimation Storyboard.TargetProperty="Width" Storyboard.TargetName="ellipse" AutoReverse="{Binding Path=AutoReverseAnimation}" Duration="{Binding Path=StoryboardDuration}" To="50" From="200" /> <DoubleAnimation Storyboard.TargetProperty="Height" Storyboard.TargetName="ellipse" AutoReverse="{Binding Path=AutoReverseAnimation}" Duration="{Binding Path=StoryboardDuration}" To="50" From="200" /> </ParallelTimeline> </Storyboard> </BeginStoryboard> </EventTrigger> </Window.Triggers> <DockPanel> <StackPanel Margin="5" DockPanel.Dock="Bottom"> <TextBlock Text="Storyboard Duration (s):" Margin="0,5" /> <Slider Width="250" Height="30" Minimum="0" Maximum="60" Value="5" ValueChanged="Slider_ValueChanged" Margin="0,5" /> <CheckBox Content="AutoReverse" IsChecked="{Binding Path=AutoReverseAnimation, Mode=TwoWay}"
Margin="0,5" /> </StackPanel> <Ellipse x:Name="ellipse" Fill="{Binding Path=EllipseFillBrush}" Stroke="Black" StrokeThickness="1" /> </DockPanel> </Window>
The following code-behind contains the dependency properties that are used to supply the animations defined in the previous markup with their data-bound values. When the value of any of these properties changes, the animation is reapplied, reflecting the new values.
using System; using System.Windows; using System.Windows.Media; using System.Windows.Media.Animation; namespace Recipe_11_02 { public partial class Window1 : Window { public Window1() { InitializeComponent(); Loaded += delegate(object sender, RoutedEventArgs e) { //Set the data context of the Window to itself. //This will make binding to the dependency properties, //defined below, a great deal easier. DataContext = this; }; } // Gets or sets the AutoReverseAnimationProperty public bool AutoReverseAnimation { get { return (bool)GetValue(AutoReverseAnimationProperty); } set { SetValue(AutoReverseAnimationProperty, value); } } public static readonly DependencyProperty AutoReverseAnimationProperty = DependencyProperty.Register("AutoReverseAnimation", typeof(bool), typeof(Window1), new UIPropertyMetadata(false, DependencyProperty_Changed));
// Gets or sets the value of the StoryboardDuration property. public Duration StoryboardDuration { get { return (Duration)GetValue(StoryboardDurationProperty); } set { SetValue(StoryboardDurationProperty, value); } } public static readonly DependencyProperty StoryboardDurationProperty = DependencyProperty.Register("StoryboardDuration", typeof(Duration), typeof(Window1), new UIPropertyMetadata(new Duration(TimeSpan.FromSeconds(1)), DependencyProperty_Changed)); // Handles changes to the value of either the StoryboardDuration dependency // property or the EllipseFillBrush dependency property and invokes the // ReapplyStoryboard method, updating the animation to reflect the new // values. private static void DependencyProperty_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e) { Window1 window1 = sender as Window1; if (window1 != null) { window1.ReapplyStoryboard(); } } // Reapplies the 'ellipseStoryboard' object to the 'ellipse' object // defined in the associated markup of the window. private void ReapplyStoryboard() { if (!this.IsInitialized) { return; } //Attempt to get the current time of the active storyboard. //If this is null, a TimeSpan of 0 is used to start the storyboard //from the beginning. TimeSpan? currentTime = ellipseStoryboard.GetCurrentTime(this) ?? TimeSpan.FromSeconds(0); //Restart the storyboard. ellipseStoryboard.Begin(this, true); //Seek to the same position that the storyboard was before it was //restarted. ellipseStoryboard.Seek(this, currentTime.Value,
TimeSeekOrigin.BeginTime); } private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { StoryboardDuration = TimeSpan.FromSeconds(e.NewValue); } } }
You need to remove one or more animations that have been applied to a control, either to stop them running or to free up composited animations (see recipe 11-4).
Several options are available, depending on whether you are working in XAML or in code. The options are as follows:
In code, call Remove
on a System.Windows.Media.Animation.Storyboard
, supplying a reference to the same object used in the call to Begin
.
In markup, use a System.Windows.Media.Animation.RemoveStoryboard
object.
In code, call the ApplyAnimationClock
or BeginAnimation
method on the object being animated, supplying the property being animated and a value of null
. This will stop any animation clocks running against the property.
In code, obtain a System.Windows.Media.Animation.ClockController
from the Controller
property of an animation clock, and call its Remove
method. This will remove the clock in question.
Based on where you need to remove the animation—in other words, from within code or markup—you'll use one of the previous options or maybe even a combination of them.
The first option allows you to remove all the animation clocks defined within a Storyboard
and is performed in code. You need to obtain a reference to the Storyboard
you want to remove and simply call its Remove
method. The method has two signatures, both taking a single parameter that points to either a System.Windows.FrameworkElement
or a System.Windows.FrameworkContentElement
. This must point to an object that was used in a call to a storyboard's Begin
method or used in a System.Windows.Media.Animation.BeginStoryboard
. This will have the effect of removing all clocks that were created for the storyboard's child System.Windows.Media.Animation.Timeline
objects, regardless of any FillBehavior
settings of System.Windows.Media.Animation.FillBehavior.HoldEnd
.
The Sytem.Winows.Media.Animation.ControllableStoryboard
and System.Windows.Media. Animation.RemoveStoryboard
can be used to achieve the same effect as calling Remove
on a Storyboard
within XAML markup. The RemoveStoryboard
can be used to remove any clocks applied by a Storyboard
that was activated using a named System.Windows.Media.Animation.BeginStoryboard
, within the same name scope as the RemoveStoryboard
. The RemoveStoryboard.BeginStoryboardName
property, of type System.String
, is used to identify the BeginStoryboard
used to activate the Storyboard
being removed.
Should you want to remove all clocks applied to a specific property of a control, you should use either the ApplyAnimationClock
method or the BeginAnimation
method of the object to which the property belongs. The two methods are available to any object that descends from System.Windows.Media.Animation.Animatable
, System.Windows.ContentElement
, System. Windows.Media.UIElement
, or System.Windows.Media.Media3D.Visual3D
. The ApplyAnimationClock
method takes two parameters: a System.Windows.DependencyProperty
indicating the property you want to remove any clocks from and a System.Windows.Media.Animation.AnimationClock
to apply to the property. Specifying a value of null
here will remove any clocks applied to the property of the owning object.
Using BeginAnimation
is similar to using ApplyAnimationClock
, and behind the scenes, both methods end up in the same place when their second parameter is null
. Like the ApplyAnimationMethod
, BeginAnimation
takes two parameters, a DependencyProperty
indicating the property on the owning object to be cleared of clocks and a System.Windows.Media.Animation.AnimationTimeline
from which all Timeline
objects are derived. Again, a null
value is supplied for the second parameter, clearing all the clocks applied to the property.
To remove a specific clock from a specific property, you will need to use the fourth option given earlier. This method is used in the code-behind and is a little trickier because it is used with animations that have been created and applied to a property using an AnimationClock
object in code. A reference to an AnimationClock
is usually obtained by calling the CreateClock
method of an AnimationTimeline
object, which is no use for working with an already active clock. To remove an instance of an AnimationClock
, a reference to its ClockController
is required. This is easily obtained through the Controller
property on an AnimationTimeline
object. The Remove
method of the clock's controller doesn't require any parameters and will result in any instances of that clock and its child clocks being removed from any properties they are animating.
The following example demonstrates the four different options for removing any animations that are active on a property of some object. The following XAML defines four buttons, each of which has an animation applied to its Width
property. When each button is clicked, the animations running on it will be stopped using one of the previous four methods.
<Window x:Class="Recipe_11_03.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_03" Height="300"
Width="300"> <Window.Resources> <Storyboard x:Key="Storyboard1"> <ParallelTimeline> <DoubleAnimation x:Name="Animation1" Storyboard.TargetProperty="Width" From="140" To="50" AutoReverse="True" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard.TargetProperty="Opacity" To="0.5" AutoReverse="True" RepeatBehavior="Forever" /> </ParallelTimeline> </Storyboard> </Window.Resources> <UniformGrid> <Button Margin="5" Content="Method 1"> <Button.Triggers> <EventTrigger RoutedEvent="Button.Loaded"> <BeginStoryboard Storyboard="{DynamicResource Storyboard1}" x:Name="BeginStoryboard1" /> </EventTrigger> <EventTrigger RoutedEvent="Button.Click"> <RemoveStoryboard BeginStoryboardName="BeginStoryboard1" /> </EventTrigger> </Button.Triggers> </Button> <Button Margin="5" Content="Method 2" Click="Button2_Click"> <Button.Triggers> <EventTrigger RoutedEvent="Button.Loaded"> <BeginStoryboard Storyboard="{DynamicResource Storyboard1}" /> </EventTrigger> </Button.Triggers> </Button>
<Button x:Name="button3" Margin="5" Content="Method 3" Click="Button3_Click" Loaded="Button3_Loaded" /> <Button Margin="5" Content="Method 4" Click="Button4_Click" Loaded="Button4_Loaded" /> </UniformGrid> </Window>
The following code-behind declares the methods that handle the click events for three of the four System.Windows.Controls.Button
objects, defined in the previous XAML:
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Media.Animation; namespace Recipe_11_03 { public partial class Window1 : Window { public Window1() { InitializeComponent(); } #region Method 2 /// <summary> /// Handler for the Button.Click event on the 'Method 2' button. /// This method removes the animations affecting the button /// using the BeginAnimation() method, passing a null reference /// for the value of the System.Windows.Media.Animation.AnimationTimeline. /// </summary> private void Button2_Click(object sender, RoutedEventArgs e) { //Cast the sender to a button. Button button2 = sender as Button; //Remove any active animations against the Button's width property. button2.BeginAnimation(Button.WidthProperty, null); //Remove any active animations against the Button's height property. button2.BeginAnimation(Button.OpacityProperty, null); }
#endregion #region Method 3 //Store a reference to the AnimationClock objects when they are created. //This allows for the clocks to be accessed later when it comes to //removing them. private AnimationClock opacityClock; private AnimationClock widthClock; //Method that handles the Button.Loaded event on the 'Method 3' button. //Animations are created and applied to 'button3', storing a reference to //the clocks that are created. private void Button3_Loaded(object sender, RoutedEventArgs e) { DoubleAnimation opacityAnimation = new DoubleAnimation(1d, 0.5d, TimeSpan.FromSeconds(1), FillBehavior.HoldEnd); opacityAnimation.RepeatBehavior = RepeatBehavior.Forever; opacityAnimation.AutoReverse = true; opacityClock = opacityAnimation.CreateClock(); button3.ApplyAnimationClock(Button.OpacityProperty, opacityClock); DoubleAnimation widthAnimation = new DoubleAnimation(140d, 50d, TimeSpan.FromSeconds(1), FillBehavior.HoldEnd); widthAnimation.RepeatBehavior = RepeatBehavior.Forever; widthAnimation.AutoReverse = true; widthClock = widthAnimation.CreateClock(); button3.ApplyAnimationClock(Button.WidthProperty, widthClock); } //Handles the Button.Click event of 'button3'. This uses the third //method of removing animations by removing each of the clocks. private void Button3_Click(object sender, RoutedEventArgs e) { opacityClock.Controller.Remove(); widthClock.Controller.Remove(); } #endregion #region Method 4 //Store a local reference to the storyboard we want to //interact with. private Storyboard method4Storyboard;
private void Button4_Loaded(object sender, RoutedEventArgs e) { method4Storyboard = TryFindResource("Storyboard1") as Storyboard; method4Storyboard.Begin(sender as FrameworkElement, true); } //Handles the Button.Click event of the 'Method 4' button. private void Button4_Click(object sender, RoutedEventArgs e) { //Make sure we got a valid reference. if (method4Storyboard != null) { //Remove the storyboard by calling its Remove method, passing the //control that it is currently running against. method4Storyboard.Remove(sender as FrameworkElement); } } #endregion } }
Specify a System.Windows.Media.Animation.HandoffBehavior
value of HandoffBehavior.Compose
to overlap animations applied to a property.
When an animation is applied to a property of a System.Windows.FrameworkElement
, it is possible to specify how any existing animations applied to the property should be handled. Two options are available to you. One stops and removes any existing animations before applying the new animation, and the second blends the two animations, merging the existing animation into the new one. Table 11-2 details these values.
Table 11-2. The Values of HandoffBehavior
Value | Description |
---|---|
| The new animation will be partly merged into any existing ones, creating a smoother transition between the two. |
| Any existing animation will be stopped at its current position, and the new one will begin. This creates a sharper transition between the two animations. |
It is important to note that when animations are applied with a HandoffBehavior
of HandoffBehavior.Compose
, any existing animation will not free up the resources it is using until the owning object of the property the animation is applied to is freed. This can cause considerable performance overheads when compositing several large animations. The best way to ensure that your application doesn't suffer a performance hit is to manually free up any composited animations. You can easily achieve this by adding a handler to the Completed
event of your System.Windows.Media.Animation.Timeline
object (see recipe 11-15) and removing any animations from the property in question (see recipe 11-3).
The behavior is strange if To
or From
is specified. If either is specified, the animation will jump straight to the value of To
.
<Window x:Class="Recipe_11_04.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_04" Height="300" Width="300"> <Window.Resources> <Storyboard x:Key="LowOpacity"> <DoubleAnimation Storyboard.TargetProperty="Opacity" /> </Storyboard> <Storyboard x:Key="HighOpacity"> <DoubleAnimation Storyboard.TargetProperty="Opacity" To="1" AutoReverse="True" RepeatBehavior="Forever" /> </Storyboard> </Window.Resources> <Grid> <Grid.ColumnDefinitions>
<ColumnDefinition Width="0.5*" /> <ColumnDefinition Width="0.5*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="35" /> </Grid.RowDefinitions> <Border Background="Firebrick" Width="100" Height="100" x:Name="Rect1" Opacity="0.4"> <Border.Triggers> <EventTrigger RoutedEvent="Mouse.MouseEnter"> <BeginStoryboard Storyboard="{DynamicResource HighOpacity}" /> </EventTrigger> <EventTrigger RoutedEvent="Mouse.MouseLeave"> <BeginStoryboard Storyboard="{DynamicResource LowOpacity}" /> </EventTrigger> </Border.Triggers> </Border> <Rectangle Fill="Firebrick" Width="100" Height="100" Grid.Column="1" /> </Grid> </Window>
You need to animate several properties of a control at the same time, that is, its height, width, and color.
Define your animations as normal but as children of a System.Windows.Media.Animation.ParallelTimeline
.
The ParallelTimeline
is a special type of System.Windows.Media.Animation.Timeline
that allows for one or more child Timeline
objects to be defined as its children, with each child Timeline
being run in parallel. Because ParallelTimeline
is a Timeline
object, it can be used like any other Timeline
object. Unlike a Storyboard
where animations are activated based on the order in which its child Timeline
objects are declared, a ParallelTimeline
will activate its children based on the value of their BeginTime
properties. If any of the animations overlap, they will run in parallel.
The Storyboard
class actually inherits from ParallelTimeline
and simply gives each child a BeginTime
based on where in the list of child objects a Timeline
is declared and the cumulative Duration
and BeginTime
values of each preceding Timeline
. The Storyboard
class goes further to extend the ParallelTimeline
class by adding a great deal of methods for controlling the processing of its child Timeline
objects. Because ParallelTimeline
is the ancestor of a Storyboard
, ParallelTimeline
objects are more suited to nesting because they are much slimmer objects.
Like other Timeline
objects, the ParallelTimeline
has a BeginTime
property. This allows you to specify an offset from the start of the owning Storyboard
to the activation of the ParallelTimeline
. As a result, if a value for BeginTime
is given by the ParallelTimeline
, its children's BeginTime
will work relative to this value, as opposed to being relative to the Storyboard
.
It is important to note that a Storyboard.Completed
event will not be raised on the owning Storyboard
until the last child Timeline
in the ParallelTimeline
finishes. This is because a ParallelTimeline
can contain Timeline
objects with different BeginTime
and Duration
values, meaning they won't all necessarily finish at the same time.
The following example defines a System.Windows.Window
that contains a single System.Windows.Shapes.Rectangle
. When the mouse is placed over the rectangle, the Rectangle.Height
, Rectangle.Width
, and Rectangle.Fill
properties are animated. The animation continues until the mouse is moved off the rectangle.
<Window x:Class="Recipe_11_05.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_05" Height="300" Width="300"> <Grid> <Rectangle Height="100" Width="100" Fill="Firebrick" Stroke="Black" StrokeThickness="1"> <Rectangle.Style>
<Style TargetType="Rectangle"> <Style.Triggers> <EventTrigger RoutedEvent="Rectangle.MouseEnter"> <BeginStoryboard> <Storyboard> <ParallelTimeline RepeatBehavior="Forever" AutoReverse="True"> <DoubleAnimation Storyboard.TargetProperty="Width" To="150" /> <DoubleAnimation Storyboard.TargetProperty="Height" To="150" /> <ColorAnimation Storyboard.TargetProperty="Fill.Color" To="Orange" /> </ParallelTimeline> </Storyboard> </BeginStoryboard> </EventTrigger> <EventTrigger RoutedEvent="Rectangle.MouseLeave"> <BeginStoryboard> <Storyboard> <ParallelTimeline> <DoubleAnimation Storyboard.TargetProperty="Width" To="100" /> <DoubleAnimation Storyboard.TargetProperty="Height" To="100" /> <ColorAnimation Storyboard.TargetProperty="Fill.Color" To="Firebrick" /> </ParallelTimeline> </Storyboard> </BeginStoryboard> </EventTrigger> </Style.Triggers> </Style> </Rectangle.Style> </Rectangle> </Grid> </Window>
Use a keyframe-based animation such as System.Windows.Media.Animation.DoubleAnimationUsingKeyFrames
. You can then use several System.Windows.Media.Animation.IKeyFrame
objects to define the keyframes in your animation.
The use of keyframes will be familiar to anyone who has ever touched on animation. For those who are new, keyframes basically allow you to specify key points in an animation where the object being animated needs to be at a required position or in a required state. The frames in between are then interpolated between these two keyframes, effectively filling in the blanks in the animation. This process of interpolating the in-between frames is often referred to as tweening.
When defining an animation using keyframes, you will need to specify one or more keyframes that define the animation's flow. These keyframes are defined as children of your keyframe animation. It is important to note that the target type of the keyframe must match that of the parent animation. For example, if you are using a System.Windows.Media.Animation.DoubleAnimationUsingKeyFrames
, any keyframes must be derived from the abstract class System.Windows.Media.Animation.DoubleKeyFrame
.
You will be pleased to hear that a good number of types have keyframe objects, from System.Int
to System.String
and System.Windows.Thickness
to System.Windows.Media.Media3D.Quarternion
. (For a more complete list of the types covered, please see http://msdn.microsoft.com/en-us/library/ms742524.aspx
.) All but a few of the types covered by animations have a choice of interpolation methods, allowing you to specify how the frames in between two keyframes are generated. Each interpolation method is defined as a prefix to the keyframe's class name and is listed in Table 11-3.
Table 11-3. Interpolation Methods for Keyframe Animation
Type | Description |
---|---|
Discrete | A discrete keyframe will not create any frames in between it and the following keyframe. Once the discrete keyframe's duration has elapsed, the animation will jump to the value specified in the following keyframe. |
Linear | Linear keyframes will create a smooth transition between it and the following frame. The generated frames will animate the value steadily at a constant rate to its end point. |
Spline | Spline keyframes allow you to vary the speed at which a property is animated using the shape of a Bezier curve. The curve is described by defining its control points in unit coordinate space. The gradient of the curve defines the speed or rate of change in the animation. |
Although keyframes must match the type of the owning animation, it is possible to mix the different types of interpolation, offering variable speeds throughout.
The following XAML demonstrates how to use linear and double keyframes to animate the Height
and Width
properties of a System.Windows.Shapes.Ellipse
control (see Figure 11-1). The animation is triggered when the System.Windows.Controls.Button
is clicked.
<Window x:Class="Recipe_11_06.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_06" Height="300" Width="300"> <Window.Resources> <Storyboard x:Key="ResizeEllipseStoryboard"> <ParallelTimeline> <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ellipse" Storyboard.TargetProperty="Height"> <LinearDoubleKeyFrame Value="150" KeyTime="0:0:1" /> <LinearDoubleKeyFrame Value="230" KeyTime="0:0:2" /> <LinearDoubleKeyFrame Value="150" KeyTime="0:0:2.5" /> <LinearDoubleKeyFrame Value="230" KeyTime="0:0:5" /> <LinearDoubleKeyFrame Value="40" KeyTime="0:0:9" /> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ellipse" Storyboard.TargetProperty="Width"> <DiscreteDoubleKeyFrame Value="150" KeyTime="0:0:1" /> <DiscreteDoubleKeyFrame Value="230" KeyTime="0:0:2" /> <DiscreteDoubleKeyFrame Value="150" KeyTime="0:0:2.5" /> <DiscreteDoubleKeyFrame Value="230" KeyTime="0:0:5" /> <DiscreteDoubleKeyFrame Value="40" KeyTime="0:0:9" /> </DoubleAnimationUsingKeyFrames> </ParallelTimeline> </Storyboard> </Window.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="40" /> </Grid.RowDefinitions>
<Ellipse Height="40" Width="40" x:Name="ellipse" HorizontalAlignment="Center" VerticalAlignment="Center"> <Ellipse.Fill> <RadialGradientBrush GradientOrigin="0.75,0.25"> <GradientStop Color="Yellow" Offset="0.0" /> <GradientStop Color="Orange" Offset="0.5" /> <GradientStop Color="Red" Offset="1.0" /> </RadialGradientBrush> </Ellipse.Fill> </Ellipse> <Button Content="Start..." Margin="10" Grid.Row="1"> <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <BeginStoryboard Storyboard="{DynamicResource ResizeEllipseStoryboard}" /> </EventTrigger> </Button.Triggers> </Button> </Grid> </Window>
Use the Seek
method on a System.Windows.Media.Animation.Storyboard
to programmatically set the location of the playhead from the offset from the start of a Storyboard
.
The Seek
method on a Storyboard
object allows you to specify a duration relative to a specified origin to which the playhead should be positioned. This allows you to move the current position of the animation with great precision. The method takes three parameters—one being the object being animated, the second being a System.TimeSpan
providing the offset to seek to, and the third a System.Windows.Media.Animation.TimeSeekOrigin
value. The TimeSeekOrigin
enumeration defines two values, details of which are given in Table 11-4.
Table 11-4. The Values of a TimeSeekOrigin
Enumeration
Value | Description |
---|---|
| The specified offset is relative to the start of the animation. |
| The specified offset is relative to end of the animation. |
By looking at the total duration of the Storyboard
, it is possible to drive the position of the animation using a control such as a System.Windows.Controls.Slider
. Handling the Slider.ValueChanged
event on a slider control, you can seek to a location in the animation based on the new location of the slider.
The following code demonstrates how to use a System.Windows.Controls.Slider
to control the progress of an animation (see Figure 11-2). It is important to note that the animation is paused while the user is interacting with the Slider
control so as to prevent the animation from progressing while the slider is being moved.
<Window x:Class="Recipe_11_07.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_07" Height="300" Width="600"> <Grid>
<Rectangle x:Name="Rectangle" Height="100" Width="100" Fill="Firebrick"> <Rectangle.RenderTransform> <MatrixTransform x:Name="RectangleMatrixTransform" /> </Rectangle.RenderTransform> <Rectangle.Triggers> <EventTrigger RoutedEvent="Rectangle.Loaded"> <BeginStoryboard x:Name="RectangleStoryboard"> <Storyboard x:Name="Storyboard" CurrentTimeInvalidated="Storyboard_Changed"> <MatrixAnimationUsingPath Storyboard.TargetName="RectangleMatrixTransform" Storyboard.TargetProperty="Matrix" Duration="0:0:10" RepeatBehavior="Forever"> <MatrixAnimationUsingPath.PathGeometry> <PathGeometry Figures="M −100,0 300, 0" /> </MatrixAnimationUsingPath.PathGeometry> </MatrixAnimationUsingPath> </Storyboard> </BeginStoryboard> </EventTrigger> </Rectangle.Triggers> </Rectangle> <Slider x:Name="Seeker" Minimum="0" Maximum="1" SmallChange="0.001" ValueChanged="Seeker_ValueChanged"> <Slider.Triggers> <EventTrigger RoutedEvent="Slider.MouseLeftButtonDown"> <StopStoryboard BeginStoryboardName="RectangleStoryboard" /> </EventTrigger> <EventTrigger RoutedEvent="Slider.MouseLeftButtonUp"> <ResumeStoryboard BeginStoryboardName="RectangleStoryboard" /> </EventTrigger> </Slider.Triggers> </Slider> </Grid> </Window>
The following code-behind defines methods that update the value of the Slider
, defined in the previous markup, and respond to the user moving the slider, seeking to a new point in the animation:
using System; using System.Windows; using System.Windows.Media.Animation; using System.Windows.Input; namespace Recipe_11_07 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } bool ignoreValueChanged = false; private void Storyboard_Changed(object sender, System.EventArgs e) { ClockGroup clockGroup = sender as ClockGroup; AnimationClock animationClock = clockGroup.Children[0] as AnimationClock; if (animationClock.CurrentProgress.HasValue) { ignoreValueChanged = true; Seeker.Value = animationClock.CurrentProgress.Value; ignoreValueChanged = false; } } private void Seeker_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { if (ignoreValueChanged && Mouse.LeftButton != MouseButtonState.Pressed) { return; } Storyboard.Seek(Rectangle, TimeSpan.FromTicks((long)(Storyboard.Children[0].Duration.TimeSpan.Ticks
* Seeker.Value)), TimeSeekOrigin.BeginTime); } } }
A PointAnimation
allows you to animate the value of a System.Windows.Point
. By naming the sections of your path, you have complete access to the object, for example, a System.Windows.Media.LineSegment
. By referring to the appropriate property, you are able to animate them, giving the appearance that the shape is changing. It is possible to target any point of a System.Windows.Media.PathSegment
object, including the control points in a System.Windows.Media.BezierSegment
.
The following XAML defines several different shapes within a path (see Figure 11-3). Each shape is animated using either a System.Windows.Media.Animation.DoubleAnimation
or a PointAnimation
.
<Window x:Class="Recipe_11_08.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_08" Height="300" Width="300"> <Grid> <Path Stroke="Black" StrokeThickness="1"> <Path.Data> <GeometryGroup> <LineGeometry x:Name="line1" StartPoint="20,20" EndPoint="264,20" /> <LineGeometry x:Name="line2" StartPoint="38,40" EndPoint="248,40" /> <LineGeometry x:Name="line3" StartPoint="140,60" EndPoint="140,150" /> <LineGeometry x:Name="line4" StartPoint="160,60" EndPoint="160,150" /> <EllipseGeometry x:Name="ellipse" Center="150,150" RadiusX="5" RadiusY="5" /> <PathGeometry> <PathFigure> <BezierSegment x:Name="bezierSegment1" IsStroked="True" Point1="200,200" Point2="105,205" Point3="280,0" /> </PathFigure> </PathGeometry> <PathGeometry> <PathFigure StartPoint="0,265"> <BezierSegment x:Name="bezierSegment2" IsStroked="True"
Point1="100,100" Point2="206,117" Point3="280,267" /> </PathFigure> </PathGeometry> </GeometryGroup> </Path.Data> <Path.Triggers> <EventTrigger RoutedEvent="Path.Loaded"> <BeginStoryboard> <Storyboard AutoReverse="True" RepeatBehavior="Forever"> <PointAnimation To="40,20" Storyboard.TargetName="line1" Storyboard.TargetProperty="EndPoint" /> <PointAnimation To="280,40" Storyboard.TargetName="line2" Storyboard.TargetProperty="StartPoint" /> <PointAnimation To="20,60" Storyboard.TargetName="line3" Storyboard.TargetProperty="EndPoint" /> <PointAnimation To="280,60" Storyboard.TargetName="line4" Storyboard.TargetProperty="EndPoint" /> <ParallelTimeline Storyboard.TargetName="ellipse"> <DoubleAnimation To="80" Storyboard.TargetProperty="RadiusX" /> <DoubleAnimation To="80" Storyboard.TargetProperty="RadiusY" /> </ParallelTimeline> <ParallelTimeline Storyboard.TargetName="bezierSegment1"> <PointAnimation Storyboard.TargetProperty="Point1" To="300,0" /> <PointAnimation Storyboard.TargetProperty="Point2" To="0,270" /> <PointAnimation Storyboard.TargetProperty="Point3" To="300,263" /> </ParallelTimeline> <ParallelTimeline Storyboard.TargetName="bezierSegment2"> <PointAnimation Storyboard.TargetProperty="Point1" To="0,0" /> <PointAnimation Storyboard.TargetProperty="Point2" To="260,300" /> <PointAnimation Storyboard.TargetProperty="Point3" To="280,0" />
</ParallelTimeline> </Storyboard> </BeginStoryboard> </EventTrigger> </Path.Triggers> </Path> </Grid> </Window>
Animation offers two properties, RepeatBehavior
and AutoReverse
, to control the looping and reversal of your animation.
The two properties mentioned earlier allow you to perform tricky functionality very quickly and easily. As you might have guessed, the RepeatBehavior
property of a System.Windows.Media.Animation.Timeline
-based object allows you to control how many times an animation is repeated. You can specify this value in one of three ways, each of which is described in Table 11-5.
Table 11-5. Specifying the Repeat Behavior of an Animation
Mode | Description |
---|---|
Iteration Count | Specify the number of times that the animation should repeat, prefixed with the character |
Duration | A repeat duration can be specified as a whole number of days, a whole number of hours, a whole number of minutes, a number of seconds, or a whole number of fractional seconds. |
Forever | The special |
The System.Windows.Media.Animation.Storyboard.Completed
event does not get raised until the last Timeline
in the animation has completed. This means that if your animation has a repeat behavior of RepeatBehavior.Forever
, the Storyboard.Completed
event will never get raised.
The AutoReverse
property of a Timeline
is a great deal simpler and is of type System.Boolean
. Setting the value to True
will result in the animation playing from end to beginning, each time it reaches the end of the timeline. The default value of this property is False
, resulting in the animation playing again from the beginning if any repeat behavior is defined. Automatically reversed animations count as a playback in terms of duration but not in terms of iterations. That is, if your animation has a repeat behavior of 2x
and AutoReverse
is True
, the animation will play through from beginning to end and back again twice. If, however, the duration of the animation is one second and a RepeatBehavior
of two seconds is specified, the animation will play from beginning to end and back again only once.
The following example demonstrates how to use the RepeatBehavior
and AutoReverse
properties. The code defines a System.Windows.Window
that contains four System.Windows.Shapes.Ellipse
controls. Each of the Ellipse
objects has a different animation applied to it, demonstrating how to use the AutoReverse
and RepeatBehavior
properties.
<Window x:Class="Recipe_11_09.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_09" Height="600" Width="600"> <UniformGrid> <Ellipse
Height="200" Width="200" Fill="Firebrick"> <Ellipse.Triggers> <EventTrigger RoutedEvent="Ellipse.Loaded"> <BeginStoryboard> <Storyboard AutoReverse="True" RepeatBehavior="Forever"> <ColorAnimation Storyboard.TargetProperty="Fill.Color" To="White" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Ellipse.Triggers> </Ellipse> <Ellipse Height="200" Width="200" Fill="Firebrick"> <Ellipse.Triggers> <EventTrigger RoutedEvent="Ellipse.Loaded"> <BeginStoryboard> <Storyboard Duration="0:0:1" RepeatBehavior="0:0:4"> <ColorAnimation Storyboard.TargetProperty="Fill.Color" To="White" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Ellipse.Triggers> </Ellipse> <Ellipse Height="200" Width="200" Fill="Firebrick"> <Ellipse.Triggers> <EventTrigger RoutedEvent="Ellipse.Loaded"> <BeginStoryboard>
<Storyboard RepeatBehavior="5x"> <ColorAnimation Storyboard.TargetProperty="Fill.Color" To="White" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Ellipse.Triggers> </Ellipse> <Ellipse Height="200" Width="200" Fill="Firebrick"> <Ellipse.Triggers> <EventTrigger RoutedEvent="Ellipse.Loaded"> <BeginStoryboard> <Storyboard AutoReverse="True" RepeatBehavior="0:0:2"> <ColorAnimation Storyboard.TargetProperty="Fill.Color" To="White" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Ellipse.Triggers> </Ellipse> </UniformGrid> </Window>
You need to set the desired frame rate of a System.Windows.Media.Animation.Storyboard
object for performance reasons or otherwise.
Use the Timeline.DesiredFrameRate
attached property to specify a desired frame rate for a Storyboard
.
The Timeline.DesiredFrameRate
attached property can be used to set a desired frame rate for a Storyboard
and is applied to each of its child Timeline
objects, if present. The property is a nullable System.Int32
value and must be greater than 0, where the specified value is measured in frames per second (fps). Setting the value to null
will cause the desired frame rate to be set to the default value of 60 fps.
The desired frame rate of an animation should be thought of more as a frame rate limit that should not be exceeded. The animation framework will attempt to run the animation at the specified value, although this may not always be possible, depending on the performance and load of the host machine. For example, if the desired frame rate is set to 200 fps and the host machine is running several other animations, 200 fps may not be achievable, in which case the animation will run at the fastest possible frame rate, up to 200 fps.
You may want to limit the frame rate of a Storyboard
to reduce the amount of processing required to run the animation as less work needs to be carried out each second. This is suited to slower animations that do not require a high frame rate. You may also want to set a high frame rate to allow animations with fast-moving objects to appear smoother and free from tearing.
The following example demonstrates how to use the Timeline.DesiredFrameRate
attached property. Three System.Windows.Media.Animation.Storyboard
objects are defined, each with a single System.Windows.Media.Animation.DoubleAnimation
. Each child DoubleAnimation
targets the Y property of a System.Windows.Media.TranslateTransform
, applied to each of the three System.Windows.Shapes.Ellipse
controls. Each Timeline
runs at the same speed but has a different frame rate, highlighting the effects of altering the frame rate.
The Ellipse
to the far left of the window will be animated with the smoothest movement and least tearing. The center Ellipse
will be animated with a smooth movement but will be affected by tearing. The final Ellipse
to the far right of the window will appear to jump from its start position to its final position because of its frame rate of 1 fps (see Figure 11-4).
<Window x:Class="Recipe_11_10.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_10" Height="300" Width="300"> <Window.Triggers> <EventTrigger RoutedEvent="Window.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation AutoReverse="True" RepeatBehavior="Forever" Storyboard.TargetName="tt1" Storyboard.TargetProperty="Y" Duration="0:0:1" To="-90" />
</Storyboard> </BeginStoryboard> <BeginStoryboard> <Storyboard Timeline.DesiredFrameRate="1"> <DoubleAnimation AutoReverse="True" RepeatBehavior="Forever" Storyboard.TargetName="tt2" Storyboard.TargetProperty="Y" To="-90" Duration="0:0:1" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Window.Triggers> <Grid> <Ellipse Width="75" Height="75" Fill="Firebrick" Stroke="Black" StrokeThickness="1"> <Ellipse.RenderTransform> <TranslateTransform x:Name="tt1" X="-75" Y="90" /> </Ellipse.RenderTransform> </Ellipse> <Ellipse Width="75" Height="75" Fill="Plum" Stroke="Black" StrokeThickness="1"> <Ellipse.RenderTransform> <TranslateTransform x:Name="tt2" X="75" Y="90" /> </Ellipse.RenderTransform> </Ellipse> </Grid> </Window>
Override the default value for the System.Windows.Media.Animation.Timeline.DesiredFrameRate
dependency property.
The default value for the Timeline.DesiredFrameRate
property is set to null
, meaning that a value for the default desired frame rate will be calculated. Because this property is a standard dependency property, its property metadata can be overridden (see recipe 1-5 for more information on overriding the PropertyMetaData
of a dependency property), allowing you to specify a new default value for any Timeline
objects in your application.
To ensure that all the animations used in your application receive the new default value, it is best to perform the override at some point during the application's startup. This value can be changed later, giving a new default value to any animations created later in the application's life.
Because the override affects only the property's default value, animations can still be given other desired frame rate values by explicitly setting a value on the parent System.Windows.Media.Animation.Storyboard
. This allows you to ensure that only certain animations are run at a higher or lower frame rate than the desired default value, which is useful if the majority of animations in your application are slow running in the background.
The following example demonstrates how to override the PropertyMetadata
for the Timeline.DesiredFrameRateProperty
dependency property. The following code details the content of the App.xaml.cs
file. Here an event handler is registered against the System.Windows.Application.Startup
event, which, when invoked, overrides the property metadata for the DesiredFrameRateProperty
dependency property.
using System.Windows; using System.Windows.Media.Animation; namespace Recipe_11_11 { public partial class App : Application { public App() { Startup += delegate(object sender, StartupEventArgs e) { Timeline.DesiredFrameRateProperty.OverrideMetadata(typeof(Timeline), new PropertyMetadata(1)); }; } } }
The following XAML declares two System.Windows.Shapes.Ellipse
objects, both of which are animated, moving them in a vertical direction. The first animation, affecting the Ellipse
to the left of the window, runs with the overridden default desired frame rate. The second animation that affects the Ellipse
to the right of the window runs at an explicitly defined frame rate.
<Window x:Class="Recipe_11_11.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_11" Height="300" Width="300"> <Window.Triggers> <EventTrigger RoutedEvent="Window.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation AutoReverse="True" RepeatBehavior="Forever"
Storyboard.TargetName="tt1" Storyboard.TargetProperty="Y" Duration="0:0:1" To="-90" /> </Storyboard> </BeginStoryboard> <BeginStoryboard> <Storyboard Timeline.DesiredFrameRate="60"> <DoubleAnimation AutoReverse="True" RepeatBehavior="Forever" Storyboard.TargetName="tt2" Storyboard.TargetProperty="Y" To="-90" Duration="0:0:1" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Window.Triggers> <Grid> <Ellipse Width="75" Height="75" Fill="Firebrick" Stroke="Black" StrokeThickness="1"> <Ellipse.RenderTransform> <TranslateTransform x:Name="tt1" X="-75" Y="90" /> </Ellipse.RenderTransform> </Ellipse> <Ellipse Width="75" Height="75" Fill="Plum" Stroke="Black" StrokeThickness="1"> <Ellipse.RenderTransform> <TranslateTransform x:Name="tt2" X="75" Y="90" />
</Ellipse.RenderTransform> </Ellipse> </Grid> </Window>
WPF kindly provides you with three ways of animating an object along a path. Each of these methods takes a System.Windows.Media.PathGeometry
as its input, defining the shape of the path that the object will follow, and produces some kind of output, depending on the time-line's target type. All three timelines generate their output values by linearly interpolating between the values of the input path. Table 11-6 describes each of these three methods.
Table 11-6. Path Animation Types
Type | Description |
---|---|
| Outputs a single |
| Generates a series of |
| Generates a series of |
Table 11-7. Values of the PathAnimationSource
Enumeration
Value | Description |
---|---|
| Values output by the |
| Values output by the |
| Values output by the |
It should be clear that each of the path timelines has a specific use and offers different levels of functionality. The MatrixAnimationUsingPath
provides the neatest method for animating both the position and the orientation of an object. The same effect is not possible at all using a PointAimationUsingPath
and would require three DoubleAnimationUsingPath
timelines, each with a different PathAnimationSource
value for the Source
property.
When using a value of PathAnimationSource.Angle
for the Source
property of a DoubleAnimationUsingPath
timeline or setting the DoesRotateWithTangent
property of a MatrixAnimationUsingPath
timeline to True
, you ensure that the object being animated is correctly rotated so that it follows the gradient of the path. If an arrow is translated using a path-driven animation, its orientation will remain the same throughout the timeline's duration. If, however, the arrow's orientation is animated to coincide with the path, the arrow will be rotated relative to its initial orientation, based on the gradient of the path. If you have a path defining a circle and the arrow initially points in to the center of the circle, the arrow will continue to point into the center of the circle as it moves around the circle's circumference.
Although the MatrixAnimationUsingPath
has the most compact output, controls will rarely expose a Matrix
property that you can directly animate. The target property of a MatrixAnimationUsingPath
timeline will most commonly be the Matrix
property of a System.Windows.Media.MatrixTransform
, where the MatrixTransform
is used in the render transform or layout transform of the control you want to animate. In a similar fashion, DoubleAnimationUsingPath
can be used to animate the properties of a System.Windows.Media.TranslateTransform
and System.Windows.Media.RotateTransform
or any just about any System.Double
property of the target control.
The following XAML demonstrates how to use a MatrixAnimationUsingPath
, where a System.Windows.Controls.Border
is translated and rotated, according to the shape of the path. The path is also drawn on the screen to better visualize the motion of the Border
(see Figure 11-5).
<Window x:Class="Recipe_11_12.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_12" Height="300" Width="550">
<Window.Resources> <PathGeometry x:Key="AnimationPathGeometry" Figures="M 50,150 C 100,-200 500,400 450,100 400,-100 285,400 50,150" /> <Storyboard x:Key="MatrixAnimationStoryboard"> <MatrixAnimationUsingPath RepeatBehavior="Forever" Duration="0:0:5" AutoReverse="True" Storyboard.TargetName="BorderMatrixTransform" Storyboard.TargetProperty="Matrix" DoesRotateWithTangent="True" PathGeometry="{StaticResource AnimationPathGeometry}" /> </Storyboard> </Window.Resources> <Grid> <Path Stroke="Black" StrokeThickness="1" Data="{StaticResource AnimationPathGeometry}" /> <Border HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" Height="50" CornerRadius="5" BorderBrush="Black" BorderThickness="1" RenderTransformOrigin="0,0"> <Border.Background> <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1"> <GradientStop Color="CadetBlue" Offset="0" /> <GradientStop Color="CornflowerBlue" Offset="1" /> </LinearGradientBrush> </Border.Background> <Border.RenderTransform> <MatrixTransform x:Name="BorderMatrixTransform" /> </Border.RenderTransform>
<Border.Triggers> <EventTrigger RoutedEvent="Border.Loaded"> <BeginStoryboard Storyboard="{StaticResource MatrixAnimationStoryboard}" /> </EventTrigger> </Border.Triggers> <TextBlock Text="^ This way up ^" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Border> </Grid> </Window>
Use a System.Windows.Media.MediaTimeline
to provide the playback of animated media objects through a System.Windows.Controls.MediaElement
control, such as audio and video files.
The MediaElement
control on its own can be used to display static content, but when driven by a MediaTimeline
, it is able to provide the playback of media files. The MediaTimeline
class inherits from System.Windows.Media.Animation.Timeline
and as such can be configured using the properties listed in recipe 11-1. The MediaTimeline
is given the location of a media file as its Source
property and, when activated, will play the media file through the MediaElement
it is targeting. Because the MediaTimeline
is a Timeline
object, it can be controlled by the several System.Windows.Media.Animation.ControllableStoryboardAction
objects, allowing the media to be paused, resumed, stopped, skipped through, and so on. This allows you to create a rich media player very easily and all in XAML. For more information on using the MediaElement
control, see http://msdn.microsoft.com/en-us/library/system.windows.controls.mediaelement.aspx
.
By default, the Duration
property of a MediaTimeline
will be set to the duration of the media it is playing, also accessible through the MediaElement.NaturalDuration
property, which returns a System.TimeSpan
. Combine this with the MediaElement.Position
property, also a TimeSpan
, and you instantly have the ability to display the progress of a media file as it plays through, something that is commonplace in media playback.
The following XAML demonstrates how to use a MediaTimeline
to drive the playback of a video file through a MediaElement
(see Figure 11-6).
<Window x:Class="Recipe_11_13.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_13" Height="300" Width="300"> <Viewbox> <MediaElement x:Name="mePlayer" Stretch="Fill"> <MediaElement.Triggers> <EventTrigger RoutedEvent="MediaElement.Loaded"> <BeginStoryboard> <Storyboard> <MediaTimeline Storyboard.TargetName="mePlayer" Source="clock.avi" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </MediaElement.Triggers>
</MediaElement> </Viewbox> </Window>
You need to ensure that any System.Windows.Media.Animation.Timeline
animations remain synchronized with a System.Windows.Media.MediaTimeline
defined in the same System.Windows.Media.Animation.Storyboard
.
Set the SlipBehavior
property of the parent System.Windows.Media.Animation.ParallelTimeline
object to SlipBehavior.Slip
.
Often when playing back rich content such as audio or video, loading times or workload on the host machine can affect the smoothness of the media. If other Timeline
animations are running alongside the media, they can get out of sync if the media's playback is disrupted. For example, if you have a Timeline
animation that is the same length as a video file played using a MediaTimeline
and the start of the MediaTimeline
is delayed by a second because of loading or buffering, the Timeline
animation will have played for one second before the MediaTimeline
starts to play the video. This will give the appearance that the Timeline
has started early or finished too early.
The ParallelTimeline.SlipBehavior
property defines how loss of synchronization between MediaTimeline
and Timeline
objects should be handled. The default value is SlipBehavior.Grow
and displays the behavior described earlier. Any Timeline
animations running alongside a MediaTimeline
can start or finish at different times to the MediaTimeline
. To combat this, the other value of the SlipBehavior
enumeration, SlipBehavior.Slip
, should be used.
When a Timeline
slips, it effectively waits for any MediaTimeline
objects to start/resume playback if hindered in any way. So if a MediaTimeline
takes two seconds to load, any Timeline
animations running in parallel to the MediaTimeline
will not be activated until after two seconds, when the MediaTimeline
is ready to continue. If during the playback of some media the MediaTimeline
halts while the media is buffered, any Timeline
animations running in parallel to the MediaTimeline
will also halt and wait for the MediaTimeline
to begin again.
The following XAML demonstrates how to use the SlipBehavior.Slip
value. A video file is played back using a MediaTimeline
, with two animations running alongside it. One of the animations is synchronized; the other is not. The red System.Windows.Shapes.Ellipse
is not synchronized and uses the default SlipBehavior.Grow
value, whereas the green Ellipse
does slip. You should observe that the green Ellipse
pauses briefly while the video is loaded, but the red Ellipse
starts straightaway (see Figure 11-7).
<Window x:Class="Recipe_11_14.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_14" Height="320" Width="450"> <Window.Resources> <PathGeometry x:Key="AnimationSyncPathGeometry" Figures="M 30,260 L 400,260" /> <PathGeometry x:Key="AnimationNonSyncPathGeometry" Figures="M 30,230 L 400,230" /> </Window.Resources> <Window.Triggers> <EventTrigger RoutedEvent="Window.Loaded"> <BeginStoryboard> <Storyboard SlipBehavior="Slip"> <MediaTimeline Storyboard.TargetName="mePlayer1" Source="clock.avi" RepeatBehavior="Forever" />
<MatrixAnimationUsingPath RepeatBehavior="Forever" Duration="0:0:12" Storyboard.TargetName="SyncEllipseMatrixTransform" Storyboard.TargetProperty="Matrix" DoesRotateWithTangent="True" PathGeometry="{StaticResource AnimationSyncPathGeometry}" /> </Storyboard> </BeginStoryboard> <BeginStoryboard> <Storyboard SlipBehavior="Grow"> <MediaTimeline Storyboard.TargetName="mePlayer2" Source="clock.avi" RepeatBehavior="Forever" /> <MatrixAnimationUsingPath RepeatBehavior="Forever" Duration="0:0:12" Storyboard.TargetName="NonSyncEllipseMatrixTransform" Storyboard.TargetProperty="Matrix" PathGeometry="{StaticResource AnimationNonSyncPathGeometry}" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Window.Triggers> <Grid> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="0.5*" /> <ColumnDefinition Width="0.5*" /> </Grid.ColumnDefinitions> <MediaElement Margin="10" Width="200" Height="200" x:Name="mePlayer1" Stretch="Fill" HorizontalAlignment="Center" VerticalAlignment="Top" /> <MediaElement Margin="10" Width="200" Height="200" x:Name="mePlayer2"
Stretch="Fill" HorizontalAlignment="Center" VerticalAlignment="Top" Grid.Column="1" /> </Grid> <Path Stroke="Black" StrokeThickness="1" Data="{StaticResource AnimationSyncPathGeometry}" Grid.ColumnSpan="2" /> <Path Stroke="Black" StrokeThickness="1" Data="{StaticResource AnimationNonSyncPathGeometry}" Grid.ColumnSpan="2" /> <Ellipse Width="20" Height="20" Fill="ForestGreen" x:Name="syncElipse" HorizontalAlignment="Left" VerticalAlignment="Top"> <Ellipse.RenderTransform> <MatrixTransform x:Name="SyncEllipseMatrixTransform" /> </Ellipse.RenderTransform> </Ellipse> <Ellipse Width="20" Height="20" Fill="Firebrick" x:Name="nosyncElipse" HorizontalAlignment="Left" VerticalAlignment="Top"> <Ellipse.RenderTransform> <MatrixTransform x:Name="NonSyncEllipseMatrixTransform" /> </Ellipse.RenderTransform> </Ellipse> </Grid> </Window>
Figure 11-7. Two media files are being played back in separate storyboards. The green Ellipse
is an animation that runs alongside the left video. It is slightly behind the red Ellipse
because it slipped while the video was being loaded, whereas the red Ellipse
started its animation immediately. The actual observed behavior will depend on the performance and workload of your machine.
You need to execute custom code when a System.Windows.Media.Animation.Storyboard
completes, that is, all the child System.Windows.Media.Animation.Timeline
objects have completed.
Register an event handler against the Timeline.Completed
event, performing any custom tasks such as cleaning up composited Timeline
objects (see recipe 11-3).
The Timeline
class defines a Completed System.EventHandler
, which is raised when the Timeline
finishes. Generally, this is when the timeline's Duration
has elapsed. By adding an event handler to the event, the handler will be invoked when the Timeline
completes. Because any Timeline
object can notify listeners of its completion, the behavior may not be quite as expected.
When several Timeline
objects are declared as children of a single Storyboard
or System.Windows.Media.Animation.ParallelTimeline
, no Timeline.Completed
events will be raised until every Timeline
in the parent System.Windows.Media.Animation.TimelineGroup
have completed. So if you have a Storyboard
with several Timeline
children, each of which has a duration of one second and a single Timeline
with a duration of ten seconds, the Completed
event will not be raised on the Storyboard
or its children until the final ten-second Timeline
has completed. When the last animation has finished, the Completed
events will be raised in a depth-first fashion, starting with the root Timeline
object, in this case, the Storyboard
.
It should be obvious that if a Timeline
object is given a System.Windows.Media.Animation.RepeatBehavior
of Forever
, no completed events on any of the child objects of a TimelineGroup
, or the TimelineGroup
object itself, will be raised.
The following code example demonstrates the behavior of completion events. The markup file contains a few simple controls, each of which has an animation applied to it. Each animation supplies a System.EventHandler
that displays a simple message when the animation finishes.
<Window x:Class="Recipe_11_15.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_15" Height="300" Width="300" Background="Black"> <Window.Triggers> <EventTrigger RoutedEvent="Window.Loaded"> <BeginStoryboard> <Storyboard Completed="Storyboard_Completed"> <ParallelTimeline Completed="ParallelTimeline_Completed"> <ColorAnimation Duration="0:0:1" Completed="Animation1_Completed" Storyboard.TargetProperty="Background.Color" To="White" /> <ColorAnimation Duration="0:0:2" Completed="Animation2_Completed" Storyboard.TargetName="bd" Storyboard.TargetProperty="Background.(SolidColorBrush.Color)" To="Black" /> </ParallelTimeline> <ColorAnimation Duration="0:0:3"
Completed="Animation3_Completed" Storyboard.TargetName="rect" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)" To="Firebrick" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Window.Triggers> <Border x:Name="bd" Margin="20" Background="HotPink"> <Rectangle x:Name="rect" Width="100" Height="100" Fill="WhiteSmoke" /> </Border> </Window>
The following code details the content of the previous markup's code-behind file. The code defines several event handlers for various Completed
events, defined in the markup.
using System; using System.Windows; namespace Recipe_11_15 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } private void Storyboard_Completed(object sender, EventArgs e) { MessageBox.Show("Storyboard complete.", "Recipe_11_15"); } private void ParallelTimeline_Completed(object sender, EventArgs e) { MessageBox.Show("ParallelTimeline complete.", "Recipe_11_15"); }
private void Animation1_Completed(object sender, EventArgs e) { MessageBox.Show("Animation 1 complete.", "Recipe_11_15"); } private void Animation2_Completed(object sender, EventArgs e) { MessageBox.Show("Animation 2 complete.", "Recipe_11_15"); } private void Animation3_Completed(object sender, EventArgs e) { MessageBox.Show("Animation 3 complete.", "Recipe_11_15"); } } }
You need to animate some color property of a control. The target property may be of type System.Windows.Media.Color
or be exposed as the abstract type System.Windows.Media.Brush
.
Use a System.Windows.Media.Animation.ColorAnimation
to animate the color of the target property. If the target property is a Brush
type, you will need to use an indirect property path to access the brush's color property.
The ColorAnimation
is no different from its siblings, other than it targets a property of type Color
. It allows you to animate a color to a given value from either a specified value or the current value of the property. Animating a color property of a control such as System.Windows.Shapes.Rectangle.Fill
, which is of type Color
, is a trivial exercise and like any other animation. Animating the color of a System.Windows.Media.SolidColorBrush
, or the value of a System.Windows.Media.GradientStop
in a System.Windows.Media.LinearGradientBrush
, is also trivial because they all expose a Color
dependency property.
Should you want to animate the color of a property that is exposed as a Brush
, you do not have a Color
property against which you can apply the animation. For example, if you attempted to use a ColorAnimation
to animate a System.Windows.Controls.Border.Background
property using Background.Color
as the target property of the animation, you will see a System.InvalidOperationException
thrown when the app is started. The exception will inform you that it cannot resolve the given property path. The trick here is to use a combination of indirect property targeting and partial path qualification, specifying that the property you are accessing belongs to a SolidColorBrush
(if the background is actually set to a SolidColorBrush
). If the background is null
, an exception will be thrown; if the property is set to a different implementation of Brush
, the animation will have no effect.
Indirect property targeting and partial path qualification are features of the System.Windows.PropertyPath
object (for more information on the PropertyPath
object, refer to http://msdn.microsoft.com/en-us/library/ms742451.aspx
). Indirect property targeting basically allows you to specify the value for a property of a property, as long as all subproperties used in the path are dependency properties. The properties must also be either primitive types (for example, System.Double, System.Int
, and so on) or System.Windows.Freezable
types. In the example of targeting the background of a Border
control, the indirect property path would be Background.Color
. This alone, though, is not enough; as stated earlier, attempting to use this path will result in an exception. This is where partial path qualification comes in.
Partial path qualification enables you to define a target property that doesn't have a specified target type; that is, it is defined in a style or template. For example, the value of a Border.Background
property could be any of the available brush types, and it is not known until the property is set which type is being used. Paths that are intended to be used in this manner are indicated by wrapping them in parentheses. This can be an entire path or subsections of a path. In the example of targeting the background property of a Border
control, you would need to use the path Background.(SolidColorBrush.Color)
to target the background's color.
To take this a step further, if the properties used in the path are defined on some base object, you are able to use the name of the base class in the path. An example of this is when dealing with the System.Windows.Shapes.Rectangle.Fill
property. The Fill
dependency property is defined in the abstract class System.Windows.Shapes.Shape
. So, should you have an animation that targets the Fill
property of any given shapes, the property path can be defined as (Shape.Fill).(SolidColorBrush.Color)
. Pretty neat!
The following example demonstrates three different ways of targeting a property for animation. The examples use a combination of partial path qualification and indirect property targeting to access the target properties.
<Window x:Class="Recipe_11_16.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_16" Height="300" Width="300" Background="Black"> <Window.Triggers> <EventTrigger RoutedEvent="Window.Loaded"> <BeginStoryboard> <Storyboard AutoReverse="True" RepeatBehavior="Forever">
<ColorAnimation Storyboard.TargetProperty="Background.Color" To="White" /> <ColorAnimation Storyboard.TargetName="bd" Storyboard.TargetProperty="Background.(SolidColorBrush.Color)" To="Black" /> <ColorAnimation Storyboard.TargetName="rect" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)" To="Firebrick" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Window.Triggers> <Border x:Name="bd" Margin="20" Background="HotPink"> <Rectangle x:Name="rect" Width="100" Height="100" Fill="WhiteSmoke" /> </Border> </Window>
Use a derived class of the abstract System.Windows.Media.Animation.ControllableStoryboardAction
class to control a running animation through triggers.
When working with animations in XAML, they are usually started through some trigger such as a control being loaded, the user running the mouse over a control, or the user clicking something in your app and invoking a System.Windows.BeginStoryboard
. WPF allows you to further control animations in a similar fashion through the use of a set of classes that derive from ControllableStoryboardAction
. Table 11-8 lists the actions that are available and how they affect an animation, all of which can be found in the System.Windows.Media.Animation
namespace.
Table 11-8. Implementations of ControllableStoryboardAction
Type | Description |
---|---|
| Halts an animation at its current position, leaving animated properties in their current state. A subsequent call to a |
| Removes a |
| Resumes a paused animation, continuing the animation from the position at which it was paused. Applying this action to a Storyboard that has not been paused will have no effect. |
| Seeks, or moves, the target storyboard to an offset specified in the |
| Allows you to alter the playback speed of an animation by setting the value of the |
| Advances a |
| Stops and resets a |
Each of these actions is relevant only to animations that were started using a named BeginStoryboard
object. The actions all have a BeginStoryboardName
property, defined in ControllableStoryboardAction
, which is used to specify the name of the BeginStoryboard
that was used to start the target Storyboard
. Applying any of the previous actions to a Storyboard
that isn't active will have no effect.
The following example demonstrates how to use the storyboard actions that derive from ControllableStoryboardAction
. There are three shapes displayed in the window, each of which is animated in a different way by a single Storyboard
. Beneath the controls are several buttons, each of which performs a different action on the storyboard when clicked.
<Window x:Class="Recipe_11_17.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_17" Height="350" Width="400"> <Window.Resources> <Storyboard x:Key="Storyboard" RepeatBehavior="10x"> <DoubleAnimation Storyboard.TargetName="rect1" Storyboard.TargetProperty="Width" To="250" FillBehavior="HoldEnd" AutoReverse="False" /> <DoubleAnimation Storyboard.TargetName="rect2" Storyboard.TargetProperty="Width" To="250" AutoReverse="True" /> <ColorAnimation Storyboard.TargetName="ellipse1" Storyboard.TargetProperty="Fill.(SolidColorBrush.Color)" To="Orange" AutoReverse="True" /> </Storyboard> </Window.Resources> <Window.Triggers> <EventTrigger RoutedEvent="Button.Click" SourceName="btnBegin"> <BeginStoryboard x:Name="beginStoryboard" Storyboard="{StaticResource Storyboard}" /> </EventTrigger> <EventTrigger RoutedEvent="Button.Click"
SourceName="btnPause"> <PauseStoryboard BeginStoryboardName="beginStoryboard" /> </EventTrigger> <EventTrigger RoutedEvent="Button.Click" SourceName="btnResume"> <ResumeStoryboard BeginStoryboardName="beginStoryboard" /> </EventTrigger> <EventTrigger RoutedEvent="Button.Click" SourceName="btnStop"> <StopStoryboard BeginStoryboardName="beginStoryboard" /> </EventTrigger> <EventTrigger RoutedEvent="Button.Click" SourceName="btnSeek"> <SeekStoryboard BeginStoryboardName="beginStoryboard" Offset="0:0:5" Origin="BeginTime" /> </EventTrigger> <EventTrigger RoutedEvent="Button.Click" SourceName="btnSkipToFill"> <SkipStoryboardToFill BeginStoryboardName="beginStoryboard" /> </EventTrigger> <EventTrigger RoutedEvent="Button.Click" SourceName="btnDoubleSpeed"> <SetStoryboardSpeedRatio BeginStoryboardName="beginStoryboard" SpeedRatio="2" /> </EventTrigger> <EventTrigger RoutedEvent="Button.Click" SourceName="btnHalfSpeed"> <SetStoryboardSpeedRatio
BeginStoryboardName="beginStoryboard" SpeedRatio="0.5" /> </EventTrigger> </Window.Triggers> <Grid> <StackPanel> <Rectangle x:Name="rect1" Width="50" Height="100" Stroke="Black" Fill="CornflowerBlue" Margin="5" /> <Ellipse x:Name="ellipse1" Width="50" Height="50" Stroke="Black" Fill="Firebrick" StrokeThickness="1" Margin="5" /> <Rectangle x:Name="rect2" Width="50" Height="100" Stroke="Black" Fill="CornflowerBlue" Margin="5" /> <StackPanel Orientation="Horizontal"> <Button x:Name="btnBegin" Content="Begin" /> <Button x:Name="btnPause" Content="Pause" /> <Button x:Name="btnResume" Content="Resume" /> <Button x:Name="btnStop" Content="Stop" /> <Button x:Name="btnSeek" Content="Seek" /> <Button x:Name="btnSkipToFill" Content="Skip To Fill" /> <Button x:Name="btnDoubleSpeed" Content="Double Speed" /> <Button x:Name="btnHalfSpeed" Content="Half Speed" /> </StackPanel> </StackPanel> </Grid> </Window>
Use a System.Windows.Media.Animation.StringAnimationUsingKeyFrames
supplying the text to appear in each of the keyframes.
The StringAnimationUsingKeyFrames
Timeline allows you to specify a series of System.Windows.Media.Animation.StringKeyFrame
keyframes. Each keyframe defines the characters that appear at that point in time. The only built-in implementation of the abstract StringKeyFrame
class is a System.Windows.Media.Animation.DiscreteStringKeyFrame
, meaning you cannot create a smooth blend between two characters; they will simply appear.
This animation is useful for simulating characters being typed on a screen or an animated type banner (see Figure 11-8).
The following XAML demonstrates how to use an animation that uses DiscreteStringKeyFrame
objects to animate the Text
property of a System.Windows.Controls.TextBlock
control.
<Window x:Class="Recipe_11_18.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_11_18" Height="130" Width="200"> <Window.Resources> <Storyboard x:Key="StringAnimationStoryboard"> <StringAnimationUsingKeyFrames AutoReverse="True" Storyboard.TargetName="MyTextBox" Storyboard.TargetProperty="Text"> <DiscreteStringKeyFrame Value="" KeyTime="0:0:0" /> <DiscreteStringKeyFrame Value="H" KeyTime="0:0:0.5" /> <DiscreteStringKeyFrame Value="He" KeyTime="0:0:1" /> <DiscreteStringKeyFrame Value="Hel" KeyTime="0:0:1.5" /> <DiscreteStringKeyFrame Value="Hell" KeyTime="0:0:2" /> <DiscreteStringKeyFrame Value="Hello" KeyTime="0:0:2.5" /> <DiscreteStringKeyFrame Value="Hello T" KeyTime="0:0:3" /> <DiscreteStringKeyFrame Value="Hello Th" KeyTime="0:0:3.5" />
<DiscreteStringKeyFrame Value="Hello Tha" KeyTime="0:0:4" /> <DiscreteStringKeyFrame Value="Hello Thar" KeyTime="0:0:4.5" /> <DiscreteStringKeyFrame Value="Hello Thar!" KeyTime="0:0:5" /> <DiscreteStringKeyFrame Value="Hello Thar!" KeyTime="0:0:5.5" /> </StringAnimationUsingKeyFrames> </Storyboard> </Window.Resources> <DockPanel> <TextBlock x:Name="MyTextBox" DockPanel.Dock="Top" FontSize="30" Margin="5" HorizontalAlignment="Center" /> <Button Content="Start Animation" Width="100" Height="20"> <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <BeginStoryboard Storyboard="{DynamicResource StringAnimationStoryboard}" /> </EventTrigger> </Button.Triggers> </Button> </DockPanel> </Window>