Animations in Silverlight for Windows Phone 7

Animations in Silverlight work by changing a particular property of a particular object, the animation classes are distinguished by the type of the property that they animate.

This chapter is taken from book "Programming Windows Phone 7" by Charles Petzold published by Microsoft press. http://www.charlespetzold.com/phone/index.html

Animations in Silverlight work by changing a particular property of a particular object, the animation classes are distinguished by the type of the property that they animate. Silverlight animations can target properties of type double, Color, Point, and Object.

Frame-Based vs. Time-Based

Suppose you want to write a little program that rotates some text using the CompositionTarget.Rendering event. You can pace this animation either by the rate that video hardware refreshes the display, or by clock time. Because each refresh of the video display is called a frame, these two methods of pacing animation are referred to as frame-based and time-based.

Here's a little program that shows the difference. The content area of the XAML file has two TextBlock elements with RotateTransform objects set to their RenderTransform properties, and a Button:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <
Grid.RowDefinitions>
        <
RowDefinition Height="*" />
        <
RowDefinition Height="*" />
        <
RowDefinition Height="Auto" />
    </
Grid.RowDefinitions>
    <
TextBlock Grid.Row="0"
               Text="Frame-Based"
               FontSize="{StaticResource PhoneFontSizeLarge}"
               HorizontalAlignment="Center"
               VerticalAlignment="Center"
               RenderTransformOrigin="0.5 0.5">
        <
TextBlock.RenderTransform>
            <
RotateTransform x:Name="rotate1" />
        </
TextBlock.RenderTransform>
    </
TextBlock>
    <
TextBlock Grid.Row="1"
               Text="Time-Based"
               FontSize="{StaticResource PhoneFontSizeLarge}"
               HorizontalAlignment="Center"
               VerticalAlignment="Center"
               RenderTransformOrigin="0.5 0.5">
        <
TextBlock.RenderTransform>
            <
RotateTransform x:Name="rotate2" />
        </
TextBlock.RenderTransform>
    </
TextBlock>
    <
Button Grid.Row="2"
            Content="Hang for 5 seconds"
            HorizontalAlignment="Center"
            Click="OnButtonClick" />
</
Grid>

The code-behind file saves the current time in a field and then attaches a handler for the CompositionTarget.Rendering event. This event handler is then called in synchronization with the video frame rate.

namespace FrameBasedVsTimeBased
{
    public partial class MainPage : PhoneApplicationPage
    {
        DateTime startTime; 
        public MainPage()
        {
            InitializeComponent(); 
            startTime = DateTime.Now;
            CompositionTarget.Rendering += OnCompositionTargetRendering;
        } 
        void OnCompositionTargetRendering(object sender, EventArgs args)
        {
            // Frame-based
            rotate1.Angle = (rotate1.Angle + 0.2) % 360; 
            // Time-based
            TimeSpan elapsedTime = DateTime.Now - startTime;
            rotate2.Angle = (elapsedTime.TotalMinutes * 360) % 360;
        }
        void OnButtonClick(object sender, RoutedEventArgs args)
        {
            Thread.Sleep(5000);
        }
    }
}

The rotation angle for the first TextBlock is increased by 0.2degree every frame. I calculated this by knowing that the phone display is refreshed at 30 frames per second. Multiply 30 frames per second by 60 seconds per minute by 0.2degree and you get 360degree.

The rotation angle for the second TextBlock is calculated based on the elapsed time. The TimeSpan structure has a convenient TotalMinutes property and this is multiplied by 360 for the total number of degrees to rotate the text.

Both work, and they work approximately the same:

fif1.gif

Click and Spin

Suppose you want to enhance a button to give some extra visual feedback to the user. You decide you actually want a lot of visual feedback to wake up a drowsy user, and therefore you choose to spin the button around in a circle every time it's clicked.

Here are a few buttons in a XAML file:

<
Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <
Grid.RowDefinitions>
        <
RowDefinition Height="*" />
        <
RowDefinition Height="*" />
        <
RowDefinition Height="*" />
    </
