Silverlight Animation

Silverlight animation lets create truly dynamic user interfaces


January 28, 2008
URL:http://www.drdobbs.com/web-development/silverlight-animation/205920349

Matthew MacDonald and Mario Szpuszta are the authors of Pro ASP.NET 3.5 in C# 2008, Second Edition, from which this article is adapted. Copyright Apress All rights reserved.


Animation is a capability of Silverlight 1.1. Animation allows you to create truly dynamic user interfaces. It's often used to apply effects -- for example, icons that grow when you move over them, logos that spin, text that scrolls into view, and so on. Animations are a core part of the Silverlight model. That means you don't need to use timers and event-handling code to put them into action. Instead, you can create them declaratively, configure them using one of a handful of classes, and put them into action without writing a single line of C# code.

Animation Basics

Silverlight animation is a scaled-down version of the WPF animation system. In order to understand Silverlight animation, you need to understand the following key rules:

Silverlight has relatively few animation classes, so you're limited in the data types you can use. At present, you can use animations to modify properties with the following data types: double Color, and Point.

As a rule of thumb, the property-based animation is a great way to add dynamic effects to an otherwise ordinary application (like buttons that glow, pictures that expand when you move over them, and so on). However, if you need to use animations as part of the core purpose of your application and you want them to continue running over the lifetime of your application, you probably need something more flexible and more powerful. For example, if you're creating a basic arcade game or using complex physics calculations to model collisions, you'll need greater control over the animation. Unfortunately, Silverlight doesn't have an option for frame-based animation, so you'll be forced to create this sort of application the old-fashioned way -- by looping endlessly, being careful to modify your visuals and check for user input after each iteration. You can see an example of this technique with the ball collision simulator at http://bubblemark.com.

Defining an Animation

Creating an animation is a multistep process. You need to create three separate ingredients: an animation object to perform your animation, a storyboard to manage your animation, and an event trigger to start your storyboard. In the following sections, you'll tackle each of these steps.

The Animation Class

There are actually two types of animation classes in Silverlight. Each type of animation uses a different strategy for varying a property value.

In this article, you'll focus exclusively on the most commonly used animation class: the DoubleAnimation class. It uses linear interpolation to change a double from a starting value to its ending value. Animations are defined using XAML markup. Although the animation classes aren't elements, they can still be created with the same XAML syntax. For example, here's the markup required to create a DoubleAnimation:

DoubleAnimation From="160" To="300" Duration="0:0:5"></DoubleAnimation>

This animation lasts 5 seconds (as indicated by the Durationproperty, which takes a time value in the format Hours:Minutes:Seconds.FractionalSeconds). While the animation is running, it changes the target value from 160 to 300. Because the DoubleAnimation uses linear interpolation, this change takes place smoothly and continuously.

There's one important detail that's missing from this markup. The animation indicates how the property will be changed, but it doesn't indicate what property to use. This detail is supplied by another ingredient, which is represented by the Storyboard class.

The Storyboard Class

The storyboard manages the timeline of your animation. You can use a storyboard to group multiple animations, and it also has the ability to control the playback of animation -- pausing it, stopping it, and changing its position. However, the most basic feature provided by the Storyboard class is its ability to point to a specific property and specific element using the TargetProperty and TargetName properties. In other words, the storyboard bridges the gap between your animation and the property you want to animate.

Here's how you might define a storyboard that applies a DoubleAnimation to the Width property of an element named rect:

Storyboard x:Name="storyboard"
Storyboard.TargetName="rect" Storyboard.TargetProperty="Width">
<DoubleAnimation From="160" To="300" Duration="0:0:5">:<:/DoubleAnimation>
</Storyboard>

Both TargetName and TargetProperty are attached properties. That means you can apply them directly to the animation, as shown here:

Storyboard x:Name="storyboard">
<DoubleAnimation
Storyboard.TargetName="rect" Storyboard.TargetProperty="Width"
From="160" To="300" Duration="0:0:5"></DoubleAnimation>
</Storyboard>

This syntax is more common, because it allows you to put several animations in the same storyboard but allow each animation to act on a different element and property.

If you give your storyboard a name (as in the previous example), you can interact with it in code. The Storyboard class includes four methods that let you manually control the animation: Begin(), Stop(), Pause(), and Resume(). However, if you simply want to start your animation in response to some event, there's an easier solution. You can wire this event directly to the BeginStoryboard action, as described in the next section.

The Event Trigger

Defining a storyboard and an animation are the first steps to creating an animation. To actually put this storyboard into action, you need an event trigger. An event trigger responds to an event by performing a storyboard action. The only storyboard action that Silverlight currently supports is BeginStoryboard, which starts a storyboard (and hence all the animations it contains).

