Export (0) Print
Expand All
Dazzling Graphics: Top Ten UI Development Breakthroughs In Windows Presentation Foundation
Distributed .NET: Learn The ABCs Of Programming Windows Communication Foundation
A First Look at InfoCard
Talking Windows: Exploring New Speech Recognition And Synthesis APIs In Windows Vista
Windows Workflow Foundation
Windows Workflow Foundation, Part 2
WinFX Workflow: Simplify Development With The Declarative Model Of Windows Workflow Foundation
XPS Documents: A First Look at APIs For Creating XML Paper Specification Documents
Expand Minimize

"Avalon" Animation: The Storyboard Story

 

Kurt Jacob
Microsoft Corporation

February 2005

Applies to
   The November 2004 Community Technology Preview Release of Avalon

Note   Community Technical Preview (CTP) builds do not go through the same rigorous testing that beta builds undergo. While betas receive a much higher level of testing and feature work, CTPs are intended to expose developers to the latest working build. CTPs are therefore unsupported, prerelease software.

Summary: XAML enables us to use markup to describe the visuals for our application interfaces, and then animate those visuals using a mechanism called storyboards. (15 printed pages)

Download the code sample that accompanies this article, StoryBoardStorySampleSource.msi.


Contents

That Was Then . . .
This Is Animation in Avalon
Triggering the Animation
More Animation!
Bouncing Back
Two Timelines Are Better than One
Where Do We Go from Here?

That Was Then . . .

One of the key features that the "Longhorn" presentation subsystem, code-named "Avalon," adds to your user interface toolbox is animation. "Why would I want animation in my user interface?" you might ask. Well, there are a number of reasons. First, animation is a great way to direct the user's attention to a particular part of your interface. For instance, imagine the Next button on a wizard bouncing to let you know that it is now possible to go to the next step in the process. Second, animation helps the user maintain context when the interface transitions between states. One example of this can be found in the Office menus when they expand from the recently selected commands to the full menu. Animation can also be used to make saving screen space in a user interface more palatable. For an example, take a look at the expanding, collapsing task categories in the Windows explorer.

So why don't we see more animation in user interfaces? To answer this, let's take a look at how I did this in the past. As a sample, let's take the simple case of an animation that runs when the user presses a button labeled "Start!" Here's what the Windows Forms sample would look like:

Aa480208.storyboardstory01(en-us,MSDN.10).gif

To do the animation I need to get callbacks when the animation should update. Since the animation will start when I click a button I can put this code in the event handler for the button:

private void startButton_Click(object sender, System.EventArgs e)
{
   if( this.animationTimer == null )
   {
      this.animationTimer = new Timer();
      this.animationTimer.Interval = 33; // 30 fps
      this.animationTimer.Tick += new EventHandler( this.OnTimer );

      // set the global start time by converting ticks to seconds
      this.startTime = System.DateTime.Now.Ticks/10000000.0; 
      this.animationTimer.Start();
   }
}

In the sample above I am using a Timer to get callbacks at a given interval when the Start button is pressed. However, the problem with using a timer is that the frequency at which you get callbacks is fairly low resolution, so in some cases the animation will look chunky if the machine is busy. Another challenge with this approach is that it may use more CPU that I want it to. Let's just assume that I have figured all this out and move on to the actual animation.

Given a source of time there are a couple of ways that I could go about animating a value. First I could just increment the value by a certain amount every time I get the timer callback.

private void OnTimer( object target, System.EventArgs args )
{
  if( this.animatedButton.Top < 116)    
      this.animatedButton.Top +=10;
}

The drawback to this method is that the change in the value will speed up or slow down depending on how busy the system is. The second approach I could take is to use an equation to calculate the new value of the property given the current time. This way my code will always change the value over a specific amount of real time. If the CPU is extremely busy, then the animation may only update a few times, and if the CPU has a lot of free time I will see a smooth animation. This method also ensures that the value will be at the endpoint of the animation when time has passed the end of the animation. Here's a look:

private void OnTimer( object target, System.EventArgs args )
{
   double timeSinceStart = DateTime.Now.Ticks/10000000.0 - this.startTime;
   this.animatedButton.Top = (int) this.CalculateCurrentValue( 
timeSinceStart, 1.0, 16, 116);

   if( timeSinceStart > duration )
   {
      this.animationTimer.Stop();
      this.animationTimer = null;
   }
}

private double CalculateCurrentValue( double currentTime, 
                  double duration, 
               double startValue, 
               double endValue)
{
   double progress;
   if( currentTime <= duration )
      progress = currentTime/duration;
   else
      progress = 1.0;

   return startValue + (endValue - startValue) * progress;
}

Here are some screenshots of the animation as it runs.

