Click here to Skip to main content
15,860,859 members
Articles / Desktop Programming / WPF
Article

FrameBasedAnimation: Animating multiple properties collectively in WPF

Rate me:
Please Sign up or sign in to vote.
4.98/5 (31 votes)
21 Sep 2008CPOL28 min read 148.5K   2.9K   60   50
Building a reusable spinning wheel throbber in WPF: Part 1.

Introduction

While animation is a very powerful feature of WPF, it is still portrayed in a property-centric manner. That is, we can only animate one property of one object per timeline. This can lead to disorganized animations, split up into multiple disjoint and overlapping timelines that can become increasingly difficult to visualize as they grow in complexity. This article looks at a way of "refactoring" such property-based animations into temporal frame-based animations, each frame of which can contain multiple property setters representing the state of those properties at specific points in time, and how the need for such a concept arose.

Note: This article is rather long, but this is mainly because I've included a lot of behind-the-scenes information on how I came up with this idea. I've labeled sections that aren't crucial to the understanding of the general idea as such.

Background

I was working on implementing an animated "spinning wheel" throbber, like the ones you see everywhere these days, using WPF, to provide a fully resolution-independent (and therefore infinitely scalable), smoothly-animated, vector-based solution. In principle, the idea was simple; but, as with many things in WPF, this turned out to be deceptive.

This article will walk you through the creation of a WPF spinning wheel throbber, and how it resulted in the creation of a new way to think about animation under WPF.

Drawing the Wheel

Although this article was written primarily to demonstrate the need for and use of a new kind of animation abstraction in WPF, its secondary purpose is to share a reusable, animated spinning throbber class; so, let's take a look at how the throbber itself will be defined and then subsequently animated. If you are only interested in the animation aspect, and are already well-versed in WPF drawing (things don't get overly complicated here), feel free to skip this section.

My first attempt at drawing the wheel was to place everything inside a DrawingImage and to use a GeometryDrawing to house the shapes. However, while I was able to get the basic form set up, I quickly ran into several problems*, and decided that using Line objects on a Canvas would be much more productive. This decision may result in some additional overhead (since Shape objects are more complex than GeometryDrawings), but since we are only dealing with eight very small Line objects, this should not be significant.

*The first problem was that hosting a GeometryDrawing inside a DrawingImage meant that the geometry was always "trimmed" (think Photoshop), even after the rotations and during the animation, which had the bizarre side effect of the spinner seeming to move around inside the image while it was animating. I was unable to solve this problem under that model. The second problem was the realization that all of the geometries inside a GeometryGroup must share the same pen, and furthermore, the Opacity setting can only be applied way up at the Image level, which would not allow us to easily control the opacity of individual spokes, as you will see later.

So, how do we go about drawing it? Well, the first thing to notice is that for the most part, we are dealing with eight identical lines that radiate outwards from a central point. We could attempt to mathematically figure out at precisely what coordinates we need to draw the start and end points of each line, but there's a much simpler solution: rotation.

Let's start by drawing a line:

XML
<Line x:Name="n" Y1="-10" Y2="-18" />

I chose these seemingly odd start and end points because we are going to want to draw our spokes radiating outwards from the origin (0,0) to make rotation easier later on. Note that I only defined the Y components of each point; this is because the default value is 0, and this is precisely what we want for X1 and X2. So, we are drawing a line from (0,-10) to (0,-18).

(The name n corresponds to North, if you haven't figured that out already.)

Now, if we don't provide any more details, we can't see anything. This is because the default stroke for shapes is Transparent for some reason. So, we need to change that to Black. We also need to change the StrokeThickness and the StrokeStartLineCap and StrokeEndLineCap. This is going to be very tedious if we have to repeat all those settings eight times! So, let's cheat and define a Style to hold these, in our UserControl.Resources:

XML
<UserControl.Resources>
    <Style
        TargetType="{x:Type Line}">
        <Setter Property="Stroke" Value="Black" />
        <Setter Property="StrokeThickness" Value="5.8" />
        <Setter Property="StrokeStartLineCap" Value="Round" />
        <Setter Property="StrokeEndLineCap" Value="Round" />
    </Style>
</UserControl.Resources>

Next, let's draw the second Line and rotate it 45 degrees:

XML
<Line x:Name="ne" Y1="-10" Y2="-14">
    <Line.RenderTransform>
        <RotateTransform Angle="45" />
    </Line.RenderTransform>
</Line>

Note: this line is only 4 points long (-10 to -14 as opposed to -18). This is because the north line should be large initially. However, in reality, these values are meaningless because they will be set by the animation, so we could just delete all of the Y2 values, and set Y1 in the style, making our line definitions even simpler, but we'll keep them for now to illustrate the rotations (which you'll be able to see in your XAML designer).

We'll repeat this for the remaining six spokes, using the angles 90 (e), 135 (se), 180 (s), 225 (sw), 270 (w), and 315 (nw).

LayoutTransform vs. RenderTransform

WPF provides two ways to rotate an object, based on the two general classes of transformation: LayoutTransform and RenderTransform. Which one should we use? Well, this one turned out to be a no-brainer, but only after I first tried to rotate the spokes using LayoutTransform, which resulted in the following mess:

wonky.png

As soon as I changed those to RenderTransform, things lined up perfectly:

straight.png