The following example uses the Triggers collection of a Canvas to attach an animation to the Loaded event. When the Silverlight content is first rendered in the browser, and the Canvas element is loaded, the rectangle begins to grow. Five seconds later, its width has stretched from 160 pixels to 300.

<Canvas ... >
  <Canvas.Triggers>
    <EventTrigger RoutedEvent="Canvas.Loaded">  
      <EventTrigger.Actions>
        <BeginStoryboard>
          <Storyboard>
            <DoubleAnimation Storyboard.TargetName="rect"
            Storyboard.TargetProperty="Width"
            From="160" To="300" Duration="0:0:5"></DoubleAnimation>
          </Storyboard>
        </BeginStoryboard>
      </EventTrigger.Actions>
    </EventTrigger>
  </Canvas.Triggers>
  <Rectangle Name="rect" Height="40" Width="160" Fill="Blue"
  Canvas.Left="10" Canvas.Top="10"></Rectangle>
</Canvas>

The Storyboard.TargetProperty property identifies the property you want to change (in this case, Width). If you don't supply a class name, the storyboard uses the parent element. If you want to set an attached property (for example, Canvas.Left or Canvas.Top), you need to wrap the entire property in brackets, like this:

<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" ... />

If you want to use multiple animations in the same storyboard, you simply need to add more than one <Animation> element inside the <Storyboard> element. For example, you could use this technique to make the rectangle grow in width and height at the same time.

Starting an Animation with Code

The EventTrigger approach is an easy way to kick off an animation. However, in the current build, not all Silverlight events can be used as event triggers. The Loaded event is supported, but mouse-related events like MouseEnter, MouseLeave, and MouseMove are not.

If you want to start an animation in response to these events, you need to interact with the storyboard programmatically. Fortunately, it's easy. The first step is to move your storyboard out of the Triggers collection and place it in another collection of the same element: the Resources collection.

All Silverlight elements provide a Resourcesproperty, which holds a collection where you can store miscellaneous objects. The primary purpose of the Resources collection is to allow you to define objects in XAML that aren't elements, and so can't be placed into the visual layout of your content region. For example, you might want to declare a Brush object as a resource so it can be used by more than one element. Resources can be retrieved in your code or used elsewhere in your markup. Here's an example that defines the rectangle-growing animation as a resource:

<Canvas x:Name="canvas" MouseLeftButtonDown="canvas_Click" ... >
  <Canvas.Resources>
    <Storyboard x:Name="growStoryboard">
       <DoubleAnimation Storyboard.TargetName="rect"
         Storyboard.TargetProperty="Width"
        Storyboard.TargetName="canvas"
        From="160" To="300" Duration="0:0:5"></DoubleAnimation>
    </Storyboard>
  </Canvas.Triggers>
 Rectangle Name="rect" Height="40" Width="160" Fill="Blue"
 Canvas.Left="10" Canvas.Top="10"></Rectangle>
Canvas>

Notice that it's now given a name, so you can manipulate it in your code. You'll also notice that you need to explicitly specify the Storyboard.TargetName property to connect it to the right element when you're using this approach.

Now you simply need to call the methods of the Storyboard object in an event handler in your Silverlight code-behind file. The methods you can use include Begin(), Stop(), Pause(), Resume, and Seek(), all of which are fairly self-explanatory.

private void canvas_Click(object o, EventArgs e)
{
   growStoryboard.Begin();
}

Configuring Animation Properties

To get the most out of your animations, you need to look a little closer at the base Animation class, which defines the properties that are provided by all animation classes. Table 1 describes them all.

