Chapter 11. Creating Animation

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 Property of a Control

Problem

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.

Solution

Animate the value of the property using one or more System.Windows.Media.Animation.Timeline objects in a System.Windows.Media.Animation.Storyboard.

How It Works

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

AccelerationRatio

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 System.Double ranging between 0 and 1, inclusive, and is 0 by default. The sum of a timeline's AccelerationRatio and DeceleratioRatio must not be greater than 1.

AutoReverse

A System.Boolean property that specifies whether the Timeline should play back to the beginning once the end has been reached. See recipe 11-9 for more details on this property.

BeginTime

A System.Nullable(TimeSpan) that specifies when a timeline should become active, relative to its parent's BeginTime. For a root Timeline, the offset is taken from the time that it becomes active. This value can be negative and will start the Timeline from the specified offset, giving the appearance that the Timeline has already been playing for the given time. The SpeedRatio of a Timeline has no effect on its BeginTime value, although it is affected by its parent SpeedRatio. If the property is set to null, the Timeline will never begin.

DecelerationRatio

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 System.Double ranging between 0 and 1, inclusive, and is 0 by default. The sum of a timeline's AccelerationRatio and DeceleratioRatio must not be greater than 1.

Duration

A nullable System.Windows.Duration specifying the length of time the animation should take to play from beginning to end. For Storyboard and ParallelTimeline objects, this value will default to the longest duration of its children. For a basic AnimationTimeline object—for example, System.Windows.Media.Animation.DoubleAnimation—this value will default to one second, and a keyframe-based animation will have a value equal to the sum of System.Windows.Media.Animation.KeyTime values for each keyframe.

FillBehavior

A value of the System.Windows.Media.Animation.FillBehavior enumeration is used to define an animation's behavior once it has completed, but its parent is still active, or its parent is in its hold period. The FillBehavior.HoldEnd value is used when an animation should hold its final value for a property until its parent is no longer active or outside of its hold period. The FillBehavior.Stop value will cause the timeline to not hold its final value for a property once it completes, regardless of whether its parent is still active.

RepeatBehavior

A System.Windows.Media.Animation.RepeatBehavior value indicating whether and how an animation is repeated. See recipe 11-9 for more details on this property.

SpeedRatio

A property of type System.Double that is used as a multiplier to alter the playback speed of an animation. A speed ratio of 0.25 will slow the animation down such that it runs at a quarter of its normal speed. A value of 2 will double the speed of the animation, and a speed ratio of 1 means the animation will play back at normal speed. Note that this will affect the actual duration of an animation.

The Code

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>

Animate a Property of a Control Set with a Data Binding

Problem

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.

Solution

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.

How It Works

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 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);
     }
   }
}

Remove Animations

Problem

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).

Solution

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.

How It Works

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 Code

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
    }
}

Overlap Animations

Problem

You need to specify how a newly applied animation interacts with an existing animation, if present.

Solution

Specify a System.Windows.Media.Animation.HandoffBehavior value of HandoffBehavior.Compose to overlap animations applied to a property.

How It Works

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

Compose

The new animation will be partly merged into any existing ones, creating a smoother transition between the two.

SnapshotAndReplace

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.

The Code

<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>

Animate Several Properties in Parallel

Problem

You need to animate several properties of a control at the same time, that is, its height, width, and color.

Solution

Define your animations as normal but as children of a System.Windows.Media.Animation.ParallelTimeline.

How It Works

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 Code

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>

Create a Keyframe-Based Animation

Problem

You need to create an animation that uses keyframes to specify key points in the animation.

Solution

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.

How It Works

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 Code

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>
An animated ellipse in its initial state (left) and after several seconds have passed (right)

Figure 11-1. An animated ellipse in its initial state (left) and after several seconds have passed (right)

Control the Progress of an Animation

Problem

You need to be able to control the location of the playhead in an animation.

Solution

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.

How It Works

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

BeginTime

The specified offset is relative to the start of the animation.

Duration

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 Code

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 Slider control is used to track the progress of an animation. The animation moves the rectangle from one side of the window to the other.

Figure 11-2. A Slider control is used to track the progress of an animation. The animation moves the rectangle from one side of the window to the other.

Animate the Shape of a Path

Problem

You need to animate the shape of a System.Windows.Shapes.Path.

Solution

Use a System.Windows.Media.PointAnimation object to animate the points of your path.

How It Works

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 Code

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>
The initial state of the shapes (left) and the final state of the shapes after being animated (right)

Figure 11-3. The initial state of the shapes (left) and the final state of the shapes after being animated (right)

Loop and Reverse an Animation

Problem

You need to run an animation indefinitely and reverse the animation each time it reaches the end.

Solution

Animation offers two properties, RepeatBehavior and AutoReverse, to control the looping and reversal of your animation.

How It Works

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 x. The value is of type System.Double and must be greater than 0. It is important to note that when accessing the Count property of a RepeatBehavior structure in code, you should first check the value of the HasCount property to ensure the RepeatBehavior is driven by an iteration count. Should the HasCount property return False, accessing the Count property will throw a System.InvalidOperationException exception. To have an animation play through twice, set the value of RepeatBehavior to 2x. The value 2.5x would play the animation through twice.

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 Forever value is used to run the animation indefinitely. In this case, the Completed event will not be raised.

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 Code

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>

Limit the Frame Rate of a Storyboard

Problem

You need to set the desired frame rate of a System.Windows.Media.Animation.Storyboard object for performance reasons or otherwise.

Solution

