Animated WPF Panels
Download CS solution - 4.56 MB
Introduction
This article discusses how to apply custom animation to an already exisiting WPF layout by extending classes such as Grid
,
StackPanel
, DockPanel
or WrapPanel
. Or indeed any other WPF control that hosts other UIElement
s.
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 rougly equivalent implementations.
Sample video

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

Background
I find that there are two easy ways of making a UI more visually appealing;
- 1. Animate stuff
- 2. Do stuff in 3D
The animation support in WPF is quite extensive, but for this article I've decided to do custom animation rather than relying on
DoubleAnimation
s and StoryBoard
s.
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)
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:
By running that calculation several times and every time update 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 needs 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:
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).
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 slighly 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:
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 callingIArrangeAnimator.Arrange
- 3. Update the current position with the returned
Rect
- 4. Call
UIElement.Arrange
with the returnedRect
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:
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 it's arrangement of child controls and it is at this point it is suitable to hook in the animation logic.
Extending existing panels
Because almost all 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
:
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
发表评论
SJmf3T Hey, thanks for the blog post.Much thanks again. Really Great.
1qmQjs Thanks so much for the blog article.Thanks Again. Awesome.
CcRdOh Appreciate you sharing, great article.Really looking forward to read more. Cool.
The anatomy sample are Assisted semen scar by an defects. Suffocating the person it with counter.