From Sets the starting values for your animation. In many situations, you won't set From. In this case, Silverlight uses the current value of your element. For example, if you didn't set the initial width in the growing rectangle example, it would start at whatever it is currently. This is particularly useful if you're animating a value that might be changed by other code or other animations. In this situation, you want the animation to start from the current value, not jump abruptly to a preset From value.
To Sets the ending value for your animation. In some situations, you won't set From or To. In this case, the property returns to whatever initial value is set in =the XAML markup. For example, you could use this technique to shrink the rectangle in the previous example back to its original size when it's clicked.
By Instead of using To, you can use By to create a cumulative animation. By sets a number that will be added to the initial value. For example, if you replace the To value in the rectangle growing example with a By value of 10, the rectangle will grow 10 pixels wider than its current width over the course of the animation. If you run this animation every time the rectangle is clicked, it will continue to grow, and grow.
Duration The length of time the animation runs, from start to finish, as a Duration object.
AutoReverse If true, the animation will play out in reverse once it's complete, reverting to the original value. This also doubles the time the animation takes.
RepeatBehavior Allows you to repeat an animation a specific number of seconds. Or, you can use Forever to repeat the animation endlessly.
BeginTime Sets a delay that will be added before the animation starts (as a TimeSpan). This delay is added to the total time, so a 5-second animation with a 5-second delay takes 10 seconds. BeginTime is useful when synchronizing different animations that start at the same time but should apply their effects in sequence.
SpeedRatio Increases or decreases the speed of the animation. Ordinarily, SpeedRatio is 1. If you increase it, the animation completes more quickly (for example, a SpeedRatio of 5 completes five times faster). If you decrease it, the animation is slowed down (for example, a SpeedRatio of 0.5 takes twice as long). You can change the duration of your animation for an equivalent result. The SpeedRatio is not taken into account when applying the BeginTime delay.
FillBehavior Determines what happens when the animation ends. Usually, it keeps the property fixed at the ending value (FillBehavior.HoldEnd), but you can also choose to return it to its original value (FillBehavior.Stop).
Table 1: Properties of the Animation Class

An Interactive Animation Example

In the previous example, you used animation to alter an element when it first appears. However, in most applications, animations will be triggered by another event, such as a mouse movement or a mouse click.

The following example demonstrates a slightly more realistic use of animation, which is shown in Figure 1. It begins with a content region that's filled with irregularly shaped rectangles. When you click a rectangle, it begins to fall toward the bottom of the Canvas, and simultaneously begins to change color. When you click another rectangle, the first animation stops and that rectangle begins to fall.

[Click image to view at full size]
Figure 1: Falling rectangles

This animation example is simple, but it demonstrates several of the subtle concepts in Silverlight animation.

The markup for this example defines a single storyboard. This storyboard isn't placed in a Triggers collection, because initially it isn't wired up to any specific rectangle. Instead, it's placed in the Canvas.Resources collection so it can be retrieved by your code when needed.

<Canvas ... >
  <Canvas.Resources>
    <Storyboard x:Name="fallingSquareStoryboard">
      <DoubleAnimation
          Storyboard.TargetProperty="(Canvas.Top)"
         To="400" Duration="0:0:2" />
    <ColorAnimation Storyboard.TargetProperty="Rectangle.Fill.Color"
        To="Blue" Duration="0:0:2" />
      </Storyboard>
   </Canvas.Resources>
</Canvas>

This storyboard wraps two animations: a DoubleAnimation that moves the rectangle, and a ColorAnimation that changes its color. The ColorAnimation uses linear interpolation, which means it will progressively blend the color from its initial value (in this example, red) to its final value (blue). You'll also notice that the Canvas doesn't contain any other elements. That's because this example uses a more flexible approach -- it generates the rectangles dynamically. When the Canvas is loaded, it creates 12 rectangles of random size, at random locations. It wires the MouseLeftButtonDown event of each one to the same event handler.

public void Page_Loaded(object o, EventArgs e)
{
   // Required to initialize variables
   InitializeComponent();
   // Generate some rectangles.
   Random rand = new Random();
   for (int i = 0; i < 12; i++)
   {
      Rectangle rect = new Rectangle();
      rect.Fill = new SolidColorBrush(Colors.Red);
      // Size and place it randomly.
     rect.Width = rand.Next(10, 40);
     rect.Height = rand.Next(10, 40);
     rect.SetValue<double>(Canvas.TopProperty,
        rand.Next((int)this.Height / 2));
     rect.SetValue<double>(Canvas.LeftProperty,
        rand.Next((int)this.Width));
     // Handle clicks.
    rect.MouseLeftButtonDown += rect_Click;
    // Give it a unique name, which is required for animation.
    rect.SetValue<string>(Rectangle.NameProperty, "rect" + i.ToString());
    // Add it to the Canvas.
    this.Children.Add(rect);
    }
}

When a rectangle is clicked, there are two steps that need to be performed. The animation for the current rectangle needs to be halted (by calling the Storyboard.Stop() method), and the existing storyboard needs to be attached to the new rectangle.

