Layout to layout animations in WCP (part 1)

At PDC, I showed a simple version of layout to layout animations, as we do in Microsoft Max.In Max, when you resize the window or change the size of the items being viewed, they animate to their new locations. This not only looks cool, but really helps usability by letting the user see what's happening to the data being viewed. I will be doing a series of posts showing how this can be done, with a couple of alternatives.

First, let's start with a non-animating version. I'll base my example on the Avalon Application template (what you get when you do a new Avalon application project from Visual Studio).

Our first step is to create the Panel we'll be using. The Panel class is used to contain child elements and controls their layout. Built in examples include DockPanel, StackPanel, WrapPanel, and Grid. Like Max, we'll use a simple panel that lays out the children in a grid and lets the child size be controlled by the user. A Panel implementation needs to implement two methods for the two pass arrangement (first measure, then arrange). MeasureOverride figures out the size of the panel based on the current constraints, and is responsible for calling Measure on all children. ArrangeOverride does the work to actually position and size the elements. To allow the child size to be controlled by the UI, we'll set it using a dependency property that the UI can bind to. The panel definition is as follows:

 public class TilePanel : Panel
{
   // Dependency property that controls the size of the child elements
   public static readonly DependencyProperty ChildSizeProperty
      = DependencyProperty.RegisterAttached("ChildSize", typeof(double), typeof(TilePanel),
         new FrameworkPropertyMetadata(1.0d, FrameworkPropertyMetadataOptions.AffectsMeasure |
         FrameworkPropertyMetadataOptions.AffectsArrange));

   // Accessor for the child size dependency property
   public double ChildSize
   {
      get { return (double)GetValue(ChildSizeProperty); }
      set { SetValue(ChildSizeProperty, value); }
   }

   // Measures the children
   protected override Size MeasureOverride(Size availableSize)
   {
      int childrenPerRow;

      // Figure out how many children fit on each row
      if (availableSize.Width == Double.PositiveInfinity)
         childrenPerRow = this.Children.Count;
      else
         childrenPerRow = Math.Max(1, (int) Math.Floor(availableSize.Width / this.ChildSize));

      // Call measure on all children
      Size childSize = new Size(this.ChildSize, this.ChildSize);
      foreach (UIElement child in this.Children)
      {
         child.Measure(childSize);
      }

      // Calculate the width and height this results in
      double width = childrenPerRow * this.ChildSize;
      double height = this.ChildSize * (Math.Floor((double) this.Children.Count / childrenPerRow) + 1);
      return new Size(width, height);
   }

   // 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);

         // Position the child and set its size
         child.Arrange(new Rect(newOffset, new Size(this.ChildSize, this.ChildSize))); 
      }
      return finalSize;
   }

   // Given a child index, child size and children per row, figure out where the child goes
   private Point CalcChildOffset(int index, int childrenPerRow, double childSize)
   {
      int row = index / childrenPerRow;
      int column = index % childrenPerRow;
      return new Point(column * childSize, row * childSize);
   }
}

Put this class in the application's namspace. Now, we'll use the panel in a simple window for the app. Here's the contents of Window1.xaml:

 <?

Mapping XmlNamespace="MyApp" ClrNamespace="AvalonApplication1" ?>
<Window x:Class="AvalonApplication1.Window1"    xmlns="https://schemas.microsoft.com/winfx/avalon/2005"    xmlns:x="https://schemas.microsoft.com/winfx/xaml/2005"    xmlns:myapp="MyApp"
   Title="AvalonApplication1"    Height="300"    Width="300"
>
<StackPanel Orientation="Vertical">
<Slider MinWidth="200" Minimum="50" Maximum="300"          SmallChange="40" Name="_slider" />
<myapp:TilePanel ChildSize="{Binding ElementName=_slider, Path=Value}">
<Border Background="Red" Margin="4" />
<Border Background="Green" Margin="4" />
<Border Background="Blue" Margin="4" />
<Border Background="Red" Margin="4" />
<Border Background="Green" Margin="4" />
<Border Background="Blue" Margin="4" />
<Border Background="Red" Margin="4" />
<Border Background="Green" Margin="4" />
<Border Background="Blue" Margin="4" />
<Border Background="Red" Margin="4" />
<Border Background="Green" Margin="4" />
<Border Background="Blue" Margin="4" />
</myapp:TilePanel>
</StackPanel>
</Window>

If your application has a different namespace than "AvalonApplication1", just replace all instances with your name. The mapping at the top of the file brings in the CLR namespace, and then the xmlns:myapp line maps it into the XML namespace. This lets us use <myapp:TilePanel> element to use our new TilePanel class, and we put a bunch of colored boxes in it. Note that the child size is bound to a slider. The slider and TilePanel are in a StackPanel, so they stack up vertically. The resulting window looks like:

As you resize the window, the panel's MeasureOverride and ArrangeOverride functions will be called with the new size and the boxes inside will instantly move to their new positions. When you change the slider, it will change the ChildSize property. Since this property is set to affect measure and arrange, it will also call MeasureOverride and ArrangeOverride. This will resize the child elements appropriately.

In the next post, I'll explain how to make the children animate to their new locations instead of just instantly appear there. I'll go through a few different approaches on doing this.