Aa480208.storyboardstory02(en-us,MSDN.10).gifAa480208.storyboardstory03(en-us,MSDN.10).gifAa480208.storyboardstory04(en-us,MSDN.10).gif

If I wanted to make my button bounce, I would have to write more code to know when to reverse directions at the right time. In order for my animation to look like a real bounce, I might also want the change in the value to speed up and slow down as time progresses, which I could do with—say it with me now—more code. Let's not do that here. Instead, let's take a look at how this animation is done with Avalon.

This Is Animation in Avalon

So how does Avalon help me with these problems? In Avalon, most of this code has been written for me. XAML allows me to use markup to describe the visuals for my interface, and then animate those visuals using a mechanism called storyboards. Let's start with the visuals for our interface.

<Window x:Class="AnimationSample.Window1"
  xmlns="http://schemas.microsoft.com/2003/xaml" xmlns:x="Definition"
  Text="AnimationSample"
  >
  <Grid>
    <Canvas>
      <Button ID="AnimatedButton" Canvas.Left="16" Canvas.Top="16" >
        Animated
      </Button>
      <Button ID="StartButton" Canvas.Left="16" Canvas.Top="140" 
         Click="StartButtonClick" >
        Animate!
      </Button>
    </Canvas>
  </Grid>
</Window>

And a screenshot of where we are in Avalon:

Aa480208.storyboardstory05(en-us,MSDN.10).gif

Here I have a button to start the animation, and a button to animate, just as in the Windows Forms sample. Now let's add the animation part.

<Window x:Class="AnimationSample.Window1"
  xmlns="http://schemas.microsoft.com/2003/xaml" xmlns:x="Definition"
  Text="AnimationSample"
  >
  <Window.Storyboards>
    <Timeline BeginTime="*null" ID="MoveDown" >
      <SetterTimeline TargetID="AnimatedButton" Path="(Canvas.Top)">
        <LengthAnimation To="116" Duration="1" />
      </SetterTimeline>
    </Timeline>
  </Window.Storyboards>
  <Grid>
    <Canvas>
      <Button ID="AnimatedButton" Canvas.Left="16" Canvas.Top="16" >
        Animated Button
      </Button>
      <Button ID="StartButton" Canvas.Left="16" Canvas.Top="140" Click="StartButtonClick" >
        Animate!
      </Button>
    </Canvas>
  </Grid>
</Window>

Above I have written the XAML description of my animation in a container attached to the window called a storyboard. If you have ever used a video editing system, this name will make more sense. In most systems there is a view of the video called the storyboard view that shows the first frame of every clip that is included in the final production. The concept here is similar. Each timeline within the storyboard above represents an animation clip that is applied to the contents of the window. This differs from the video-editing view in that these clips do not necessarily play in sequence as they do in a movie, but can be triggered independently by the user. This is why the storyboard and its animations are separated from the rendered content of the window; this way, multiple animations can be run on the same rendered content without duplicating that content. The Storyboard property holds all the animations for a particular container such as a Window. An animation in a storyboard can animate most properties of any element in the container to which the storyboard is attached. In this case the element to which the storyboard is attached is the Window element (AnimationSample.Window1) and this storyboard can animate any property of the elements inside that window (a Grid, a Canvas, and two Buttons).

Inside the Storyboard tag is a Timeline tag. This tag serves as a grouping element to bring several animations that run at the same time together into one piece of markup. Think of a Timeline as a Canvas for time. Any Timeline inside another Timeline tag is constrained by its parent Timeline. In the sample above, note that the start of my top-level timeline, the timeline with an ID of "MoveDown, is "*null", which means that it will not start until told to start by code. The SetterTimeline and LengthAnimation inside that timeline have a default BeginTime of 0. This means that they will start at an offset of 0 seconds from the time that their parent, the "MoveDown" timeline, starts.

Inside the Timeline tag is a SetterTimeline. The SetterTimeline "sets" the output of the animation inside it to a particular property value on a particular element. The element is set by the TargetID property on the SetterTimeline, and the property to be animated is described in the Path property using something called a PropertyPath. In the example above, the SetterTimeline is mapping the output of the LengthAnimation inside of it to the Canvas.Top property of the element with an ID of "AnimatedButton."

The value of the PropertyPath property bears some explaining. In this sample I am animating the Canvas.Top property of the "AnimatedButton." This property is an attached property that tells the Canvas parent of "AnimatedButton" where to position that button. It is surrounded by parenthesis so that more complex values can be animated. For instance, I could also animate the Foreground property of the button, which I know is a SolidColorBrush. A SolidColorBrush has a single property, which is the color of the brush itself. I can animate this by using the property path: "(Button.Foreground). (SolidColorBrush.Color)" This property path goes to the Button.Foreground property of the button, and then attempts to find a SolidColorBrush.Color property on that value. You can find out more about PropertyPath in the WinFX SDK.

