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.
Grid
StackPanel
DockPanel
WrapPanel
UIElement
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.
I've posted a YouTube video of this project, check it out, the wrapping text thingy looks really cool.
I find that there are two easy ways of making a UI more visually appealing:
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.
DoubleAnimation
StoryBoard
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.
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):
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:
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.
DispatcherTimer
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.
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:
AnimationBase
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.
OverrideArrangeProperty
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).
IArrangeAnimator
UIElelemt.Arrange
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.
Rect
elapsedTime
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:
FractionDistanceAnimator
fraction
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
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:
IArrangeAnimator.Arrange
AnimatorBase.Arrange
UIElement.Arrange
There's (to me) surprisingly little code required for all this:
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 its arrangement of child controls and it is at this point that it is suitable to hook in the animation logic.
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:
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.
Inherits
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.