Now that we have our wheel properly drawn and looking good, let's get that wheel turnin'!

Animating it!

WPF comes with some powerful built-in animation classes called Timeline and Storyboard, but these names are somewhat deceptive because they don't allow you to truly "lay-out" the various pieces of your animation as you would in a real-life storyboard. Rather, WPF expects you to have a different Storyboard for every property you are animating in your window, and as a result, it can become very difficult to visualize the final result of your animations this way, especially if they involve multiple objects and properties, like our spinning wheel. To put it another way, WPF considers the target property dominant, and puts it on the outside of the breakdown, while time is subordinate and sits inside the animation for a particular property. What this amounts to is that you can't easily think of multiple properties progressing through time on a global level.

Storyboard is actually a subclass of ParallelTimeline, which is kind of ironic because one would expect a storyboard to be sequential. Timelines placed in a Storyboard are actually executed in parallel with each other, unless an explicit start time is specified for each child (a nuisance if you want to tweak the duration of children, since the ParallelTimeline class allows child animations to overlap, and therefore the offsets must be precisely calculated by hand). Note also that WPF does not currently support the notion of a sequential timeline, in which start times of children are computed automatically based on the duration of the preceding children. FrameBasedAnimation offers a truly sequential storyboard, and helps to alleviate this oversight.

I argue that "Storyboard" was not the best name for this class, because in real life, a storyboard is a sequential set of scenes that depict the progression of an animation—of all parts of an animation—through time. In real life, there is no notion of scenes overlapping in a storyboard. FrameBasedAnimation provides a true-to-life storyboard that allows you to lay-out your animations from a top-down, time-based perspective, encompassing all objects involved in the animation into a global timeline, and we're about to see how that will work.

The Theory

Feel free to skip over this part—I provide it only for intellectual value and understanding—it is by no means necessary for using the throbber control or the new animation class, but will help you to understand its motivation.

I call FrameBasedAnimation a "refactored" animation because it involves refactoring (in the mathematical sense) the representation of an animation from WPF's native form into a different but equivalent form. In mathematics, factoring involves changing the order of superiority of operators, generally from multiplication (*) to addition (+). For example, if we have the "expanded" form of an expression like so:

xa + ya

...which is represented in the natural order of mathematical precedence (multiplication before addition), we can "factor" this into another form, determined by placing addition before multiplication (hence the need for parentheses), resulting in:

a(x + y)

