Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / WPF

Animated WPF Panels

Rate me:
Please Sign up or sign in to vote.
4.96/5 (43 votes)
14 Feb 2011CPOL6 min read 140K   8.3K   67   45
Adding animation support to existing WPF Panels

Introduction

This article discusses how to apply custom animation to an already existing WPF layout by extending classes such as Grid, StackPanel, DockPanel or WrapPanel. Or indeed any other WPF control that hosts other UIElements. An important part of this implementation is that is should be able to extend any existing panel and cope with any child controls of that panel.

By animate in this context, I'm referring to the process of having all the components of a panel move to their positions and change their size over a time.
This is a VB.NET article so all the snippets will be in VB.NET, but for completeness I've included both a C# and a VB.NET solution with roughly equivalent implementations.

Sample Video

movie.png

I've posted a YouTube video of this project, check it out, the wrapping text thingy looks really cool.

screenshot.png

Background

I find that there are two easy ways of making a UI more visually appealing:

  1. Animate stuff
  2. Do stuff in 3D

Note that I'm not saying that the UI will have a higher level of usability by doing this, but to me it just looks neater if things are sliding in, flipping over or zooming out. Things look really cool when you manage to combine both points and animate your 3D stuff as I did here.
The animation support in WPF is quite extensive, but for this article I've decided to do custom animation rather than relying on DoubleAnimations and StoryBoards. The reason for this is I wanted a way to create animated versions of existing panels with as little code as possible.

Using the Code

Pick the language of your choice, download the solution, unzip and open. Each solution has a class library and a WPF test app.
It's all been written using VS2010 Express Edition.

The Approach

So how does one go about adding animation to an existing panel?
Well, the way I approached it was to consider two positions on screen (actually it's three positions and three sizes, but for now I'll stick to only discuss position, and only two of those):

  1. The current position (or Cp) of the UIElement
  2. The position as suggested by whatever panel is being extended (desired position, or Dp)

If these two positions can be established, then the job of animating the UIElement towards its desired position is simply done by calculating a vector from the current position and then pick another position along that vector.
In maths, the operation of calculating this new position (or Np) is expressed as:

Np = (Dp - Cp) * AnimationSpeed * ElapsedTimeSinceLastFrame 

It looks something like this:

positions.png

By running that calculation several times and every time updating the current position with the just calculated new position, the UIElement will animate. In my case, I went for a panel local timer, a DispatcherTimer, and recalculate the Np on every tick.

The additional position I mentioned is an override position that can be used if the position suggested by the extended panel should be ignored for some reason. This could be if the controls should animate of the screen or something like that.
In addition to position, WPF panels also set the size of the child elements, so a current, desired and override version of the sizes also need to be animated.

Attaching Some Properties

So, the math behind the animation is simple enough, but since this implementation has to work with controls that have no knowledge of any desired positions or override positions but only of their actual current position, there needs to be a way of storing these values for each child control of the panel.

To solve this, I decided to create attached properties for all the properties that my approach required, but were not already part of UIElement. In a class called AnimationBase, I declare all the attached properties required:

VB.NET
Public Shared ReadOnly CurrentPositionProperty As DependencyProperty = _
    DependencyProperty.RegisterAttached("CurrentPosition", _
    GetType(Point), _
    GetType(AnimationBase), _
    New PropertyMetadata(New Point()))

Public Shared ReadOnly CurrentSizeProperty As DependencyProperty = _
    DependencyProperty.RegisterAttached("CurrentSize", _
    GetType(Size), _
    GetType(AnimationBase), _
    New PropertyMetadata(New Size()))

Public Shared ReadOnly OverrideArrangeProperty As DependencyProperty = _
    DependencyProperty.RegisterAttached("OverrideArrange", _
    GetType(Boolean), _
    GetType(AnimationBase), _
    New PropertyMetadata(False))

Public Shared ReadOnly OverridePositionProperty As DependencyProperty = _
    DependencyProperty.RegisterAttached("OverridePosition", _
    GetType(Point), _
    GetType(AnimationBase), _
    New PropertyMetadata(New Point()))