Grid.RowDefinitions>
    <Button Content="Button No. 1"
            Grid.Row="0"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            RenderTransformOrigin="0.5 0.5"
            Click="OnButtonClick">
        <
Button.RenderTransform>
            <
RotateTransform />
        </
Button.RenderTransform>
    </
Button>
    <
Button Content="Button No. 2"
            Grid.Row="1"
            HorizontalAlignment="Center
            VerticalAlignment="Center"
            RenderTransformOrigin="0.5 0.5"
            Click="OnButtonClick">
        <
Button.RenderTransform>
            <
RotateTransform />
        </
Button.RenderTransform>
    </
Button>
    <
Button Content="Button No. 3"
            Grid.Row="2"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            RenderTransformOrigin="0.5 0.5"
            Click="OnButtonClick">
        <
Button.RenderTransform>
            <
RotateTransform />
        </
Button.RenderTransform>
    </
Button>
</
Grid>

Each of the buttons has its RenderTransform property set to a RotateTransform, and its RenderTransformOrigin set for the element center.

The Click event handler is responsible for defining and initiating the animation that spins the clicked button. (Of course, in a real application, the Click handler would also perform something important to the program!) The handler begins by obtaining the Button that the user touched, and the RotateTransform associated with that particular Button:

namespace ClickAndSpin
{
    public partial class MainPage : PhoneApplicationPage
    {
        public MainPage()
        {
            InitializeComponent();
        } 
        void OnButtonClick(object sender, RoutedEventArgs args)
        {
            Button btn = sender as Button;
            RotateTransform rotateTransform = btn.RenderTransform as RotateTransform
            // Create and define animation
            DoubleAnimation anima = new DoubleAnimation();
            anima.From = 0;
            anima.To = 360;
            anima.Duration = new Duration(TimeSpan.FromSeconds(0.5)); 
            // Set attached properties
            Storyboard.SetTarget(anima, rotateTransform);
            Storyboard.SetTargetProperty(anima, new PropertyPath(RotateTransform.AngleProperty)); 
            // Create storyboard, add animation, and fire it up!
            Storyboard storyboard = new Storyboard();
            storyboard.Children.Add(anima);
            storyboard.Begin();
        }
    }
}

Getting the animation going requires three steps:

  1. Define the animation itself. The animation needed here will target the Angleproperty of aRotateTransform, and the Angle property is of typedouble, so that suggests a DoubleAnimation:

    DoubleAnimation anima = new DoubleAnimation();
    anima.From = 0;
    anima.To = 360;
    anima.Duration = new Duration(TimeSpan.FromSeconds(0.5));

    This DoubleAnimation will animate a property of typedouble from a value of 0 to a value of 360 in 1/2 second. TheDuration property of DoubleAnimation is of type Duration, and in code it is very common to set it from aTimeSpan object. But the Duration property is not itself of type TimeSpan primarily due to legacy issues. You can alternatively set the Duration property to the static Duration.Automatic value, which is the same as not settingDuration at all (or setting it to null), and which creates an animation with a duration of 1 second.
     

  2. Set the attached properties. TheDoubleAnimationmust be associated with a particular object and property of that object. You specify these using two attached properties defined by theStoryboard class:

    Storyboard.SetTarget(anima, rotateTransform);
    Storyboard.SetTargetProperty(anima, new PropertyPath(RotateTransform.AngleProperty));

    The attached properties areTarget and TargetProperty. As you'll recall, when you set attached properties in code, you use static methods that begin with the wordSet.

    In both cases, the first argument is theDoubleAnimation just created. The SetTargetcall indicates the object being animated (in this caseRotateTransform), and the SetTargetProperty call indicates a property of that object. The second argument of theSetTargetProperty method is of type PropertyPath, and you'll note that I've specified the fully-qualified dependency property for the Angle property of RotateTransform.
     

  3. Define, set, and start the Storyboard.

    At this point, everything seems to be ready. But there's still another step. In Silverlight, animations are always enclosed in Storyboard objects. A particular Storyboard can have multiple children, so it is very useful for synchronizing multiple animations. But even if you have just one animation running by itself, you still need a Storyboard:

    Storyboard storyboard = new Storyboard();
    storyboard.Children.Add(anima);
    storyboard.Begin();

    As soon as you call Begin on theStoryboard object, the clicked button spins around in half a second, giving the user perhaps a little too much visual feedback.

fif2.gif

Key Frame Animations

If you like the idea of giving the user some visual feedback from a button, but the 360 degree spin is just a bit too ostentatious, perhaps jiggling the button a little might be more polite. So you open a new project named JiggleButtonTryout and begin experimenting.

Let's start with just one Button with a TranslateTransform set to the RenderTransform property:

<
Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <
Button Content="Jiggle Button"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Click="OnButtonClick">
        <
Button.RenderTransform>
            <
TranslateTransform x:Name="translate" />
        </
Button.RenderTransform>
    </
Button>
</
Grid>

The code-behind file starts the animation when the button is clicked:

void OnButtonClick(object sender, RoutedEventArgs args)
{
    jiggleStoryboard.Begin();
}

To bring the time values down to something reasonable, I want to show you a little trick. Often when you're developing animations you want to do run them very slowly to get them working correctly, and then you want to speed them up for the final version. Of course, you could go through and adjust all the KeyTime values, or you could simply specify a SpeedRatio on the animation, as in the version of the animation in the JiggleButtonTryout project:

<phone:PhoneApplicationPage.Resources>
    <
Storyboard x:Name="jiggleStoryboard">
        <
DoubleAnimationUsingKeyFrames Storyboard.TargetName="translate"
                                       Storyboard.TargetProperty="X"
                                       RepeatBehavior="3x"
                                       SpeedRatio="40">
            <
DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0" />
            <
LinearDoubleKeyFrame KeyTime="0:0:01" Value="-10" />
            <
LinearDoubleKeyFrame KeyTime="0:0:03" Value="10" />
            <
LinearDoubleKeyFrame KeyTime="0:0:04" Value="0" />
        </
DoubleAnimationUsingKeyFrames>
    </
Storyboard>
</
phone:PhoneApplicationPage.Resources>

Each cycle of the key frames requires 4 seconds; this is repeated 3 times for a total of 12 seconds, but the SpeedRatio value of 40 effectively speeds up the animation by a factor of 40 so it's only 0.3 seconds total.

Trigger on Loaded

The Windows Presentation Foundation has somewhat more flexibility than Silverlight in defining and using animations. WPF includes objects called triggers, which respond to event firings or to changes in properties and which can start animations going entirely in XAML, eliminating the need for the code-behind file to start the Storyboard. Triggers are largely gone from Silverlight-mostly replaced by the Visual State Manager that I'll discuss in the next chapter.

However, one trigger remains in Silverlight. This is a trigger that responds to the Loaded event. This allows you to define an animation entirely in XAML that automatically starts up when the page (or another element) is loaded.

The FadeInOnLoaded project contains the following XAML near the bottom of the page, right above the PhoneApplicationPage end tag. This is the traditional spot for event triggers:

<phone:PhoneApplicationPage.Triggers>
    <
EventTrigger>
        <
BeginStoryboard>
            <
Storyboard>
                <
DoubleAnimation Storyboard.TargetName="TitlePanel"
                                 Storyboard.TargetProperty="Opacity"
                                 From="0" To="1" Duration="0:0:10" />
            </
Storyboard>
        </
BeginStoryboard>
    </
EventTrigger>
</
phone:PhoneApplicationPage.Triggers>

The markup begins with a property-element tag for the Triggers property defined by FrameworkElement. The Triggers property is of type TriggerCollection, which sounds quite extensive and versatile, but in Silverlight the only thing you can put in there is an EventTrigger tag that is always associated with the Loaded event. This next tag is BeginStoryboard. This is the only place you'll see a BeginStoryboard tag in Silverlight. And now we get to something familiar: A Storyboard with one or more animations that can target any dependency object of any object on the page.

This one targets the Opacity property of the TitlePanel, which is the StackPanel containing the two titles at the top of the page. I made the animation 10 seconds long so you don't miss it. As the page is loaded, the titles fade into view:

fif3.gif