(I won't get into the details of how to do this—it's not important. I'm sure you all remember high-school algebra with fondness.) The exact same principle is applied to factoring animations in WPF, except, instead of multiplication and addition, we are now considering target property and time as our "operators".

WPF naturally places an operational dominance on the target property (or a precedence on time), meaning that the target property is the outer-most thing represented in an animation (like addition in the first example), followed by time, such that each key time inside the animation pertains to the common outer property. Think of the properties as x and y in the above example, and time as a. To get a proper understanding of what is happening, let's introduce another time point, b. We'll start with the WPF native factoring:

x(a + b) + y(a + b)

Notice how the property is the outer element, and the times a and b are applied internally to both properties. When we expand this, we get:

xa + ya + xb + yb (intermediate, expanded form)

FrameBasedAnimation works by refactoring this expanded expression so that time—rather than target property—is the outer-most (factored-out) expression:

a(x + y) + b(x + y)

Note that the two expressions are equivalent, but they look very different. Now, if this were actual mathematics, we could factor that even further into:

(a + b)(x + y)

...but we can't do that in XAML, because XAML is very hierarchical, and has no way of understanding what this would mean (or to put that another way, there is simply no way to express this in XAML). However, as we'll see later, we can actually take advantage of this principle in code, to make our job even easier still. (For now, just realize that this arises due to the fact that at two points in time, a and b, we are largely performing the same steps (x + y), and therefore this has the potential to be automated.)

So, what this results in is a refactored view of animation, placing time on the outside (like a real storyboard), and controlling what is happening among all objects collectively for that frame. I personally find this much easier to visualize, rather than thinking about changes to each individual property atomically, and as you will see, it will open up the door for some neat possibilities, including finally being able to implement our spinning wheel.

So, let's get to work, and take a look at how this theory is actually implemented.

Implementation

For those who skipped over it, the above theory outlined the origin of my idea: to flip the order of precedence of target property vs. time, such that time is on the outside, and the changes to potentially numerous objects' states are on the inside—the opposite of WPF's native representation of animation, which places the target property on the outside, and changes to that property through time on the inside.

To prove how useful this will be, I actually thought-up the first test implementation of the spinner's animation using my hypothetical time-dominant animation model, then manually translated this into WPF's native property-dominant model. I started with only four of the spokes to keep things simple. Here's what I came up with:

Given a set of time-based "frames" that are defined using Setters, and which adjust the states of several properties simultaneously, resulting in a snapshot of multiple properties for that frame, we want to take those groupings and refactor them into singular property-based animations containing the changes through time, and run these property-based timelines in parallel.

So, for example, suppose we have something like this (in pseudo-code):

XML
<FrameBasedAnimation Duration="0:0:4" RepeatBehavior="Always">
    <Frame KeyTime=t0>
        <Setter Target="w" Property="Y2" Value="-14" />
        <Setter Target="n" Property="Y2" Value="-18" />
        <Setter Target="e" Property="Y2" Value="-14" />
    </Frame>
    <Frame KeyTime=t1>
        <Setter Target="n" Property="Y2" Value="-14" />
        <Setter Target="e" Property="Y2" Value="-18" />
        <Setter Target="s" Property="Y2" Value="-14" />
    </Frame>
    <Frame KeyTime=t2>
        <Setter Target="e" Property="Y2" Value="-14" />
        <Setter Target="s" Property="Y2" Value="-18" />
        <Setter Target="w" Property="Y2" Value="-14" />
    </Frame>
    <Frame KeyTime=t3>
        <Setter Target="s" Property="Y2" Value="-14" />
        <Setter Target="w" Property="Y2" Value="-18" />
        <Setter Target="n" Property="Y2" Value="-14" />
    </Frame>
    <!-- Time t4 == t0 -->
</FrameBasedAnimation>

...and turn it into this (also pseudo-code):

XML
<DoubleAnimationUsingKeyFrames Target="n" Property="Y2">
    <LinearDoubleKeyFrame KeyTime=t0 Value="-18" /> // starting point
    <LinearDoubleKeyFrame KeyTime=t1 Value="-14" />
    <LinearDoubleKeyFrame KeyTime=t3 Value="-14" /> // no change
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Target="e" Property="Y2">
    <LinearDoubleKeyFrame KeyTime=t0 Value="-14" />
    <LinearDoubleKeyFrame KeyTime=t1 Value="-18" />
    <LinearDoubleKeyFrame KeyTime=t2 Value="-14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Target="s" Property="Y2">
    <LinearDoubleKeyFrame KeyTime=t1 Value="-14" />
    <LinearDoubleKeyFrame KeyTime=t2 Value="-18" />
    <LinearDoubleKeyFrame KeyTime=t3 Value="-14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Target="w" Property="Y2">
    <LinearDoubleKeyFrame KeyTime=t0 Value="-14" />
    <LinearDoubleKeyFrame KeyTime=t2 Value="-14" />
    <LinearDoubleKeyFrame KeyTime=t3 Value="-18" />
</DoubleAnimationUsingKeyFrames>

Let's look at what's going on here. First of all, in my ideal form above, we are qualifying each frame with a KeyTime property. This is analogous to the KeyTime property of, for example, LinearDoubleKeyFrame, except that it now applies to a group of changes, represented as Setters.

Now, this raises the next major concern: when dealing with property-based animations in WPF, every type of animation has its own corresponding animation class. For example, there are DoubleAnimation, DoubleAnimationUsingKeyFrames, PointAnimation, PointAnimationUsingKeyFrames, and so on and so on (I heard the figure 21 tossed around as to how many different animation classes there are—at any rate, there are a lot). So, one problem we run into is the abstract nature of the Setter compared to the concrete nature of the animation classes. In particular, we need to be able to figure out what type of animation we are going to need, based on the type of the property involved in the Setter. However, with a bit of work, this should be possible.*

*For the purposes of this example, I am not going to implement all 21 concrete animation classes. We will stick primarily with the double type, since that is what is needed to animate our spinning wheel. However, by downloading the source code, you can easily add support for any other type you need. More details are in the source code; feel free to post a comment if you need help.

To visualize what is going on, each Frame in the SetterAnimation represents a specific point in time. At time t0, we want the north spoke to be large (values are negative because we are drawing from the center of the wheel outwards, starting the spoke at the top and then rotating), and both the east and west spokes surrounding it to be small, so that spokes grow and shrink fully in the span of one frame, and so that we always have just one spoke that is large at the frame boundaries. We could also define the south spoke explicitly, but because it is between the east and west spokes that are both already small, there is no need. Feel free to, though, if you like having your code explicit, or it makes things more understandable. The key thing to remember is we're defining the collective states of multiple properties and objects at key points in time. FrameBasedAnimation will automatically animate those properties from one point in time to the next.

To visualize the refactoring itself, we're simply taking all frames mentioning the same object/property combination (for example n.Y2) and grouping these into a single property-based animation. So, for example, the property n.Y2 is mentioned at times t0, t1, and t3, which is exactly what you see compiled under the first DoubleAnimationUsingKeyFrames object (corresponding to n) in the translated code above.

Anyhow, I took this manually-calculated pseudo-XAML and turned it into real XAML, to see if it would work, and lo and behold (after a few tweaks—see below), it did! For reference's sake, here's what the above looked like after I turned it into real XAML:

XML
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="n" 
     Storyboard.TargetProperty="Y2" Duration="0:0:4">
    <LinearDoubleKeyFrame KeyTime="0:0:0" Value="-18" />
    <LinearDoubleKeyFrame KeyTime="0:0:1" Value="-14" />
    <LinearDoubleKeyFrame KeyTime="0:0:3" Value="-14" />
    <LinearDoubleKeyFrame KeyTime="0:0:4" Value="-18" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="e" 
    Storyboard.TargetProperty="Y2" Duration="0:0:4">
    <LinearDoubleKeyFrame KeyTime="0:0:0" Value="-14" />
    <LinearDoubleKeyFrame KeyTime="0:0:1" Value="-18" />
    <LinearDoubleKeyFrame KeyTime="0:0:2" Value="-14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="s" 
    Storyboard.TargetProperty="Y2" Duration="0:0:4">
    <LinearDoubleKeyFrame KeyTime="0:0:1" Value="-14" />
    <LinearDoubleKeyFrame KeyTime="0:0:2" Value="-18" />
    <LinearDoubleKeyFrame KeyTime="0:0:3" Value="-14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="w" 
    Storyboard.TargetProperty="Y2" Duration="0:0:4">
    <LinearDoubleKeyFrame KeyTime="0:0:0" Value="-14" />
    <LinearDoubleKeyFrame KeyTime="0:0:2" Value="-14" />
    <LinearDoubleKeyFrame KeyTime="0:0:3" Value="-18" />
    <LinearDoubleKeyFrame KeyTime="0:0:4" Value="-14" />
</DoubleAnimationUsingKeyFrames>

There are a few points to observe in comparing the actual XAML to my guessed pseudo-XAML. The first is that there is a fourth entry under each DoubleAnimationUsingKeyFrames. This is because, for some reason, WPF didn't loop the animation automatically, and treated the transition between the final frame and the first frame as a discrete step. There might be a way to make this part of the interpolation automatically, but at any rate, explicitly repeating the initial state as the final state did the trick (I use a similar trick in code later on to achieve this in the final class). Note also that the east and south nodes above didn't need a fourth state, because their final and initial states were already identical. Adding it wouldn't have hurt, of course, but was unnecessary.

A Plan of Attack

So, now that we know it's possible (yay!), let's start working on the actual means of translating our hypothetical pseudo form into a real form that WPF can understand. What we want to do here is create a new animation class that can be configured in XAML, but at the same time, have that class appear to WPF as a traditional Timeline so that it can be included in a regular Storyboard and triggered in the same ways. We'll do this by deriving our class from ParallelTimeline. I chose ParallelTimeline because it is essentially a lightweight version of Storyboard, and will allow us to compute parallel animations based on our new representation (they do need to be parallel because the whole point is that we are transforming time-dominant representation into parallel property-dominant representation; so, while the frame-to-frame progression of our FrameBasedAnimation is sequential, when it is refactored into property-dominant form, those factored properties' timelines will almost certainly overlap).

