How to create generic animated panels in Silverlight 2 and 3 (Animations part I)
Introduction
I’ve always had mixed feelings toward animations in cases of generic controls, panels or item controls. From one side it is extremely easy to define standard animations associated with visual states. From the other implementing any more sophisticated animation means that a generic control becomes a user control. In this first article in series I shall present ways we know in our team to make Framework Elements with non-trivial animations without sacrificing theirs genericness. In short - it is easy to create a generic FrameworkElement, but it is hard to create such with advanced animations so it is still generic. In part number one we shall deal with animations in Panels.
Customizable animations
When we talk about animations in Silverlight the problem is that the nicer animation should look, the more difficult (or impossible) is to go with simple storyboards in XAML. That is especially painful for generic controls. Someone would say that something like this, at least for controls should be possible:
<Storyboard> <ColorAnimationUsingKeyFrames Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)"> <SplineColorKeyFrame KeyTime="0" Value="{Binding RelativeSource={RelativeSource TemplatedParent} Path=MyFancyColorViaDepProp}"/> </ColorAnimationUsingKeyFrames> </Storyboard> |
However that won’t work. The problem is that despite Silverlight, since version 3, supports relative binding, it does not support context inheritance. What is the context inheritance is well described in this Nick’s blog entry. It is a pity as in this situation we won’t directly expose animation to user so it may be completely customized. We can mitigate it thought and for a UI Element containing animations we can expose two dependency properties:
· Animation time
· Easing Function (SL3 only)
If you think about it, despite it is far less elegant than customizable animations in XAML, a user is still able to do a lot with it. The problem is of course if we have many different animations in an UI element and number of such properties grows…
Animating panel with attached properties
I got a rather fancy, animated panel to refactor. Unfortunately not the one I’m presenting in the example… well NDA world… sigh... I hope one day you shall see that in a released product because it is very impressive. Anyway I’m going to present you all tools to do whatever kind of 3D panel you can imagine. The source code for sample can be downloaded from attachments to this post. It requires SL3 and contains a picture “roller” panel reacting to mouse move with pressed left button. I tried it to be as simple as possible and I didn’t include validation code in order to simplify the sample as much as possible.
The panel I got to refactor was implemented in a traditional way, displaying each frame in ArrangeOverride – one of many descriptions for this approach can be found here. I wanted something more because of following reasons:
· This solution is not well scalable. Each frame means ArrangeOverride which causes the layout updates for all children, all callbacks with associated dependency property updates are fired etc.
· We practically simulating animation mechanism that is already a part of Silverlight API. If we wish to have easing effects again we would need to implement them ourselves and calculations for each frame can be painful
· It drains your UI thread to generate more frames than necessary (IMHO 30 per sec but 50 someone else would say), competing for CPU with other controls. Yes, you can probably workaround that but again you would mind reimplementing a part of animation engine on your own
In the solution I’m presenting ArrangeOverride is very light weighted and is fired only when visibility changes.
Dealing with animations
The approach is really straightforward. Each panel’s child needs to be animated in a different way – we can implement that using attached dependency property defined in the panel that will specify animation properties for each child. We need also an access function (GetProperties in our case) to handle the initialization of this property in children if necessary.
From the panel perspective we need to declare the attached property and handle its initialization for each child:
/// <summary> /// An animated panel /// </summary> public class SampleAnimPanel : Panel { ... internal SampleAnimPanelProperties Properties { get { return (SampleAnimPanelProperties)GetValue(SampleAnimPanelPropertiesProperty); } set { SetValue(SampleAnimPanelPropertiesProperty, value); } } internal static readonly DependencyProperty SampleAnimPanelPropertiesProperty = DependencyProperty.RegisterAttached( "Properties", typeof(SampleAnimPanelProperties), typeof(SampleAnimPanel), null);
private SampleAnimPanelProperties GetProperties(DependencyObject element) { object animProp = element.ReadLocalValue(SampleAnimPanelPropertiesProperty); if (animProp != DependencyProperty.UnsetValue) { return (SampleAnimPanelProperties)animProp; } else { SampleAnimPanelProperties properties = new SampleAnimPanelProperties((FrameworkElement)element, AnimationStoryboard); element.SetValue(SampleAnimPanelPropertiesProperty, properties); return properties; } } ... } |
Then we need following animation properties in our SampleAnimPanelProperties:
/// <summary> /// A class used to hold animation parameter in per panel element fashion /// </summary> public class SampleAnimPanelProperties { #region Animation Properties
/// <summary> /// Gets or sets RotationAnimation used rotate the elements /// </summary> public DoubleAnimation RotationAnimation { get; set; } /// <summary> /// Gets or sets ElementVisibilityAnimation. /// </summary> public ObjectAnimationUsingKeyFrames ElementVisibilityAnimation { get; set; } /// <summary> /// Gets or sets when to show a control in animation /// </summary> public DiscreteObjectKeyFrame ShowKeyFrame { get; set; } /// <summary> /// Gets or sets when to hide a control in animation /// </summary> public DiscreteObjectKeyFrame HideKeyFrame { get; set; } /// <summary> /// Gets or sets Projection. Taken from UIElement projection to which /// properties are attached. /// </summary> public PlaneProjection Projection { get; set; } /// <summary> /// Reference to the UIElement we provide parameters to /// </summary> private UIElement _element; ... } |
Now when we have all the ammo let’s see the heart of our animation:
/// <summary> /// Method that starts the slide animation. /// </summary> private void Animate() { if (Children != null && Children.Count > 0) { _animating = true; ExponentialEase easingFunction = new ExponentialEase { Exponent = ExponentialEaseFactor}; SampleAnimPanelProperties properties = GetProperties(Children[0]); double animationTime = FullRotatonAnimationTime * Math.Abs(properties.Projection.RotationY - _rotationOffset) / FULL_ROTATION; Duration animationDuration = new Duration(TimeSpan.FromSeconds(animationTime)); StopAnimation(); AnimationStoryboard.Duration = animationDuration; for (int i = 0; i < Children.Count; i++) { properties = GetProperties(Children[i]); properties.RotationAnimation.To = _rotationOffset + GetBaseRotation(i); properties.RotationAnimation.EasingFunction = easingFunction; SetVisibilityAnimation(properties, Children[i], i, animationTime); properties.ElementVisibilityAnimation.Duration = animationDuration; properties.RotationAnimation.Duration = animationDuration; }
AnimationStoryboard.Begin(); } } |
We should comment couple of things here:
· GetBaseRotation just gives back the initial rotation of the element
· FullRotatonAnimationTime time is given as a time required for 180 degree animation of the item, therefore as couple of such rotations can happen we need to calculate the total time basing on the actual rotation
· We have to stop animation and persevere animated values in StopAnimation(). We need to do it because if it is not stopped we can’t add or remove KeyFrames to visibility animations.
· SetVisibilityAnimation is a method dealing with visibility animation – we’ll look into it later.
Now let’s think about performance. We use the most straightforward approach – one storyboard and animation from all children attached to it. It can cause a problem if all calculations and the storyboard start takes too long (more than a visible frame - 1/30 sec at least). An alternative is to include the storyboard to children attached properties (one storyboard per each child), and do calculations in separate threads (leveraging multiple cores) then fire animations one by one, at cost of potentially (small) desynchronizations between children elements. Another benefit would be that starting one storyboard with numerous animations inside can also take a perceptible amount of time.
Another approach would be not to animate object that shall not be visible and in such case being able to tell their “current” position when this animation ends. It is doable in the example above, however it won’t fix anything in worst case scenario when every element needs to be visible at least for a brief moment.
Dealing with visibility
Now let’s look into visibility animation. We need to align KeyFrame(s) Visibility animation with Double animation. There is no rocket science here. We know the total distance in DoubleAnimation for every child object, we know total time so we can estimate when to hide or show particular children. Basically we have following cases:
private void SetVisibilityAnimation( SampleAnimPanelProperties properties, UIElement element, double targetPosition, double animationTime) { // Remove key frames from previous runs properties.ElementVisibilityAnimation.KeyFrames.Clear(); double currentPosition = properties.Projection.RotationY; // Can happen because timespan is rounded to milliseconds and // there can be discrepancy between visibility and the position if (GetElementVisibility(currentPosition) == Visibility.Collapsed && element.Visibility == Visibility.Visible) { element.Visibility = Visibility.Collapsed; } else if ((GetElementVisibility(currentPosition) == Visibility.Visible && element.Visibility == Visibility.Collapsed)) { element.Visibility = Visibility.Visible; } // Check if animation is necessary if (element.Visibility == Visibility.Collapsed) { if (GetElementVisibility(targetPosition) == Visibility.Visible) { // Show the object double invisDistance = GetInvisibleDistance(currentPosition); double totDistance = Math.Abs(currentPosition - targetPosition); properties.ShowKeyFrame.KeyTime = EaseVisibilityAnimation( invisDistance, totDistance, animationTime); properties.ElementVisibilityAnimation.KeyFrames.Add(properties.ShowKeyFrame); } else if (GetElementVisibility(targetPosition) == Visibility.Collapsed && currentPosition * targetPosition < 0) { // Special case – show the object, then collapse ... } } else { // Visible if (GetElementVisibility(targetPosition) == Visibility.Collapsed) { // Hide the object ... } } } |
The special case mentioned in the code handles a perfectly legal case when in one animation we need to first show and later hide a child in the panel which is a special case in which we need to add two KyFrames – one for appearance, one for disappearance.
One optimization note from panels’ children point of view – despite there is no such event as VisibilityChanged you can bind to Visibility property and for ex. turn off unused gadgets in the panel which can have a tremendous effect on the scalability your application.
Dealing with easing
I wanted to have something cool and giving more natural, almost physical behavior. What’s the problem let’s apply easing to DoubleAnimation – no problem.
Once we applied the ease effect on DoubleAnimation used to animate RotiationY, we need to adjust show & hide keyframes to double animations. Seems like an easy task to do. Turns out, it is not:
· First what you would try is to somehow apply easing effect on ObjectAnimationUsingKeyFrames. Unfortunately the easing effect cannot be applied on it.
· We can try to create a new dependency property in SampleAnimPanelProperties attach and bind it to animated dependency property (RotationY in our case) and on per frame fashion check if a panel object should be visible or not in a dependency change callback. That won’t work though. We would bind to a non-animated dependency property while the animated value is taking the precedence and our property wouldn’t get chance to be updated till we stop an animation.
· Easing essentially is a function based on time and influencing target value so it would be great if at least for a given progress of DoubleAnimation, we can get the time when it is reached (progress to time). Unfortunately there is no such method – only an opposite transformation (time to progress) exists in the API – Ease(Double) in EasingFunctionBase. Moreover the reverse function wouldn’t exist for some easing effects like bouncing effect as it is not 1to1 function so the method would need to return a collection.
You can’t get it from the API, you need to do it yourself. First let’s visit the msdn page concerning exponential ease to check the ease function algorithm:
But here comes more. The exponential ease function works by default in EasingOut mode which influences the equation above. We can check how in reflector in Ease function in EasingFunctionBase:
Public double Ease(double normalizedTime) { switch (this.EasingMode) { case EasingMode.EaseOut: return (1.0 - this.EaseInCore(1.0 - normalizedTime)); case EasingMode.EaseIn: return this.EaseInCore(normalizedTime); case EasingMode.EaseInOut: if (normalizedTime < 0.5) { return (this.EaseInCore(normalizedTime * 2.0) / 2.0); } return (((1.0 - this.EaseInCore(2.0 - (normalizedTime * 2.0))) / 2.0) + 0.5); } return normalizedTime; } |
So in reality our equation is:
y = 1 – (e^(a(1-t)) -1)/(e^a-1)
And we’re looking for normalized time t, while y being the distance to go to reach hide/show point (numerator) divided by the total distance (denominator). By solving this equation we get:
t = 1 - (Math.Log(1 + (1 - numerator /denominator) * (Math.Exp(ExponentialEaseFactor) - 1)) / ExponentialEaseFactor)));
And this is it. The only thing left is to multiply t by the total animation time to denormalize it. The function looks like this:
/// <summary> /// This function simulates easing effect on visibility animation /// </summary> /// <param name="numerator">Uneased visibility change animation shift</param> /// <param name="denominator">Total animation shift</param> /// <param name="animTime">Total time in seconds for animation</param> /// <returns>TimeSpan with eased visibility change time point</returns> private TimeSpan EaseVisibilityAnimation(double numerator, double denominator, double animTime) { return TimeSpan.FromSeconds(animTime * (1 - (Math.Log(1 + (1 - (numerator / denominator)) * (Math.Exp(ExponentialEaseFactor) - 1)) / ExponentialEaseFactor))); } |
Making animations configurable
We can make panel animations configurable by exposing two animation properties:
· AnimationTime
· Exponjential Ease Factor
Unfortunately we’re not able to expose Easing Function due to our Visibility animation. We need to…
Get rid of the visibility animation
After some time I realized that there is a way around it by a different, much simpler approach to dealing with visibility. Here is the trick:
public SampleAnimPanel() { AnimationStoryboard = new Storyboard(); CompositionTarget.Rendering += new EventHandler(CompositionTarget_Rendering); } void CompositionTarget_Rendering(object sender, EventArgs e) { if (_animating && Children != null && Children.Count > 0) { for (int i = 0; i < Children.Count; i++) { double currentPosition = GetProperties(Children[i]).Projection.RotationY; if (GetElementVisibility(currentPosition) == Visibility.Collapsed && Children[i].Visibility == Visibility.Visible) { Children[i].Visibility = Visibility.Collapsed; } else if (GetElementVisibility(currentPosition) == Visibility.Visible && Children[i].Visibility == Visibility.Collapsed) { Children[i].Visibility = Visibility.Visible; } } } } |
Rendering event occurs when the core Silverlight rendering process renders a frame. For each frame to be displayed in animation we just check if an object should be visible or not. It should be faster than 150 lines of code we have just written for the visibility animation. I decided to leave the parts for Visibility Animation in article and samples, because they definitely have a high educational value. And now we can expose easing function as well J
What’s next?
In the next article I will be playing again with panels. I’ll show how to implement animations in panels without modifying them in both Silverlight 2 and 3.
PanelAnimationViaDepProperty.zip
Comments
Anonymous
May 14, 2009
Thank you for submitting this cool story - Trackback from DotNetShoutoutAnonymous
May 15, 2009
Marek Latuskiewicz, a senior development lead on our AdCenter team, has recently launched a blog aboutAnonymous
May 17, 2009
Introduction In the previous article on my Blog we have implemented a Panel with 3D (or rather projections)Anonymous
May 24, 2009
I’ve brought up this topic in the recent post , but I was pointed out it can be lost among other contentAnonymous
July 21, 2011
Nice tutorial, it is really a good one for me as I am quite new to silverlight