Public Shared ReadOnly OverrideSizeProperty As DependencyProperty = _
    DependencyProperty.RegisterAttached("OverrideSize", _
    GetType(Size), _
    GetType(AnimationBase), _
    New PropertyMetadata(New Size()))

The OverrideArrangeProperty is there to dictate if the animation should strive to go to the desired position or the override position.
Using these properties, the class responsible for the animation can keep track of where the control is now, and where it should be, but not necessarily how to get there. In order to find that out, I went for an implementation where the way that the distance from Cp to Dp is traversed can be swapped out for different implementations.

Replacing the Animation Calculations

In order to be able to replace the logic that calculates how much of the distance between Cp and Dp needs to be traversed in this frame, the AnimationBase class relies on an interface called IArrangeAnimator (I picked the name Arrange because the implementation of this project relies on values from the UIElelemt.Arrange method).

VB.NET
Public Interface IArrangeAnimator

    Function Arrange(ByVal elapsedTime As Double, _
                     ByVal desiredPosition As Point, _
                     ByVal desiredSize As Size, _
                     ByVal currentPosition As Point, _
                     ByVal currentSize As Size) As Rect

End Interface  

Essentially, this interface takes a desired position and size along with a current position and size and returns a Rect indicating where the UIElement should be after elapsedTime. In the sample solution, I've only included a single implementation of this interface but it's easy to add your own should you require it.

The IArrangeAnimator implementation included is called FractionDistanceAnimator, because it animates with a speed that is set to x pixels per second where x is a fraction of the remaining distance. This means that if the FractionDistanceAnimator is initialized with a fraction value of 0.5 and the distance from Cp to Dp is 100 pixels, the speed it'll move with is 50 pixels per second. Obviously, at the next update the distance will be slightly shorter so the next update will run at a slightly lower speed causing the control to ease in to its position.
The implementation of the FractionDistanceAnimator looks like this:

VB.NET
Namespace Animators
    Public Class FractionDistanceAnimator
      Implements IArrangeAnimator

        Private fraction As Double

        Public Sub New(ByVal fraction As Double)
            Me.fraction = fraction
        End Sub

        Public Function Arrange(ByVal elapsedTime As Double, _
                                ByVal desiredPosition As Point, _
                                ByVal desiredSize As Size, _
                                ByVal currentPosition As Point, _
                                ByVal currentSize As Size) As Rect _
                              Implements IArrangeAnimator.Arrange

            Dim deltaX As Double = _
                (desiredPosition.X - currentPosition.X) * fraction
            Dim deltaY As Double = _
                (desiredPosition.Y - currentPosition.Y) * fraction
            Dim deltaW As Double = _
                (desiredSize.Width - currentSize.Width) * fraction
            Dim deltaH As Double = _
                (desiredSize.Height - currentSize.Height) * fraction

            Return New Rect(currentPosition.X + deltaX, _
                currentPosition.Y + deltaY, _
                currentSize.Width + deltaW, _
                currentSize.Height + deltaH)
        End Function

    End Class
End Namespace

I Like to Move it, Move it

To calculate the Rect returned by IArrangeAnimator.Arrange, the current and desired position have to be passed in (obviously). This is all handled by the method AnimatorBase.Arrange which for each child control in the panel performs four steps:

  1. Get the current position and desired position using the attached properties discussed earlier
  2. Calculate the Rect by calling IArrangeAnimator.Arrange
  3. Update the current position with the returned Rect
  4. Call UIElement.Arrange with the returned Rect

There's (to me) surprisingly little code required for all this:

