Introduction
When we (Christian and Nish) started our WPF series last July, we didn't
exactly plan to have a 9 month gap between parts 1 and 2. We were both busy
with various projects and we kept procrastinating writing the next part. Well,
now we are ready with the 2nd part of our WPF series and as they say - better
late than never. In this article, we will talk about how animations can be
applied on properties that do not have an associated animation class, and we
will specifically focus on animating the GridLength
property
with respect to a Grid
control's columns and rows.
Trouble with animating a Grid's columns and rows
Before we get into animating the GridLength
, we'll look
at why it's different from animating a property such as Width
or Opacity
. And while we expect you to have a basic
understanding of how animations work in WPF, let's briefly look at how regular
animations work, where by regular we mean animating properties that have
associated animation classes.
How do regular animations work?
Typical usages of animations involve changing a property (usually a dependency
property) over a specific duration via linear interpolation. As an example, the
following Xaml shows how a button's opacity can be animated from fully opaque to
fully transparent.
<Button Name="button1">
One
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="button2"
Storyboard.TargetProperty="Opacity"
From="1" To="0" Duration="0:0:2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
We could specify double values 1 and 0 as start and stop values for the
Opacity
property. The DoubleAnimation
class
that we used is specialized to work with properties of type double
(and Opacity
, Width
, Length
etc. are all of type double
). If you look at the
System.Windows.Media.Animation
namespace, you'll see that there are
other specialized animation classes for handling values of other common types
such as Boolean
, Char
,
Byte
, Color
, Point
etc.
Next lets look at why the Grid
's column and row
dimensions cannot be animated this way.
Why can't regular animations work with a Grid?
The ColumnDefinition
and RowDefinition
classes have Width
and Height
properties
(respectively) of type GridLength
. The GridLength
is a struct
whose purpose is to support Star
-based
units in addition to pixel-based units. Star
-units
specify a dimension as a weighted proportion of the total available space. So
if you have two columns, where the first has a width {*}
and the second has a width of {3*}
, the first column
will take up 25% of the total width of the containing panel, while the second
column will take up the remaining 75% of space. This offers us a lot of
flexibility when using grids and we don't need to specify hard coded values -
which has the added advantage that it's easier to add rows and columns in
future.
The side-effect of the fact that the Grid
uses
GridLength
for dimensions is that we cannot use any of the library's
built-in animation classes with it since none of them were intended to support
GridLength
. But that does not mean there's nothing we can do about
it. We can (and will) write an animation class specifically for handling units
of type GridLength
.
Writing a custom animation class for GridLength
Our aim is write a GridLengthAnimation
class that will
support animations based on the GridLength
property. To
keep the example simple and to the point, we will only support the From
and To
properties, and will not support properties such
as By
, which are supported by other classes such as
DoubleAnimation
. It would be trivial to add a By
property and this is left as an exercise for the reader (shouldn't take you
more than a few minutes).
We derive a class from AnimationTimeline
which
represents a time line over which values are produced (in our case we'll
produce GridLength
values between the From
and To
range).
namespace GridAnimationDemo
{
internal class GridLengthAnimation : AnimationTimeline
{
We have to override the TargetPropertyType
property
which is abstract
in AnimationTimeline
.
This is a get
-only property that returns the type of the
property that will be animated across a range of supported values. Our
implementation is simple and returns the type of the GridLength
object.
public override Type TargetPropertyType
{
get
{
return typeof(GridLength);
}
}
AnimationTimeLine
has a protected
constructor, and thus any animation object that derives from it has to be
created indirectly. Animation classes indirectly derive from Freezable
, which defines objects that have two states - mutable(unfrozen) and
immutable(frozen). Such classes need to implement (override) a CreateInstanceCore
method which will be used to construct the animation (freezable) object.
CreateInstanceCore
will be called by GetCurrentValueAsFrozenCore
to return a freezable clone of the current object (which may or may not be in a
frozen state at that moment). Again our implementation is very simple.
protected override System.Windows.Freezable CreateInstanceCore()
{
return new GridLengthAnimation();
}
Next, we'll add two dependency properties to handle From
and To
properties. For an excellent write-up on
dependency properties outside of MSDN, read WPF guru and MVP Josh Smith's blog
entry on this topic :
Dependency Properties by Josh Smith. The implementation is
straightforward and there's nothing special to be done here.
static GridLengthAnimation()
{
FromProperty = DependencyProperty.Register("From", typeof(GridLength),
typeof(GridLengthAnimation));
ToProperty = DependencyProperty.Register("To", typeof(GridLength),
typeof(GridLengthAnimation));
}
public static readonly DependencyProperty FromProperty;
public GridLength From
{
get
{
return (GridLength)GetValue(GridLengthAnimation.FromProperty);
}
set
{
SetValue(GridLengthAnimation.FromProperty, value);
}
}
public static readonly DependencyProperty ToProperty;
public GridLength To
{
get
{
return (GridLength)GetValue(GridLengthAnimation.ToProperty);
}
set
{
SetValue(GridLengthAnimation.ToProperty, value);
}
}
Now all that's left is to override GetCurrentValue
and
return the current animated value of the property that's being animated.
public override object GetCurrentValue(object defaultOriginValue,
object defaultDestinationValue, AnimationClock animationClock)
{
double fromVal = ((GridLength)GetValue(GridLengthAnimation.FromProperty)).Value;
double toVal = ((GridLength)GetValue(GridLengthAnimation.ToProperty)).Value;
if (fromVal > toVal)
{
return new GridLength((1 - animationClock.CurrentProgress.Value) *
(fromVal - toVal) + toVal, GridUnitType.Star);
}
else
{
return new GridLength(animationClock.CurrentProgress.Value *
(toVal - fromVal) + fromVal, GridUnitType.Star);
}
}
What we do is calculate and return a gradated value based on the current value
of the AnimationClock
object - which will be between 0
and 1. We create a GridLength
object by using the
constructor that accepts a GridUnitType
as the second
argument for which we specify Star
. That's it - our
GridLengthAnimation
class is ready, and we'll now see how it can be
put to use.
Class usage
Let's look at how the class can be used from both procedural code and from
Xaml.
The sample app
The sample project has a grid with three rows and two columns and each cell has
an image. Note that all six photos shown in the screen shot and available in the
project zip were taken by Nish, and those images are royalty free and may be
reused in whatever legitimate way the reader needs to. You can click on
any of the six images and that cell will animate to fill the window, while the
other cells will diminish in size till they vanish. And if you click on the
maximized image, the reverse animation occurs - where the current image sizes
back to its original dimensions, and the other cells will obviously increase at
the same time, until they return to their starting positions. The GridLengthAnimation
class is used from procedural code in the demo project as shown below.
void image_MouseDown(object sender, MouseButtonEventArgs e)
{
Image image = sender as Image;
if (image != null)
{
int col = Grid.GetColumn(image);
int row = Grid.GetRow(image);
for (int indexRow = 0; indexRow < mainGrid.RowDefinitions.Count;
indexRow++)
{
if (indexRow != row)
{
GridLengthAnimation gla = new GridLengthAnimation();
gla.From = new GridLength(bSingleImageMode
? 0 : 1, GridUnitType.Star);
gla.To = new GridLength(bSingleImageMode
? 1 : 0, GridUnitType.Star); ;
gla.Duration = new TimeSpan(0, 0, 2);
mainGrid.RowDefinitions[indexRow].BeginAnimation(
RowDefinition.HeightProperty, gla);
}
}
for (int indexCol = 0;
indexCol < mainGrid.ColumnDefinitions.Count; indexCol++)
{
if (indexCol != col)
{
GridLengthAnimation gla = new GridLengthAnimation();
gla.From = new GridLength(bSingleImageMode
? 0 : 1, GridUnitType.Star);
gla.To = new GridLength(bSingleImageMode
? 1 : 0, GridUnitType.Star);
gla.Duration = new TimeSpan(0, 0, 2);
mainGrid.ColumnDefinitions[indexCol].BeginAnimation(
ColumnDefinition.WidthProperty, gla);
}
}
}
bSingleImageMode = !bSingleImageMode;
}
Note that while the demo uses procedural code (since it needs to dynamically
apply the animation to the clicked on image cell), you can use it from Xaml too
just as you would use any other animation class. Also note how in the sample
code, we've iterated through the rows and columns and run animations one after
the other. For a more complicated scenario, you would want to create a
StoryBoard
and have all the animations run in parallel, instead of
one after the other as we've done above.
Using from Xaml
Here's some sample Xaml that shows how the GridLengthAnimation
class can be used from Xaml to animate a grid's column width.
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Name="Col0" Width="*"/>
<ColumnDefinition Name="Col1" Width="*"/>
</Grid.ColumnDefinitions>
<Button Name="button1">
One
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<proj:GridLengthAnimation
Storyboard.TargetName="Col1"
Storyboard.TargetProperty="Width"
From="*" To="2*" Duration="0:0:2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
<Button Name="button2" Grid.Column="1">Two</Button>
</Grid>
History
-
Apr 12, 2007 - Article first published on The Code Project