Use the Timeline.DesiredFrameRate attached property to specify a desired frame rate for a Storyboard.

How It Works

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 Code

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>
The ellipse to the left of the window is smoothly animated between its start and end points, and the ellipse to the right jumps between the top and bottom of the screen.

Figure 11-4. The ellipse to the left of the window is smoothly animated between its start and end points, and the ellipse to the right jumps between the top and bottom of the screen.

Limit the Frame Rate for All Animations in an Application

Problem

You need to set the desired frame rate for all animations within your application.

Solution

Override the default value for the System.Windows.Media.Animation.Timeline.DesiredFrameRate dependency property.

How It Works

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 Code

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>

Animate an Object Along a Path

Problem

You need to animate some control so that it moves along a path.

Solution

Use one of the three available path animation timeline objects.

How It Works

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

DoubleAnimationUsingPath

Outputs a single System.Double value, generated from the input PathGeometry. Unlike the other two path-based timelines, the DoubleAnimationUsingPath also exposes a Source property that is a System.Windows.Media.Animation.PathAnimationSource. Table 11-7 describes the value of this enumeration.

PointAnimationUsingPath

Generates a series of System.Windows.Point objects, describing a position along the input PathGeometry, based on the current time of the animation. PointAnimationUsingPath is the only timeline of the three that does not provide any values for the angle of rotation to the tangent of the path at the current point.

MatrixAnimationUsingPath

Generates a series of System.Windows.Media.Matrix objects describing a translation matrix relating to a point in the input path. If the DoesRotateWithTrangent property of a MatrixAnimationUsingPath timeline is set to True, the output matrix is composed of a translation and rotation matrix, allowing both the position and orientation of the target to be animated with a single animation.

Table 11-7. Values of the PathAnimationSource Enumeration

Value

Description

X

Values output by the DoubleAnimationUsingPath correspond to the interpolated X component of the current position along the input path.

Y

Values output by the DoubleAnimationUsingPath correspond to the interpolated Y component of the current position along the input path.

Angle

Values output by the DoubleAnimationUsingPath correspond to the angle of rotation to the tangent of the line at the current point along the input path.

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 Code

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>
A control midway through a path animation. Notice how the control is oriented such that it follows a tangent to the gradient of the curve.

Figure 11-5. A control midway through a path animation. Notice how the control is oriented such that it follows a tangent to the gradient of the curve.

Play Back Audio or Video with a MediaTimeline

Problem

You need to play a media file, such as a WMV video file or WAV audio file, in your application.

Solution

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.

How It Works

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 Code

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>
A video file (with sound), eight seconds into its playback

Figure 11-6. A video file (with sound), eight seconds into its playback

Synchronize Timeline Animations with a MediaTimeline

Problem

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.

Solution

Set the SlipBehavior property of the parent System.Windows.Media.Animation.ParallelTimeline object to SlipBehavior.Slip.

How It Works

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 Code

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>
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.

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.

Receive Notification When an Animation Completes

Problem

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.

Solution

Register an event handler against the Timeline.Completed event, performing any custom tasks such as cleaning up composited Timeline objects (see recipe 11-3).

How It Works

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 Code

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");
       }
    }
}

Animate the Color of a Brush with Indirect Property Targeting

Problem

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.

Solution

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.

How It Works

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 Code

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>

Control Animations Through Triggers

Problem

You need to pause/resume, stop, skip forward, or skip backward in an animation once it has begun.

Solution

Use a derived class of the abstract System.Windows.Media.Animation.ControllableStoryboardAction class to control a running animation through triggers.

How It Works

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

PauseStoryboard

Halts an animation at its current position, leaving animated properties in their current state. A subsequent call to a BeginStoryboard will result in the animation's clocks being replaced with new clocks and the animation restarting.

RemoveStoryboard

Removes a Storyboard, halting any child Timeline objects and freeing up any resources they may be using. See recipe 11-3 for more information on removing storyboards.

ResumeStoryboard

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.

SeekStoryboard

Seeks, or moves, the target storyboard to an offset specified in the Offset property. It is important to note that seeking in a storyboard ignores a SpeedRatio value, treating the property as having a value of 1 and no SlipBehavior. For example, if a Storyboard has a Duration of two seconds and a SpeedRatio of two, an Offset of one second will seek to the midpoint of the animation. The Origin property is set to a value of the System.Windows.Media.Animation. TimeSeekOrigin enumeration, indicating whether the supplied offset is relative to the start or end of the target Storyboard.

SetStoryboardSpeedRatio

Allows you to alter the playback speed of an animation by setting the value of the SpeedRatio property to some System.Double value. A speed ratio of 0.25 will slow the animation down such that it runs at a quarter of its normal speed. A value of 2 will double the speed of the animation, and a speed ratio of 1 means the animation will play back at normal speed. Note that this will affect the actual duration of an animation.

SkipStoryboardToFill

Advances a Storyboard and all child Timeline objects to its fill period, if set (see recipe 11-1 for more information).

StopStoryboard

Stops and resets a Storyboard. When a Storyboard is stopped in this fashion, the Completed event does not get raised, although both the CurrentGlobalSpeedInvalidated and CurrentStateInvalidated events are raised.

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 Code

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>

Animate Text

Problem

You need to animate a string of characters.

Solution

Use a System.Windows.Media.Animation.StringAnimationUsingKeyFrames supplying the text to appear in each of the keyframes.

How It Works

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 Code

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>
The result of the text animation

Figure 11-8. The result of the text animation

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset