Swivel Behavior in Silverlight 3

Friday, August 07 2009 - , ,

A 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:

image

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>

 

 



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!

17 comment(s)

Pingback from Swivel Behavior in Silverlight 3 : JohnPapa.net

Thank you for submitting this cool story - Trackback from DotNetShoutout

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!!

Pingback from Links (8/9/2009) « Steve Pietrek – Everything SharePoint

A while back I created a pair of Storyboards that make a pair of panels swivel to show a front side and

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?

Pingback from links for 2009-08-10 › Life – Me, Myself, and We

you're great n I can get some experience from u

thanks

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.

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.

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!

Last week I posted the code for a swivel behavior I created . It rotates a set of panels 180 degrees. The next step in the rotation process is to add a button that causes the rotation from front to back and then from back to front. This post will talk

Last week I posted the code for a swivel behavior I created . It rotates a set of panels 180 degrees

Thank you for submitting this cool story - Trackback from progg.ru

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.

Last week I posted my swivel behavior on my web site and today I uploaded it to the Expression Gallery. If you are working on Silverlight and are not aware of the Expression Gallery, you need to check it out. There are some great community contributions

Last week I posted my swivel behavior on my web site and today I uploaded it to the Expression Gallery