Animate anything anywhere with behaviors in Silverlight 3 (and 2!) (Animations part II)
Introduction
In the previous article on my Blog we have implemented a Panel with 3D (or rather projections) animations. It was cool but we had also two, innate shortcomings there. The first was that the architecture required to implement a new panel and the second was it didn’t provide a good separation of concerns between layout and animation algorithms. Therefore, we’ll look into alternative ways to provide animations.
You need Silverlight 3 Beta to consume samples attached. Blend 3 preview is recommended.
Animations via Behaviors per Element
Behaviors are really powerful weapon in the arsenal of a Silverlight 3 developer. I think that the most evident example of this is the physic engine showed during the MIX (starts around 60:00). For the readers who are not familiar with the topic: Behavior is basically an object that is attached to a visual element. It may change the properties of the element attached to or even properties of other elements in the tree. In our case we would like to achieve as follows:
<StackPanel x:Name="PerElementAnimatedPanel">
...
<Button Content="Button 1">
<i:Interaction.Behaviors>
<local:PerElementAnimationBehavior/>
</i:Interaction.Behaviors>
</Button>
...
</StackPanel>
The PerElementAnimationBehavior object will be responsible for defining all animations for a Button to which is attached. As we can see we would like to implement animations in an ordinary Stock Panel. How can we do it?
First we need to inherit from Behavior <T> class in which we shall override just one method - protected override void OnAttached(). The type T defines types to which the behavior can be attached. This method is called at the time we actually attach to the object in the visual tree and can serve us as a constructor:
protected override void OnAttached()
{
_attachedElement = AssociatedObject;
...Initalize Animations and storyboard...
_attachedElement.LayoutUpdated += OnLayoutUpdated;
}
AssociatedObject is an object to which we attach our behavior (Button in the example above). Except for initializing the animations, we start listening to the LayoutUpdated event of a Button. Let’s see what we do when LayoutUpdated happens.
Despite the event handler is a bit long I have decided to list it fully. The code with comments will really help to understand how it works. I would like to highlight one key aspect before looking into it: LayoutUpdated event is fired after ArrangeOverride and before the item is rendered. This means that the position of the object in the panel you get in this event handler is the target position after animation is completed. We fix it by applying a Render Transform to start animation in the position where the object was before ArrangeOverride. We also apply the render transform on the object directly as the animation won’t start before next frame is rendered.
void OnLayoutUpdated(object sender, EventArgs e)
{
UIElement parent = _attachedElement.Parent as UIElement;
if (parent == null)
{
return;
}
// Please notice that also in later calculations we must take into account the case
// when the animation starts when a previous one is not completed (!)
if (_animating)
{
StopAnimation();
}
// TransformToVisual returns a real position. By this I mean it takes into account
// the actual value of the Render transform applied to the object (happens if previous
// animation was in progress. We need to remove transform for calculations.
// We do so by passing an actual transform value in the point structure
Point targetPanelPostion = _attachedElement.TransformToVisual(parent).Transform(
new Point(-_positionTransform.X, -_positionTransform.Y));
// Calculates where we really are in the panel
Point originPanelPostion = new Point(
_lastTargetPanelPostion.X - targetPanelPostion.X + _positionTransform.X,
_lastTargetPanelPostion.Y - targetPanelPostion.Y + _positionTransform.Y);
// You can think as the transform is the element which applied on the target position
// places the object into position in which we start the animation
_positionAnimationX.From = originPanelPostion.X;
_positionAnimationY.From = originPanelPostion.Y;
_positionAnimationX.To = 0;
_positionAnimationY.To = 0;
// Doesn't make sense to start animation and consume CPU if nothing changed
if (_positionAnimationX.To != _positionAnimationX.From ||
_positionAnimationY.To != _positionAnimationY.From)
{
// Make sure that even before the animation starts the initial position is set
// (means From position is rendered even before animation. It is possible to render a frame
// before the animations starts)
_positionTransform.X = originPanelPostion.X;
_positionTransform.Y = originPanelPostion.Y;
_attachedElement.RenderTransform = _positionTransform;
double perUnitDuration = GetDurationFromParent(parent);
// Sets duration
TimeSpan duration = TimeSpan.FromSeconds(perUnitDuration * (Math.Sqrt(Math.Pow(originPanelPostion.X, 2) +
Math.Pow(originPanelPostion.Y, 2)) / ANIM_UNITS));
_containerStoryBoard.Duration = duration;
_positionAnimationY.Duration = duration;
_positionAnimationX.Duration = duration;
_positionAnimationX.EasingFunction = EasingFunction;
_positionAnimationY.EasingFunction = EasingFunction;
StartAnimation();
}
// Remember last target position
_lastTargetPanelPostion = targetPanelPostion;
}
Just one more comment. It is important to stop the animation before we start tinkering with animated parameters. We do it because today, the value of the parameter is practically not specified (in C++ standard meaning of this word) if modified in a running animation. More information can be found in David Anson post here.
Once we know the algorithm let’s see the extra plumbing.
First let’s see the behaviors support in blend. We expose EasingFunction as a dependency property in our behavior. To make it configurable from Blend we need to add just one Category attribute:
/// <summary>
/// Easing function used in animation
/// </summary>
[Category("VSM EasingFunction")]
public EasingFunctionBase EasingFunction
{
get { return (EasingFunctionBase) GetValue(EasingFunctionProperty); }
set { SetValue(EasingFunctionProperty, value); }
}
/// <summary>
/// EasingFunction Dependency Property.
/// </summary>
...
The effect in Blend when you check behavior properties is below:
Powerful and simple isn’t it?
A question that can arise: OK, what if I would like to have some object data shared by all elements in the panel? You can do it by defining an attached property like this:
public static readonly DependencyProperty Per50PixelAnimationTimeProperty =
DependencyProperty.RegisterAttached(
"Per50PixelAnimationTime",
typeof(double),
typeof(Panel),
new PropertyMetadata(0.5, new PropertyChangedCallback(Per50PixelAnimationTimeChanged)));
,then set it in a panel and consume via attached to object's parent reference in the behavior.
Animations via ContentControl
Now, there is a question if I can do it without behaviors in Silverlight 2?
Yes, You can just reverse the situation – in your panel each child would be a ContentControls in which we would define animations and the content of the ContentControl would be the element we actually animate. It will look like below:
<StackPanel x:Name="PerElementAnimatedPanel">
...
<local:PerElementAnimationContainer>
<local:PerElementAnimationContainer.EasingFunction>
<BounceEase Bounces="5" Bounciness="3"/>
</local:PerElementAnimationContainer.EasingFunction>
<Button Content="Button 3">
</Button>
</local:PerElementAnimationContainer>
...
</StackPanel>
You can ask how much code from the behavior defined in the paragraph earlier you can reuse. I guess you can bet 100$ that we have to change more than couple of lines? Well, then email me and I can give you my account number. The answer is: four lines. You don’t believe me? Let’s count:
One:
public class PerPanelAnimationBehavior : Behavior<Panel>
to:
public class PerElementAnimationContainer : ContentControl
Two:
protected override void OnAttached()
to:
public PerElementAnimationContainer()
Three:
_attachedElement = AssociatedObject;
to:
_attachedElement = this;
Four (in red):
public static readonly DependencyProperty EasingFunctionProperty =
DependencyProperty.Register(
"EasingFunction",
typeof(EasingFunctionBase),
typeof(PerElementAnimationBehavior),
new PropertyMetadata(new ExponentialEase { Exponent = 4.0 }, new PropertyChangedCallback(EasingFunctionChanged)));
to:
public static readonly DependencyProperty EasingFunctionProperty =
DependencyProperty.Register(
"EasingFunction",
typeof(EasingFunctionBase),
typeof(PerElementAnimationContainer),
new PropertyMetadata(new ExponentialEase { Exponent = 4.0 }, new PropertyChangedCallback(EasingFunctionChanged)));
Even three lines if we don’t change the class name. Cool isn’t it? The drawback of this solution is that we no longer get the Blend designers support that is specific to the Behaviors.
Animations via Behaviors per Panel
With regular panels and just one animation style per such panel, it can be tiresome to remember to attach the same behavior to each child in the container. In that case why not to attach just one behavior to the panel that would provide animations for all children in the panel.
<StackPanel x:Name="PanelBehavior">
<i:Interaction.Behaviors>
<local:PerPanelAnimationBehavior/>
</i:Interaction.Behaviors>
<Button ...
...
</StackPanel>
To do it we exercise a design very similar to what we have implemented in the previous article. We shall need 2 elements:
· Behavior attached to the panel
· An attached dependency property object applied to each child and containing its animations and animation parameters.
Because the code is well documented and the design has been explained last time, I’ll limit code snippets just to the main animation loop:
if (_animating)
{
StopAnimation();
}
// Remove all children - we'll add only object we need to animate.
_panelStoryBoard.Children.Clear();
foreach (UIElement child in _attachedElement.Children)
{
// TransformToVisual returns a real position. By this I mean it takes into account
// the actual value of the Render transform applied to the object (happens if previous
// animation was in progress. We need to remove transform for calculations.
// We do so by passing an actual transform value in the point structure
PerPanelAnimationBehaviorProperties animProperties = GetProperties(child);
Point targetPanelPostion = child.TransformToVisual(_attachedElement).Transform(
new Point(-animProperties.PositionTransform.X, -animProperties.PositionTransform.Y));
// Skip not animated objects
if (animProperties.LastTargetPanelPostion == targetPanelPostion)
{
continue;
}
... Animation calculations similar to ‘Animations via Behaviors per Element’
_panelStoryBoard.Children.Add(animProperties.PositionAnimationX);
_panelStoryBoard.Children.Add(animProperties.PositionAnimationY);
// Remember last target position
animProperties.LastTargetPanelPostion = targetPanelPostion;
}
if (maxDuration.Ticks > 0)
{
_panelStoryBoard.Duration = maxDuration;
StartAnimation();
}
If we wish to have specific animations per animated element we can use the same trick as before – an attached dependency property. I would think twice before doing it. If you need to do it, it is a good indicator you probably need to implement Animations via Behavior per element.
Can we implement it without behaviors? Yes, we can - All you need to do is basically attach to the panel LayoutUpdated event.
Summary
It’s a good moment to ask the question which architecture is superior. The answer is not simple:
· Animations in Panel – when you need to implement a new panel and there is no other reasonable way to animate elements in the panel without changing the layout algorithm. A standard carousel could be an example here.
o Easy to use - attaching anything to the panel is not required.
o It is difficult to reuse algorithm between different type of panels
Yes, I know some purist would say that the only responsibility of a panel should be only the layout, and that animations should never be a part of it.
· Animations via Behavior per Element or a Content Control – good if elements can be animated in different ways in your panel and/or you have already a panel to animate in
o Provides a good separation of concerns between the layout and animation.
o Requires attaching a behavior to each element added to the panel.
o Can be reused between different Panels and specialized for particular ones and/or specialized for particular types of panels' children.
· Animations via Behavior per Panel or an object attached to the LayoutUpdate event - good if elements in your panel can be reasonably animated only in one way at the same time and / or you have already a panel to animate in
o Provides a good separation between the layout and animation concerns.
o You need to attach behavior to the Panel only.
o Can be reused between different Panels and specialized for particular ones.
If you implement for Silverlight 3 I would prefer Behaviors over non-behaviors alternatives as they provide an awesome designer support in Blend.
In next part in series we’ll look how the techniques we presented can be applied to ItemsControl and TreeView.
Comments
- Anonymous
May 17, 2009
Thank you for submitting this cool story - Trackback from DotNetShoutout