Silverlight 2 Samples: Animating Controls

UPDATE: Get the latest animating control code from Blacklight, our new CodePlex project!  

Whilst developing the MSCUI Patient Journey Demonstrator, we made use of animation in a number of places to show how the user interface is moving and adapting to the user tasks. Animation is a great tool for showing users how the application changes, allowing them to watch how the content changes size and position, without it ‘clunking’ into place and the user losing sight of everything.

I wanted to create an animation system that allowed us to consistently change a controls size and position, with animation, that was very easy and natural to work with in code. Something I wanted to prevent was the need to duplicate storyboard XAML for each and every control that we wanted to animate.

The approach I decided to take was to create a new class that extended Control- called AnimatedControl. The new class would ultimately add 2 public methods to Control:

· public void AnimateSize(double width, double height)

· public void AnimatePosition(double x, double y)

...as well as some additional properties that would allow me to change the duration of the animations.

AnimatedControl works by creating a Canvas in the AnimatedControl constructor which contains a Rectangle and 2 storyboards. The Canvas is created using a XAML string and XamlReader.Load(...). The XAML string used is shown below...

<!--

        Canvas containing rectangle for control animation

      -->

      <Canvas xmlns="https://schemas.microsoft.com/client/2007"

                xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">

        <Canvas.Resources>

         

          <!-- Size animation storyboard -->

          <Storyboard x:Key="sizeStoryboard" BeginTime="00:00:00">

            <!-- Animates the rectangles width -->

            <DoubleAnimationUsingKeyFrames

              Storyboard.TargetName="animatedElement"

              Storyboard.TargetProperty="(FrameworkElement.Width)">

              <SplineDoubleKeyFrame Value="0"

              KeyTime="00:00:0.5"

                                    KeySpline="0.528,0,0.142,0.847" />

           

            </DoubleAnimationUsingKeyFrames>

           

            <!-- Animates the rectangles height -->

            <DoubleAnimationUsingKeyFrames

              Storyboard.TargetName="animatedElement"

              Storyboard.TargetProperty="(FrameworkElement.Height)">

              <SplineDoubleKeyFrame Value="0"

                          KeyTime="00:00:0.5"

                                    KeySpline="0.528,0,0.142,0.847" />

            </DoubleAnimationUsingKeyFrames>

          </Storyboard>

         

          <!-- Position animation storyboard -->

          <Storyboard x:Key="positionStoryboard" BeginTime="00:00:00">

            <!-- Animates the rectangles X position in a Canvas -->

            <DoubleAnimationUsingKeyFrames

              Storyboard.TargetName="animatedElement"

            Storyboard.TargetProperty="(Canvas.Left)">

              <SplineDoubleKeyFrame Value="0"

                                    KeyTime="00:00:0.5"

                                    KeySpline="0.528,0,0.142,0.847" />

            </DoubleAnimationUsingKeyFrames>

            <!-- Animates the rectangles Y position in a Canvas -->

            <DoubleAnimationUsingKeyFrames

              Storyboard.TargetName="animatedElement"

              Storyboard.TargetProperty="(Canvas.Top)">

              <SplineDoubleKeyFrame Value="0"

                                    KeyTime="00:00:0.5"

                                    KeySpline="0.528,0,0.142,0.847" />

            </DoubleAnimationUsingKeyFrames>

          </Storyboard>

        </Canvas.Resources>

        <!-- Hidden rectangle -->

        <Rectangle x:Name="animatedElement"

                   Height="0" Width="0"

                   Canvas.Top="0" Canvas.Left="0" />

      </Canvas>

Once the Canvas has been created, the important elements are fished out and stored in private members. The code below shows creating the canvas from the XAML string below, storing a reference to the storyboards and finally hooking up their completed events...

// Create a canvas from the XAML

Canvas animatedElement = XamlReader.Load(animatedElementXaml) as Canvas;

// Pull out the rectangle into a memeber

this.animatedElement = animatedElement.Children[0] as Rectangle;

// Pull out the storyboards into a member and hook up completed events

this.sizeAnimation = animatedElement.Resources["sizeStoryboard"]

as Storyboard;

this.sizeAnimation.Completed += new EventHandler(sizeAnimation_Completed);

               

this.positionAnimation = animatedElement.Resources["positionStoryboard"]

                    as Storyboard;

this.positionAnimation.Completed += new EventHandler(positionAnimation_Completed);

When AnimateSize or AnimatePosition is called on the control, the corresponding storyboard is kicked off in the Canvas, changing the properties of the rectangle. At the same time, a DispatcherTimer is started that ticks ‘as quickly as possible’ to update the controls size and position to match that of the rectangles, as shown below...

void timer_Tick(object sender, EventArgs e)

{

// If the size is animating, update the controls size

if (this.sizeAnimating)

{

this.Width = this.animatedElement.Width;

this.Height = this.animatedElement.Height;

}

// If the position is animating, update the control position

if (this.positionAnimating)

{

Canvas.SetLeft(this, Canvas.GetLeft(this.animatedElement));

Canvas.SetTop(this, Canvas.GetTop(this.animatedElement));

}

}

Now with each tick of the timer, the controls size and position will animate. When the size and position animations have both completed, the timer will stop ticking.

There are some requirements and limitations with my AnimatedControl that I have listed here, but I have suggested some additions too...

· The Position animation only changes the Canvas.Left and Canvas.Top properties. So, if you control is not in a Canvas (or a panel that derives Canvas), it won’t move. AnimatedControl could indeed be extended however, to animate the controls render transform to move it if it is not in a Canvas...

· AnimatedControl currently extends Control, so if you wanted an AnimatedControl that extends ContentControl, or UserControl, you would need to copy the code over to a new class - updating then becomes a pain.

· AnimatedControl only supports one KeyFrame at the moment. Again, this could be updated by adding and removing key frames in the AnimateSize / AnimatePosition methods.

· You can of course extend AnimatedControl to animate more properties such as Opacity, ScaleTransform, Background color etc.

I have attached a project here that very simply, shows AnimatedControl working on some basic controls (just rectangles really!). You can also see the example project running here.

Please enjoy and feedback any additions, bugs etc.

Thanks,

Martin

AnimatedControlSample.zip