Jaa


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.

Comments

  • Anonymous
    October 02, 2005
    Excellent post! Just what i've been looking for [some insights on how apps like Max were created, and these small details about them]. Looking forward to your next post on the animation techniques used.

  • Anonymous
    October 03, 2005
    works really well, thanks, and is a nice intro to layout engine creation... is this preferrable to using a control's LayoutTransform and RenderTransform methods?

  • Anonymous
    October 03, 2005
    Hmm, something strange seems to have happened to the comments. Someone asked about this method of layout vs. LayoutTransform and RenderTransform. Yes, this is the way to do simple layouts. We'll use RenderTransform to add animations.

  • Anonymous
    February 16, 2006
    Ok, we finally get to a full implementation with this post. I’ll be showing the implementation of a VirtualizingTilePanel....

  • Anonymous
    February 22, 2006
    The comment has been removed

  • Anonymous
    February 22, 2006
    Another Max teammate, Jeff&amp;nbsp;Simon is blogging!&amp;nbsp;He's started off with a series on adding drag/drop...

  • Anonymous
    February 23, 2006
    Ok, it took me a lot longer to get to this, but I finally have a post about a better way to do layout...

  • Anonymous
    January 06, 2007
    Ok, it took me a lot longer to get to this, but I finally have a post about a better way to do layout

  • Anonymous
    January 06, 2007
    Ok, we finally get to a full implementation with this post. I’ll be showing the implementation of a VirtualizingTilePanel.

  • Anonymous
    January 06, 2007
    This post continues my series on layout to layout animations in WCP (Avalon). In part 1 , I showed to