In other words, FrameBasedAnimation is really nothing more than a converter from a custom representation to a standard one. As such, we'll introduce a Render() method later on to actually perform this conversion. Before we get to Render(), however, let's look at how our class will be specified in XAML, and how we can take advantage of some special XAML attributes to make our life easier. The first thing we need to do is represent a collection of frames. So, let's define a FrameCollection class as an ObservableCollection:

C#
public class FrameCollection: ObservableCollection<Frame> { }

Next, let's look at what a Frame itself ought to be. If we look at our pseudo-code, we can see that a Frame should essentially consist of a collection of Setters, so let's likewise define Frame as an ObservableCollection<Setter>, adding a KeyTime property so we can specify the point in time the frame should represent:

C#
public class Frame: ObservableCollection<Setter> {
    public KeyTime KeyTime { get; set; }
}

Finally, we'll implement FrameBasedAnimation itself. We already mentioned this class should inherit from ParallelAnimation, and we know that it should contain a FrameCollection (call it Frames). We'll also add a LoopAnimation property to address that problem we ran into earlier about animations not properly looping back to their starting point, and of course, our Render() method:

C#
[ContentProperty( "Frames" )]
public class FrameBasedAnimation: ParallelTimeline {

    protected FrameCollection _frames;
    public FrameCollection Frames {
        get {
            if ( _frames == null )
                _frames = new FrameCollection();
            return _frames;
        }
    }

    public bool LoopAnimation { get; set; }

    public FrameBasedAnimation() {
        LoopAnimation = true; // default value
    }

    public void Render() {
        ...
    }

    ...
}

The ContentProperty Attribute

WPF comes with a handy attribute that lets us declare the content property of a class. The content property of a class is the default property that is initialized when elements are defined "inside" a parent element in XAML, and allows us to avoid having to use the clumsy property element syntax.

For example, in the following piece of XAML:

XML
<Button>Hello world!</Button>

The text "Hello world!" is the content of the button, and the property it is assigned to is the content property of Button. Each class can define its own content property, and ContentProperty is an inherited attribute, meaning a class will inherit its content property from its parent class if we don't explicitly override ContentProperty. Furthermore, content properties implicitly support initializing collections, so long as the property designated as the content property is a collection*. Since ObservableCollection is a collection, our Frames property can be set as FrameBasedAnimation's content property, and initialized directly with a set of Frame elements, without having to wrap them explicitly in a FrameCollection:

XML
<FrameBasedAnimation ...>
    <Frame ... />
    <Frame .../>
    ...
</FrameBasedAnimation>

*There is one little snag you should be aware of: in order for implicit collection content property initialization to work, the property designated as the class's content property must be read-only. In other words, we can only expose a getter, not a setter. If you expose both, it will not be implicitly treated as a collection initialization, but rather a singular content property.

Render()

Now, we're ready to take a look at the Render() method. Here's where things get really fun! While developing the idea for my new animation class, I only had a rough idea of how the Render() method would actually work, as I've outlined above, but nothing concrete. Here, I'll show you what I figured out when it finally came time to implement it.

Let's start by looking at what we have to work with. FrameBasedAnimation is essentially a collection of Frames, each of which is essentially a collection of Setters. Keep in mind, our goal is to transform this into a collection of WPF property-based animations. Therefore, the "inside" of our hierarchy (the properties of the Setter) needs to become the outside. We'll start by iterating over these two levels of collections recursively, but, let's first just make sure the Frames is in chronological order (this will become important later on):

C#
Frames.OrderBy<Frame, KeyTime>( delegate( Frame target ) {
    return target.KeyTime;
} );

foreach ( Frame frame in Frames ) {
    foreach ( Setter setter in frame ) {
        ...
    }
}

Next, we will need some way to group the properties—this is analogous to us factoring out the common properties of our hypothetical expanded form. We need to keep track of this grouping across iterations of our loop(s), so the best way to do this is via a Dictionary, keyed on the property.

However, we run into a problem here: "property" is not a singular notion to the perspective of WPF animations: it is a combination of a TargetName and a TargetProperty. To solve this, we need to create a new class that treats the pair of TargetName and TargetProperty as a single comparable (and hashable) value. Without getting into too much detail, here's what we need:

C#
internal class ObjectPropertyPair {
    public string TargetName { get; set; }
    public DependencyProperty TargetProperty { get; set; }

    public ObjectPropertyPair( string targetName, 
           DependencyProperty targetProperty ) {
        this.TargetName = targetName;
        this.TargetProperty = targetProperty;
    }

    public override bool Equals( object obj ) {
        ObjectPropertyPair next = obj as ObjectPropertyPair;

        if ( next == null )
            return false;

        return this.TargetName == next.TargetName && 
               this.TargetProperty == next.TargetProperty;
    }

    public override int GetHashCode() {
        return TargetName.GetHashCode() ^ TargetProperty.GetHashCode();
    }
}

Note that it's important we override both Equals() (to perform the comparison) and GetHashCode() because we will be using this object as a key in a Dictionary, which internally calls GetHashCode() of every key value in order to efficiently distribute and search for keys. If we didn't override GetHashCode() (which, I found out the hard way), our Dictionary would be unable to look-up entries based on instances of ObjectPropertyPair, rendering it effectively as a List.

Since the construction of this class is very general and not type-specific, it could be implemented generically as a Pair<T1,T2> class. I did a quick search on Google to see if there was any built-in system class for this purpose, but did not find anything, hence I wrote my own.

This solves the key problem, and allows us to index our Dictionary using an ObjectPropertyPair. The value type of our Dictionary will be an instance of a WPF animation class specific to the target property, and specific to a certain type that we will use to group time-based key frames specific to that target property. Since we will be storing actual concrete instances of WPF animation classes here, such as DoubleAnimationUsingKeyFrames (note these will always be the key frame variant because our implementation is based specifically on the concept of mapping to key frames), we can use the common AnimationTimeline class, which also gives us typesafe access to the Duration property, which we will set equal to the Duration provided to FrameBasedAnimation, so that all of our parallel child animations will have the same duration.

So, we now have our Dictionary set up, and are stepping through each inner Setter of our frame-based representation. The rest is pretty straightforward. First, we create an instance of ObjectPropertyPair which we will use to key our Dictionary:

C#
ObjectPropertyPair pair = new ObjectPropertyPair( setter.TargetName, setter.Property ); 

Next, we'll check to see if there is an existing animation in place for this particular pair (this is the factoring in action), creating one if there is not:

C#
AnimationTimeline animation;
if ( !index.ContainsKey( pair ) ) {
    animation = CreateAnimationFromType( setter.Property.PropertyType );
    Storyboard.SetTargetName( animation, setter.TargetName );
    Storyboard.SetTargetProperty( animation, new PropertyPath( setter.Property ) );
    animation.Duration = this.Duration;

    index.Add( pair, animation );
}
animation = index[ pair ];

The first time we create the animation instance, we need to assign it the information it will need to identify the target object and property, as well as assign it the same duration as the overall FrameBasedAnimation.

CreateAnimationFromType() is how we get around the abstract/concrete impedance mismatch (if you will) of our representation compared to WPF's representation. The operation is quite simple: we just pass the type of the property being set (obtained helpfully from the Setter), and CreateAnimationFromType() returns an empty instance of the UsingKeyFrames variant of the appropriate concrete animation class:

C#
private AnimationTimeline CreateAnimationFromType( Type type ) {
    switch ( type.ToString() ) {
        case "System.Double":
            return new DoubleAnimationUsingKeyFrames();

        // *** Add support for additional types here and below. ***

        default:
            throw new ArgumentException( ... );
    }
}

As you can see, I've made it very simple to extend with additional animation types, should you so desire.

Now that we have our blank concrete instance, we need to assign its TargetName and TargetProperty. Since these are attached properties (belonging to Storyboard), we can use the Storyboard class' static SetTargetName() and SetTargetProperty() methods, passing them the instance of the animation just created and the appropriate values (again, obtained from the Setter).

Note that we must pass a PropertyPath to the SetTargetProperty() method, not a reference to the property itself. Luckily, SetTargetProperty() enforces this in its method signature; however, the alternative (universal) form for setting a DependencyProperty (shown below) does not have this luxury. What's worse, the type it expects and the type of setter.Property are both DependencyProperty, which can be very misleading, so watch out!

C#
animation.SetValue( Storyboard.TargetPropertyProperty, 
                    new PropertyPath( setter.Property ) );

