This post continues my series on layout to layout animations in WCP (Avalon). In part 1, I showed to create a panel that lays out its children in a grid and allows the child size to be controlled with a slider through data binding. In this post, I'll describe one method to animate the children to their new position.
Here's the basic approach. When LayoutOverride needs to move a child, it moves the child, but also applies a RenderTransform to the child to translate it back to its original position. It then applies an animation to the transform so that it decays to zero over time. This has the effect of moving the child to its final position over time. This works even if the item is currently being animated.
In this post, I'll describe the method I showed at PDC. I'll include a couple of alternate approaches in future posts.
In order to implement this approach, we need to figure out the current location of each child before we arrange it so that we can apply the correct transform. This is a bit easier said than done. There's no easy way to get the current location a child in the same coordinate space as the arguments to Arrange(). If you use child.TranslatePoint(), you can get the upper-left corner of the child, but it takes margins and alignment into account. You could try to take those into account yourslef, but that's a very fragile approach. Instead, what we'll do is keep track of the parameters necessary to compute where we positioned the child on last layout, and then apply the current RenderTransform. With the layout algorithm used for TilePanel, we just have to keep track of the child size and children per row to calculate where we put any child.
Here are the modifications to TilePanel. The new code is in bold. I didn't include the methods that did not change.
// Arrange the children
protected override Size ArrangeOverride(Size finalSize)
// Calculate how many children fit on each row
int childrenPerRow = Math.Max(1, (int) Math.Floor(finalSize.Width / this.ChildSize));
for (int i = 0; i < this.Children.Count; i++)
UIElement child = this.Children[i];
// Figure out where the child goes
Point newOffset = CalcChildOffset(i, childrenPerRow, this.ChildSize);
if (_oldChildrenPerRow != -1)
// Figure out where the child is now
Point oldOffset = CalcChildOffset(i, _oldChildrenPerRow, _oldChildSize);
if (child.RenderTransform != null)
child.RenderTransform.TransformPoint(oldOffset, out oldOffset);
// Transform the child from the new location back to the old position
TranslateTransform childTransform = new TranslateTransform();
child.RenderTransform = childTransform;
// Decay the transformation with an animation
childTransform.BeginAnimation(TranslateTransform.XProperty, MakeAnimation(oldOffset.X - newOffset.X));
childTransform.BeginAnimation(TranslateTransform.YProperty, MakeAnimation(oldOffset.Y - newOffset.Y));
// Position the child and set its size
child.Arrange(new Rect(newOffset, new Size(this.ChildSize, this.ChildSize)));
_oldChildrenPerRow = childrenPerRow;
_oldChildSize = this.ChildSize;
// Create an animation to decay from start to 0 over .5 seconds
private static DoubleAnimation MakeAnimation(double start)
DoubleAnimation animation = new DoubleAnimation(start, 0d, new Duration(TimeSpan.FromMilliseconds(500)));
animation.AccelerationRatio = 0.2;
private double _oldChildSize = 1d;
private int _oldChildrenPerRow = -1;
The changes end up being pretty simple. _oldChildSize and _oldChildrenPerRow are used to cache the old layout parameters, and then it's a pretty simple matter to calculate the current child positions. Then, we apply the transform to this.
This method is not incredibly general. We're taking advantage of the fact that it's easy to calculate the positions of the children based on just a couple of parameters. You also can't apply it to an existing layout. I'll talk about some alternate approaches soon.