In my recent class I had a student ask me about animating a brush. I quickly dove into how to animation a *color* in a brush and he indicated that isn't what he wanted. He had a resource-based brush that he wanted to switch out in the Visual State Manager. In Blend when he was setting the Brush during the animation recording of the MouseOver event but that wasn't creating a storyboard but changing the natural brush of the control. To solve this we had to dive into the XAML as Blend doesn't seem to know about the ObjectAnimationUsingKeyFrames animation type.
First a small discussion about animations. Normally animations will take a value and transition between the values. For example a DoubleAnimation that is set up to change the Opacity of a visual element from zero to one, will interpolate the values to create a smooth transition from invisible to visible. Keyframed animations support several types of interpolation including linear (the default), spline and discrete. Linear keyframes simply create a smooth transition between values. Spline keyframes allow you to shape the interpolation between the values to ease-in or ease-out the transition. Lastly, the Discrete keyframes set a value at the time of the keyframe without any interpolation.
The ObjectAnimationUsingKeyFrames animation allows you to use object instead of primitive values for your animation keyframes. The only type of keyframe allowed in the ObjectAnimationUsingKeyFrames animation are Discrete keyframes. The reason is that there is no good way to interpolate between object values. So back to our original problem, animation a Brush. In the MouseOver state, the student wanted to change the Fill of a rectangle from one resource-based Brush to another. For example, here are the brushes defined in the Resources section:
<UserControl.Resources>
<LinearGradientBrush x:Key="backBrush"
EndPoint="0.5,1"
StartPoint="0.5,0">
<GradientStop Color="#FFFF0000" />
<GradientStop Color="#FFE50000"
Offset="1" />
</LinearGradientBrush>
<LinearGradientBrush x:Key="mouseOverBackBrush"
EndPoint="0.5,1"
StartPoint="0.5,0">
<GradientStop Color="#FF006DFF" />
<GradientStop Color="#FF0064E9"
Offset="1" />
</LinearGradientBrush>
...
The student had set the backBrush to the Rectangle's fill during the design phase and wanted to set the mouseOverBackBrush during the Visual State Manager's MouseOver state. Since Blend wouldn't let us do it, we had to write the ObjectAnimationUsingKeyFrames XAML ourselves.
To do this, we created a Storyboard and an ObjectAnimationUsingKeyFrames element inside the MouseOver state (of the Visual State Manager for our Button's ControlTemplate). In the new element we specified the Storyboard.TargetName and Storyboard.TargetProperty to "theBack" and "Fill". Inside the animation, we created a DiscreteObjectKeyFrame for our one value. We just wanted to change it to the since new brush so we only needed one. We set the Keytime to "0:0:0" to specify that this should happen immediately. Lastly, we specified the StaticResource for the mouseOverBackBrush as the Value. Here's the entire MouseOver state XAML:
...
<VisualState x:Name="MouseOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="theBack"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0:0:0"
Value="{StaticResource mouseOverBackBrush}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
...
We didn't need to specify another animation in the Normal state the Visual State Manager automatically handles the resetting of our value back to the original state. It just works. Here's two pictures of the button before and after the mouse over:


You can download the example here.
© 2008 Shawn Wildermuth. All Rights Reserved.
Add Comment | digg this

I’m pleased to announce that we’ve posted three new videos on DataBinding and on DataTemplates (creating templates that control the display of a collection of data in a list control such as a list box)
Unfortunately, we swapped the images for the Blend video and the Xaml video which is a bit confusing but we’ll get that fixed. And, while the three videos do stand alone, watching them in the order 37 –>39 -> 38 will make a good bit more sense <smile>.
In any case, I hope you find them interesting and useful. The first, An Overview… explains the fundamental concepts of DataBinding and ListTemplates. The “second”, DataBinding and DataTemplates in Xaml shows how to bind the properties of business objects to Silverlight controls in Xaml and how to create DataTemplates by hand.
Finally, the “third”, DataBidning and DataTemplates With Expression Blend recreates that same work using Expresion Blend (and note that it is 9 minutes vs. 22!).


I've been playing around with RenderTransforms and decorators and such to make layout transitions, but for this WrapPanel, my goal was to introduce no additional UI elements, animations or transforms. I also wanted to use some easing equations to do some cool transitions, and separate that logic from the WrapPanel. I wanted the elements the be "live" while they were on the screen, and I wanted them to enter the screen in a nice fashion. I also wanted each element to animate individually. One of my earlier versions of this kept track of a single starting time for all animations, and it wasn't cool enough. Sometimes, it is important to be cool.
WrapPanels
A WrapPanel is a layout container that places its elements in rows. It fills up one row, then moves on to the next one. The logic isn't all that tough. What I wanted to do was to make the items move smoothly when the WrapPanel was resized, and also enter the WrapPanel from the upper left and move to their new position.
Below are some of the interesting bits of the panel.
Using a zero-duration Storyboard to simulate a per-frame callback
Not using animations was one of my requirements, since I wanted to use something other than linear interpolation. The AnimateableWrapPanel uses a zero-duration Storyboard to simulate a per-frame callback. Here's how that is done. In the constructor, an empty Storyboard is created and started.
public AnimatedWrapPanel()
{
// This sets up the per-frame callback that we use for animation.
_tick = new Storyboard();
_tick.Duration = new Duration(TimeSpan.Zero);
_tick.Completed += Tick;
_tick.Begin();
}
The Tick method does whatever I want to do each frame, and restarts the Storyboard.
/// <summary>
/// This tells the layout system to call Arrange on the Panel if there are any elements that
/// must be moved.
/// </summary>
private void Tick(object sender, EventArgs e)
{
// If there are still elements to be animated, make sure that ArrangeOverride will
// get called. Note that the InvalidateArrange may already have been called due to
// other operations, but calling it again does not hurt, and all of the work will
// be done in ArrangeOverride regardless of where the invalidation came from.
if (_animatingElements > 0)
{
InvalidateArrange();
}
// Restart the storyboard so we get called back on the next frame.
_tick.Begin();
}
This technique does not guarantee smooth animations. It is possible to overload the panel with too many elements for the framewrate to animate 100% smoothly, or maybe the rest of the app is doing so much work that the framerate slows down, but since I use the elapsed time to figure out where to put the elements, the animations will take the same amount of time on all machines. If there is too much going on in the panel and the app, on a low-end machine, the animation might be choppy, but will still finish in the specified time, or one frame longer if the timing is just off.
Using layout to "animate" the elements
I'm a little conflicted about this technique, but I have seen other people use it, so I thought I'd try it. It is a two stage process. The MeasureOverride call figures out where everything should go, and is the only place where the animations are started. This works because MeasureOverride will be called if the AnimatedWrapPanel changes size, or any of its children change size, or children are added or removed. So size changes, adding items etc. kicks off the animations. In ArrangeOverride, any element that is not where it is supposed to be is moved in that direction according to how much it should be done, and the interpolations. Here's what the ArrangeOverride looks like:
/// <summary>
/// A "normal" ArrangeOverride would just put things where they belong. What this one does
/// is to move the children towards their destinations according to the virtual animation
/// data that has been attached to each element. When they get there, the virtual animation
/// is turned off.
/// </summary>
protected override Size ArrangeOverride(Size finalSize)
{
DateTime now = DateTime.Now;
foreach (UIElement child in Children)
{
AnimatedWrapPanelAttachedData data = GetAnimatedWrapPanelAttachedData(child);
TimeSpan elapsed = data.GetElapsed(now);
if (elapsed < Duration || data.TargetPosition != data.CurrentPosition)
{
// The virtual animation is not done yet, so figure out how far along it is...
double progress = (Duration.TimeSpan != TimeSpan.Zero) ? Math.Min(elapsed.TotalMilliseconds / Duration.TimeSpan.TotalMilliseconds, 1.0) : 1;
// ...and what the next position is.
Point newPosition = BlendPoint(_interpolation, data.StartPosition, data.TargetPosition, progress);
child.Arrange(new Rect(newPosition.X, newPosition.Y, child.DesiredSize.Width, _rowHeights[data.Row]));
data.CurrentPosition = newPosition;
}
else
{
// This element is not animating, but it might have become invalid on its own, so it still
// needs to be arranged. The layout system will do as little as possible.
child.Arrange(new Rect(data.CurrentPosition.X, data.CurrentPosition.Y, child.DesiredSize.Width, _rowHeights[data.Row]));
if (data.IsAnimating)
{
--_animatingElements;
// This is the only place where IsAnimating is set to false. This turns off the virtual animation.
data.IsAnimating = false;
}
}
}
return finalSize;
}
Attached property bag
Rather than have a bunch of hash tables or something in the panel itself, I wanted to keep all of the state that I needed on the elements, which sounded like a job for attached DependencyProperties. However, I got tired of creating and modifying them as I changed my mind, and decided that it would be easier at design-time and more efficient at run-time if I had a single attached property that was actually a class that had a bunch of properties--an attached property bag. This would be efficient because I always used all of the properties for each element.
Interpolations using easing equations
Rather then hard-coding animations into the panel, or using a simple linear interpolation, I wanted to use pluggable interpolations. The interpolations take a double that indicates progress, where zero is "just starting" and one is "completed". They return a number (alpha) where zero refers to the start position, and one refers to the end position, although the number may be less than zero or greater than one. The general equation is:
alpha = fn(progress)
The linear interpolation is the simplest: it just gives returns the progress, i.e. alpha = progress.
In practice, the interpolations are used to blend the starting and target positions. Here's how the intermediate points are arrived at:
/// <summary>
/// Given an interpolation, the starting and ending positions and a number from 0-1 that represents how
/// far along the virtual animation is, calculate the new position.
/// </summary>
Point BlendPoint(Interpolation interpolation, Point from, Point to, double progress)
{
Point p = new Point();
double alpha = interpolation.GetAlpha(progress);
p.X = from.X + alpha * (to.X - from.X);
p.Y = from.Y + alpha * (to.Y - from.Y);
return p;
}
I used some standard interpolations, but it is possibly and easy to define your own, and then just plug them into the panel. I set up the interpolations with some default values that I thought looked nice, but it is possible to get quite creative.
Using the demo app
This should be pretty self-explanatory, but you can add pretty, randomly-colored Rectangles, text and image thumbnails. Clicking on a Rectangle deletes it from the panel. You can also change the size of all of the children, select the duration of the animations, and the interpolation that will be used. Resize the browser to watch the UI move around. I had wanted to have UI to modify the interpolation parameters, but at some point, you have to stop polishing and move one. Shipping is the art of stopping. So that bit of UI is left as an exercise for the developer.
When putting content on the web, it's important to get that content to the user as quickly as possible. In general, reducing the amount of data users need to download is a big step in the right direction. As such, the XAP packaging format used by Silverlight 2 wraps everything in a single, compressed archive file that's browser-friendly. At the moment, the compression for XAP files in Beta 2 is not as efficient as it could be, and XAP files tend to be a bit larger than necessary. Until this is addressed in a future release of the Silverlight tools, here is something that's almost as good. :)
XAP files are really just standard ZIP files with a different extension, so we can use ZIP-file tools to improve the XAP situation quite easily. I wrote a simple batch file named XapReZip.cmd (contents below) that can be added to a Visual Studio 2008 Silverlight project to automatically re-compress the project's XAP file more efficiently. All you need to do is add a single command to your project's post-build events (detailed below) and from then on all your XAP files will be even smaller and quicker to download!
To demonstrate its effectiveness, I've run XapReZip against three test applications, two simple demonstration applications of mine, and the current version of two professional-grade applications you're probably already familiar with. The table below shows the starting XAP file size, the size after running XapReZip, the difference between the two, and the percent difference. (All sizes are in bytes.)
| Scenario |
Starting Size |
After XapReZip |
Difference |
Reduction |
| New Silverlight Project |
3,980 |
3,202 |
778 |
20% |
| ... with DatePicker |
73,228 |
56,513 |
16,715 |
23% |
| ... and DataGrid |
194,452 |
151,646 |
42,806 |
22% |
| HtmlTextBlock |
12,360 |
10,312 |
2,048 |
17% |
| SimpleSilverlightXpsViewer |
13,879 |
11,408 |
2,471 |
18% |
| NBC Olympics Video Player |
789,523 |
615,239 |
174,284 |
22% |
| Line Rider |
789,496 |
538,784 |
250,712 |
32% |
Basically, you can expect to see about a 22% reduction in size for XAP files in general - slightly less for code-intensive XAPs and potentially much more for larger, richer XAPs. For example, it looks like Line Rider could shave a quarter of a megabyte off its download size without breaking a sweat - which is even more compelling given how easy it would be! :)
The complete code for XapReZip.cmd is below. As written, XapReZip assumes that Zip.exe and UnZip.exe will be located in the same directory as XapReZip.cmd (this is easy to change) and that all of the files that are part of the XAP are available in the same directory as the XAP file itself (which just happens to be true after compiling with Visual Studio or MSBuild). For the purposes of this demonstration, I've used Zip 2.32 and UnZip 5.52 from the Info-ZIP group because they're free and have a nice license. Of course, you can use whatever tools you're most comfortable with.
@REM XapReZip - Recompresses XAP files smaller
@REM Invoke as a VS post-build event like so:
@REM C:\XapReZip\XapReZip.cmd $(TargetName).xap
@echo off
setlocal
REM Define paths to zip.exe and unzip.exe
set ZIPEXE="%~p0zip.exe"
set UNZIPEXE="%~p0unzip.exe"
REM Define paths for intermediate files
set XAP=%1
set XAPDIR=%~p1
set XAPBAK=%1.bak
set XAPZIP=%1.zip
REM Output a banner message
echo XapReZip: %XAP%
REM Change to XAP file directory
pushd %XAPDIR%
REM Create new XAP file with maximum compression
%UNZIPEXE% -Z -1 %XAP% | %ZIPEXE% -@ -9 -X %XAPZIP%
REM Abort if something went wrong
if ERRORLEVEL 1 goto :DONE
REM Replace original XAP file with smaller one
copy /y %XAP% %XAPBAK% > NUL
copy /y %XAPZIP% %XAP% > NUL
del %XAPZIP%
REM Output a success message
echo XapReZip: Success
:DONE
REM Restore previous directory
popd
endlocal
Adding XapReZip to a Silverlight Project in Visual Studio is easy. Just go to the Project menu, choose Properties (it's the last item), switch to the Build Events tab, and set a post-build event command line like this: C:\XapReZip\XapReZip.cmd $(TargetName).xap. Here's what it looks like in Visual Studio:
After you've set that one command, you can forget about it - XapReZip will go to work every time you recompile the application and you'll be able to run and debug it just like always. Except a little smaller. :)
So if you're eager to keep your Silverlight applications as lean as possible, consider adding XapReZip to your project. It'll probably be the easiest 22% savings you get all day!