Suppose you have a program with a button, and when the user clicks that button, you want the button to increase in size. You would prefer that the button not just jump from one size to a larger size. You want the button to increase in size smoothly. In other words, you want the increase in size to be animated. I'm sure there are some who would insist that buttons really shouldn't do such things, but their protests will be ignored in this chapter as the rest of us explore the animation facilities of the Microsoft Windows Presentation Foundation. In the early chapters of this book, I showed you how to use DispatcherTimer to implement animation. Increasing the size of a button based on timer ticks is fairly easy.
The button has an initial FontSize of 12 device-independent units. When clicked, the event handler creates a DispatcherTimer that generates Tick events every tenth second. The TimerOnTick method increases the FontSize by two units every tenth second until the size reaches 48 units, at which point the button is restored to its original size and the timer is stopped. It's easy to calculate that the whole process takes about 1.8 seconds: just multiply the timer period by the difference in the initial and final FontSize values, divided by the increment to the FontSize property. What happens if you click the button a second time while it's expanding? The Click event handler creates a second timer, so the button's size increases at twice the speed. When one of the two timers increases the size to 48 units, that Tick handler restores the button's size and stops that timer, but the other timer increases the button to 48 units again. Of course, such behavior is easy to avoid if you set a Boolean variable when the timer is going and don't start a second timer when that variable is set. A couple of features of the WPF help make this animation a success. The retained graphics system is designed so that visual objects can change size or position without flickering or causing excessive redrawing. The dependency property system ensures that changing just the FontSize property of the button will make the button larger to accommodate the new size. Although you can always fall back on using DispatcherTimer if a particular animation requires a lot of custom code, for many animations you can use the animation facilities built into the WPF. Most of the animation-related classes are defined in the System.Windows.Media.Animation namespace. Although this namespace contains almost 200 classes, very many of them fall into just a few categories. WPF animations always target dependency properties. The System.Windows.Media.Animation namespace has many classes because different classes exist for animating properties of various types. In alphabetical order, the 22 animatable types are Boolean, Byte, Char, Color, Decimal, Double, Int16, Int32, Int64, Matrix, Object, Point, Point3D, Quaternion, Rect, Rotation3D, Single, Size, String, Thickness, Vector, and Vector3D. The FontSize property is of type double, so to animate the FontSize property, you can use a double animationan animation that changes the size of a dependency property of type double. Properties of type double are very common throughout the WPF, and consequently, the double is probably the most common animated type. There are actually three classes that implement double animations, as shown in the following partial class hierarchy: Object For each of the 22 data types that you can animate, there exists an abstract class that begins with the data type followed by AnimationBase, such as DoubleAnimationBase. I'll refer to these classes collectively as <Type>AnimationBase. (But don't assume from my syntax that these classes are implemented as generics; they are not.) For all 22 data types, there also exists a <Type>AnimationUsingKeyFrames class. All but five types have a <Type>Animation class, and only three types have a <Type>AnimationUsingPath class. In total, there are 64 classes of <Type>AnimationBase and its derivatives. The most straightforward of the classes that derive from DoubleAnimationBase is the DoubleAnimation class, which simply changes a property of type double linearly from one value to another. Here's a program that uses a DoubleAnimation object to mimic the behavior of the EnlargeButtonWithTimer program.
The constructor creates the Button object in the same way as the previous program, but the Click handler creates an object of type DoubleAnimation. Every animation has a duration, which you specify with a Duration structure, generally based on a TimeSpan object. In this case, the duration of the animation is set to two seconds. The ButtonOnClick handler then sets the From and To properties of the DoubleAnimation, indicating that this animation will change a double property from an initial value of initFontSize (or 12) to a final value of maxFontSize (or 48). The animation essentially performs a linear interpolation between the two values to determine how large the FontSize of the button should be at any particular point in time during the animation. Setting the From property isn't required in this example because the button's FontSize is initially 12 anyway. But if the From property indicated a value different from the value already set on the element being animated, the animation would begin by jumping to the value specified by the From property. The FillBehavior property indicates what happens when the double reaches its value of 48 and the animation ends. The default value is FillBehavior.HoldEnd, which means that the value remains at 48. The FillBehavior.Stop option causes the double property to go back to its initial pre-animation value. The BeginAnimation method is defined by UIElement (and a few other classes) and, of course, inherited by Button. The first argument is the dependency property being animated, and the second argument is the DoubleAnimation object to animate that property. Rather than calling the BeginAnimation method on the Button object, you could call BeginAnimation for the window: BeginAnimation(Window.FontSizeProperty, anima); This animation would change the FontSize of the Window, and the Button would then inherit that animated FontSize through normal property inheritance. Any other child of the window would also inherit that FontSize. In the complete scheme of dependency properties, animations have the highest priority in setting a property. Rather than calling BeginAnimation, the program could kick off the animation with a call to ApplyAnimationClock, another method defined by UIElement: btn.ApplyAnimationClock(Button.FontSizeProperty, anima.CreateClock()); Notice that the second argument is an object of type AnimationClock returned from the CreateClock method that is defined by AnimationTimeline and inherited by DoubleAnimation. An object of type Timeline represents a period of time. This is the class that defines the Duration and FillBehavior properties, and some other time-related properties that I'll discuss shortly. The AnimationTimeline class derives from Timeline and defines the CreateClock method. The clock is the mechanism that paces the animation. The DoubleAnimation class itself defines the From and To properties, which are of type double. The two C# programs shown so far begin the animation in response to a Click event from a Button. However, the animation could be started based on other criteria. For example, you could set a timer that checks the time of day and start an animation at the stroke of midnight. Or a program might use the FileSystemWatcher class to monitor the file system and start an animation whenever a new directory is created. Or a program might display an animation based on information obtained from a second thread of execution in the program. Animations defined in XAML, however, are always associated with triggers. You are familiar with triggers and trigger collections from Chapters 24 and 25. The Style, ControlTemplate, and DataTemplate classes all define properties named Triggers that are collections of TriggerBase objects. In Chapter 24, I discussed four of the classes that derive from TriggerBase. These are Trigger, MultiTrigger, DataTrigger, and MultiDataTrigger. As you'll recall, these triggers have the power to change properties of elements based on changes in other properties or changes in data bindings. Chapter 24 did not discuss the fifth class that derives from TriggerBase, which is EventTrigger, because this trigger is used mostly in connection with animations. (It can also trigger sounds.) This is the class you generally use in XAML in connection with animations, although you can also start an animation based on other types of triggers. You might have discovered on your own that FrameworkElement defines a property named Triggers that is a collection of Trigger objects. I didn't discuss this Triggers collection in earlier chapters because it is very special. The Triggers collection defined by FrameworkElement can contain only EventTrigger objects, and you can't do much with EventTrigger objects except to trigger animations or play sounds. EventTrigger defines three properties. The SourceName property is a string that refers to an element's Name or x:Name attribute. This is the element with the event that triggers the animation. (In common practice, the SourceName property is often implicitly defined by the context of EventTrigger and doesn't need to be explicitly specified.) The RoutedEvent property of EventTrigger is the particular triggering event. EventTrigger also defines an Actions property that indicates what happens when that event occurs. The Actions property is a collection of TriggerAction objects, and the most important class that derives from TriggerAction is BeginStoryboard. A Triggers section of a Button control might look something like this: <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <BeginStoryboard ... > ... </BeginStoryboard> </EventTrigger> </Button.Triggers> Animations in XAML always involve storyboards, which are objects that extend Timeline to provide information about property targeting. (You can also use storyboards in code, but as you've seen, you're not required to use them.) The child of BeginStoryboard is often a Storyboard element. Like DoubleAnimation, Storyboard derives from Timeline, as the following class hierarchy indicates: Object The hierarchy is complete from Timeline on down, except for the ellipsis, which encompasses all of the various animation classes for all 22 animatable data types. The Storyboard class has the ability to consolidate multiple timelines and also has attached properties to indicate the name and property of the element to which the animation applies. (Sometimes the element name is implied from the context.) The TimelineGroup class defines a Children property for a collection of timelines so that a DoubleAnimation can be a child of a Storyboard. Here's typical and more complete animation markup that goes all the way down to the DoubleAnimation object: <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <BeginStoryboard ... > <Storyboard ... > <DoubleAnimation ... /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> These layers of markup might seem to form an excessively deep hole for the DoubleAnimation to reside in, but nothing is superfluous. The Button.Triggers section can have multiple EventTrigger elements. Often the trigger action is a BeginStoryboard object, but it can also be a SoundPlayerAction or a ControllableStoryboardAction. BeginStoryboard always has a Storyboard child, but that Storyboard can have multiple animations as children. Here is the XAML equivalent of the animation that enlarges the button when you click it.
Notice that the Triggers section is part of the Button control, so the source element for the EventTrigger is implicitly the Button itself. The triggering event is Click. The Storyboard element refers to the TargetProperty of the animation. This is the property that the animation animates, and it must be a dependency property. TargetProperty is more than just a property of Storyboard. It is an attached property of Storyboard, and the property alternatively can be moved to the DoubleAnimation element where it appears as Storyboard.TargetProperty: <Storyboard> <DoubleAnimation Storyboard.TargetProperty="FontSize" ... /> </Storyboard> Storyboard also defines an attached property of TargetName so that different animations can target different elements in the XAML file. Both the Storyboard.TargetName and Storyboard.TargetProperty attached properties are usually defined in children of the Storyboard element, but if all the child animations within a Storyboard have the same TargetName or TargetProperty, you can transfer the attribute to the Storyboard element, as I've done in the EnlargeButtonInXaml.xaml file. Here's a variation of the previous XAML file that has two buttons in a StackPanel. They are given Name attributes of "btn1" and "btn2."
Two EventTrigger elements are consolidated in the Triggers section of the Page rather than the Triggers section of the Button. Because these EventTrigger elements are actually concerned with Button events, they require a SourceName attribute to indicate the element generating the event. Each DoubleAnimation also has a Storyboard.TargetName attribute to indicate the control being animated. In an early version of this program, clicking a button caused that button to be enlarged, but I decided it was more interesting to switch the targets to that clicking a button causes the other button to expand. The animation occurs over a period of time indicated by Duration, which commonly has a format of hours:minutes:seconds. Fractional seconds are allowed. For example, here's how you set the duration to 2.5 seconds: Duration="0:0:2.5" If you have long animations, you can simplify this syntax. The following attribute indicates three minutes: Duration="0:3" This next one is seven hours: Duration="7" Try it! (You don't have to let the animation continue to the end if you don't want to.) For durations of 24 hours or longer, you specify a number of days followed by a period, followed by the hours. The duration that follows is 4 days, 12 hours, and 7 minutes: Duration="4.12:7" If you leave out the Duration attribute or set it to the string "Automatic", the animation will run for one second. (In other contexts, it is also possible to set Duration to the string "Forever", but that option doesn't work in this particular context.) The two XAML animations I've shown you change the FontSize property of the Button from a starting value to an ending value. The From and To attributes of DoubleAnimation indicate these values. If you leave out the From attribute, the animation will work pretty much the same because the FontSize of the Button is initially set to 12. (The difference occurs when you click the button in the middle of an animation. With the From attribute present, the animation begins again at 12; without this attribute, the animation continues from the value at the time you click the button.) You can set the ending value to something less than the starting value: From="48" To="12" In this case, the animation begins by giving the button a FontSize of 48, which then decreases to 12. An alternative to To is the By attribute: By="100" With By, the ending value is the starting value plus the By amount. If you use By by itself, the animation changes the button's font size from 12 to 112. The To and By attributes are mutually exclusive; if you include both, the By attribute is ignored. This combination of attributes causes the FontSize to change from 50 to 100: From="50" To="100" With the following attributes, FontSize changes from 50 to 150: From="50" By="100" You can have a From attribute by itself with no To or By attributes: From="50" Now the animation changes the FontSize property from 50 to 12, which is the value specified in the Button element. If the Button element had no explicit FontSize attribute, the animation would change the property from 50 to the default FontSize value for the Button (typically 11). If the animation includes From and To attributes, or From and By attributes, you can also include the IsAdditive attribute. Its default value is false; if you set it to true, the values specified in the From, To, and By attributes are added to the initial value of the target property. Here's an example: From="50" To="100" IsAdditive="True" If the initial value of FontSize is 12, the animation changes FontSize from 62 to 112. With the following markup with a By attribute instead of To, FontSize changes from 62 to 162: From="50" By="100" IsAdditive="True" So far, as you've seen, the FontSize snaps back to its starting value after the animation has completed. That's a result of setting the FillBehavior property to the enumeration value FillBehavior.Stop. The default value is FillBehavior.HoldEnd, which causes the FontSize to remain at the final value of the animation. If you've been experimenting with either EnlargeButtonInXaml.xaml or EnlargeButtonsInXaml.xaml, return to the file in its pristine state so that the DoubleAnimation element causes the FontSize to change from 12 to 48 over two seconds. You can set the AutoReverse property of DoubleAnimation to play the timeline in reverse after it's played forward: AutoReverse="True" Now the animation increases the FontSize from 12 to 48 over two seconds and then decreases FontSize from 48 back to 12 over two seconds. The Duration attribute indicates the time for just a single forward play of the Timeline. So far, all the animations have run just once. You can change that by setting the RepeatBehavior attribute. Here's how you make the timeline play three times: RepeatBehavior="3x" This is called an iteration count. It indicates the total number of times the timeline is to play. (A value of 1x is the default.) The Duration attribute indicates the time for playing the timeline once. If you have the Duration attribute set to 0:0:2, AutoReverse set to true, and RepeatBehavior set to 3x, the entire animation lasts 12 seconds and the final FontSize value is 12. If you leave out the AutoReverse attribute or set it to false, the animation runs for six seconds, and the final value is 48 before the FillBehavior setting causes it to snap back to 12. You can set RepeatBehavior to fractional iteration counts: RepeatBehavior="1.5x" If AutoReverse is false, the animation last three seconds, and the value when the animation ends is 30. (First the FontSize increases from 12 to 48, and then it begins increasing from 12 again but stops halfway through.) If AutoReverse is set to true, the animation lasts six seconds. The FontSize goes from 12 to 48 and then back to 12 (that's one iteration) and then from 12 to 48 (that's half of the second iteration). Another approach is setting RepeatBehavior to a duration of time: RepeatBehavior="0:0:10" If AutoReverse is false, the animation changes the FontSize from 12 to 48 five times. If AutoReverse is true, the animation changes the FontSize from 12 to 48 and back to 12 twice, and then from 12 to 48. The duration specified in RepeatBehavior can be less than that in the Duration attribute, in which case the timeline doesn't finish playing once. And then there is the ever-popular animation that continues forever or until boredom kicks in: RepeatBehavior="Forever" FillBehavior has no effect if RepeatBehavior is Forever. When you set RepeatBehavior to play the timeline more than once, you can also set IsCumulative to true. Each iteration causes the values to accumulate. For example, consider the following markup: <DoubleAnimation From="12" To="24" Duration="0:0:2" AutoReverse="true" RepeatBehavior="3x" IsCumulative="true" /> The first iteration goes from 12 to 24 and back to 12. The second iteration is from 24 to 36 and back to 24. The third iteration takes the values from 36 to 48 and back to 36. If you want a delay between the event that triggers the animation and the time at which the timeline begins, you can specify that as the BeginTime attribute: BeginTime="0:0:2" Now there's a delay of two seconds before the timeline begins. After that, the BeginTime has no effect on repetitions and reversals. You can set BeginTime to a negative value to start the animation at a point beyond its actual beginning. For example, in the current examples, if you set BeginTime to 0:0:1, the animation begins with a FontSize of 30. You can also set CutoffTime to end an animation at a particular time. CutoffTime encompasses the duration you set in BeginTime. Because an animation can last longer (or shorter) than the time you specify in the Duration property, I'll be using the term "total duration" to indicate the overall length of the animation as it's affected by BeginTime, CutoffTime, RepeatBehavior, and AutoReverse. Storyboard derives from TimelineGroup, which defines a property named Children of type Timeline. This means that Storyboard can host multiple animations. All animations that share the same Storyboard are triggered by the same event. The following stand-alone XAML file uses Ellipse to display a ball that is initially in the upper-left corner of a Canvas panel. The EventTrigger is based on the MouseDown event of the Ellipse. The Storyboard contains two DoubleAnimation objects. One animation changes the Canvas.Left attached property to make the ball move back and forth horizontally. The second animation changes the Canvas.Top attached property to make the ball move down and then back up. Notice that both DoubleAnimation elements include attributes for the Storyboard.TargetProperty attached property, and that the target properties of these two animations are themselves attached properties (Canvas.Left and Canvas.Top), so they must be enclosed in parentheses.
Click the Ellipse to start the animation. The horizontal back-and-forth motion has a Duration setting of one second, whereas the up-and-down motion has a Duration of five seconds. Both have AutoReverse set to true. The total duration of the first animation is two seconds; the second is 10 seconds. These two animations begin at the same time. The composite animation has a total duration that is equal to the longest total duration of its components. That's 10 seconds. What you'll see is the ball begin moving both down and to the right. After one second, the ball stops moving right and reverses itself to start moving left. When it reaches a horizontal position of 0, that first animation has concluded. The second animation continues moving the ball down and then back up. If you want these two animations to have different Duration settings but to end at the same time, you need to set the RepeatBehavior property of the shorter animation. Here's a similar program, except that the horizontal motion has a Duration setting of one-fourth of a second. The vertical motion is five seconds. Both have AutoReverse set to true. For both animations to end at the same time, the first animation must have a RepeatBehavior setting of 20x.
Alternatively, you can set the RepeatBehavior of the first animation to a period of time. Because the second animation runs a total of 10 seconds, this would be as follows: RepeatBehavior="0:0:10" In either case, after the ball completes its 20 back-and-forth trips and its single up-and-down trip, the animation stops. If you want that show to run three times in a row, you can set the RepeatBehavior of the first animation to 60x and the RepeatBehavior of the second animation to 3x, or you can include a RepeatBehavior attribute right in the Storyboard element that applies to the two DoubleAnimation elements: <Storyboard RepeatBehavior="3x"> You can also set that RepeatBehavior to Forever. Or you can leave out the RepeatBehavior in the Storyboard and set the RepeatBehavior to Forever in both DoubleAnimation elements. If you decide that you'd like to speed up the animation by 50 percent, you don't need to change the Duration settings in the individual DoubleAnimation elements. You can just set the SpeedRatio property in the Storyboard: <Storyboard SpeedRatio="1.5"> This feature is ideal for debugging animations, particularly when you are dealing with parallel animations. You can initially set Duration values so the animations run very slowly for debugging, and then you can eventually speed everything up by setting a SpeedRatio to apply to the composite animation. All sibling animations begin at the same time. If that's not what you want, you need to use the BeginTime property to delay the start of an animation. In some cases, you might have a group of parallel animations that, when concluded, should be followed by another group of parallel animations. You can use the same BeginTime property on all the animations of the second group so that they follow the completion of the first group, or you can enclose the second group of animations in a ParallelTimeline element and set the BeginTime just for that element. Here's an example.
The idea here is that after the ball returns to its original position, two DoubleAnimation elements quickly increase the Width and Height of the ball to 5 inches. Then the ball "explodes" and returns to its original size. The two DoubleAnimation objects that increase the size are grouped in a ParallelTimeline element. This ParallelTimeline has a BeginTime of 10 seconds, which is the total duration of the first two animations. The ParallelTimeline element also sets the FillBehavior to Stop. Both the BeginTime and the FillBehavior apply to the two children of the ParallelTimeline. These two properties could have been included in both DoubleAnimation children, but consolidating them in the ParallelTimeline element makes the program easier to understand and change. So far, most of the programs I've shown have altered an element or control in response to an event on that element or control. One element's events can also trigger animations in other elements. The following stand-alone XAML file displays a Page with three radio buttons labeled red, green, and blue. Clicking any button changes the background color of the Page. This program uses ColorAnimation elements, which work pretty much the same as DoubleAnimation except that the From, To, and By properties are of type Color, and the TargetProperty must be of type Color.
The Page root element is assigned a Name property of page, and each of the ColorAnimation elements refers to that target. The TargetProperty is the Color property of the Background property of the Page, under the assumption that this Background property is a SolidColorBrush object, which is not normally the case. For this markup to work, the Page needs to be initialized with a SolidColorBrush, as it is in the root element. The animations have no From property, so the background color changes from the current color to the color specified as To over a period of one second. The quantity of repetitious markup here will probably convince you to implement these radio buttons in code if you want to have more than just a couple of them. Why didn't I use a Style to reduce the repetitious markup in ColorRadiusButtons.xaml? It is certainly possible to include animations in a Triggers section of a Style definition, but the animations in the Style cannot target an element other than the one being styled. Here's a stand-alone XAML file that includes a Style for Button elements. The Style includes a couple of Setter elements and, within the Triggers section, two EventTrigger elements. The first is triggered by the MouseEnter event, and the second by the MouseLeave event. Both start a DoubleAnimation that changes the FontSize of the Button.
These two EventTrigger elements cause the buttons to exhibit a "fish-eye" effect when the pointer passes over them. As the mouse pointer rests on a button, the button gradually grows to three times its normal size. When the pointer leaves, the button quickly returns to its normal size. As you'll note, if you move the pointer over a button and then pull it away quickly, the button won't go to the full size of 36 specified in the first DoubleAnimation. This is a result of the default HandoffBehavior property defined by BeginStoryboard. This property governs the interaction of two different animations that affect the same property. The default setting for HandoffBehavior is the enumeration value HandoffBehavior.SnapshotAndReplace, which stops the previous animation, takes a "snapshot" of its current value, and then replaces the animation with the new one. To see an alternative approach, change the BeginStoryboard element in the MouseLeave section to have a HandoffBehavior setting of HandoffBehavior.Compose: <BeginStoryboard HandoffBehavior="Compose"> Also, change the Duration to one second. Now when you move the pointer over a button and then pull it away, the button continues to expand a bit before the second animation kicks in. When you define an animation in an element, it needs to be triggered by an EventTrigger in the Triggers section of that element. In a Style, however, you don't need to trigger the animation with EventTrigger. Here's an alternative approach to the fish-eye buttons that triggers the animation with a regular Trigger for the property IsMouseOver.
In this case, it's necessary for BeginStoryboard to be a child of the EnterActions and ExitActions collections that are defined by TriggerBase. Changing the size of a Button by changing its FontSize is convenient, but it's not a general solution to changing the size of elements. In the general case, you'll probably want to set a transform for the element and then animate that transform. Animating transforms is particularly handy when you are implementing animated rotation. You may wonder about the propriety of a Button that rotates, but consider a program in which it might be useful to deliver a little feedback after a button click to assure the user that the program has recognized the click. You could, for example, use rotation to "shake" the button a bit. When animating transforms, you can make the TargetName either the element that contains the transform, or the transform itself. Generally, you animate RenderTransform rather than LayoutTransform. The TargetProperty of the animation refers to a property of the RenderTransform of the element being animated. By default, the RenderTransform property is a MatrixTransform object, so if you don't want to animate individual properties of a Matrix structure, you must first set RenderTransform to a particular type of transform (for example, RotateTransform) and then animate a property of that transform. The following XAML file defines a Style for a Button, where the RenderTransform property of the Button is set to a RotateTransform object with default settings (that is, no rotation). In addition, the RenderTransformOrigin is set to the value (0.5, 0.5), so any rotation occurs around the center of the button.
The TargetProperty of the animation is RenderTransform.Angle, so obviously the animation is assuming that RenderTransform has already been set to an object of type RotateTransform. Over the course of 0.05 seconds, the rotation angle changes from 5 degrees to 5 degrees. It's then reversed and repeated twice more. If you slowed down this animation, you would see that the button initially jumps from its normal position to a rotation of 5 degrees, and at the end jumps back. At high speeds, this is imperceptible, but in other cases, you might want to first animate the button from 0 degrees to 5 degrees, then move it repeatedly between 5 degrees and 5 degrees, and conclude with a third animation that moves it gracefully from 5 degrees back to 0 degrees. This is a job for a key-frame animation, which I'll discuss later in this chapter. An animation can be controlled while in progress by making use of classes that derive from ControllableStoryboardAction. The following class hierarchy shows how these classes fit in with the other derivatives of TriggerAction. Object To use these classes, you set up an animation normally with BeginStoryboard, and you assign BeginStoryboard a Name attribute. ControllableStoryboardAction defines a property named BeginStoryboardName that you use to refer to this name. Like BeginStoryboard, these derivatives of ControllableStoryboardAction are defined as part of a TriggerAction collection. Here's a stand-alone XAML file with a Rectangle that has two RotateTransform objects definedone centered around its lower-left corner and the other centered around its lower-right corner. The storyboard (defined normally except that the BeginStoryboard has its Name attribute defined) uses a DoubleAnimation to change the angle of the first RotateTransform from 90 degrees to 0 degrees, and then a second DoubleAnimation changes the angle of the second RotateTransform from 0 degrees to 90 degrees, making it appear as if the rectangle stands up, and then falls down in the other direction.
Six buttons sit under the rectangle, each of which is given a Name. The StackPanel.Triggers section contains EventTrigger elements associated with the Click events for each of these buttons. The Begin button starts the animation like normal. The Pause button triggers the PauseStoryboard action, which pauses the animation. The Resume button resumes it. The Stop button returns the rectangle to its pre-animated state. The button labeled Skip To End triggers the SkipStoryboardToFill action, which skips to the end of the animation and whatever the FillBehavior has in store. The SeekStoryboard action can go to an arbitrary point in the animation. I chose to make it go to the middle. I originally defined a Triggers section for each of the buttons and put the individual EventTrigger elements in these Triggers sections, but I couldn't get the program to work that way. Apparently, all the EventTrigger objects that begin and control the storyboard must be consolidated in the same Triggers section. In many XAML demonstration programs, an animation begins as soon as the program is loaded and continues until the program is terminated. In these files, the Loaded event triggers the animation. This trigger can be the Loaded event for the element being animated or the Loaded event for another element, perhaps a Panel or the Page. The following stand-alone XAML file is an animated version of the RenderTransformAndLayoutTransform.xaml file from the previous chapter. It demonstrates why you should usually animate RenderTransform to cause as little disruption to the element's environment as possible, unless you actually want the animated element to affect layout, in which case you should definitely animate LayoutTransform.
As you might recall from the nonanimated version of this program, two 3-by-3 UniformGrid panels are filled with buttons, and the middle button of each is rotated, the first with RenderTransform and the second with LayoutTransform. In this version, the two angles of rotation are animated. The root element is a StackPanel, and the two animations are included in the Triggers section for the StackPanel, with a triggering event of Loaded. The target properties of the animations are the transforms themselves, which have both been given x:Name attributes. The TargetProperty is just Angle. The animations continue forever. When you're using a program like XAML Cruncher to experiment with an animation triggered by a Loaded event, keep in mind that the animation begins anew when the file passes through the XamlReader.Load method and the elements are recreated. In XAML Cruncher, this happens with each editor keystroke that alters the text of the file. You might instead want to suspend automatic parsing and press F6 to parse manually and begin the animation. Animations that start automatically and continue forever can be fun and educational as well. The following stand-alone XAML file defines a single Path based on two EllipseGeometry objects, the second of which is rotated by the animation. As the first circle meets up with the second circle, it provides a dramatic visual reminder of the default FillRule of EvenOdd.
The following XAML program has a TextBlock with both a ScaleTransform and a RotateTransform defined in a TransformGroup. The ScaleX property of the ScaleTransform is animated between 1 and 1 to flip the text around the vertical axis, almost simulating text rotating in three dimensions around a vertical axis. The RotateTransform rotates the text around its center. The two animations have different durations and repeat forever for a mix of effects.
So far I've shown programs that use DoubleAnimation and ColorAnimation. Another common type of animation is PointAnimation, which is an animation between two Point objects. The following stand-alone XAML program defines a Path made of four Bezier splines.
The 13 PointAnimation objects change the 13 points that make up the figure. (The last point is the same as the first.) The From values define a circle centered on the point (0, 0), with a radius of 100. The To values define a square with sides of 177 units, so that the square occupies approximately the same area as the circle. The animation shifts between the circle and the square. As defined by the To values, the corners of the square are at the left, top, right, and bottom, so the figure is more like a diamond shape. I decided I wanted the sides horizontal and vertical, but instead of changing all the coordinates, I simply added a RotateTransform to the Path to rotate the figure 45 degrees. The RenderTransform for the Canvas moves the origin to the point (300, 300) and doubles the size of the figure. DrawingContext is the class you use for drawing when you override the OnRender method defined by UIElement and also when you create objects of type DrawingVisual. Many of the drawing methods defined by DrawingContext have overloads that include AnimationClock arguments. In this way you can define elements that intrinsically animate themselves, potentially forever. Here's an example of a class that derives from FrameworkElement and overrides OnRender.
You could put an instance of such a class in a XAML file or in code. I made it part of the following two-file project named RenderTheAnimation that also includes this C# file.
The next stand-alone XAML file is an animation of an Ellipse that is triggered by the Loaded event of the Ellipse itself. The animation has a TargetProperty of Width, but the Height property is a Binding to the Width property, so both properties change in the same way.
The DoubleAnimation has a BeginTime of one second, so the animation doesn't do anything during that time. The animation then increases the Width (and, through the binding, also the Height) quickly from one-half inch to three inches, snapping back to one-half inch. The RepeatBehavior setting makes this happen twice. The Storyboard itself also has a RepeatBehavior setting of Forever so that the DoubleAnimation is repeated indefinitely. For each repetition, the BeginTime delays the two pulses for one second, perhaps giving an overall effect of a heartbeat. Here's an irritating little visual of a Rectangle that's animated in several ways. Two DoubleAnimation objects increase the Width and Height of the Rectangle. At the same time, two other DoubleAnimation objects decrease the Canvas.Left and Canvas.Top attached properties so that the Rectangle seems to stay in place. Two PointAnimation objects change the StartPoint and EndPoint properties of the LinearGradientBrush. These two points are initially the upper-left and lower-right corners of the rectangle, but they move to the upper-right and lower-left. Finally, two ColorAnimation objects swap the two colors used in the gradient brush.
These eight animation objects have three different Duration settings, and all have RepeatBehavior set to Forever. You might be tempted to set the RepeatBehavior for the Storyboard to Forever and to remove the individual RepeatBehavior attributes, but that wouldn't give the same effect. What would be repeated would be an animation that lasted 10 seconds (the total duration of the longest animation object taking AutoReverse into account). Animations with durations shorter than 10 seconds would occur only at the beginning of each 10-second period. If you want all multiple animations to go on forever, you can move the RepeatBehavior setting of Forever to the Storyboard only if all the individual animations have the same total durations. AutoReverse is very handy in animations that repeat forever because it helps avoid discontinuities when the animation repeats. But it's not the only way to make animations smooth. You can make nonreversing continuous animations smooth by making the ending position (or size or whatever) of one animated object coincide with the beginning position (or whatever) of another animated object. The following XAML program uses five EllipseGeometry objects to define five concentric circles. (I used EllipseGeometry rather than Ellipse because it's easier to keep EllipseGeometry objects centered on the same point.) Ten DoubleAnimation objects increase the RadiusX and RadiusY of these five circles in a uniform way. All 10 DoubleAnimation objects have the IsAdditive property set to true, and all increase the dimension from 0 to 25. Because the radii of the five circles are 0, 25, 50, 75, and 100 units, each circle ends up at the initial size of the next-larger circle, making it seem as if all the circles are uniformly increasing in size. To complete the illusion, special handling is required for the smallest circle and the largest circle. The smallest circle begins with a RadiusX and RadiusY of 0, and the first DoubleAnimation animates the StrokeThickness from 0 to the value specified in the Path element (12.5). Consequently, the smallest circle seems to arise out of nothingness. The final DoubleAnimation in the long list decreases the Opacity property of the largest circle, making it seem to fade away. Because all the animations have the same Duration, the RepeatBehavior can be moved to the parent Storyboard in this program.
Of course, I could have eliminated some of these DoubleAnimation objects with bindings between the RadiusX and RadiusY properties of the individual EllipseGeometry elements. Sometimes you can replace a bunch of simultaneous animations with a single animation, but doing so might take some thought and perhaps even mathematics. The following program began with the Infinity.xaml file that I created in Chapter 28. In that file I used Bezier splines to draw an infinity sign colored with a LinearGradientBrush using the seven colors of the rainbow. For this chapter I decided I wanted to animate that brush by shifting to the right the colors that make up the gradient. The following file has seven DoubleAnimation objects that animate the Offset properties of the seven GradientStop elements of the Brush. Each of these DoubleAnimation objects has the same Duration (seven seconds, but tripled in speed with a SpeedRatio setting of 3) and the same From and To values of 0 and 1, but each has a different BeginTime, from zero seconds to six seconds.
When the program begins, all GradientStop objects have a default Offset property of 0. Normally that would result in the entire infinity sign being colored with the last GradientStop in the list, which is violet. However, the first DoubleAnimation begins immediately, assigning the first GradientStop a nonzero Offset, and the infinity sign turns red. A second later (actually a third of a second because of the SpeedRatio setting), the Offset of the first GradientStop is approximately 0.14, and the second DoubleAnimation kicks in, which colors the beginning of the infinity sign with orange. Each color progressively becomes active. Once all the DoubleAnimation objects have started, the cycling colors continue smoothly. The program works, but I later realized that the LinearGradientBrush itself has a Transform property, and this property might serve to simplify the program considerably. Sure enough. The following revised version of the program defines the Offset properties of the GradientStop objects explicitly but also includes a named TranslateTransform object for the brush.
The single DoubleAnimation changes the X property of the TranslateTransform from 0 to 621, with a Duration of two seconds, repeated forever. By default the brush has a WrapMode of Tile, so the brush is essentially repeated beyond its bounds, and the transform shifts the brush with the same effect as in the earlier program. So where does the 621 come from? You'll recall from diagrams in Chapter 2 that the default gradient of a LinearGradientBrush is from the upper-left to the lower-right of an element. Lines perpendicular to that diagonal line are colored the same. The InfinityAnimation2.xaml program defines eight GradientStop objects. The first and last are both red, and they have an offset of 0 and 1.0, respectively. The dashed lines in the following diagram symbolize the areas colored red.
The element being colored is the infinity sign. It has a height of H and a width of W. Looking at the coordinates, one might conclude that H is 200 and W is 500, but the stroke thickness is 25, which extends the dimensions of the image 12.5 units on all four sides. H is actually 225, and W is 525. The TranslateTransform shifts the brush right. When it shifts W plus X units, the brush appears the same as the unshifted version. What is X? Based on similar triangles, the ratio of X to H is the same as the ratio of H to W, so X can be easily calculated as approximately 96, so that W plus X equals 621. And now I will confess that I originally derived this number empirically in XAML Cruncher by commenting out the animation and experimenting with the X property in the TranslateTransform element. I kept trying values until the brush remained the same with and without the translation factor. One advantage of using the WPF animation facilities instead of DispatcherTimer is that animated values are calculated based on elapsed time. When using a timer, often animations are based solely on timer ticks, and if the program misses a few timer ticks because the system is overloaded, the animation could be slowed down. That would be disastrous for a clock application, and that's why most clock applications obtain the actual time on each timer tick and update the clock hands from that. The WPF animation facilities are designed to be accurate enough that you can use them to pace the hands of a clock, as in the XamlClock.xaml program that follows. The following XAML file draws a clock. It uses a Canvas within a Viewbox, so the clock is as large or as small as the window that displays it. Take note of the two Path elements that draw the tick marks. These two elements display dotted lines in a circle with a radius of 90 units. The first element defines a StrokeDashArray of 0 and 3.14159 and a StrokeThickness of 3, which means that the dots are 3p units apart. The circumference of the circle is 180p, so the dotted line displays 60 small tick marks. The second element defines a StrokeDashArray of 0 and 7.854 (which is actually 2.5p) and a StrokeThickness of 6, so the dots are 15p units apart, which means that 12 large dots are displayed in the circle. Following the definition of the tick marks, three additional Path elements draw the hands of the clock. All three clock hands point straight up. Each hand is associated with a RotateTransform that has an xName attribute set. The three DoubleAnimation elements change the angles of the transforms from 0 degrees to 360 degrees, with a Duration of 12 hours for the hour hand, one hour for the minute hand, and one minute for the second hand.
You can load this XAML file into XAML Cruncher, remove the x:Class attribute near the top, and precisely at noon or the stroke of midnight, press F7 to open the window. You then have a clock that will keep the correct time (until daylight-saving time starts or ends, of course). Are you not quite happy about the inconvenience of starting the program at noon or midnight? Okay then, just before you press F7, check the current time. Perhaps it's 4:38. In the Storyboard element, set the BeginTime property to the negative of that time: <Storyboard Name="storyboard" BeginTime="-4:38"> Now if you launch the window, you will see that the clock is set correctly. You're essentially saying, "I want the animation to begin 4 hours and 38 minutes ago, which means I want all hands pointing straight up 4 hours and 38 minutes ago." As you've probably figured out, XamlClock.xaml is part of a project named XamlClock, and the Storyboard element has a Name attribute set for a reason. A little bit of C# code is all that's necessary to initialize the BeginTime property to the negative of the current time.
The TimeOfDay property of DateTime returns an object of type TimeSpan, which is the type of the BeginTime property. In the previous chapter, I developed a technique that put transforms to work performing calculations in XAML. Might it be possible to use this technique to create a clock entirely in XAML? Yes, it is possible, and the following stand-alone XAML file demonstrates it. The Resources section of the Page begins with a FrameworkElement whose Tag is set to the current DateTime. I thought it would be wise to obtain the current DateTime just once rather than several times and avoid the risk of the time changing between accesses. Three TransformGroup objects perform all the necessary calculations. For example, the initial angle of the hour hand must be set to the sum of 30 degrees times the hour and 0.5 degrees times the minute. (For 3:30, that's 105 degrees, halfway between the 3 and 4.) That's accomplished by setting the X and Y offsets of a TranslateTransform to the hour and minute values, and multiplying by a matrix with M11 set to 30 and M21 set to 0.5. The product has an OffsetX property equal to 30 times the hour plus 0.5 times the minute. The RotateTransform for the hour hand initializes the Angle property to this value. The DoubleAnimation elements all have the IsAdditive property set to true to add to these initial values the animated values from 0 degrees to 360 degrees. In addition, I've used Bezier splines to make the hour hand and minute hand just a little fancier.
I wanted to have a ball bounce between the midpoints of the four sides of its container. Notice that the animation in the following stand-alone XAML file is triggered on the SizeChanged event of the Canvas, so it starts anew whenever the window holding the Canvas changes size.
The first DoubleAnimation changes the horizontal location of the ball between 0 and the ActualWidth of the canvas, whereas the second DoubleAnimation changes the vertical location between 0 and ActualHeight, both with AutoReverse turned on. Normally, this would cause the ball to bounce between the upper-left corner and the lower-right corner, but the first DoubleAnimation has a BeginTime set to the negative of half the Duration, so the initial position of the ball is at the middle of the top of the canvas. Everything works fine except that the ball goes beyond the right side and bottom of the container. The Canvas.Left property can't change from 0 to the ActualWidth of the canvas; it has to change from 0 to ActualWidth minus the diameter of the ball. I could have used a similar technique as in AllXamlClock to perform this calculation, but I figured it was easier to put the ball in the upper-left quadrant of a four-cell Grid, where the rightmost column and bottom row were given the same dimensions as the ball, as shown in the following program.
The following stand-alone XAML file is intended to simulate a ball bouncing on a floor. The floor is a horizontal Line element. The animation varies the Canvas.Top property of the ball between 96 and 480 units over the course of a second, with AutoReverse turned on, of course, and repeating forever. Because the maximum Canvas.Top property is 480, the Height of the ellipse is 24, and StrokeThickness of the floor is 5, I set the Y coordinate of the Line element that defines the floor to 480 plus 24 plus 2 (about half the StrokeThickess).
This animation has many problems that prevent it from looking realistic. Of course, by repeating forever, it violates physical laws, but even that might be forgiven if only the speed of the ball weren't quite so consistent. The ball should speed up as it drops and slow down as it rises. You can vary the speed of an animation with the AccelerationRatio and DecelerationRatio attributes. By default these are both 0. You can set them to values between 0 and 1, but the sum must not exceed 1. An AccelerationRatio of 0.25 means that the animation speeds up during the first 25 percent of its Duration time. The DecelerationRatio similarly slows down the animation at the end of the Duration. To more closely simulate a bouncing ball, insert the following attribute into the DoubleAnimation element: AccelerationRatio="1" This setting speeds up the animation during its entire duration so it's moving fastest as the ball hits the floor. When the animation reverses, the same AccelerationRatio causes the ball to slow down as it reaches its peak. To see a different effect, set the DecelerationRatio: DecelerationRatio="1" Now it looks like a ball suspended from a spring and bouncing up and down. Set both attributes to 0.5 to achieve an effect where the ball travels fastest midway in its journey up or down, much like a pendulum. The following BetterBouncingBall.xaml file has an AccelerationRatio set and also attempts to make the ball flatten out a bit as it strikes the floor.
A pendulum should accelerate as it approaches the midpoint of its arc, and then it should decelerate, so both the acceleration and the deceleration ratios can be set to 0.5. The following stand-alone XAML file constructs a pendulum from a Line element and a Button that (rather perversely) displays the date and time at which the program was loaded. To keep both the Line and Button swinging together, I assembled them on a StackPanel, gave the panel a RotateTransform, and animated the Angle property between 30 degrees and 30 degrees.
Although AccelerationRatio and DecelerationRatio are handy, you have no control over the rate at which the animation speeds up or slows down. If you need that controlor if you want to create an animation that is more complex than a linear change from one value to anotheryou'll want to explore the key-frame animation. To animate a property of type double by using key frames, you use a DoubleAnimationUsingKeyFrames element rather than DoubleAnimation. DoubleAnimationUsingKeyFrames contains a property named KeyFrames, of type DoubleKeyFrameCollection. This collection consists of elements of type DiscreteDoubleKeyFrame (to jump to discrete values), LinearDoubleKeyFrame (to linearly change a value), and SplineDoubleKeyFrame (to change a value at a nonlinear rate based on a Bezier spline). These children of DoubleAnimationUsingKeyFrames are all derived from the abstract DoubleKeyFrame class, as shown in the following class hierarchy: Object A <Type>KeyFrame class and a <Type>KeyFrameCollection class exist for all 22 animatable types. All 22 types have a Discrete<Type>KeyFrame class; all but five types have Linear<Type> KeyFrame and Spline<Type>KeyFrame classes. The total number of classes involved here is 100. The following table summarizes the types of animations supported for the 22 animatable types.
Five of these typesspecifically Boolean, Char, Matrix, Object, and Stringcan only take on discrete values. You can't interpolate between two values of these types. (Although Char can be interpolated in theory, it probably doesn't make much sense for an animation to animate from one Unicode character to another. Objects of type Matrix can be interpolated in theory, but the results might not be quite meaningful.) I'll discuss <Type>AnimationUsingPath toward the end of this chapter. The abstract DoubleKeyFrame class defines two properties named KeyTime (of type KeyTime) and Value. Neither DiscreteDoubleKeyFrame nor LinearDoubleKeyFrame defines any additional properties. Each key frame element essentially indicates the value of the animated property at a particular time. The key frame elements must be specified in the order in which they take effect. Here's a simple example using two elements of type LinearDoubleKeyFrame and two elements of type DiscreteDoubleKeyFrame.
The ball begins at the location (480, 96), as specified with the Canvas.Left and Canvas.Top attached properties in the Ellipse element. The animation changes the Canvas.Left property. For the first five seconds of the animation, that property changes from 480 to 0. As the first LinearDoubleKeyFrame indicates, at time 0:0:5, the value of Canvas.Left should equal 0. The second LinearDoubleKeyFrame element indicates that at time 0:0:5.5, the value of Canvas.Left should equal 48. Therefore, over the next half second, the ball moves right because Canvas.Left changes from 0 to 48. A half second laterwhen the time from the beginning of the animation is six secondsthe ball jumps to a position of 144. A second later it jumps to 240. The key frames are finished, but the DoubleAnimationUsingKeyFrames element indicates that the total duration of the animation is 10 seconds, so the ball sits at the position (240, 96) for three seconds. The DoubleAnimationUsingKeyFrames element also indicates a RepeatBehavior of Forever, so at the end of the 10 seconds, the ball jumps back to its initial position of (480, 96), and the animation begins again. With both DiscreteDoubleKeyFrame and LinearDoubleKeyFrame, the value of the animated property at the time indicated by KeyTime is the value given as Value. With DiscreteDoubleKeyFrame, the animated value jumps to Value at KeyTime. With LinearDoubleKeyFrame, the animated value changes linearly from its previous value to the Value at KeyTime. How fast it appears to change depends on the previous Value and the previous KeyTime. Here's a program that uses four elements of type DiscretePointKeyFrame.
Without the animation, the ellipse would be centered at the point (0, 0), but the first DiscretePointKeyFrame element indicates it should be located at the point (288, 96) at the key time of 0. If you look closely, you might be able to see the ellipse jump from (0, 0) to (288, 96) as the program loads. The ellipse remains at (288, 96) for one second until the key time of one second in the second DiscretePointKeyFrame element. When three seconds have elapsed from the time the program was loaded, the ellipse is moved to point (96, 288). That's the last DiscretePointKeyFrame element, but the PointAnimationUsingKeyFrames element indicates that the total Duration of the animation is four seconds, so the ellipse sits at the point (96, 288) for the last of those four seconds. If you leave out the Duration attribute, the last key time governs the total length of the animation. In this example, that's three seconds, so the animation ends at the same time the last DiscretePointKeyFrame is supposed to take effect. You won't even see it. The ellipse will seem to jump directly from the bottom position at (288, 480) to the top position at (288, 96). The following file uses LinearPointKeyFrame to move a ball between four points, seemingly bouncing between the four sides of a rectangle.
If the KeyTime properties are not present, the default is the static property KeyTime.Uniform, which means that every key frame has the same length of time. The Duration indicated for the overall animation is apportioned equally. If there is no Duration attribute, a default of one second is assumed. If you remove all the KeyTime attributes from the previous XAML file, it will work the same way. Another option is to specify a KeyTime as a percentage of the Duration. Such percentages must consecutively increase. If you replace the LinearPointKeyFrame elements in the previous program with the following markup, the program runs the same way: <LinearPointKeyFrame Value="430 240" KeyTime="25%" /> <LinearPointKeyFrame Value="240 430" KeyTime="50%" /> <LinearPointKeyFrame Value="50 240" KeyTime="75%" /> <LinearPointKeyFrame Value="240 50" KeyTime="100%" /> The final option is KeyTime.Paced, which allocates the time among the key frames so that the overall rate of change is constant. For example, if one LinearDoubleKeyFrame changes a double from 0 to 500, and another changes a double from 500 to 750, twice as much time is allocated to the first than to the second. Here's an example.
The ball moves in a bow tie pattern, but each leg of the trip gets enough time so that the overall speed is constant. The clock programs I showed earlier moved the second hand continuously in a smooth motion. You might prefer a second hand that makes discrete jumps between the seconds. (Or, better yet, you might prefer giving your users the option to change the clock's second hand to suit their own preference.) Here's a little program that shows two second hands without anything else. The first has the Angle property of its RotateTransform animated with DoubleAnimation from 0 to 360 degrees, with a Duration of one minute, just as in the earlier clock programs. The right-hand second hand uses DoubleAnimationUsingKeyFrames with a single child of DiscreteDoubleKeyFrame. The KeyTime for the key frame is one second and the Value is 6, meaning 6 degrees. However, the IsCumulative property on the DoubleAnimationUsingKeyFrames element is set to true, so those 6-degree values are accumulated.
The following XAML file has two simultaneous DoubleAnimationUsingKeyFrames elements, each containing two DiscreteDoubleKeyFrame elements that alternate between values of 0 and 1. The TargetProperty is the Opacity property of a TextBlock. The two TextBlock elements display the words "EAT" and "HERE", and as the Opacity properties change values, the two words alternate on the screen, much like the neon sign on a diner.
The highest value of KeyTime in this example is two seconds, which is the length of the animation. The two TextBlock elements have initial Opacity values of 1 and 0. At the KeyTime of one second, they are changed to values of 0 and 1, respectively. The second DiscreteDoubleKeyFrame in each group changes the values back to their initial values at the end of the animation. The animation then repeats. If you set a Duration property of 0:0:2 in the Storyboard element, you can remove the second DiscreteDoubleKeyFrame in each collection. As usual with XAML animations (or programming in general), you can build the diner sign in several different ways. Here's a version with just one TextBlock element.
This single TextBlock element has no Text property set, and the Foreground property exists solely to ensure that a SolidColorBrush exists. The StringAnimationUsingKeyFrames alternates the Text property between EAT and HERE. The ColorAnimationUsingKeyFrames alternates the Color property of Foreground between Red and Blue. In each case, the first key frame has a KeyTime of zero seconds to specify the initial values (the word "EAT" in red), and the second key frame has a KeyTime of one second to change the values of the word "HERE" in blue. The Storyboard indicates a Duration of two seconds. The following XAML file has eight LinearColorKeyFrame elements that change the background of the Canvas to each of the colors of the rainbow. The animation lasts as long as the highest KeyTime, which is seven seconds. At the beginning of the animation, the color is set to red and immediately starts changing to orange. Between second six and second seven, the color changes from violet to red, so when the animation begins again after those seven seconds have elapsed, there's no discontinuity as the color starts again at red.
I've discussed Discrete<Type>KeyFrame and Linear<Type>KeyFrame but not yet the third possibility for key frame elements, which is Spline<Type>KeyFrame. The Spline<Type>KeyFrame interpolates between the starting and ending values based on a spline rather than a straight line. Using this key frame, you can get effects similar to the Acceleration and Deceleration properties but with much more control. The Spline<Type>KeyFrame classes inherit the KeyTime and Value properties and define just one additional property named KeySpline, which consists of just two control points of a Bezier curve. The Bezier curve is assumed to begin at the point (0, 0) and end at the point (1, 1). The two points specified in the KeySpline must have X and Y coordinates not less than 0 or greater than 1. With the values of the control points restricted in this way, it's not possible for the Bezier curve to loop. The resultant curve defines a relationship between time (the X axis) and the value of the animation (the Y axis). For example, suppose you're animating a double between the values of 100 and 200 over a period of 10 seconds. What is the value of the double at five seconds? With a plain DoubleAnimation or LinearDoubleKeyFrame, the value is obviously 150. With a SplineDoubleKeyFrame, the value depends on the spline you've defined. The time of five seconds is the halfway point between 0 and 10 seconds, so it corresponds to an X coordinate of 0.5. If the Y coordinate of the spline at that point is 0.75, the value of the double is 75 percent of the amount between 100 and 200, or 175. Here's an example of applying SplineDoubleKeyFrame to the bouncing ball problem.
The Canvas.Top attached property is set to 96 by the initial DiscreteDoubleKeyFrame element. It is animated to 480 by the first SplineDoubleKeyFrame and back to 96 by the second. Each SplineDoubleKeyFrame element sets the KeySpline property to a string consisting of two points. How did I derive the points I used for the KeySpline values? I created a program, named SplineKeyFrameExperiment, that allows me to experiment with the control points. The program consists of a XAML file that lays out the window as well as the following C# file that defines several properties and event handlers.
This file defines two properties named ControlPoint1 and ControlPoint2 that are backed by dependency properties. These are the two control points for the Bezier curve used in the animation. Whenever one of these properties changes, the static ControlPointOnChanged method in the class is called, setting the properties of a KeySpline object defined in the XAML portion of the window (coming up). The C# code also has an event handler for MouseDown and MouseMove events that sets ControlPoint1 and ControlPoint2 based on mouse coordinates. The last method of the file sets the content of a Label (also defined in the XAML file) with the values of ControlPoint1 and ControlPoint2. Here's the XAML file. The light-gray Canvas displays a surface whose upper-left corner is the point (0, 0) and lower-right corner is the point (1, 1). The window constructor in the C# portion of the program draws lines marking every 0.1 units. This Canvas also displays a Bezier curve between the two corners whose two control points are the ControlPoint1 and ControlPoint2 properties of the Window object. You change these points by clicking and dragging on the surface of the Canvas.
At the lower-right corner of the window is a Button labeled "Go!" When you click this Button, two animations are triggered. A PointAnimation moves a little ball with a target name of "time" at a linear rate across the bottom of the grid to represent the passing of time. A PointAnimationUsingKeyFrames moves a little ball with a target name of "value" down the right side of the grid to represent changing values based on a SplinePointKeyFrame with a KeySpline set from the Bezier spline you've specified. In this way you can experiment with control points to get the effect you want. If both control points are values of (0.1, 0.9), the animated property changes very quickly at first and then slows down. If both control points are (0.9, 0.1), the opposite happens: the animation starts off slow but then speeds up. If the first control point is (0.1, 0.9) and the second is (0.9, 0.1), the animation starts up fast, then slows down, then speeds up again. Switch the two to get the opposite effect: the animation starts slowly, then speeds up, and then slows down again. If you need something more elaborate, use multiple Spline<Type>KeyFrame elements in a row. An object in free fall covers a distance proportional to the square of the time. To mimic this effect with a KeySpline, you'll want a curve that maps a time of 0.1 to a value of 0.01, a time of 0.2 to a value of 0.04, a time of 0.3 to a value of 0.09, and so forth. You won't be able to get the beginning of the curve quite right, but the rest of it is well approximated with a first control point of (0.25, 0) and a second of (0.6, 0.2). That's what I used in the AnotherBouncingBall.xaml program for the fall. I used the mirror image of the curvethe points (0.75, 1) and (0.4, 0.8)for the bounce back up. I use those same values for the movement of the five suspended balls in the physics apparatus (and executive toy) known as Newton's Cradle.
A DoubleAnimationUsingKeyFrames element is associated with each of the five suspended balls. Each collection of key frames is a combination of DiscreteDoubleKeyFrame and SplineDoubleKeyFrame. The two balls on each side stop in the center; the one in the middle does not. All animations described so far either set a property to a specified value (Discrete<Type>KeyFrame) or perform an interpolation between two values. The interpolated value is always somewhere along the straight line that connects the starting value and the ending value. The difference between Linear<Type>KeyFrame and Spline<Type>KeyFrame is how the interpolation calculation treats timelinearly or as an X coordinate of a spline. But in neither case does the interpolated value deviate from that straight line between the starting and ending values. This does not imply that you can't move an animated object in anything other than a straight line. You've seen how to animate the Angle property of a Rotate transform to make an object move in circles. But how would you like a general approach to moving an object along an arbitrary path? That's the purpose of the final category of animation classes, which is restricted to just three classes: DoubleAnimationUsingPath, PointAnimationUsingPath, and MatrixAnimationUsingPath. Each of these classes defines IsAdditive and IsCumulative properties and, most importantly, a PathGeometry property that you set to a graphics path. Here's an example of a PointAnimationUsingPath that sets the PathGeometry property to a Bezier curve that makes a little loop. The target of the animation is the center of a little ball (what else?), such that the ball moves back and forth along the Bezier curve.
To actually see the PathGeometry that's controlling the movement of the ball, you can include another Path element right after the Canvas start tag: <Path Stroke="Black" Data="M 96 288 C 576 0, 0 0, 480 288" /> The center of the ball tracks this path exactly. PointAnimationUsingPath animates a property of type Point by forcing the property to take on values defined by a graphics path, in this case a Bezier curve. The path can alternatively be a rectangle, ellipse, arc, or any combination of those figures. PointAnimationUsingPath is very different from SplinePointKeyFrame, which it might be confused with because Bezier splines are often (but not exclusively) used with PointAnimationUsingPath and Bezier splines are always (but in a restricted way) used with SplinePointKeyFrame. It's important to distinguish between these two classes. With SplinePointKeyFrame, the interpolated point always lies on the straight line between a starting point and an ending point. Elapsed time is used to calculate an X coordinate of the spline (between 0 and 1), and the Y coordinate of the spline (also between 0 and 1) is then used to calculate the interpolated value. With PointAnimationUsingPath, time is allocated based on the total length of the path you've set to the PathGeometry property. For example, if the animation has a Duration of four seconds, and one second has elapsed, the animation determines the X and Y coordinates of the path one-quarter of the distance from the beginning to the end. These become the Point value of the animation. What if you want to move an object along a path and the element does not have a Point property? Perhaps you want to move a Button along a path. You position a Button object on a Canvas panel by specifying the Left and Right attached properties. To move the Button, you must animate these properties. To move any arbitrary object (such as a Button) along a path, you can mimic PointAnimationUsingPath with two DoubleAnimationUsingPath objects. One animation controls the X coordinate, and the other controls the Y coordinate. DoubleAnimationUsingPath defines a Source property specifically for this purpose. You set this property to a member of the PathAnimationSource enumerationX, Y, or Angle. (More on the Angle option shortly.) Here's a XAML file that defines a Bezier curve in a PathGeometry as a resource. It uses this resource both to draw the Bezier curve with the Path element and also to set the PathGeometry in two DoubleAnimationUsingPath elements, the first controlling the Canvas.Left property of the Button by setting Source to X, and the second controlling the Canvas.Top property by setting Source to Y.
You can also set the Source property of DoubleAnimationUsingPath to PathAnimationSource .Angle, and when you specify that member, DoubleAnimationUsingPath animates a property based on the slope of the path in degrees. Typically, you would then use this DoubleAnimationUsingPath to animate the Angle property of a RotateTransform. The following program demonstrates this technique. It is very similar to the previous XAML file, except that the PathGeometry is a bit more elaborate and defines a continuous curved line based on two Bezier curves. The Button element now includes a RotateTransform that is set to its RenderTransform property. A third DoubleAnimationUsingPath animates the Angle property of this transform with the same Bezier curve and Source set to Angle.
Run this program and you'll see the upper-left corner of the button tracing the Bezier curve as you might expect, but the rotation of the button also traces the curve so that the top of the button is always parallel to the curve. Keep in mind that although it seems natural to animate the horizontal position of an element by setting Source to X, to animate the vertical position by setting Source to Y, and to animate the Angle of rotation by setting Source to Angle, you're not restricted to those options. For example, the following program animates the Opacity property of a TextBlock by using a path constructed from three straight lines that if graphed form a rectangle.
The DoubleAnimationUsingPath element has a Source property set to Y, which means the Y values of the path will be used to animate the Opacity property of the TextBlock. (These Y values should be between 0 and 1.) The path is a series of straight lines from (0, 0) to (0, 1) to (2, 1) to (2, 0). The total length of this path is 4 units. The first line is from the point (0, 0) to (0, 1). That's 1 unit in length, so it will govern the first quarter of the duration of the animation. The animation is conveniently four seconds in duration, so over the first second of the animation, the Y coordinate of the path (and the Opacity of the TextBlock) changes from 0 to 1. The next line in the path is (0, 1) to (2, 1), a distance of two units, so over the next two seconds, the Y coordinate (and the Opacity) remains at 1. The final line is from (2, 1) to (2, 0), a distance of 1 unit, which brings the Y coordinate and Opacity back to 0 over the final second. (Of course, you can easily do something similar with a combination of LinearDoubleKeyFrame and DiscreteDoubleKeyFrame.) If you use three DoubleAnimationUsingPath elements (or one PointAnimationUsingPath and one DoubleAnimationUsingPath) to animate horizontal and vertical position and rotation angle, you can replace those elements with a single MatrixAnimationUsingPath element. This element animates a Matrix object. The X and Y coordinates of the path become the OffsetX and OffsetY properties of the transform matrix; the remainder of the matrix optionally defines a rotation based on the tangent angle of the path. (You need to set DoesRotateWithTangent to true to get the rotation.) The visuals of the MatrixAnimatedButton.xaml file are identical to those of the previous PathAngleAnimatedButton.xaml..
I will freely admit that applications involving moving and rotating a button along a path don't arise very frequently. However, one common application of path animations is the unicycle-man cyborg, who must navigate a treacherous path over the hills of Bezier Boulevard.
Notice that the unicycle-man is a single Path object that has its RenderTransform set to a MatrixTransform with a name of "xformUnicycleMan." This is the transform subjected to the MatrixAnimationUsingPath element. The point (0, 0) of the object animated with MatrixAnimationUsingPath (or PointAnimationUsingPath) traces the path exactly. For paths drawn from left to right on the screen, positive Y coordinates of the object appear below the path, and negative Y coordinates appear above the path. That's why the unicycle-man is drawn with negative Y coordinates (and why the Button in previous programs hangs below the initial portion of the path). Two other components of the unicycle-man are also animated. The spokes of the wheel are a series of Line elements in a GeometryGroup, and this GeometryGroup has its own animated RotateTransform named "xformSpokes." The arms are a single Line element with an animated RotateTransform named "xformArm." These animations occur within the context of the complete unicycle-man, who is moved and rotated along the path. The TextGeometryDemo program in Chapter 28 demonstrates how to obtain a Geometry object that describes the outlines of text characters and how to put that Geometry to use in graphics programming. It's also possible to use those Geometry objects in animations. The companion content for this chapter includes a project named AnimatedTextGeometry that demonstrates animations based on the outlines of text characters. A program in which unicycle-man travels up, down, across, and through text characters is an exercise left to the reader. |