Defining
key frames with splines is easy in one sense—there are only four
numbers involved—but also hard: You need to approximate a certain
desired effect with a Bézier spline, and that’s not always obvious.
You might prefer
something more “canned” that gives you an overall impression of
adherence to physical law without requiring a lot of thought. This is
the purpose of the animation easing functions. These are classes that derive from EasingFunctionBase with common types of transitions that you can add to the beginning or end (or both beginning and end) of your animations. DoubleAnimation, PointAnimation, and ColorAnimation all have properties named EasingFunction of type EasingFunctionBase. There are also EasingDoubleKeyFrame, EasingColorKeyFrame, and EasingPointKeyFrame classes.
EasingFunctionBase defines just one property: EasingMode of the enumeration type EasingMode, either EaseOut (the default, which uses the transition only at the end of the animation), EaseIn, or EaseInOut. Eleven classes derive from EasingFunctionBase and you can derive your own if you want to have even more control and power.
The project named TheEasingLife lets you choose among the eleven EasingFunctionBase derivatives to see their effect on a simple PointAnimation involving a ball-like object. The content area is populated with two Polyline elements and a Path but no coordinates are supplied; that’s done in code.
Example 1. Silverlight Project: TheEasingLife File: MainPage.xaml (excerpt)
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <Polyline Name="polyline1" Stroke="{StaticResource PhoneForegroundBrush}" />
<Polyline Name="polyline2" Stroke="{StaticResource PhoneForegroundBrush}" />
<Path Fill="{StaticResource PhoneAccentBrush}"> <Path.Data> <EllipseGeometry x:Name="ballGeometry" RadiusX="25" RadiusY="25" /> </Path.Data> </Path> </Grid>
|
The Resources collection contains a Storyboard with a PointAnimation targeting the Center property of the EllipseGeometry. The PointAnimation is given a Duration property but nothing else:
Example 2. Silverlight Project: TheEasingLife File: MainPage.xaml (excerpt)
<phone:PhoneApplicationPage.Resources> <Storyboard x:Name="storyboard" Completed="OnStoryboardCompleted"> <PointAnimation x:Name="pointAnimation" Storyboard.TargetName="ballGeometry" Storyboard.TargetProperty="Center" Duration="0:0:2" /> </Storyboard> </phone:PhoneApplicationPage.Resources>
|
Notice that a handler is set for the Completed event of the Storyboard. This Completed event is defined by Timeline and is often convenient for letting a program know when an animation has completed.
The ApplicationBar has two buttons for “animate” and “settings”:
Example 3. Silverlight Project: TheEasingLife File: MainPage.xaml (excerpt)
<phone:PhoneApplicationPage.ApplicationBar> <shell:ApplicationBar> <shell:ApplicationBarIconButton IconUri="/Images/appbar.transport.play.rest. png" Text="animate" Click="OnAppbarPlayButtonClick" /> <shell:ApplicationBarIconButton IconUri="/Images/appbar.feature.settings.rest. png" Text="settings" Click="OnAppbarSettingsButtonClick" /> </shell:ApplicationBar> </phone:PhoneApplicationPage.ApplicationBar>
|
The coordinates for the two Polyline elements and EllipseGeometry are set during the Loaded event handler based on the size of the content panel. The ball is intended to be animated between a Polyline at the top and a Polyline at the bottom; the actual points are stored in the ballPoints array. The direction (going down or going up) is governed by the isForward field.
Example 4. Silverlight Project: TheEasingLife File: MainPage.xaml.cs (excerpt)
public partial class MainPage : PhoneApplicationPage { PointCollection ballPoints = new PointCollection(); bool isForward = true;
public MainPage() { InitializeComponent(); Loaded += OnMainPageLoaded; }
public EasingFunctionBase EasingFunction { get; set; }
void OnMainPageLoaded(object sender, RoutedEventArgs args) { double left = 100; double right = ContentPanel.ActualWidth - 100; double center = ContentPanel.ActualWidth / 2; double top = 100; double bottom = ContentPanel.ActualHeight - 100;
polyline1.Points.Add(new Point(left, top)); polyline1.Points.Add(new Point(right, top));
polyline2.Points.Add(new Point(left, bottom)); polyline2.Points.Add(new Point(right, bottom));
ballPoints.Add(new Point(center, top)); ballPoints.Add(new Point(center, bottom));
ballGeometry.Center = ballPoints[1 - Convert.ToInt32(isForward)]; } . . . }
|
Notice also the public property named EasingFunction. When you press the “animate” button, the Click handler fills in the missing pieces of the PointAnimation (including the EasingFunction property) and starts it going:
Example 5. Silverlight Project: TheEasingLife File: MainPage.xaml.cs (excerpt)
void OnAppbarPlayButtonClick(object sender, EventArgs args) { pointAnimation.From = ballPoints[1 - Convert.ToInt32(isForward)]; pointAnimation.To = ballPoints[Convert.ToInt32(isForward)]; pointAnimation.EasingFunction = EasingFunction;
storyboard.Begin(); }
void OnStoryboardCompleted(object sender, EventArgs args) { isForward ^= true; }
|
The Completed handler toggles the isForward value in preparation for the next animation.
When you press the “settings” button, the program navigates to the EasingFunctionDialog page that lets you choose which easing function you want:
Example 6. Silverlight Project: TheEasingLife File: MainPage.xaml.cs (excerpt)
void OnAppbarSettingsButtonClick(object sender, EventArgs args) { NavigationService.Navigate(new Uri("/EasingFunctionDialog.xaml", UriKind .Relative)); }
protected override void OnNavigatedFrom(NavigationEventArgs args) { if (args.Content is EasingFunctionDialog) { (args.Content as EasingFunctionDialog).EasingFunction = EasingFunction; } base.OnNavigatedTo(args); }
|
When the OnNavigatedFrom override determines that a transition from MainPage to EasingFunctionDialog page is in progress, it transfers the contents of its EasingFunction property to EasingFunctionDialog, which also has a public EasingFunction property.
The content area of the EasingFunctionDialog.xaml file just has a StackPanel in a ScrollViewer:
Example 7. Silverlight Project: TheEasingLife File: EasingFunctionDialog.xaml (excerpt)
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <ScrollViewer> <StackPanel Name="stack" /> </ScrollViewer> </Grid>
|
In its OnNavigatedTo override, the dialog uses reflection to fill up the StackPanel with RadioButton elements. By the time OnNavigatedTo is called, the EasingFunction property already has a valid value set by the OnNavigatedFrom override in MainPage:
Example 8. Silverlight Project: TheEasingLife File: EasingFunctionDialog.xaml.cs (excerpt)
public partial class EasingFunctionDialog : PhoneApplicationPage { public EasingFunctionDialog() { InitializeComponent(); }
public EasingFunctionBase EasingFunction { get; set; }
. . .
protected override void OnNavigatedTo(NavigationEventArgs args) { // Create "None" RadioButton RadioButton radio = new RadioButton(); radio.Content = "None"; radio.IsChecked = (EasingFunction == null); radio.Checked += OnRadioButtonChecked; stack.Children.Add(radio);
Assembly assembly = Assembly.Load("System.Windows");
// Create RadioButton for each easing function foreach (Type type in assembly.GetTypes()) if (type.IsPublic && type.IsSubclassOf(typeof(EasingFunctionBase))) { radio = new RadioButton(); radio.Content = type.Name; radio.Tag = type; radio.IsChecked = (EasingFunction != null && EasingFunction.GetType() == type); radio.Checked += OnRadioButtonChecked; stack.Children.Add(radio); } base.OnNavigatedTo(args); } . . . }
|
Notice how the Tag property of each RadioButton is a Type object indicating the EasingFunctionBase derivative associated with that button. When the user presses one of the RadioButton elements, that Tag property is used to make a new object of that type:
Example 9. Silverlight Project: TheEasingLife File: EasingFunctionDialog.xaml.cs (excerpt)
void OnRadioButtonChecked(object sender, RoutedEventArgs args) { Type type = (sender as RadioButton).Tag as Type;
if (type == null) { EasingFunction = null; } else { ConstructorInfo constructor = type.GetConstructor(Type.EmptyTypes); EasingFunction = constructor.Invoke(null) as EasingFunctionBase; } }
|
Finally, when you’re finished choosing the easing function you want, you press the Back button and the dialog’s OnNavigatedFrom override is called. This responds by storing the current selection back in MainPage:
Example 10. Silverlight Project: TheEasingLife File: EasingFunctionDialog.xaml.cs (excerpt)
protected override void OnNavigatedFrom(NavigationEventArgs args) { if (args.Content is MainPage) { (args.Content as MainPage).EasingFunction = EasingFunction; } base.OnNavigatedFrom(args); }
|
Keep in mind that these EasingFunctionBase derivatives have all default property settings, including the EasingMode property that restricts the effect only to the end of the animation. You’ll find that a couple of these effects—specifically BackEase and ElasticEase—actually
overshoot the destination. While this doesn’t matter in many cases, for
some properties it might result in illegal values. You don’t want to
set Opacity to values outside the range of 0 and 1, for example.