In my last post on WPF Animations, I covered the basics of defining and using an animation purely in the XAML markup. Today I’ll be going through the process or and reasons for defining animations in markup, but firing them from code.
I’ll be looking at the FadeIn/FadeOut and ExpandOut/ShrinkDown animation pairs defined in the JewelCaseExpand.xaml markup. This control is what is displayed when you mouse over a jewel case on the shelf. I wanted to give the user the rough illusion of the jewel case being pull down off of the shelf, and being turned so they can see the full album jacket. In version 0.1 I do this with something of a cheat. Instead of rotating the object so you start by looking at the spine, and end with looking at the front panel, I simply start the jewel case off squashed and then expand it to full size.
Like our animations before, these are defined in the JewelCaseExpand UserControl.Resources tag:
<Storyboard x:Key=”FadeIn”>
<DoubleAnimationUsingKeyFrames BeginTime=”00:00:00″ Storyboard.TargetName=”MainImage” Storyboard.TargetProperty=”(FrameworkElement.Opacity)”>
<SplineDoubleKeyFrame KeyTime=”00:00:00.4000000″ Value=”1″/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key=”FadeOut”>
<DoubleAnimationUsingKeyFrames BeginTime=”00:00:00″ Storyboard.TargetName=”MainImage” Storyboard.TargetProperty=”(FrameworkElement.Opacity)”>
<SplineDoubleKeyFrame KeyTime=”00:00:00.2000000″ Value=”0″/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key=”ExpandOut”>
<DoubleAnimationUsingKeyFrames BeginTime=”00:00:00″ Storyboard.TargetName=”MainImage” Storyboard.TargetProperty=”(FrameworkElement.Width)”>
<SplineDoubleKeyFrame KeyTime=”00:00:00.2000000″ Value=”75″/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key=”ShrinkDown”>
<DoubleAnimationUsingKeyFrames BeginTime=”00:00:00″ Storyboard.TargetName=”MainImage” Storyboard.TargetProperty=”(FrameworkElement.Width)”>
<SplineDoubleKeyFrame KeyTime=”00:00:00.2000000″ Value=”15″/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
As the names suggest, these are used to fade the control in and out, and expand/shrink it.
The trigger for these animations is not something that can be defined on the JewelCaseExpand control itself because the user will be mousing over a completely separate control (a JewelCase on the Shelf) to activate them.
I also can’t define a trigger in the JewelCase XAML to fire these, again because they aren’t in the same control. Even if I could cross-reference control resources like this, I wouldn’t want to because that would couple these two user controls together too tightly.
As a result, what I’m left with is having the Main form detect when the JewelCaseExpand control should be made visible, and firing its animations programmatically.
First, let’s detect when the JewelCaseExpand control should be made visible – when the user mouses over a JewelCase object on the Shelf. While it’s important to determined WHEN the user mouses over a JewelCase, it’s equally important to know WHICH JewelCase they touched.
Each JewelCase object sits on a Shelf object (a Shelf can hold one or more JewelCases). In the 0.1 release of the Media Player, there is only Shelf shown at a time on the Main form (eventually the application will allow for one or more). What this leads to the following hierarchy: Main –> Shelf –> JewelCase
I want each object to respect the boundaries of the other objects up and down the hierarchy, so that something at the top level (like the Main form) isn’t directly manipulating or watching something more than one level away (like the JewelCase). To do that, I decided to have the lower-level objects throw custom Events to let the higher levels know when something happened.
In the JewelCase control, I have handled the MouseEnter and MouseLeave events. These throw custom JewelCase.Spotlight and JewelCase.UnSpotlighted events (programmers have the most baddest grammer, don’t we?). The Shelf object catches these, and rethrows them as Shelf.AlbumSpotlighted and Shelf.AlbumUnSpotlighted. The Main form catches the Shelf events, and handles them with the Main.ShowAlbumFlyout and Main.HideAlbumFlyout methods.
For the purposes of today’s post, we’re only really concerned about the parts of these two methods that reference the VisibleWithAnimation property of the _CurrentJewelCaseExpand variable. I’ll discuss how this variable is managed in a later post, but for now understand that this variable points to the one and only JewelCaseExpand user control available to the Main window. The additional information that is sent back by the Spotlight events is also a topic for later.
By the time the code execution makes it to the ShowAlbumFlyout method, we know that the user has mouse over a JewelCase, and we know which one (similarly when the execution reaches HideAlbumFlyout, we know that the user has moused away from the JewelCase).
Now, on to the second part of our task – firing the animations. This is handled by the custom JewelCaseExpand property called “VisibleWithAnimation”:
Public Property VisibleWithAnimation() As Boolean
Get
Return (Me.Visibility = Windows.Visibility.Visible)
End Get
Set(ByVal value As Boolean)
If (value) Then
Me.Visibility = Windows.Visibility.Visible
Me.BeginStoryboard(CType(FindResource(“FadeIn”), System.Windows.Media.Animation.Storyboard))
Me.BeginStoryboard(CType(FindResource(“ExpandOut”), System.Windows.Media.Animation.Storyboard))
Else
Me.BeginStoryboard(CType(FindResource(“FadeOut”), System.Windows.Media.Animation.Storyboard))
Me.BeginStoryboard(CType(FindResource(“ShrinkDown”), System.Windows.Media.Animation.Storyboard))
Me.Visibility = Windows.Visibility.Hidden
End If
End Set
End Property
This property wraps the Visibility property, but it also fires the animations that were defined in the XAML. The JewelCaseExpand control has a built in method called “BeginStoryboard”, to which I pass the resource that defines the storyboard to run. When the control should be made visible, I fire the FadeIn and ExpandOut animations (explicity cast to the StoryBoard type). When the control should be hidden, I fire the FadeOut and ShrinkDown storyboards.
It is important to notice that while the FadeIn and FadeOut animations change the Opacity of the control, it isn’t the same as setting the Visibility to Visible and Hidden. Visually, the effect is the same – you either see the control or you don’t. However, settings a control’s Visibility to Hidden allows mouse clicks to register for the controls that it was previously covering. Setting the Opacity to 0 doesn’t allow this; you can click all you want in the 75×75 pixel area represented by a completely opaque JewelCaseExpand control, and the mouse clicks will not register.
So, I have to not only run the animations, but set the Visibility as well. This leads to a different problem, and one that still exists in the code above (and the version 0.1 release). The FadeOut and ShrinkDown animations are designed to run to completion in 2/10 of a second. The last line of the Property Set clause sets the Visibility to Hidden. Unfortunately, the two lines above it are executed in a lot less than 2/10 of a second (the animations run asynchronously, so they are STARTED and then execution continues). What this means is that while the animations do run to completion you never see most of it because the entire control is Hidden well before it can finish.
To get around this, I could handle the Storyboard “Completed” event, which as the name suggests fires when the Storyboard completes. If I did this, I could put the Visibility assignment there, allowing the animations to run to completion. I haven’t tried this yet, but that’s the theory and it’s on my TODO: list.
We looked at the reasons why the animations needed to be fired within code, but why did I continue to define them in the markup? Why not put everything in code? It comes down to how WPF can be used by a real programming team on a real project.
By leaving the animations defined in the markup, we let the designer create and refine them in their XAML tool of choice. We also allow the designer to change them after the fact, and as long as the names remain the same, the programmer doesn’t have to get back involved. We de-couple the designer experience from the programmer experience.
Next time we’ll go one step further and not only fire the animations in code, but define them there as well. We’ll also look at the scenarios where this is necessary.