Tuesday, November 15, 2011

Binding Animation KeyFrame Values in WPF


Recently, I encountered a requirement for my WPF application (actually a Surface application) where I needed to navigate between two controls. One control, we'll call it Control1, can be dragged around the screen by the user. The other control, we'll call it Control2, has a similar shape to Control1, but is larger, and has a fixed location on the screen. Essentially, I needed Control1 to morph into Control2.
To summarize the requirements:
  • Control1 is moveable around the screen.
  • Control2 has a fixed location on the screen.
  • Control1 needs to appear to morph into Control2.

My hope was that I would be able to implement this transition from Control1 to Control2 by using storyboards and data binding. I wasn't certain, however, that data binding was supported in animation properties.
To fake Control1 growing into Control2, and moving across the screen to its home position, I decided to use an animation of Control2's ScaleTransform and TranslateTransform properties. Since Control1 is moveable, the begin values of the animation must be set dynamically. Hopefully, I would be able to bind the begin points of Control2's animations to properties in the view model of its container. I added a Point property to hold this data, which would need to be set when the user triggers the transition to Control2.

The storyboards in the container for Control1 and Control2 are shown below. Notice the highlighted values, which are bound to my Control1Location property. The animation occurs over the span of one second, so notice the KeyTimes of"00:00:00" and "00:00:01". For the sake of brevity, I left out the ScaleTransform animation, as well as KeySplines.

<Storyboard x:Key="TransitionControl1ToControl2"> <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Control1" Storyboard.TargetProperty="(UIElement.Visibility)">  <DiscreteObjectKeyFrame KeyTime="00:00:00" Value="{x:Static Visibility.Hidden}"/>ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Control2" Storyboard.TargetProperty="(UIElement.Visibility)">
  
<DiscreteObjectKeyFrame KeyTime="00:00:00" Value="{x:Static Visibility.Visible}"/>ObjectAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Control2"Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)">
  
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="{Binding Path=Control1Location.X}"/>  <SplineDoubleKeyFrame KeyTime="00:00:01" Value="0"/>DoubleAnimationUsingKeyFrames>
<
DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Control2"
    
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.Y)">
  
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="{Binding Path=Control1Location.Y}"/>
  <
SplineDoubleKeyFrame KeyTime="00:00:01" Value="0"/>DoubleAnimationUsingKeyFrames>Storyboard><Storyboard x:Key="TransitionControl2ToControl1"><DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Control2"
    
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)">
  
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
  
<SplineDoubleKeyFrame KeyTime="00:00:01" Value="{Binding Path=Control1Location.X}"/>DoubleAnimationUsingKeyFrames><DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Control2"
    
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.Y)">
  
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
  
<SplineDoubleKeyFrame KeyTime="00:00:01" Value="{Binding Path=Control1Location.Y}"/>DoubleAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Control1" Storyboard.TargetProperty="(UIElement.Visibility)">
  
<DiscreteObjectKeyFrame KeyTime="00:00:01" Value="{x:Static Visibility.Visible}"/>ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Control2"
    
Storyboard.TargetProperty="(UIElement.Visibility)">
  
<DiscreteObjectKeyFrame KeyTime="00:00:01" Value="{x:Static Visibility.Hidden}"/>ObjectAnimationUsingKeyFrames>Storyboard>

So as long as I set the value of the Control1Location property before starting this animation, it should all work…right?
It turns out that this almost worked. For some strange reason, the storyboard properties do not seem to know they're bound until the second time the animation is run. I tried every way imaginable to notify the UI that the property was set, but each time the animation was first run, the bound animation keyframe values were 0.0, which I assume meant they were unset. The second time running the animation, the set values would be bound correctly. The issue wasn't related to raising the PropertyChanged event a second time—just running the animation a second time.
By much trial and error, I discovered an admittedly hacky way to apparently get the binding to "wake up" before running the first animation.

// HACK: This is a hack to get the storyboard bindings to "wake up" for first time use.// Without it, the first run of the storyboards does not seem to respect the bindings.// Any subsequent run of the storyboards works correctly.BindingOperations.IsDataBound((Storyboard)Resources["TransitionControl1ToControl2"],
    Storyboard.AccelerationRatioProperty); // Arbitrary property.BindingOperations.IsDataBound((Storyboard)Resources["TransitionControl2ToControl1"],
    Storyboard.AccelerationRatioProperty); // Arbitrary property.

It seems that calling any of the static BindingOperations methods against the two storyboards causes the binding to wake up. I chose a lighter-weight method IsDataBound(), and picked an arbitrary property that was not used in my storyboards. I'm convinced that this same wake up is occurring on the first run of the animation, but by calling aBindingOperations method we're forcing the wake up beforehand.


No comments:

Post a Comment