Finally, we need to create the actual KeyFrame to add to the animation object based on the value of the Setter and the KeyTime of the parent Frame:

C#
( (IKeyFrameAnimation)animation ).KeyFrames.Add(
    CreateKeyFrameFromType( setter.Property.PropertyType, 
                            setter.Value, frame.KeyTime ) );

Note that we have to cast animation to IKeyFrameAnimation, because its native type, AnimationTimeline, does not know that it is a UsingKeyFrames variant. For this reason, it is important that we only instantiate the UsingKeyFrames variant of an animation class inside CreateAnimationFromType().

CreateKeyFrameFromType() is almost identical to CreateAnimationFromType(), so I will refrain from illustrating it here. It simply creates an instance of LinearDoubleKeyFrame, passing it the value from the Setter and the KeyTime from the Frame.

That's all for the double loop. Next, we just have to iterate over each of the animation objects added to our Dictionary, and add them one by one to the Children property of our FrameBasedAnimation (which, you'll recall, is inherited from ParallelTimeline and represents the collection of property-based animations that WPF will see). But, before we do this, there's one last thing to implement, and that is the LoopAnimation property. This is a little trick that simply duplicates the first KeyFrame of each concrete animation instance and projects it beyond the duration of the animation. You might ask what the point is in adding frames to our animation that are beyond its duration. While it's true the animation will loop before those frames are reached, it is still possible that we will have ongoing interpolations affecting the states of properties within the animation's duration.

To visualize this, consider that FrameBasedAnimation only requires we specify the final state of properties for a given frame (i.e., at that point in time), and that it could very well occur that several frames go by before the same property is mentioned again*. FrameBasedAnimation (thanks to the WPF animation engine) interpolates these points so that between these frames, the animation occurs smoothly. However, when the animation is looped, and the next frame actually occurs on the next iteration of the loop, WPF does not automatically interpolate these property values for some reason, and we must "trick" it into thinking the value is still coming up. Therefore, by projecting the frame into the future by the span of Duration, it will appear to WPF the same amount of time away as it will be once the animation loops around and the "real" frame takes effect.

C#
foreach ( AnimationTimeline animation in index.Values ) {
    if ( LoopAnimation ) {
        // Finally, tie each animation closed by projecting its initial frame.
        // Assume there will always be at least one frame.
        IKeyFrame firstFrame = 
          (IKeyFrame)( (IKeyFrameAnimation)animation ).KeyFrames[ 0 ]; 
        ( (IKeyFrameAnimation)animation ).KeyFrames.Add( CreateKeyFrameFromType(
            firstFrame.Value.GetType(), firstFrame.Value.ToString(),
            KeyTime.FromTimeSpan( firstFrame.KeyTime.TimeSpan + 
                                  this.Duration.TimeSpan ) ) );
    }

    this.Children.Add( animation );
}

*We can assume it will be mentioned at least one other time; otherwise, there would be no interpolation occurring, and no animation would take place for that property. While it would not do any harm (that is, we are only assuming this superficially, not as part of the implementation), it would be rather pointless, unless perhaps it makes understanding your code easier.

There is a bit of wonky typecasting involved, again, thanks to WPF's use of overly-concrete animation classes. Note the assumption that an animation will always have at least one frame. This is acceptable because we never create an animation object unless there is a frame to add to it. Note also we are assuming the first frame in the KeyFrames collection is the first frame chronologically. This is why it was important to sort Frames by KeyTime at the beginning of the method.

That's it! We're now done with the Render() method, and we can give things a try. Here's what the final, working XAML looks like, based on our brand-new FrameBasedAnimation class!

XML
<Animation:FrameBasedAnimation
    x:Name="frameAnim" RepeatBehavior="Forever" SpeedRatio="2" Duration="0:0:4">
    <Animation:Frame KeyTime="0:0:0">
        <Setter TargetName="w" Property="Line.Y2" Value="-14" />
        <Setter TargetName="n" Property="Line.Y2" Value="-18" />
        <Setter TargetName="e" Property="Line.Y2" Value="-14" />
    </Animation:Frame>
    <Animation:Frame KeyTime="0:0:1">
        <Setter TargetName="n" Property="Line.Y2" Value="-14" />
        <Setter TargetName="e" Property="Line.Y2" Value="-18" />
        <Setter TargetName="s" Property="Line.Y2" Value="-14" />
    </Animation:Frame>
    <Animation:Frame KeyTime="0:0:2">
        <Setter TargetName="e" Property="Line.Y2" Value="-14" />
        <Setter TargetName="s" Property="Line.Y2" Value="-18" />
        <Setter TargetName="w" Property="Line.Y2" Value="-14" />
    </Animation:Frame>
    <Animation:Frame KeyTime="0:0:3">
        <Setter TargetName="s" Property="Line.Y2" Value="-14" />
        <Setter TargetName="w" Property="Line.Y2" Value="-18" />
        <Setter TargetName="n" Property="Line.Y2" Value="-14" />
    </Animation:Frame>
</Animation:FrameBasedAnimation>

Nice! We can now compile and run this, and see our frame-based animation running as it should!

Optimizations

I mentioned earlier that we can take advantage of the fact that each frame looks largely the same, but from the perspective of a different "spoke", by actually generating our animation in code, rather than in XAML, using loops. So, let's take a quick look at how to do that, and finally, get those other four spokes involved too, as well as add some neat effects like fading transparency that'll really make our spinner look professional.