Finally, inside the SetterTimeline tag we get to the tag that describes the animation. The animation that I am using is a LengthAnimation because the value of the property that I am animating, (Canvas.Top), is of type Length. Animations in Avalon are tied to the types that they animate so that the system can validate the values set as start, end, and intermediate points in the animation. The LengthAnimation also has a Duration set on it. Animations are timelines that allow me to set any Timeline property on an animation in order to control (for instance) when it starts and for how long it plays. As in my Windows Forms code above, I need to know how long I want the animation from one value to another to take before it is really meaningful. Thus I set the Duration of my LengthAnimation.

Triggering the Animation

Now that we have described the animation, it is time to tell it to play when the button is pressed. Note that, in the markup above, I have set the Click handler on the Start button to call the StartButtonClick method, so all I have to do is implement the handler for that event.

private void StartButtonClick(object sender, RoutedEventArgs e) 
{
    // Find the timeline that we want to start
    Timeline moveDownTimeline = this.Storyboards[0];
    // Find the Clock
    TimelineClock timelineClock = this.FindStoryboardClock(moveDownTimeline);
    if (timelineClock == null)
      return;

    // Start it!
    timelineClock.InteractiveController.Begin();
}

In the November 2004 Community Technology Preview Release of Avalon I have to find the clock that corresponds to the timeline that I want to play. The timeline that appears in the markup is a description of an animation that can be applied to any element tree. It knows when the timeline will start relative to its parent, what its duration is, and a host of other data. It is possible for this timeline to be applied to more than one set of visuals. When the timeline is used in multiple places it is no longer possible for it to know what the current time is in any one instance. Also, starting a timeline in this case does not make much sense unless I also specify which instance of the timeline I want to start. As a result, when a timeline is applied to a particular set of visuals, a clock is created to correspond to that particular instance of the timeline. The clock keeps track of the current time and other data that is specific to that instance of the timeline, and exposes methods that allow you to manipulate how that instance is playing.

Once I have found the clock with the FindStoryboardClock method, I have to tell the clock to start. This is done by getting the InteractiveController from the clock, and using that to begin the clock. This will start all the clocks for the animations that are defined under the timeline that I used to find the clock.

Aa480208.storyboardstory06(en-us,MSDN.10).gifAa480208.storyboardstory07(en-us,MSDN.10).gifAa480208.storyboardstory08(en-us,MSDN.10).gif

More Animation!

Now that we have the code to start the timeline, let's add another animation just for fun.

<Timeline BeginTime="*null" ID="MoveDown" AutoReverse="True" >
      <SetterTimeline TargetID="AnimatedButton" Path="(Canvas.Top)">
        <LengthAnimation To="116" Duration="1"/>
      </SetterTimeline>
      <SetterTimeline TargetID="AnimatedButton" Path="(Button.Width)">
        <LengthAnimation From="75" To="100" BeginTime="0:0:0.5" 
          Duration="0.5" />
      </SetterTimeline>
    </Timeline>

I have added a new setter timeline and animation that animate the width of the button. Note that I have set the BeginTime property of the animation so that it starts half a second after the position animation starts. The special format for the BeginTime property is needed so that Avalon will recognize the value as a time value and corresponds to what you might see on a stopwatch (hours:minutes:seconds). This animation has a From property as well, which explicitly sets the value at which the animation starts. No matter what the value of the Width property is when this new animation starts, the value will animate from the From value to the To value.

Bouncing Back

Now, if you try the above sample you will find that the button pops back to its original position at the end of the animation. This isn't really what I wanted to do. If you recall, I really want to make it look as if the button is bouncing. I can do this by setting the AutoReverse property on the animation.

<Window.Storyboards>
    <Timeline BeginTime="*null" ID="MoveDown" AutoReverse="True">
      <SetterTimeline TargetID="AnimatedButton" Path="(Canvas.Top)">
        <LengthAnimation To="116" Duration="1"/>
      </SetterTimeline>
      <SetterTimeline TargetID="AnimatedButton" Path="(Button.Width)">
        <LengthAnimation From="75" To="100" BeginTime="0:0:0.5" 
          Duration="0.5" />
      </SetterTimeline>
    </Timeline>
</Window.Storyboards>

Well, that was easy. No more code, and my animation now reverses when it gets to its endpoint and returns to where it started. When AutoReverse is set to true the timeline runs twice as long it did before to keep from changing the speed of my animation. Note that I put the AutoReverse property on the timeline that contains my animation, and that the reversal affects that animation. This is another example of how a parent timeline affects the timelines and animations that it contains.

Two Timelines Are Better than One