However, there's a trick here. Animations don't actually change the underlying value of a property, they simply override it temporarily. When the end of an animation is reached, the property is held indefinitely at its final value (unless you've set the FillBehavior property of the animation class to FillBehavior.Stop). But in this example, the animation needs to be repeatedly stopped. If you don't take any extra steps, each time you stop the animation of a falling rectangle, its position will be reset to its original value, meaning it will "jump" back up to the top of the Canvas.

The solution is to retrieve the current value of the Canvas.Top property for the rectangle, then stop the animation, and then set the animated value. This last step moves the rectangle to its most recent animated position. The result is that every time you click a new rectangle, the rectangle that was falling previously halts in its tracks, but remains in the same position.

Here's the code that implements this design:

// Keep track of the rectangle that's being animated.
private Rectangle currentlyFallingRectangle;
private void rect_Click(object o, EventArgs e)
{
    // Retrieve the Storyboard.
    Storyboard sb = (Storyboard)this.FindName("fallingSquareStoryboard");
    if (currentlyFallingRectangle != null)
   {
       // Stop the current animation and move the rectangle
       // to its current position.
      double top =
          (double)currentlyFallingRectangle.GetValue(Canvas.TopProperty);
      sb.Stop();
          currentlyFallingRectangle.SetValue<double>(Canvas.TopProperty, top);
   }
  // Get the rectangle that was clicked.
  currentlyFallingRectangle = (Rectangle)o;
  // Start the animation for the new rectangle.
  sb.SetValue<string>(Storyboard.TargetNameProperty,
  currentlyFallingRectangle.Name);
  sb.Begin();
}

Although the Canvas.Top property is set manually after the animation is stopped, the color is not. As a result, the rectangle reverts to its initial blue color as soon as another rectangle starts falling. There's another interesting quirk in this example. The animation always uses the same duration (2 seconds). However, the square you click may be close to the bottom or far from the bottom. As a result, squares closer to the bottom will fall more slowly, and squares farther from the bottom will fall faster.

In this example, there's only one storyboard at work at a time. It's reasonable to ask if you could create a similar example where every rectangle you click continues falling. This is possible, but a different design is required.

In the current build of Silverlight 1.1, Storyboard objects can't be fully configured programmatically. Thus, you need to have the storyboard and animations you need defined in your XAML. This is obviously a challenge if you're creating elements dynamically, and don't know how many storyboards you'll use. The solution is to create a custom control that has its own animation behavior. To implement this design in the previous example, you'd create a custom rectangle that has its own XAML template. This XAML would specify the animation that should be used for that rectangle. Thus, every time you create an instance of this custom control, it comes pre-wired with the animation support. Unfortunately, this more modular design takes a fair bit more code, and it's out of the scope of this chapter. However, if you're interested in learning more, check out the Silverlight Balloons example at http://tinyurl.com/398qf4. It illustrates this principle neatly with an endless sequence of rising balloons (each of which is an instance of a custom Balloon control).

Transforms

As you've already learned, Silverlight animations work by modifying the value of a property. Elements have several properties that can be usefully changed. For example, you can use Canvas.Left and Canvas.Top to move an element around. Or, you can alter the Opacity setting to make an element fade into or out of view. However, it's not immediately clear how you can perform more exciting alterations, like rotations.

The secret is "transforms." A transform is an object that alters the way a shape or other element is drawn by shifting the coordinate system it uses. You can use transforms to stretch, rotate, skew, and otherwise manipulate the shapes, images, and text in your Silverlight user interface. Transforms are useful for getting the right shape you want, but they're even more interesting when you're animating. By animating a property in a transform, you can rotate a shape, move it from one place to another, or warp it dynamically.

Table 2 lists the transforms that are supported in Silverlight.

TranslateTransform Displaces your coordinate system by some amount. This transform is useful if you want to draw the same shape in different places. X,Y
RotateTransform Rotates your coordinate system. The shapes you draw normally are turned around a center point you choose. Angle, CenterX, CenterY
ScaleTransform Scales your coordinate system up or down so that your shapes are drawn smaller or larger. You can apply different degrees of scaling in the X and Y dimensions, thereby stretching or compressing your shape. ScaleX, ScaleY, CenterX, CenterY
SkewTransform Warps your coordinate system by slanting it a number of degrees. For example, if you draw a square, it becomes a parallelogram. AngleX, AngleY, CenterX, CenterY
MatrixTransform Modifies your coordinate system using matrix multiplication with the matrix you supply. This is the most complex option -- it requires some mathematical skill. Matrix
TransformGroup Combines multiple transforms so they can all be applied at once. The order in which you apply transformations is important -- it affects the final result. For example, rotating a shape (with RotateTransform) and then moving it (with TranslateTransform) sends the shape off in a different direction than if you move it and then rotate it. N/A
Table 2: Transform Classes

Technically, all transforms use matrix math to alter the coordinates of your shape. However, using prebuilt transforms such as TranslateTransform, RotateTransform, ScaleTransform, and SkewTransform is far simpler than using the MatrixTransform and trying to work out the right matrix for the operation you want to perform. When you perform a series of transforms with TransformGroup, Silverlight fuses your transforms together into a single MatrixTransform, ensuring optimal performance.

Using a Transform

To transform an element, you set its RenderTransform property with the transform object you want to use. Depending on the transform object you're using, you'll need to fill in different properties to configure it, as detailed in Table 2.

For example, if you're rotating a shape, you need to use the RotateTransform and supply the angle in degrees. Here's an example that rotates a square clockwise by 25 degrees:

<Rectangle Width="80" Height="10" Stroke="Blue" Fill="Yellow"
  Canvas.Left="100" Canvas.Top="100">
    <Rectangle.RenderTransform>
      <RotateTransform Angle="25" />
   </Rectangle.RenderTransform>
</Rectangle>

When you rotate a shape in this way, you rotate it about the shape's origin (the top-left corner). If you want to rotate a shape around a different point, you can use the handy RenderTransformOrigin property. This property sets the center point using a proportional coordinate system that stretches from 0 to 1 in both dimensions. In other words, the point (0, 0) is designated as the top-left corner, and (1, 1) is the bottom-right corner. (If the shape region isn't square, the coordinate system is stretched accordingly.)