First, comment-out the Frames inside our instance of FrameBasedAnimation, and open up Spinner.xaml.cs. We'll be adding the frames dynamically in the constructor. Rather than walk you through this, I'll just show you the result, and give a rundown of how it works:

C#
KeyTime time = KeyTime.FromTimeSpan( TimeSpan.FromSeconds( 0 ) );
int numFrames = canvas.Children.Count;

frameAnim.Duration = new Duration( TimeSpan.FromSeconds( numFrames ) );

for ( int i = 0; i < numFrames; i++ ) {
    Frame f = new Frame();
    frameAnim.Frames.Add( f );

    f.KeyTime = time;

    // One frame per second (we can speed this up with SpeedRatio)
    time = KeyTime.FromTimeSpan( time.TimeSpan + TimeSpan.FromSeconds( 1 ) );

    f.Add( new Setter( Line.Y2Property, "-14",
        ( (Line)canvas.Children[ ( i + numFrames - 1 ) % numFrames ] ).Name ) );
    f.Add( new Setter( Line.Y2Property, "-16",
        ( (Line)canvas.Children[ ( i ) % numFrames ] ).Name ) );
    f.Add( new Setter( Line.Y2Property, "-14",
        ( (Line)canvas.Children[ ( i + 1 ) % numFrames ] ).Name ) );
}

frameAnim.Render();

First, initialize a time counter that will keep track of the KeyTime of each frame (which we will increment by a second, since we can always adjust the speed of the final animation by means of SpeedRatio). Next, determine the number of frames we will need by counting the Line segments in canvas.Children (set the Duration of the entire animation to that many seconds). We then want to iterate over each frame, and add the appropriate frame and Setters to the Frames collection, setting the KeyTime to our time counter and incrementing it.

Then, we just add the Setters themselves. This part is a bit complicated because we need to programmatically determine the names of the Line objects based on their index in the collection. I use modulus (% numFrames) to "wrap" the increment/decrement so that it falls within the bounds of the array. Note also that we are passing a string to the Setter rather than an actual double. Setter is a XAML class that expects to receive its parameters as strings. Note also that we have to actually pass the name of the target rather than the target itself, for similar reasons.

Finally, call frameAnim.Render(), and we're done!

Animating Opacity

The final step in the creation of a professional-looking spinning wheel animation is to animate the opacities of each spoke as the wheel spins, giving the leading spoke a sort of "comet" look, with a tail dissipating behind it as it rotates. Achieving this is surprisingly easy, thanks to our new animation mechanism.

The key to working with FrameBasedAnimation is to think of a snapshot of the animation. Because each "frame" is constructed in exactly the same manner, with the only things changing being the current spoke, we can think of things relatively. So, for example, if we want the "tail" of our comet to be four spokes long, we just have to set the Opacity of the current spoke to 1.0 and the Opacity of four spokes back (i - 4 % numFrames) as 0.2 (which still shows the dots in a dim manner at all times). Finally, we need to set the spoke directly ahead of the current spoke to 0.2 as well, so that a non-interpolating "line" is constructed between that spoke and the end of the tail. This involves three Setters, outlined below:

C#
f.Add( new Setter( Line.OpacityProperty, "0.2",
    ( (Line)canvas.Children[ ( i + numFrames - 4 ) % numFrames ] ).Name ) );
f.Add( new Setter( Line.OpacityProperty, "1",
    ( (Line)canvas.Children[ ( i ) % numFrames ] ).Name ) );
f.Add( new Setter( Line.OpacityProperty, "0.2",
    ( (Line)canvas.Children[ ( i + 1 ) % numFrames ] ).Name ) );

It doesn't matter what order you add the Setters to the Frame.

XML
<Line x:Name="sw" Opacity="0.4">
    <Line.RenderTransform>
        <RotateTransform Angle="225" />
    </Line.RenderTransform>
</Line>
<Line x:Name="w" Opacity="0.6">
    <Line.RenderTransform>
        <RotateTransform Angle="270" />
    </Line.RenderTransform>
</Line>
<Line x:Name="nw" Opacity="0.8">
    <Line.RenderTransform>
        <RotateTransform Angle="315" />
    </Line.RenderTransform>
</Line>

Once this is taken care of, we can compile and run, and get a nice, professional-looking spinning wheel animation!

final.png

Limitations

FrameBasedAnimation currently only supports linear interpolation between frames; however, it would be a simple matter to add support for discrete or spline-based interpolations by adding a property to the FrameBasedAnimation class and applying it recursively (in Render()) to each animation.

Similarly, only support for double animations is currently implemented, but I've intentionally left this easy to extend, by placing switch statements in a couple isolated methods. Take a look at the source code, or post a comment if you need help. It should be pretty obvious what to do.

Part 2

Part 2 will focus on polishing-up our spinning wheel throbber into a reusable component, and will no longer focus on the animation side of things. Look for it sometime next month!

Conclusion

I had a lot of fun writing this article—it's always great to be able to manipulate an existing platform to work in fun new ways, and it really showcases the incredible extensibility of WPF. My hope is that others will benefit from my animation class as I feel it is a much more natural way to think about animation. At the very least, I hope this article will serve as a tutorial to the finer details of the WPF animation mechanism, the understanding of which is valuable to all WPF beginners and experts alike.

