Chapter 7: Using Windows Animation Manager
Animation is the process through which an image changes over time, and if the interval between the changes is small, then to the human eye the changes appear continuous and give the impression of movement. Some animations may be simple linear changes with time, others may be more complex involving changes that accelerate or decelerate, and some animations are built up of several component movements. All of this can involve some quite complex coding and so to address this challenge Windows 7 provides a component called the Windows Animation Manager.
Using The Windows Animation Manager
Animation can be viewed as a series of frames in a storyboard. Each frame is made up of items and the changes to these items between frames makes up the animation. The changes may be made to the color, position or shape of the items, and the changes may be smooth or discrete, and if smooth, the change could be constant, or the rate of change may increase or decrease. In some cases several properties of an item change. Figure 1 shows a simple storyboard made up of five frames where a red circle changes position on each frame.
Figure 1 Storyboard showing variables and transition
In the terminology of the Windows Animation Manager, these changes are called transitions, and they change a floating point value called a variable. The animation manager does not change pixels on the screen, it just changes the variable, and you have to provide the drawing code that uses the variable. A transition determines how a variable changes over a period of time (called the duration) and the variable will start the transition with an initial value.
Transitions can be chained, that is, one transition describing the change of a variable is followed by another transition describing a different change to the same variable. At the hand over point between the transitions the variable will be changing in a particular way according to the first transition (called the velocity), and since some transitions depend on the velocity this value is made available to the next transition. Figure 2 shows a smooth stop transition and illustrates the initial and final values and the duration of the transition. Other transition types are supported by the Windows Animation Manager—for details see the Storyboard Overview in the Windows Animation Development Guide. The arrows indicate the initial velocity of the transition. Over the duration of the smooth stop transition the variable will decrease from the initial value to the final value.
Figure 2 Illustrating the properties of a smooth stop transition
The animation manager will ensure that the variable’s value changes over the duration in the manner specified, and the application obtains the value of the variable during this time to render each frame. The variable clearly depends on the time since transition started, so the animation manager must have an accurate value of the time the transition starts, and the time when each frame is drawn. Windows provides a high precision timer object that can be used by the animation manager. In addition, Windows also defines several stock transitions through an object called the transition library.
Animation involves more than just changing pixels on a screen. Careful attention must be made to prevent flicker through synchronization with the monitor refresh. Direct2D does this for you, and so the combination of the Windows Animation Manager and Direct2D are the best way to provide animation in a Windows 7-based application.
Creating the Windows Animation Manager
The Windows Animation Manager is a COM object, and it must be created in a single-threaded COM apartment (STA).The thread that will use the animation manager (typically the thread that handles the messages for a window) must be initialized with a call to CoInitialize. Once an application has created an animation manager it can create a storyboard object which encapsulates one or more transitions. Listing 1 shows the code to create the main objects, these objects will be used by all animations and so typically these will be global to the application.
Listing 1 Creating the animation objects
// Objects used by the application
IUIAnimationManager* g_animationManager = nullptr;
IUIAnimationTimer* g_animationTimer = nullptr;
IUIAnimationTransitionLibrary* g_transitionLibrary = nullptr;
CoCreateInstance(
CLSID_UIAnimationManager,
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&g_animationManager)
);
CoCreateInstance(
CLSID_UIAnimationTimer,
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&g_animationTimer)
);
CoCreateInstance(
CLSID_UIAnimationTransitionLibrary,
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&g_transitionLibrary)
);
}
The animation manager must be updated periodically with the time and this is the reason for the animation timer object. The transition library is a factory object that is used to provide transition objects for most of the common transitions used in animation.
Creating Animation Variables
The animation manager updates an animation variable during the duration of the animation. The change of the animation variable is determined by transition objects and so an animation variable must be associated with one or more transition objects, therefore the variable must be accessible to the code that creates the transitions. The variable has a value that will be used to draw the screen, so the variable must be accessible by the rendering code. The variable may live just for the time of a single animation, or if the animation will be repeated starting at the position it ended previously then the variable object may live for several animations. These implementation details are determined by the developer, but the important point is that a variable object may live longer than the transition it is attached to and be accessible by different methods in the application.
Listing 2 shows how to create an animation variable. The call to IUIAnimationManager::CreateAnimationVariable creates a variable with the specified initial value. The application can use the IUIAnimationVariable interface to provide maximum and minimum limits by calling the SetUpperBound and SetLowerBound methods and it can obtain the current value of the variable by calling GetValue.
Listing 2 Creating an animation variable
// The variable is declared elsewhere
IUIAnimationVariable* m_variable;
if (nullptr == m_variable)
{
hr = g_animationManager->CreateAnimationVariable(initialValue, &m_variable);
}
Creating and Initializing a Storyboard Object
When the application starts the animation it creates a storyboard object. The storyboard contains one or more transitions that describe the animation. The transition objects and the storyboard live just for the duration of the animation. If you want to repeat the animation then you have to create a new instance of the storyboard and transitions.
Listing 3 shows the code to create a storyboard object. This object is created through a call to the IUIAnimationManager::CreateStoryboard method that returns a reference to the storyboard object. When you call this method, the animation manager will hold an additional reference to the storyboard for the duration of all the transitions that are part of the storyboard. This is why the code in Listing 3 releases the reference to the storyboard object when the application has finished initializing it.
The storyboard is initialized with one or more transitions, and each transition is related to a specific variable. In Listing 3 the transition library is used to create an accelerate-decelerate transition. The duration of the transition is specified by the second parameter and the variable will have a value of endValue when the transition is complete. The shape of this transition is determined by the third and fourth parameters, the value of accRatio specifies the proportion of the duration spent initially accelerating, and decRatio specifies the proportion of the duration spend decelerating to the final value. This transition object is then added to the storyboard along with the variable that it will change. Adding the transition to the storyboard means that the storyboard holds a reference to the transition object and so once the code has finished initializing the transition it releases its reference. The lifetime of the transition is now controlled by the storyboard object and the lifetime of the storyboard object is controlled by the animation manager.
Listing 3 Initializing the Animation Manager with a storyboard
IUIAnimationStoryboard* storyboard;
IUIAnimationTransition* transition;
HRESULT hr = g_animationManager->CreateStoryboard(&storyboard);
if (SUCCEEDED(hr))
{
hr = g_transitionLibrary->CreateAccelerateDecelerateTransition(
duration,
endValue,
accRatio,
decRatio,
&transition);
if (SUCCEEDED(hr))
{
hr = storyboard->AddTransition(m_variable, transition);
}
// Schedule the storyboard (see Listing 5)
}
transition->Release();
storyboard->Release();
The IUIAnimationTransitionLibrary::CreateAccelerateDecelerateTransition method creates a transition that lasts for a fixed time, the duration parameter. The transition library also allows you to create transitions whose duration depends on values determined at runtime. For example, Listing 4 shows code that uses the IUIAnimationTransitionLibrary::CreateLinearTransitionFromSpeed method. This method is initialized with just the initial rate of change of the variable (speed) and the expected final value. The duration of the transition will depend on the initial value of the variable immediately before the transition starts.
Listing 4 Creating a transition without a known duration
hr = g_animationManager->CreateAnimationVariable(initialValue, &m_variable);
if (SUCCEEDED(hr))
{
hr = g_transitionLibrary->CreateLinearTransitionFromSpeed(
speed,
endValue,
&transition);
if (SUCCEEDED(hr))
{
hr = storyboard->AddTransition(m_variable, transition);
}
}
The application can use the IUIAnimationTransition interface to set the initial value of the variable and the initial velocity (rate of change) of the variable.
Chaining Transitions
A storyboard can contain more than one variable and a variable can be added to the storyboard associated with different transitions. Transitions added to a storyboard and associated with the same variable are run serially. That is, the first transition added to the storyboard is run for its duration and then the second transition is run. This is illustrated in Figure 3 where a variable 1 is added twice to the storyboard which means that the two transitions are performed in the order that they are added to the storyboard.
Figure 3 Chaining two transitions on the same variable
If a storyboard is initialized with transitions that are associated with different variables then when the animation starts the two variables will be updated in parallel. This is illustrated by Figure 4 where three transitions are added to the storyboard and these are associated with two1X and Y. This means that when the animation starts the first transition associated with X and the first transition associated with Y are started at the same time and so the variables X and Y are changed in parallel.
When you add a transition for another variable by using the IUIAnimationStoryboard::AddTransition method the two transitions will be scheduled to start at the same time. You can specify that a transition starts at a later time by creating a key frame. A key frame is essentially a defined point in time after the storyboard has started. You can create a key frame by calling the IUIAnimationStoryboard::AddKeyframeAtOffset method. Once you have created a key frame you can add a transition at this point by calling the IUIAnimationStoryboard::AddTransitionAtKeyFrame method.
Figure 4 Illustrating two variables added to the storyboard
Scheduling a Storyboard
The code in Listing 3 creates and initializes the storyboard, to start the animation you have to schedule it, as shown in Listing 5.
Listing 5 Scheduling the storyboard
if (SUCCEEDED(hr))
{
UI_ANIMATION_SECONDS secondsNow = static_cast<UI_ANIMATION_SECONDS>(0);
if (SUCCEEDED(hr))
{
hr = g_animationTimer->GetTime(&secondsNow);
}
if (SUCCEEDED(hr))
{
hr = storyboard->Schedule(secondsNow);
}
}
The IUIAnimationStoryBoard::Schedule method schedules the storyboard to run. If there is no other storyboard with the same variables already scheduled to run the storyboard will start immediately. Only one storyboard with the same collection of variables can run at any time, but a new storyboard can be initialized to cancel, trim, conclude, or compress conflicting storyboards.
Obtaining the Value of a Variable
At this point the storyboard is initialized and started. The storyboard will run for the duration of all the transitions, and will update the variable objects. It is now up to the application to obtain the values of the variables and use them to determine how the window is drawn. The storyboard is scheduled with the start time and so the animation manager will know at what point it currently is in each transition (and hence the value of the variables). Thus, before the application can obtain the value of a variable it must first update the animation manager with the current time. The code to do this is shown in Listing 6.
Listing 6 Code to update the animation manager
UI_ANIMATION_SECONDS secondsNow = 0;
hr = g_animationTimer->GetTime(&secondsNow);
if (SUCCEEDED(hr))
{
hr = g_animationManager->Update(secondsNow);
}
When the application calls the IUIAnimationManager::Update method the animation manager updates all the variables, and you can then obtain the value of a variable by calling the IUIAnimationVariable::GetValue method, as shown in Listing 7.
Listing 7 Retrieving the value of a variable
double value;
m_variable->GetValue(&value);
The window rendering code will use the variable value to draw the new frame of the animation. The rendering code needs to know if there are more frames to be drawn in the animation and the animation manager will be able to calculate this from the current time. If there is time for more frames to be drawn then the rendering must be called again. Listing 8 shows how to determine if more frames can be drawn by obtaining the status of the animation manager.
Listing 8 Determining whether the animation has finished
UI_ANIMATION_MANAGER_STATUS status;
hr = g_animationManager->GetStatus(&status);
if (SUCCEEDED(hr))
{
if (status == UI_ANIMATION_MANAGER_BUSY)
{
InvalidateRect(hWnd, NULL, FALSE);
}
}
There are two possible status codes, UI_ANIMATION_MANAGER_IDLE indicates that all storyboards have completed and so the rendering code need not be called again. UI_ANIMATION_MANAGER_BUSY indicates that there is at least one storyboard running or scheduled to run, and hence the code in Listing 8 invalidates the window to ensure that the rendering code is called again to display the next frame of the animation.
Using the Windows Animation Manager in Hilo
The basics of animation using the Windows Animation Manager have been covered in the sections above. The Hilo Browser project contains classes that provide the various animations that are used in the Browser application. Each class encapsulates one or more animation variables and provides methods to provide the initial values for these variables and create and schedule the storyboard. The main animation classes in the Browser project are described in the following table along with a description of the values that are changed as part of the animation.
Class |
Description |
---|---|
CarouselAnimation |
Rotates the folder thumbnails on the inner orbital. Called when you spin the carousel. In the animation, the folder thumbnails change size, opacity, and angular position on the orbital. |
CarouselThumbnailAnimation |
Moves a folder thumbnail to the history stack. Called when you select a folder. In the animation, the folder thumbnails change their x and y position and opacity. |
FlyerAnimation |
Fills the media pane with images. Called when a folder is first opened, so old images fly out and new images fly in. In the animation, images move along a predefined path. |
MoverAnimation |
Moves images in the media pane when you resize the Browser window. Images move to fill in free space as the window increases. In the animation the x and y position changes. |
OrbitAnimation |
Animates the size and position of the inner orbital. This Called when you select a folder and the new inner orbital comes into view, and when the history stack is expanded. In the animation, the position, the size and the opacity of the ellipse changes. |
SlideAnimation |
Fills the media pane with images. Called when you move to another page so new images slide into view. In the animation the x position of the images changes. |
The classes have a similar form, with a two stage construction pattern where there is an Initialize method to create the animation variables that will live the lifetime of the instance of the class. These classes have a method to create and schedule the storyboard (typically called Setup), and in the process create transitions and associate them with the animation variables. Finally, they all have one or more methods to obtain the values of the animation variables during the animation.
Instances of these classes are created by a separate class, AnimationFactoryImpl, which contains methods that wrap calls to SharedObject<>::Create to create instances of the appropriate class on the C++ heap.
The Hilo Browser animates three types of objects: the carousel (the folder thumbnails and the inner orbital), the folder thumbnails on the history stack, and the photo thumbnails in the media pane.
Initializing the Windows Animation Manager Objects
The Hilo Common Library provides a class called AnimationUtility with static methods that give access to the main animation objects and provides standard code used in animations. The AnimationUtility class has static members for the animation manager, animation timer, and transition library. A private method, Initialize, creates all three objects if they are not already created. The accessor methods return an interface pointer to these objects, so it means that the first time one accessor method is called all three objects are created. The AnimationUtility class also has a method to check the state of the animation manager and a method to schedule a storyboard.
The Hilo animation classes all have an Initialize method that accesses the animation manager to create the animation variables used by the animation. The lifetime of the animation variables is the lifetime of the animation object. The handler objects for the Browser carousel pane and the media pane create these animation objects using an animation factory object, which has methods that simply call the SharedObject<>::Create method on the appropriate animation class. The lifetime of the animation object depends on the type of animation performed.
The CarouselPaneMessageHandler class provides animation for the inner orbital of the carousel and the history stack. The history stack is made up of zero or more history items each of which can be animated. In code, each history item is a CarouselHistoryItem object (shown in Listing 9) and this contains a pointer to the animation objects used to animate the item (a CarouselThumbnailAnimation object and an OrbitalAnimation object). When the user selects a folder it is added to the history stack and a CarouselHistoryItem object created for the folder and added to a vector called m_carouselHistoryItems. When the user navigates back, the last CarouselHistoryItem is removed from the vector, and the animation objects for this folder are destroyed.
Listing 9 History item
struct CarouselHistoryItem
{
ComPtr<IThumbnail> Thumbnail;
ComPtr<ICarouselThumbnailAnimation> ThumbnailAnimation;
ComPtr<IOrbitAnimation> OrbitAnimation;
};
The inner orbital involves two animations. The first is the rotation of the carousel, which is the rotation of the folder thumbnails about the orbital and is provided by CarouselAnimation. The second animation occurs when the user selects a folder and the inner orbital expands to give the impression of zooming into the folder. This animation is provided by an OrbitalAnimation object. These two objects are created when the CarouselPaneMessageHandler object is first created and their lifetime is the same as the handler object.
The MediaPaneMessageHandler class provides the animation code for the media pane. There are three types of animation that can occur in the media pane: flyer, when the pane is first populated with photos; slide, when you use the arrow buttons to show another page of photos in the pane, and mover, when you resize the window so more or fewer photos are shown in the pane. Only one of these animations can occur at any one time, so the MediaPaneMessageHandler object only holds a reference to an object for the current animation. This means that the animation objects (provided by the FlyerAnimation, SlideAnimation or MoverAnimation classes) are only created when needed and live until the next animation.
Animating the Carousel
The CarouselPaneMessageHandler class provides the message handler code for the carousel window. This class has objects for the animation of the inner orbital and the history stack. The m_carouselAnimation object, an instance of the CarouselAnimation class provides two types of animation for the thumbnails on the inner orbital. The m_innerOrbitalAnimation object, an instance of the OrbitalAnimation class, provides animation of the ellipse shown for the inner orbital. Instances of the CarouselThumbnailAnimation and OrbitalAnimation classes are created for each folder on the history stack and provide animation for expanding and contracting the stack. The CarouselThumbnailAnimation class provides thumbnail animation and the OrbitalAnimation class provides the animation of the orbitals. Figure 5 shows the members of the CarouselPaneMessageHandler class.
Figure 5 Showing the animation members of the CarouselPaneMessageHandler class
The first type of animation provided by the CarouselAnimation class is when the items in the inner orbital rotate. When you drag one of the folders in the inner orbital the carousel will rotate in the direction you drag the item, but the rotation will continue after you lift your finger. This rotation continues but slows down until the carousel stops spinning. The transition used here is an accelerate-decelerate transition where the entire transition is a deceleration. The pertinent code from CarouselAnimation::SetupRotation is shown in Listing 10. The important point is that the third parameter is 0 and the fourth is 1 which means that there is no acceleration. This is a deceleration from the current value to the value represented by the rotation parameter, and this occurs over the period of time given by the duration parameter. The rotation value indicates by how much the carousel will rotate, and since this is an angle, the animation variable attached to the transition object will decrease if the rotation is clockwise and increase if the rotation is counter-clockwise. The deceleration refers to the rate of change to get to this final value. When the carousel is drawn the CarouselPaneMessageHandler::DrawOrbitalItems method is called and this obtains the rotation value from the m_carouselAnimation object and draws the currently selected folder at the specified position on the inner orbital. The DrawOrbitalItems method then draws the other items at equal angular positions around the orbital.
Listing 10 Initializing the carousel rotation
hr = m_transitionLibrary->CreateAccelerateDecelerateTransition(
duration,
rotation,
0.0,
1.0,
&transition);
The other animation that the CarouselAnimation class provides occurs when the user expands the history stack. In this situation the inner orbital shrinks, moves to the right side and fades; during this animation the folder thumbnails on this orbital shrink to half size and their opacity reduces to 60 percent. When the expanded history list is restacked the inner orbital grows back to full size, moves to the center of the pane, and opacity changes back to 100 percent; during this animation the thumbnails grow back to full size and their opacity changes back to 100 percent. The CarouselAnimation class provides two transitions for the size and opacity of the thumbnails, both transitions are linear which means that the size or opacity variable changes at a constant rate. Listing 11 shows the code that does this in the CarouselAnimation::SetupScale method and a similar transition is created in the CarouselAnimation::SetupOpacity method. Both the SetupScale and SetupOpacity methods create a storyboard, add the transition to the storyboard, and schedule it. However, the code in CarouselPaneMessageHandler always calls these methods as a pairthat means that the two animations will always be performed at the same time: the inner orbital will shrink and get less opaque; or the orbital will grow and get more opaque.
Listing 11 Initializing the carousel scaling
hr = m_transitionLibrary->CreateLinearTransition(
duration,
thumbnailScale,
&transition);
The carousel pane object has a member m_innerOrbitalAnimation which is an instance of the OrbitalAnimation class. As the name suggests this object provides the size and position of the inner orbital. When a user selects a folder on the carousel the folder is added to the history stack and if the folder has items, the inner orbital appears to grow from the centre to give the impression of zooming into the folder. This animation is provided by the m_innerOrbitalAnimation object which animates the size and opacity of the orbital. The changes are linear, so the OrbitalAnimation class creates linear transition objects.
Animating the History Stack
When you click on a folder in the carousel it is added to the history list represented by a vector called m_carouselHistoryItems as shown in Figure 5. This vector contains the CarouselHistoryItem items shown in Listing 9. The IThumbnail reference in the CarouselHistoryItem structure is to an object that gives access to the bitmap image and the current position of a thumbnail image. The other two members are used to animate members in the history list and refer to instances of the CarouselThumbnailAnimation and OrbitalAnimation classes.
When you click on a folder in the carousel, the folder is opened and the subfolders are shown in the inner orbital. At this point two animations occur relevant to the history list. The ellipse representing the orbital containing the folder added to the history list expands to give the impression that the user is zooming into the folder’s contents. This animation is performed by the OrbitalAnimation class, described earlier. The second animation involves the folder icon moving from the inner orbital to the history stack. This is performed by the CarouselThumbnailAnimation class.
During expansion of the history stack all orbitals are expanded eccentrically, that is, as the orbital expands the center of the ellipse moves. This is performed by the OrbitalAnimation class which has five variables, two describe the ellipse dimensions and two describe the ellipse center. The fifth variable describes the opacity of the ellipse. When the stack is expanded or contracted all five of these variables are changed by using linear transitions.
The CarouselThumbnailAnimation class provides the animation of thumbnails on the stack. This class encapsulates three variables, the x and y position of the thumbnail, and the opacity. The class provides two types of animation, one has linear transitions for all three variables, but the other provides a accelerate-decelerate transition for all three variables. This latter animation involves first an acceleration and then a deceleration. The way that this is performed is using several transations scheduled using a key frame.
Animating the Media Pane
The media pane uses animation in three situations and there are three concrete classes to provide this: FlyerAnimation, SlideAnimation, and MoveAnimation. The relationship between these classes is shown in Figure 6. Since only one of these animations can be shown at any one time, the media pane message handler class holds a reference to an instance of just one of these classes, m_animationController, and the type of the animation is stored in a field called m_currentAnimation.
Figure 6 The animation members of the MediaPaneMessageHandler class
The MediaPaneAnimation base class has two abstract methods called CreateAnimatedThumbnailCells and BuildStoryboard that are called in the Setup method to create the storyboard specific to the derived class animation. Figure 6 has another abstract class called LineAnimation which provides a base implementation to get the position of the animated photo thumbnails.
The derived class implementations of the BuildStoryboard method create the storyboard, transitions, and animation variable objects. An instance of each class animates the positions of all the items shown in the media pane, so each such instance has a collection that contains information about each photo, including the animation variables for that particular photo.
The SlideAnimation is the simplest of the three animations. This is used when the user views another page of photos and the animation involves the new page of photos sliding into view from either the left or the right. For each photo that is animated there are two animation variables, one each for the x and y coordinates. Since the slide is horizontal, the y coordinate does not change, so the IUIAnimationTransitionLibrary::CreateConstantTransition method is called to create a constant transition. The x coordinate animation variable is associated with an acceleration-deceleration transition where half of the transition is acceleration and the other half a deceleration.
The MoverAnimation class provides the animation that rearranges photos in the media pane when the window is resized. The photo thumbnails are rearranged so that the space available is filled with photos and this may mean adding extra rows. The MoverAnimation class calculates how the photo will move and creates a variable for the x and y coordinate for each photo. The AddTransition private method creates a parabolic transition on one coordinate so that the value decelerates to zero (by calling the IUIAnimationTransitionLibrary::CreateParabolicTransitionFromAcceleration method), and it creates a accelerate-decelerate transition on the other coordinate where the first 30 percent is an acceleration and the last 30 percent is a deceleration.
The final animation is provided by the FlyerAnimation class. This is called when you open a folder, the new photos fly in from the left and any photos in the media pane fly out to the right. Each thumbnail has a Direct2D graphics path defined for it and an animation variable that determines at which point on the path the photo is currently. The graphics path is an object with an ID2D1Geometry interface and is an arc. The animation variable determines the position along this arc and changes according to a parabolic transition. This position is converted to actual x-y coordinates y calling the ID2D1Geometry::ComputerPointAtLength method.
Conclusion
This article shows how you can create transition objects, which define how a variable changes over time, and how to use a storyboard object to specify the relationship between transitions. It also shows how you can use the animation manager object to obtain the value of the animation objects so that you can draw the animation. Because animation involves redrawing the screen, often at a fast rate, to prevent flicker you should use a graphics API that synchronizes drawing with the monitor refresh, like Direct2D. The Windows Animation Manager and the Direct2D API are the perfect combination to provide animation on Windows 7. The next chapter will cover how Hilo uses the Windows Library API to get access to photo files on the computer.