VB.NET
Public Sub Arrange(ByVal elapsedTime As Double, _
                   ByVal elements As UIElementCollection,
                   ByVal animator As IArrangeAnimator)

    For Each element As UIElement In elements
        Dim desiredPosition As Point
        Dim currentPosition As Point = _
            element.GetValue(AnimationBase.CurrentPositionProperty)
        Dim desiredSize As Size
        Dim currentSize As Size = _
            element.GetValue(AnimationBase.CurrentSizeProperty)
        Dim override As Boolean = _
            DirectCast(element.GetValue(AnimationBase.OverrideArrangeProperty), Boolean)

        If override Then
            desiredPosition = _
              DirectCast(element.GetValue(AnimationBase.OverridePositionProperty), Point)
            desiredSize = _
              DirectCast(element.GetValue(AnimationBase.OverrideSizeProperty), Size)
        Else
            desiredPosition = element.TranslatePoint(New Point(), owner)
            desiredSize = element.RenderSize
        End If

        Dim rect As Rect = _
            animator.Arrange(elapsedTime, desiredPosition, _
		desiredSize, currentPosition, currentSize)

        element.SetValue(AnimationBase.CurrentPositionProperty, rect.TopLeft)
        element.SetValue(AnimationBase.CurrentSizeProperty, rect.Size)

        element.Arrange(rect)
    Next

End Sub

Calling that method on a timer is essentially all that's required to animate any existing panel. And since a timer is always required, the AnimationBase class provides a helper method for creating it:

VB.NET
Public Function CreateAnimationTimer(ByVal owner As UIElement, _
                                     ByVal animationInterval As TimeSpan)
    Me.owner = owner
    animationTimer = New DispatcherTimer(DispatcherPriority.Render, _
        owner.Dispatcher)
    animationTimer.Interval = animationInterval
    
    Return animationTimer
End Function

Private Sub AnimationTick(ByVal sender As Object, ByVal e As EventArgs) _
      Handles animationTimer.Tick
    owner.InvalidateArrange()
End Sub

Note that the tick handler does not call the animation method directly, but instead just invalidates the current arrange of the animated panel. This in turn causes the panel to recalculate its arrangement of child controls and it is at this point that it is suitable to hook in the animation logic.

Extending Existing Panels

Because almost all the work is done in AnimationBase and the IArrangeAnimator, there's very little to do in the extended classes. The little code that is required is always the same for every panel and it looks like this for the Grid:

VB.NET
Public Class AnimatedGrid
      Inherits Grid

    Private animationBase As AnimationBase = New AnimationBase()
    Private animator As IArrangeAnimator
    Private lastArrange As DateTime

    Public Sub New()
        animationBase.CreateAnimationTimer(Me, TimeSpan.FromSeconds(0.05))
        animator = New FractionDistanceAnimator(0.1)
    End Sub

    Public Sub New(ByVal animator As IArrangeAnimator, _
			ByVal animationInterval As TimeSpan)
        animationBase.CreateAnimationTimer(Me, animationInterval)
        Me.animator = animator
    End Sub

    Protected Overrides Function ArrangeOverride(ByVal arrangeSize As Size) As Size
        Dim size As Size = MyBase.ArrangeOverride(arrangeSize)

        animationBase.Arrange(Math.Max(0, _
            (DateTime.Now - lastArrange).TotalSeconds), _
        Children, animator)
            lastArrange = DateTime.Now

        Return size
    End Function

End Class  

The implementation of animated version of other panels are identical except for the class name and the Inherits statement. This makes it really easy to add animation support.

Points of Interest

If you haven't already, have a look at the video at the top, I think it neatly illustrates how cool a standard WrapPanel or Grid can be made to look with just a bit of animation.

History

  • 2011-02-03: First version

License

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


Written By
Software Developer (Senior)
Sweden Sweden
Article videos
Oakmead Apps Android Games

21 Feb 2014: Best VB.NET Article of January 2014 - Second Prize
18 Oct 2013: Best VB.NET article of September 2013
23 Jun 2012: Best C++ article of May 2012
20 Apr 2012: Best VB.NET article of March 2012
22 Feb 2010: Best overall article of January 2010
22 Feb 2010: Best C# article of January 2010

Comments and Discussions

 
QuestionVery useful Pin
Yuriy Zanichkovskyy27-Nov-12 3:42
Yuriy Zanichkovskyy27-Nov-12 3:42 

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.