Many of us build Line of Business (LOB) applications that our users sit in front of for upwards of five hours a day. It is the duty of the UI developer to make that experience as pleasant as possible. A happy user is a more productive one. Whilst animations are not going to transform a badly designed UI into a usable one, they can at least give it a less harsh, more organic feel. This can be achieved by things such as softening state transitions or giving users instant visual feedback.
In the real world things change organically over a period of time, whereas navigation and transitions in traditional UIs are immediate. Not only does this feel unnatural, it can also give the user no clue to the path the UI has taken to get to its current state. By animating these transitions, the extra information given to the user can be used to better inform future navigations, or to provide visual clues as to how to reverse their actions to get back to where they came from.
Unfortunately for many of us, animations have us reaching for the “Skip Intro” link and are all too often over the top. However, they do not need to be OTT to be bad; studies show that our ability to concentrate on something directly in front of us is greatly affected by things moving in our peripheral vision. Having secondary parts of your UI moving whilst the user is trying to focus on another task can be significantly off-putting.
Animations in WPF can be very subtle and discrete, and with such a massive number of animation-friendly properties available, things that you would have never considered animating before can now be animated easily.
Animation before WPF
Some of us have tried animating our Windows Forms UIs, and frankly, it hurts. Take the simplest animation: a MouseOver effect on an Image that “raises” up the image from the UI layout. The three main issues we have with trying to animate the location of the image are:- No simple way to control the properties over a period of time
- Having to update each property in our own code
- Blunt control over position information
Point 2 makes for code that can quickly get complex once we start animating multiple UI elements. We also have to manage the “to” and “from” boundaries, and handle when to stop the “animation”. WPF’s animation classes work alongside the dependency property (DP) infrastructure to mange the updating of properties.
Point 3 refers to Windows Forms using integers to control the Top and Left location values. Top and Left (like many other properties) are Doubles in WPF. A double offers finer-grained control, allowing the property changes to be smoother.
Animation infrastructure
Animation in WPF is property based, by which I mean that the ultimate target of an animation must be a property. The pre-requisites for an animation are: the target property must be a DependencyProperty; the object that the property belongs to must be or inherit from FrameworkElement; and there must be a supporting animation type.This is not as restrictive as it might sound, as all the WPF controls are FrameworkElements and in practice most properties are DPs. The need for a supporting animation type is because the type needs to know how to animate the Type of your target property. For example, the DoubleAnimation type would be used to animate the Y position of a button, but to animate the colour of the Button from blue to yellow you would use a ColorAnimation type.
ColorAnimations are a great demonstrator of what animations give you over directly changing the value in one hit. The transition from blue through purple, then orange and finally to yellow so clearly shows how the animation infrastructure updates the properties over time.
Although you can create your own animation types, there are plenty available out-of-the-box. They follow the simple naming of TypeAnimation, e.g. the animation for a Boolean is BooleanAnimation.
PropertyPath
As mentioned previously, the target object must be a FrameworkElement, so how do you animate the Fill Color of a Rectangle? The Color property of the Fill’s brush fulfils the DependencyProperty requirement, but Brush is not a FrameworkElement – only the Rectangle itself is. You need to be able to target the Rectangle and then drill down to the Color property of the Brush. It is the PropertyPath class that provides this drilldown functionality.Using PropertyPath in XAML:
<ColorAnimation Storyboard.TargetName= ”secondRectangle” Storyboard.TargetProperty= “(Ellipse.Fill).( SolidColorBrush.Color)” From=”Blue” To=”Yellow”/>Using ProperyPath in code behind:
PropertyPath ellipseFillColorProperty = new PropertyPath(“(0).(1)”, Ellipse.FillProperty, SolidColorBrush.ColorProperty);When targetting a specific child , such as the ScaleX property of a ScaleTransform in a TransformGroup, you use the [] index syntax.
Indexing children with PropertyPath:
PropertyPath scaleXProperty = new PropertyPath(“(0).(1)[0].(2)”, Grid.RenderTransformProperty, TransformGroup.ChildrenProperty, ScaleTransform.ScaleXProperty);
Animation’s long-lasting effect
After an animation has run to the end of its duration it enters “Fill Time”. What happens at this time depends on what you set as the FillBehavior for your animation:- FillBehavior.Stop sets the property back to its original value
- FillBehavior.HoldEnd will mean the property retaining the value as it was at the end of the animation
The code below is fired by the Completed event of the stretch animation.
private void stretch_Completed(
object sender, EventArgs e)
{
Console.WriteLine(ourRect.Height);
ourRect.Height = 25;
Console.WriteLine(ourRect.Height);
}
I was expecting to see 75 then 25 written to the Output window, whilst debugging. The actual result is 75 both times.
After some investigation I discovered this behaviour is by design, and is down to two things:
- The animation is still attached to the property after it has run for its duration and fired its Completed event. Therefore it still has a say in what the value of the Rectangle’s Height is. Animations “hanging around” are what the WPF team wanted, and it is in keeping with their opinion that “once you go animation, you never go back”. What they mean by this is that if you animate a property once you are more than likely going to want to employ animations in all future updates as well. Whilst this confused me at first I am totally in agreement with this philosophy now.
- A Rectangle’s Height property, like most properties in WPF, is no simple instance variable within Rectangle, it is a DependencyProperty. Although I can “Set” Height to 25 the “Get” logic has other factors to take into consideration, such as styling and animations, before it can return the value. The final value of the attached animation, 75, takes precedence over the local value, 25. This logic is encapsulated within the DependencyProperty infrastructure.
A simple animation example
Having looked at some of the animation infrastructure, let’s take a simple animation and look at the basic properties. This animation simply stretches a Rectangle; I have created it all in XAML including the MouseClick so that it can be easily dropped into XAMLPad and played with.<Grid Background=”Blue”
xmlns=”http://schemas.microsoft.com/
winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/
winfx/2006/xaml” Height=”300”
Width=”300”>
<Grid.Resources>
<Storyboard x:Key=”stretch”
RepeatBehavior=”Forever”>
<DoubleAnimation
Duration=”0:0:1”
From=”75” To=”100”
AccelerationRatio=”0.1”
DecelerationRatio=”0.9”
AutoReverse=”True”
Storyboard.TargetName=”ourRect”
Storyboard.TargetProperty=”Height”>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
<Button HorizontalAlignment=”Left”
VerticalAlignment=”Top”>
Animate
<Button.Triggers>
<EventTrigger
RoutedEvent=”Button.Click”>
<EventTrigger.Actions>
<BeginStoryboard
Storyboard=
“{StaticResource stretch}”>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Button.Triggers>
</Button>
<Rectangle Name=”ourRect”
Height=”75” Width=”75” Fill=”Red”>
</Rectangle>
</Grid>
Now let’s walk through the key parts of the animation:
- Height is a double so I need to use the DoubleAnimation type
- Duration is set to 1 second, but if you time it the complete animation, from the initial value of 75, up to 100 and then back down to 75 takes 2 seconds. This is the effect of the setting AutoReverse to True.
- From and To are set to explicit values, but I normally leave the From value unspecified and let the animation deal with it. As well as not having to keep track of the current Height of your Rectangle yourself, not setting an explicit From has the side benefit of smoother transitions. Say for instance the height was already being animated. If I was explicitly setting the From value, my new animation would set the value back down to 75, and you might see the height jump rather than make a smooth transition.
- AccelerationRatio and DecelerationRatio control the percent of the duration that the animation takes getting up to full speed and the percent of time it takes slowing down. The effect of these does not show up that well in this animation but play about with the two values to see how they can affect the feel of the animation. When added together the two values must total 1.0.
Co-ordinating animations
In the “Simple animation” example we only had one animation. However, once you start animating more than one property you are going to want to organise them into groups with a common clock.The two most often used classes to group animations are ParallelTimeline and StoryBoard, with Storyboard being a superset of ParallelTimeline.
All the TypeAnimation types such as DoubleAnimation, ColorAnimation and BooleanAnimation inherit from Timeline, as does ParallelTimeline and consequently Storyboard. This commonality make organisation across a StoryBoard much simpler with properties like BeginTime, Acceleration\Deceleration and SpeedRatio affecting the contained animations. For example, any explicit begin times set by contained animations are all relative to the BeginTime of the containing Timeline be that a Storyboard or ParallelTimeline.
Defining animations
As so often these days there is more than one way to do the same thing, and the same goes for defining animations. In XAML, animations are defined within Storyboard elements, and these are contained within Resources elements such as Window.Resources.If the Storyboard is used across the UI, then the best way to share these is with merged resource dictionaries:
<ResourceDictionary xmlns=”http://schemas.microsoft.com/ winfx/2006/xaml/presentation” xmlns:x=”http://schemas.microsoft.com/ winfx/2006/xaml”> <Storyboard x:Key=”stretchBarUp”> <DoubleAnimation Storyboard.TargetName=”playerControls” Storyboard.TargetProperty=”Height” To=”30” Duration=”0:0:0.5”/> <DoubleAnimation BeginTime=”0:0:0.6” Storyboard.TargetName=”playerControls” Storyboard.TargetProperty=”Height” To=”25” Duration=”0:0:0.5”/> </Storyboard> </ResourceDictionary>The way to control Storyboards from XAML is within the Actions element of a Trigger. The storyboard actions available are begin, pause, remove, resume, seek, set SpeedRatio, skip to fill and stop. The “Simple Animation” example used triggers to begin the “stretch” animation. See the EventTrigger associated with the Button.Click RoutedEvent.
Defining in Code
Creating animations in code allows better runtime control of the animation, for instance moving UI elements to new positions in response to the UI being resized or repositioned. If you’re working with a single animation then you can just create an instance of the relevant animation type and attach it to the target. If however you need the power of a Storyboard, you create an instance of it in code and add your animations to the Storyboard’s children. The two attributes Storyboard.TargetName and Storyboard.TargetProperty are not available as properties of your code-based animations and properties. Instead they are static methods off of the Storyboard class.Where you would have done the following in XAML:
<DoubleAnimation Storyboard.TargetName=”mainPane” Storyboard.TargetProperty=”Opacity” ... />You would do this in code-behind:
DoubleAnimation windUpOpacityAnimation = new DoubleAnimation(); ... PropertyPath opacityP = new PropertyPath(Border.OpacityProperty); Storyboard.SetTargetProperty( windUpOpacityAnimation, opacityP); Storyboard.SetTargetName( windUpOpacityAnimation, mainPane.Name);When I first started using animations I found it easier to build them in code. However, I would encourage you to construct as much as possible of your animation in XAML. It reads much better, with the hierarchical nature of XML better able to show the structure of an animation than C# or VB.NET.
Kicking animations off with an EventTrigger defined in XAML cannot give you the level of control you get with code-behind. There will be times when there just isn’t a suitable event available to trigger your animation. If your animation is defined in XAML, you can still “begin” it in code. Assuming it was defined in a Resources element such as Window.Resources or in a ResourceDictionary, you can pull it out like this:
Storyboard liftStoryboard = (Storyboard)this.FindResource(“lift”);Single animations, targeting a single property, can be begun using the target control’s BeginAnimation method:
mainPane.BeginAnimation( Border.OpacityProperty, windUpOpacityAnimation);The more complex Storyboard will have had its targeting information setup using SetTargetName and SetTargetObject as mentioned earlier. They can then be begun by calling the Begin method on the storyboard.
A complex animation example
Now we will use all the things I have discussed so far in a final example. The XAML for this example is too big to be inlined into the document, so I will just pick out key parts. The complete XAML file is downloadable.Again I am using EventTriggers to begin the animations.
<Border.Triggers>
<EventTrigger
RoutedEvent=”Mouse.MouseEnter”>
<EventTrigger.Actions>
<BeginStoryboard
Storyboard=”{StaticResource lift}”/>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger
RoutedEvent=”Mouse.MouseLeave”>
<EventTrigger.Actions>
<BeginStoryboard
Storyboard=”{StaticResource drop}”/>
</EventTrigger.Actions>
</EventTrigger>
</Border.Triggers>
To get the effect of the image rising up from the page I have a Border around an Image and a Black Rectangle under the image. As the mouse is passed over the Image I scale the Image up from its bottom right-hand corner; scale the Rectangle up from its upper left-hand corner; and fade the Rectangle’s opacity.
This hopefully gives the effect of the image rising up, and a shadow fading (as a shadow does when the object casting the shadow gets further away). To give a sense of perspective I also show a border where the image was. This StoryBoard is defined in Grid.Resources and has a resource key of “lift”. One new technique in it is the animation for the blue border; it’s done by updating the border’s BorderThickness. With this property being of type thickness I use a ThicknessAnimation. Remember the naming convention of TypeAnimation for animation classes.
<ThicknessAnimation Storyboard.TargetName=”outerBorder” Storyboard.TargetProperty= ”BorderThickness” To=”3.0”/>The animation that drops the image back down is kept within the StoryBoard keyed “drop”. To get the effect of the image being dropped back into place, with a small bounce, I have three child StoryBoards. The first brings everything back to the starting position, the second lifts the image back up slightly and the third brings everything back down again. The Storyboard and Timeline structure is as follows:
<Storyboard x:Key=”drop” AccelerationRatio=”0.9” DecelerationRatio=”0.1”> <ThicknessAnimation Storyboard.TargetName=”outerBorder” Storyboard.TargetProperty= ”BorderThickness” To=”0”/> <!-- Bounce Part 1--> <Storyboard> <ParallelTimeline Storyboard.TargetName= ”dropShadowRectangle”> <!-- Animations to bring the “shadow” back to starting position--> </ParallelTimeline> <ParallelTimeline Storyboard.TargetName= ”containingGrid”> <!-- Animations to bring the image back to starting position--> </ParallelTimeline> </Storyboard> <!-- Bounce Part 2--> <Storyboard BeginTime=”0:0:0.3”> <ParallelTimeline Storyboard.TargetName= ”dropShadowRectangle”> <!-- Animations to extend the “shadow” back out a little--> </ParallelTimeline> <ParallelTimeline Storyboard.TargetName= ”containingGrid”> <!-- Animations to lidt the image up a little--> </ParallelTimeline> </Storyboard> <!-- Bounce Part 3 --> <Storyboard BeginTime=”0:0:0.5”> <ParallelTimeline Storyboard.TargetName= ”dropShadowRectangle”> <!-- Animations to finally bring the “shadow” back to starting position--> </ParallelTimeline> <ParallelTimeline Storyboard.TargetName= ”containingGrid”> <!-- Animations to finally bring the image back to starting position--> </ParallelTimeline> </Storyboard> </Storyboard>Hopefully this has given you a feel for what you can do with animations. I encourage you take the example XAMl and play with it in XAMLPad.
Pat Long is a Senior Technologist with Charteris PLC, a Microsoft Gold Partner in the UK, where he specialises in User Interface design and development. He can be contacted at patrick.long@charteris.com.
Comments