Silverlight Tutorial - Animating Elements using Render Transforms

Skill

Silverlight Tutorial - Animating Elements using Render Transforms

Posted in:

What started out as a tutorial about Silverlight 3's new BitmapCache feature has turned into something a little simpler. The documentation clearly states that render transforms will be hardware accelerated when bitmap caching is enabled, but I couldn't get it to work. It wasn't a total waste, however, as I did create an application that illustrates how to animate an object using render transforms, which is probably one of the fastest ways to animate an element's visual state.

The application we'll be building today is something we here at Switch On The Code refer to as 'Ball World'. Whenever we get our hands on a new technology or framework, one of the first things we do is figure out how to bounce a bunch of balls around on the screen.



The first thing we need is something to animate. For this I created a simple UserControl and put a red Ellipse inside it. Here's the XAML for this object.

<UserControl x:Class="BitmapCache.Ball"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            VerticalAlignment="Top"
            HorizontalAlignment="Left">
  <Grid>
    <Ellipse Width="15"
            Height="15"
            Fill="Red" />
  </Grid>
</UserControl>

Pretty simple right? Now we need to be able to keep track of the ball's current position and velocity. I simply added some properties to the code-behind for these.

public partial class Ball : UserControl
{
  /// <summary>
  /// Gets or sets the X and Y velocity of the ball.
  /// </summary>
  public Point Velocity { get; set; }

  /// <summary>
  /// Gets or sets the X and Y coordinates of the ball.
  /// </summary>
  public Point Position { get; set; }

  public Ball()
  {
     InitializeComponent();
  }
}

That's actually it for the ball. All the rest of the work is done in the app's MainPage control. As you can see from the example, we need a black border to hold the balls and a button to add more. Here's the XAML for the MainPage UserControl.

<UserControl x:Class="BitmapCache.MainPage"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            mc:Ignorable="d"
            Width="400"
            Height="400">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="*" />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Border BorderBrush="Black"
           BorderThickness="1"
           Margin="5">
      <Grid x:Name="_ballContainer" />
    </Border>
    <Button HorizontalAlignment="Right"
           Grid.Row="1"
           Content="Add Ball"
           Click="AddBallClicked" />
  </Grid>
</UserControl>

First I have a Grid with two rows - one for the ball container and one for the button. I use a border and a grid for the container and apply a little bit of margin to make things look a little better. The button is simply a regular old button with a handler for the click event.

Let's take a look at what happens when the user clicks the button.

private void AddBallClicked(object sender, RoutedEventArgs e)
{
  AddBall();
}

private void AddBall()
{
  Ball newBall = new Ball();

  // Initialize the velocity to a random speed.
  newBall.Velocity = new Point(_random.Next(1, 10), _random.Next(1, 10));
  newBall.Position = new Point(0, 0);

  // Give it a render transform that will be updated later.
  newBall.RenderTransform = new TranslateTransform() { X = 0, Y = 0 };

  // Lock the collection so this ball isn't added while others
  // are being updated.
  lock (_ballContainer.Children)
  {
    _ballContainer.Children.Add(newBall);
  }
}

When the button is clicked, AddBall is called. This function creates a new Ball object and initializes it's properties. We set the velocity's X and Y values to random numbers between 1 and 10. _random is a .NET Random object that I create in the constructor of this class. There are several available transforms that can be applied to a render transform, however since we just want to move our object, a TranslateTransform will work great. Before we can add our ball to the container, we need to lock it's Children collection. This is because, as you'll see next, the balls are updated using a timer, and we don't want to add balls to the collection while we're iterating it and updating other balls.

Next let's take a look at the function that iterates through the collection of balls and updates their positions. This function is called periodically using a DispatcherTimer.

private void UpdateBallPositions(object sender, EventArgs e)
{
  // Lock the ball collection so new balls aren't added
  // while we're updating the display.
  lock (_ballContainer.Children)
  {
    foreach (Ball ball in _ballContainer.Children)
    {
      // Set the new position of the ball.
      Point newPosition = new Point(
          ball.Position.X + ball.Velocity.X,
          ball.Position.Y + ball.Velocity.Y);

      Point newVelocity = ball.Velocity;

      //
      // Check that the ball's new position isn't outside of the container.
      // When the ball reaches the edge, it's velocity is reversed to
      // simulate a bounce.
      //

      if (newPosition.X + ball.ActualWidth >= _ballContainer.ActualWidth)
      {
        newPosition.X = _ballContainer.ActualWidth - ball.ActualWidth;
        newVelocity.X = -newVelocity.X;
      }
      else if (newPosition.X <= 0)
      {
        newPosition.X = 0;
        newVelocity.X = -newVelocity.X;
      }

      if (newPosition.Y + ball.ActualHeight >= _ballContainer.ActualHeight)
      {
        newPosition.Y = _ballContainer.ActualHeight - ball.ActualHeight;
        newVelocity.Y = -newVelocity.Y;
      }
      else if (newPosition.Y <= 0)
      {
        newPosition.Y = 0;
        newVelocity.Y = -newVelocity.Y;
      }

      // Update the ball's velocity and position.
      ball.Velocity = newVelocity;
      ball.Position = newPosition;

      // Apply the new position to the ball's render transform.
      TranslateTransform transform = ball.RenderTransform as TranslateTransform;
      transform.X = ball.Position.X;
      transform.Y = ball.Position.Y;
    }
  }
}

This is the meat of the application. The first thing we do is lock the collection of balls so new ones can't be added while we're iterating it. We then update the ball's position by adding it's velocity. Next we need to check that the ball's new position isn't outside of the container. If the position will be outside of the bounds, we simply set it equal to the bounds and reverse it's velocity. This will make the ball bounce around the screen. All that's left to do is modify the ball's render transform to the new position.

I mentioned the DispatcherTimer a little earlier. Here's the constructor of my class where the timer, among other things, is initialized.

public MainPage()
{
  InitializeComponent();

  _random = new Random();

  // Create a timer to update the display every 10 milliseconds.
  _timer = new DispatcherTimer();
  _timer.Interval = new TimeSpan(0, 0, 0, 0, 10);
  _timer.Tick += UpdateBallPositions;
  _timer.Start();

  // Initialize the display with 5 balls.
  for (int i = 0; i < 5; i++)
  {
    AddBall();
  }
}

I first create the Random object that is used to set the ball's velocity. Next I create the timer. I used a DispatcherTimer because it will raise the Tick event on the same thread that was used to create the timer. That way I don't have to call Invoke when I want to update the user interface. The last thing I do is create a few balls so there's something to look at when the app is loaded.

And that's it! We've created an app that animates user controls by periodically updating their RenderTransform properties. If I ever figure it out, I'll create another tutorial explaining how to hardware accelerate the animation. If you've got any questions or comments, please leave them below.

Anonymous
08/20/2009 - 17:08

Thanks. If the AddBall() and UpdateBallPositions() methods are always executed on the same thread, is the lock necessary?

reply

Add Comment

Put code snippets inside language tags:
[language] [/language]

Examples:
[javascript] [/javascript]
[actionscript] [/actionscript]
[csharp] [/csharp]

See here for supported languages.

Javascript must be enabled to submit anonymous comments - or you can login.

Sponsors