Swivel Behavior in Silverlight 3
Silverlight Friday, August 07 2009A while back I created a pair of Storyboards that make a pair of panels swivel to show a front side and a back side. The key ingredients in the storyboards are:
- Plane Projection using Perspective 3D
- Easing
- Visibility (Opacity can be used instead if you prefer)
Easing and 3D features are new to Silverlight 3 but really give it a nice effect. The easing makes it start slowly, then appear faster at the 90 degree turn, and then ease out.
Storyboard
The storyboards are not that involved, in fact here is the code for the storyboard that swivels the panels to show the backside:
<Storyboard x:Name="SwivelToBackStoryboard">
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="SwivelPanelBack"
Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
<DiscreteObjectKeyFrame KeyTime="00:00:00.5000000">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="SwivelPanelFront"
Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00.0000000">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
<DiscreteObjectKeyFrame KeyTime="00:00:00.5000000">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="SwivelPanelFront"
Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationY)">
<EasingDoubleKeyFrame KeyTime="00:00:00.0000000" Value="0">
<EasingDoubleKeyFrame.EasingFunction>
<CubicEase EasingMode="EaseIn"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime="00:00:00.5000000" Value="-90">
<EasingDoubleKeyFrame.EasingFunction>
<CubicEase EasingMode="EaseIn"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime="00:00:01" Value="-180">
<EasingDoubleKeyFrame.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="SwivelPanelBack"
Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationY)">
<EasingDoubleKeyFrame KeyTime="00:00:00.0000000" Value="180">
<EasingDoubleKeyFrame.EasingFunction>
<CubicEase EasingMode="EaseIn"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime="00:00:00.5000000" Value="90">
<EasingDoubleKeyFrame.EasingFunction>
<CubicEase EasingMode="EaseIn"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime="00:00:01" Value="0">
<EasingDoubleKeyFrame.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
The front-to-back storyboard is pretty straightforward. I animate the visibility from collapsed to visible for the panels at the 90 degree turn. I use keyframes over a period of 1 second to animate the plane projection from 0 to -180 degrees for the front panel and from 180 to 0 for the back panel. The key here is that whatever panel is at 0 degrees will have clear text when it is at rest at 0 degrees. I found that if I animate them and have the front panel be at 360 degrees or even 180 degrees (if it starts at an offset), the text looks fuzzy. So this swivel storyboard works nicely for text.
You can see try this storyboard by downloading the code here.
Behavior
So the immediate problem I have with this solution is that I want to reuse this storyboard in an application. I certainly do not want to copy and paste it everywhere. This is where behaviors and triggers make a big difference. Behaviors are one of the most exciting features in Silverlight 3 and Blend 3 to me.
The goal is to make it as simple as possible to implement the swivel effect. So I rewrote the storyboards for the front to back to front swivels of the panels in .NET code inside of a behavior. This admittedly took a bit as I had to do something in .NET code that is considerably easier and faster to create in Blend. Download the code and you will see the difference between the first example, which uses storyboards in XAML, and the second example which uses .NET code in a behavior. It is not difficult by any means … but in Blend it took me 5 minutes to create the storyboard I wanted. It took me about an hour to convert and test the .NET code.
Below you can see the code for the Swivel behavior class itself. The complete code can be downloaded here.
public class Swivel : TriggerAction<FrameworkElement>
{
#region Front element
public static readonly DependencyProperty FrontElementNameProperty =
DependencyProperty.Register("FrontElementName", typeof(string),
typeof(Swivel), new PropertyMetadata(null));
[Category("Swivel Properties")]
public string FrontElementName { get; set; }
#endregion
#region Back element
public static readonly DependencyProperty BackElementNameProperty =
DependencyProperty.Register("BackElementName", typeof(string),
typeof(Swivel), new PropertyMetadata(null));
[Category("Swivel Properties")]
public string BackElementName { get; set; }
#endregion
#region Duration
public static readonly DependencyProperty DurationProperty =
DependencyProperty.Register("Duration", typeof(Duration),
typeof(Swivel), new PropertyMetadata(null));
[Category("Animation Properties")]
public Duration Duration { get; set; }
#endregion
#region Rotation Direction
public static readonly DependencyProperty RotationProperty =
DependencyProperty.Register("Rotation", typeof(RotationDirection),
typeof(Swivel), new PropertyMetadata(RotationDirection.LeftToRight));
[Category("Animation Properties")]
public RotationDirection Rotation { get; set; }
#endregion
private readonly Storyboard frontToBackStoryboard = new Storyboard();
private readonly Storyboard backToFrontStoryboard = new Storyboard();
private bool forward = true;
protected override void Invoke(object parameter)
{
if (AssociatedObject == null) return;
FrameworkElement parent = AssociatedObject; // as FrameworkElement;
UIElement front = null;
UIElement back = null;
front = parent.FindName(FrontElementName) as UIElement;
back = parent.FindName(BackElementName) as UIElement;
if (front == null || back == null) return;
if (front.Projection == null || back.Projection == null)
{
front.Projection = new PlaneProjection();
front.RenderTransformOrigin = new Point(.5, .5);
front.Visibility = Visibility.Visible;
back.Projection = new PlaneProjection { CenterOfRotationY = .5, RotationY = 180.0 }; //, CenterOfRotationZ = this.CenterOfRotationZ };
back.RenderTransformOrigin = new Point(.5, .5);
back.Visibility = Visibility.Collapsed;
RotationData showBackRotation = null;
RotationData hideFrontRotation = null;
RotationData showFrontRotation = null;
RotationData hideBackRotation = null;
var frontPP = new PlaneProjection(); // { CenterOfRotationZ = this.CenterOfRotationZ };
var backPP = new PlaneProjection(); // { CenterOfRotationZ = this.CenterOfRotationZ };
switch (Rotation)
{
case RotationDirection.LeftToRight:
backPP.CenterOfRotationY = frontPP.CenterOfRotationY = 0.5;
showBackRotation = new RotationData { FromDegrees = 180.0, MidDegrees = 90.0, ToDegrees = 0.0, RotationProperty = "RotationY", PlaneProjection = backPP, AnimationDuration = this.Duration };
hideFrontRotation = new RotationData { FromDegrees = 0.0, MidDegrees = -90.0, ToDegrees = -180.0, RotationProperty = "RotationY", PlaneProjection = frontPP, AnimationDuration = this.Duration };
showFrontRotation = new RotationData { FromDegrees = -180.0, MidDegrees = -90.0, ToDegrees = 0.0, RotationProperty = "RotationY", PlaneProjection = frontPP, AnimationDuration = this.Duration };
hideBackRotation = new RotationData { FromDegrees = 0.0, MidDegrees = 90.0, ToDegrees = 180.0, RotationProperty = "RotationY", PlaneProjection = backPP, AnimationDuration = this.Duration };
break;
case RotationDirection.RightToLeft:
backPP.CenterOfRotationY = frontPP.CenterOfRotationY = 0.5;
showBackRotation = new RotationData { FromDegrees = -180.0, MidDegrees = -90.0, ToDegrees = 0.0, RotationProperty = "RotationY", PlaneProjection = backPP, AnimationDuration = this.Duration };
hideFrontRotation = new RotationData { FromDegrees = 0.0, MidDegrees = 90.0, ToDegrees = 180.0, RotationProperty = "RotationY", PlaneProjection = frontPP, AnimationDuration = this.Duration };
showFrontRotation = new RotationData { FromDegrees = 180.0, MidDegrees = 90.0, ToDegrees = 0.0, RotationProperty = "RotationY", PlaneProjection = frontPP, AnimationDuration = this.Duration };
hideBackRotation = new RotationData { FromDegrees = 0.0, MidDegrees = -90.0, ToDegrees = -180.0, RotationProperty = "RotationY", PlaneProjection = backPP, AnimationDuration = this.Duration };
break;
case RotationDirection.BottomToTop:
backPP.CenterOfRotationX = frontPP.CenterOfRotationX = 0.5;
showBackRotation = new RotationData { FromDegrees = 180.0, MidDegrees = 90.0, ToDegrees = 0.0, RotationProperty = "RotationX", PlaneProjection = backPP, AnimationDuration = this.Duration };
hideFrontRotation = new RotationData { FromDegrees = 0.0, MidDegrees = -90.0, ToDegrees = -180.0, RotationProperty = "RotationX", PlaneProjection = frontPP, AnimationDuration = this.Duration };
showFrontRotation = new RotationData { FromDegrees = -180.0, MidDegrees = -90.0, ToDegrees = 0.0, RotationProperty = "RotationX", PlaneProjection = frontPP, AnimationDuration = this.Duration };
hideBackRotation = new RotationData { FromDegrees = 0.0, MidDegrees = 90.0, ToDegrees = 180.0, RotationProperty = "RotationX", PlaneProjection = backPP, AnimationDuration = this.Duration };
break;
case RotationDirection.TopToBottom:
backPP.CenterOfRotationX = frontPP.CenterOfRotationX = 0.5;
showBackRotation = new RotationData { FromDegrees = -180.0, MidDegrees = -90.0, ToDegrees = 0.0, RotationProperty = "RotationX", PlaneProjection = backPP, AnimationDuration = this.Duration };
hideFrontRotation = new RotationData { FromDegrees = 0.0, MidDegrees = 90.0, ToDegrees = 180.0, RotationProperty = "RotationX", PlaneProjection = frontPP, AnimationDuration = this.Duration };
showFrontRotation = new RotationData { FromDegrees = 180.0, MidDegrees = 90.0, ToDegrees = 0.0, RotationProperty = "RotationX", PlaneProjection = frontPP, AnimationDuration = this.Duration };
hideBackRotation = new RotationData { FromDegrees = 0.0, MidDegrees = -90.0, ToDegrees = -180.0, RotationProperty = "RotationX", PlaneProjection = backPP, AnimationDuration = this.Duration };
break;
}
front.RenderTransformOrigin = new Point(.5, .5);
back.RenderTransformOrigin = new Point(.5, .5);
front.Projection = frontPP;
back.Projection = backPP;
frontToBackStoryboard.Duration = this.Duration;
backToFrontStoryboard.Duration = this.Duration;
// Rotation
frontToBackStoryboard.Children.Add(CreateRotationAnimation(showBackRotation));
frontToBackStoryboard.Children.Add(CreateRotationAnimation(hideFrontRotation));
backToFrontStoryboard.Children.Add(CreateRotationAnimation(hideBackRotation));
backToFrontStoryboard.Children.Add(CreateRotationAnimation(showFrontRotation));
// Visibility
frontToBackStoryboard.Children.Add(CreateVisibilityAnimation(showBackRotation.AnimationDuration, front, false));
frontToBackStoryboard.Children.Add(CreateVisibilityAnimation(hideFrontRotation.AnimationDuration, back, true));
backToFrontStoryboard.Children.Add(CreateVisibilityAnimation(hideBackRotation.AnimationDuration, front, true));
backToFrontStoryboard.Children.Add(CreateVisibilityAnimation(showFrontRotation.AnimationDuration, back, false));
}
if (forward)
{
frontToBackStoryboard.Begin();
forward = false;
}
else
{
backToFrontStoryboard.Begin();
forward = true;
}
}
private static ObjectAnimationUsingKeyFrames CreateVisibilityAnimation(Duration duration, DependencyObject element, bool show)
{
var animation = new ObjectAnimationUsingKeyFrames();
animation.BeginTime = new TimeSpan(0);
animation.KeyFrames.Add(new DiscreteObjectKeyFrame { KeyTime = new TimeSpan(0), Value = (show ? Visibility.Collapsed : Visibility.Visible) });
animation.KeyFrames.Add(new DiscreteObjectKeyFrame { KeyTime = new TimeSpan(duration.TimeSpan.Ticks / 2), Value = (show ? Visibility.Visible : Visibility.Collapsed) });
Storyboard.SetTargetProperty(animation, new PropertyPath("Visibility"));
Storyboard.SetTarget(animation, element);
return animation;
}
private static DoubleAnimationUsingKeyFrames CreateRotationAnimation(RotationData rd)
{
var animation = new DoubleAnimationUsingKeyFrames();
animation.BeginTime = new TimeSpan(0);
animation.KeyFrames.Add(new EasingDoubleKeyFrame { KeyTime = new TimeSpan(0), Value = rd.FromDegrees, EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseIn } });
animation.KeyFrames.Add(new EasingDoubleKeyFrame { KeyTime = new TimeSpan(rd.AnimationDuration.TimeSpan.Ticks / 2), Value = rd.MidDegrees });
animation.KeyFrames.Add(new EasingDoubleKeyFrame { KeyTime = new TimeSpan(rd.AnimationDuration.TimeSpan.Ticks), Value = rd.ToDegrees, EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
Storyboard.SetTargetProperty(animation, new PropertyPath(rd.RotationProperty));
Storyboard.SetTarget(animation, rd.PlaneProjection);
return animation;
}
}
Once you create the behavior, it shows up in Blend in the Assets window, like this:
You can then drag the behavior onto whatever element you want to control the behavior. In my demo, I use the MouseLeftButtonUp event of the Grid, like this:
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonUp">
<local:Swivel
Rotation="RightToLeft"
Duration="00:00:01"
BackElementName="SwivelPanelBack"
FrontElementName="SwivelPanelFront" />
</i:EventTrigger>
</i:Interaction.Triggers>
- Download the source code here
- Watch the demo here or watch it below … (just click the panel to see it in action)
UPDATE: I forgot to mention that I added a bit of code to this behavior based on a great behavior I saw from Joel Nuebeck. So I want to give him props for giving me the idea of making the 4 directional options so the developer can set the direction of the swivel. Originally I had forced the behavior to swivel in a specific direction. Thanks Joel!



8.08.2009 at 11:59 AM
So I have pretty bad implementation of the very same swivel. Yours is great.. I'm going through yours to see what you did... My question: Is there anyway to Start a Behavior from the code-behind. Say for instance a button is clicked and i want to validate some data, then Start the behavior if all is well.. I like reusability of a behavior but how can i Invoke it from the codebehind? Thanks!!
8.10.2009 at 11:27 AM
I have a ListBox where I perform some logic in the MouseButtonUp Event. In this logic there are situations where I want to execute the Flip behavior, how can I achieve this?
8.13.2009 at 9:09 AM
you're great n I can get some experience from u
thanks
8.13.2009 at 10:18 AM
John, this doesn't seem to work with a button on the front and a button on the back panel. Even in your demo it fails to turn the backpanel to the front.
Unfortunately I can't use the leftmousebuttonup event, because there is a listbox in the panel...
Can you confirm this? Or is it my mistake? And what would be a solution :)
Thx anyway for a great Behavior, I wish more like you would make my life as a designer easier.
8.13.2009 at 10:22 AM
Antoni - This behavior works with a single object to kick off both front and back flips. It does work with a single button ... usually I use a toggle button, but its not necessary. I do have a version that works with a button on front and back, I will share that when I get some time.
Is the only thing flipping the listbox? If so, then yeah, you'd have to use another event, or expand the panel it is in and use a button or some other kick off control.
8.13.2009 at 6:46 PM
John, I did use a Swivel behavior on both the front and the back panel. When I put it one on the parent with a LeftMouseButtonUp event, it worked as expected.
I've placed a button in the parent and use it's Click event to start the rotation. This works fine, but the button remains visible during the rotation. I am interested in your solution using two buttons in the panels...
Thx!
8.21.2009 at 11:51 PM
I wrote a similar effect for my book in a coding scenario for giving the user choice in viewing data visualizations.
One thing to note is that before the flip the element should almost "drop back" to 80% of the original size. This will make the corners of the flip not cut off (note how they do in your demo above)
I noticed the my iPhone did the same thing and it looks a little better...so that's where I got the idea from.