You might be asking yourself at this point why all this structure is involved in doing an animation. Sure, it's neat that you can easily reverse an animation and that the interpolation is done for you, but why do I have to have this storyboard thing anyway?

The answer is that interactive animation gets really interesting when more than one timeline is involved. For instance, I might want my bounce animation to play when one button is clicked and another animation to play when I click a second button. In order to do this, first I need another button:

<Window x:Class="AnimationSample.Window1"
  xmlns="http://schemas.microsoft.com/2003/xaml" xmlns:x="Definition"
  Text="AnimationSample"
  >
  <Window.Storyboards>
    …
  </Window.Storyboards>
  <Grid>
    <Canvas>
      <Button ID="AnimatedButton" Canvas.Left="16" Canvas.Top="16" Width="75">
        Animated
      </Button>
      <Button ID="StartButton" Canvas.Left="16" Canvas.Top="160" 
Click="StartButtonClick" >
        Animate!
      </Button>
      <Button ID="Start2Button" Canvas.Left="16" Canvas.Top="190" 
Click="Start2ButtonClick">
        Animate 2!
      </Button>
    </Canvas>
  </Grid>
</Window>

Now I add my new timeline to be triggered by that button. In this case it animates the height of my animated button:

  <Window.Storyboards>
    <Timeline BeginTime="*null" ID="MoveDown" AutoReverse="True" >
      …
    </Timeline>
    <Timeline BeginTime="*null" ID="GetTaller" AutoReverse="True">
      <SetterTimeline TargetID="AnimatedButton" Path="(Button.Height)">
        <LengthAnimation From="25" To="50" Duration="0.4"/>
      </SetterTimeline>
    </Timeline>
  </Window.Storyboards>

This is pretty similar to the timeline that I set up before.

I also need to update the code that we use to trigger the timelines so that it is a bit more robust.

private void Start2ButtonClick(object sender, RoutedEventArgs e)
{
    Debug.Assert( BeginClockForTimeline("GetTaller") );
}

private void StartButtonClick(object sender, RoutedEventArgs e) 
{
    Debug.Assert( BeginClockForTimeline("MoveDown") );
}

// Returns false if it fails to find the timeline or the clock.
private bool BeginClockForTimeline(string name)
{
    Timeline timeline = FindTimeline(name);
    if (timeline == null)
        return false;

    TimelineClock timelineClock = this.FindStoryboardClock(timeline);
    if (timelineClock == null)
        return false;

    // Start it!
    timelineClock.InteractiveController.Begin();

    return true;
}

private Timeline FindTimeline(string name)
{
    Timeline foundTimeline = null;
    foreach (Timeline curTimeline in this.Storyboards)
    {
        if (curTimeline.ID == name)
        {
            foundTimeline = curTimeline;
            break;
        }
    }
    return foundTimeline;
}

Above is the code for a couple of new methods. The first begins a clock given a timeline ID. It does pretty much what the Start button event handler did in the previous code, except that it uses the second method. The second method finds a timeline given a string ID. It gets the Storyboard collection from the window and iterates over it looking for a timeline with a matching ID and returns it if it finds it. These two methods make it easy to start any timeline in the Window's Storyboard collection given the string ID of that timeline.

Given the new methods, the event handlers for my buttons are now trivial and just call the BeginClockForTimeline method (asserting if it fails to find the clock).

Here are some screenshots from the final animation. Notice that the height animation plays while the top animation is still playing.

Aa480208.storyboardstory09(en-us,MSDN.10).gifAa480208.storyboardstory10(en-us,MSDN.10).gifAa480208.storyboardstory11(en-us,MSDN.10).gifAa480208.storyboardstory12(en-us,MSDN.10).gif

If you run the sample with these changes you will find that the "Start!" button starts the bounce timeline, and the "Start2!" button starts the new "GetTaller" timeline. Here is the fun part. The timelines will play any time the corresponding button is pressed, even if the other timeline is already playing. This is crucial to the support of interactive animation. The person designing the animations cannot predict when an animation will start because those times are set by user interaction or some other unpredictable event. The designer can, however, define the animations that happen in response to those events, and Avalon will run those animations concurrently and resolve conflicts when they try to animate the same property at the same time.

When we first encountered the Storyboard tag I mentioned that a storyboard may be thought of as a collection of animation "clips" that can be applied to a set of elements. Here we can see this in action. The "GetTaller" timeline is an animation clip that can be triggered independently of the "MoveDown" clip. There can be as many independent clips of animation for this application as I can dream up, and they are all linked to the content of my application window by ID only, so I can change the elements that are animated simply by changing IDs or assigning this storyboard to another window.

Where Do We Go from Here?

So now we have gone through the basics of what a storyboard is and how to create animations with it. In a future article I hope to cover more advanced animations and show how to create an animated control.

Show:
© 2015 Microsoft