In closing, you are completely free to use and modify this source code for any free or commercial project. Enjoy!

Points of Interest

I would have liked to include animated GIF screen captures of the spinner in action, but I don't have the appropriate software. If anyone can recommend a good (and either free, or with a free trial) animated GIF screen capture utility that works with Vista, please leave me a note in the comments.

History

  • September 22, 2008 – First publication!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
President The Little Software Company
Canada Canada
My name is Logan Murray and I'm a Canadian. I'm interested primarily in C# and Windows desktop application development (learning WPF at the moment and then hopefully LINQ), though I hope to branch-out more to the web side of things eventually. I am the president and owner of The Little Software Company and am currently working on the Windows version of OrangeNote, to be released soon. Check out my RSS reader, FeedBeast™.

Comments and Discussions

 
GeneralStop animation Pin
Adrian Cannon27-May-11 3:29
Adrian Cannon27-May-11 3:29 
GeneralMy vote of 5 Pin
GeorgeSaid20-Dec-10 2:04
GeorgeSaid20-Dec-10 2:04 
Questionupdate? Pin
ryan234234534515-Dec-10 7:41
ryan234234534515-Dec-10 7:41 
AnswerRe: update? Pin
chaiguy133715-Dec-10 16:54
chaiguy133715-Dec-10 16:54 
GeneralVery good work. Some changes for different System Regional Settings Pin
gdimauro21-Mar-09 1:32
gdimauro21-Mar-09 1:32 
GeneralRe: Very good work. Some changes for different System Regional Settings Pin
chaiguy133721-Mar-09 4:17
chaiguy133721-Mar-09 4:17 
Questionflickering? Pin
Leblanc Meneses16-Nov-08 20:54
Leblanc Meneses16-Nov-08 20:54 
AnswerRe: flickering? Pin
chaiguy133717-Nov-08 4:33
chaiguy133717-Nov-08 4:33 
Generalfixed flickering Pin
Leblanc Meneses17-Nov-08 13:15
Leblanc Meneses17-Nov-08 13:15 
GeneralRe: fixed flickering Pin
chaiguy133717-Nov-08 14:38
chaiguy133717-Nov-08 14:38 
GeneralRe: fixed flickering Pin
Leblanc Meneses17-Nov-08 17:12
Leblanc Meneses17-Nov-08 17:12 
GeneralRe: fixed flickering Pin
chaiguy133717-Nov-08 17:19
chaiguy133717-Nov-08 17:19 
GeneralRe: fixed flickering - not really [modified] Pin
Leblanc Meneses18-Nov-08 6:45
Leblanc Meneses18-Nov-08 6:45 
GeneralRe: fixed flickering - not really Pin
chaiguy133719-Nov-08 6:24
chaiguy133719-Nov-08 6:24 
GeneralRe: fixed flickering Pin
chaiguy133719-Nov-08 15:55
chaiguy133719-Nov-08 15:55 
GeneralRe: fixed flickering Pin
Leblanc Meneses19-Nov-08 19:42
Leblanc Meneses19-Nov-08 19:42 
GeneralRe: fixed flickering Pin
chaiguy133720-Nov-08 6:28
chaiguy133720-Nov-08 6:28 
GeneralRe: fixed flickering Pin
Leblanc Meneses20-Nov-08 6:56
Leblanc Meneses20-Nov-08 6:56 
GeneralRe: fixed flickering Pin
chaiguy133720-Nov-08 6:58
chaiguy133720-Nov-08 6:58 
AnswerWoo! Pin
chaiguy133720-Nov-08 11:52
chaiguy133720-Nov-08 11:52 
I think I've solved disabling when invisible. I'm not sure if this will have any effect on the flickering you noticed, since I didn't experience that personally. However it definitely cuts down on the CPU time when not needed.

It was actually simple and although I tried something similar previously I don't know why it didn't work before. Basically I just subscribe to IsVisibleChanged (this.IsInvisibleChanged inside Spinner.xaml.cs), then call storyboard.Stop if IsVisible is false, and storyboard.Begin() if IsVisible is true.

I did move the storyboard out of the Triggers section and into the Resources, which may have been part of the original problem, but it seems to work now.

The mixed-up spoke order glitch doesn't seem to be fixed, unfortunately, but maybe isn't quite as bad.

Logan

“It behooves every man to remember that the work of the critic, is of altogether secondary importance, and that, in the end, progress is accomplished by the man who does things.”
–Theodore Roosevelt

{o,o}.oO( Check out my blog! )
|)””’)          http://pihole.org/
-”-”-

GeneralWow! Pin
Paul Conrad19-Oct-08 11:31
professionalPaul Conrad19-Oct-08 11:31 
GeneralRe: Wow! Pin
chaiguy133719-Oct-08 11:37
chaiguy133719-Oct-08 11:37 
GeneralRe: Wow! Pin
Paul Conrad19-Oct-08 17:03
professionalPaul Conrad19-Oct-08 17:03 
GeneralGood job! Pin
Patrick Klug5-Oct-08 20:18
Patrick Klug5-Oct-08 20:18 
GeneralRe: Good job! Pin
chaiguy13376-Oct-08 5:26
chaiguy13376-Oct-08 5:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.