With the help of the RenderTransformOrigin property, you can rotate any shape around its center point using markup like this:

<Rectangle Width="80" Height="10" Stroke="Blue" Fill="Yellow"
  Canvas.Left="100" Canvas.Top="100"    RenderTransformOrigin="0.5,0.5">
    <Rectangle.RenderTransform>
   <RotateTransform Angle="25" />
  </Rectangle.RenderTransform>
 </Rectangle>

Animating a Transform

To use a transform in animation, the first step is to define the transform. (An animation can change an existing transform but not create a new one.) For example, imagine you want to allow a rectangle to rotate. This requires RotateTransform:


<Rectangle x:Name="rect" Width="80" Height="50" Stroke="Blue" Fill="Yellow"
  Canvas.Left="100" Canvas.Top="100" RenderTransformOrigin="0.5,0.5">
    <Rectangle.RenderTransform>
      <RotateTransform></RotateTransform>
    </Rectangle.RenderTransform>
  </Rectangle>

Now here's a storyboard that makes the rectangle rotate when the mouse moves over it. It uses the target property (UIElement.RenderTransform).Angle -- in other words, it reads the RenderTransform property of the Rectangle and modifies the Angle property of the RotateTransform object that's defined there. The fact that the RenderTransform property can hold a variety of different transform objects, each with different properties, doesn't cause a problem. As long as you're using a transform that has an angle property, this trigger will work.

<Rectangle x:Name="rect" Width="80" Height="50" Stroke="Blue" Fill="Yellow"
Canvas.Left="100" Canvas.Top="100" RenderTransformOrigin="0.5,0.5"
MouseEnter="rect_Enter">
   <Rectangle.RenderTransform>
      <RotateTransform></RotateTransform>
         </Rectangle.RenderTransform>
     <Rectangle.Resources>
        <Storyboard x:Name="rotateStoryboard">
          <DoubleAnimation
          Storyboard.TargetName="rect"
          Storyboard.TargetProperty="(UIElement.RenderTransform).Angle"
          To="360" Duration="0:0:0.8" RepeatBehavior="Forever"></DoubleAnimation>
       </Storyboard>
    </Rectangle.Resources>
  </Rectangle>

Finally, an event handler starts the storyboard:

private void rect_Enter(object o, EventArgs e)
{
    rotateStoryboard.Begin();
}

The rectangle rotates one revolution every 0.8 seconds and continues rotating perpetually. While the rectangle is rotating, it's still completely usable -- for example, it still raises the MouseLeftButtonDown event if you click it.

To stop the rotation, you can use a second trigger that responds to the MouseLeave event. At this point, you could call the Storyboard.Stop() method, but this will cause the button to jump back to its original orientation in one step. A better approach is to start a second animation that replaces the first. Here's how the second animation is defined:

<Storyboard x:Name="revertStoryboard">
   <DoubleAnimation
   Storyboard.TargetName="rect"
   Storyboard.TargetProperty="(UIElement.RenderTransform).Angle"
   To="0" Duration="0:0:0.2"></DoubleAnimation>
</Storyboard>

This animation seamlessly rotates the rectangle back to its original orientation in a snappy 0.2 seconds. You can place this storyboard in the same Rectangle.Resources collection as the first animation.

All you need to do is attach an event handler to the Rectangle.MouseLeave event that runs the storyboard:

private void rect_Leave(object o, EventArgs e)
{
   revertStoryboard.Begin();
}

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.