WPF: Auto Arrange Animated Panel


My attention has been diverted as of late, from WPF commands to trying to create something cool in WPF. I’ve been greatly inspired by Kevin Moore, with his WPF Bag of Tricks, especially his cool animated panel that can rearrange things inside. I looked through the code, and there were many things I did not understand. Being fustrated, it was a perfect opportunity to learn something new, at the same time building something fun. After all programming should be fun. 🙂

Before I start talking about specifics, let’s look at the UI for this sample.

arrangepanel1 Please have a look at the demo video, I’m sure some of you will enjoy this.

The essential part of this sample is in the custom ArrangePanel class inherited from System.Windows.Controls.Panel. Before we go into that, there are some interesting things about WPF’s layout mechanism you should know about. WPF booasts a dynamic layout mechanism where controls should not be statically placed on the Window/Page, like how we tranditionally do in WinForms. The controls in the visual tree “talk” to each other before they decide how to lay themselves out . This happens in the FrameworkElement‘s MeasureOverride and ArrangeOverride methods. Here’s an illustration of this intent.

Consider a scenario where we have a Window on the UI, with manyTextBoxes as children…this is what happens when the Window control is loaded…

** Window‘s Size MeasureOverride(Size availableSize) is invoked. **
Window: “Look, I got ‘availableSize’ worth of space on the UI…cool. Let’s see how much I actually need…”

** Window iterates through it’s collection of Children, and invokes the Measure(Size availableSize) on each child element, in this case TextBox. **
Window to each TextBox: “Yo TextBox, I giving you ‘availableSize’ worth of space to display yourself, you are free to decide how much you need, but do not exceed what’s given to you alright.”

** Window then obtains each child’s actual size they took up by using child.DesiredSize property. Window calculates the total width and height required (using your own logic of course), then returns that size as return value. **

** Next Window‘s Size ArrangeOverride(Size finalSize) is invoked. **
Window: “So ‘finalSize’ is all the size I get to position myself. Crao now I have to figure out how to display my children.”

** After working out how to display it’s children, Window invokes Arrange(Rect finalRect) on each child, telling it’s position and size (which is defined as a Rect) **
Window to each TextBox: “I finally figured out the layout, you should position your at this spot, just use this ‘finalRect’ value.”

** After telling every child how to position themselves, Window returns the ‘finalSize’ value as the return value. **

I hope this rather kiddish dialogue helped you understand what’s going on. If you have been following, you would like notice a pattern, where calling Measure triggers MeasureOverride and Arrange triggers ArrangeOverride. It’s analogous to a chain of responsiblity where each element questions/specifies it’s child about size and position.

So let’s start with creating a custom Panel. I’m going to skip the code for the dependency properties for this panel, namely ItemWidth, ItemMargin and CrazyMode. I’m just going to concentrate of the two layout methods.

protected override Size MeasureOverride(Size availableSize)
{
    int childrenPerRow = 0;

    foreach (UIElement ele in Children)
    {
        ele.Measure(new Size(ItemSize + ItemMargin, ItemSize + ItemMargin));
    }

    if (availableSize.Width == double.PositiveInfinity)
    {
        childrenPerRow = Children.Count;
    }
    else
    {
        childrenPerRow = Math.Max(1, (int)availableSize.Width / (ItemSize + ItemMargin));
    }

    double totalHeight = this.ItemSize * (Math.Floor((double)this.Children.Count / childrenPerRow) + 1);

    double rowWidth = (ItemSize + ItemMargin) * childrenPerRow;

    return new Size(rowWidth, totalHeight);
}

protected override Size ArrangeOverride(Size finalSize)
{
    Point currentPosition = new Point(ItemMargin, ItemMargin);

    int childrenPerRow = Math.Max(1, (int)finalSize.Width / (ItemSize + ItemMargin));
    int count = 1;

    foreach (FrameworkElement ele in Children)
    {
        // check if should still be on current row
        if (count < = childrenPerRow) // still on same row
        {
            ArrangeChild(ele, currentPosition, finalSize);
        }
        else // new row
        {
            currentPosition.Y += ItemSize + ItemMargin; // move position down to new row
            currentPosition.X = ItemMargin; // reset X position
            count = 1; // reset count
            ArrangeChild(ele, currentPosition, finalSize);
        }
        currentPosition.X += ItemSize + ItemMargin; // move position right
        count++;
    }
    return finalSize;
}
&#91;/sourcecode&#93;

Important thing to note about <span style="color:#0000ff;">MeasureOverride is you <span style="text-decoration:underline;"><strong>MUST</strong></span><strong> </strong>call Measure for each child. Only after that will each child return your their <span style="color:#0000ff;">DesiredSize</span>. You can then perform your own logic to return the actual width/height your panel requires. For <span style="color:#0000ff;">ArrangeOverride</span>, again you need to call <span style="color:#0000ff;">Arrange </span>for each child so they know how to position themselves, according to your layout logic. One thing to note is that it's perfectly possible for '<em>availableSize</em>' to have <span style="color:#0000ff;">Double.PositiveInfinity</span> as width or height, because you can have unlimited space in either direction.  However, if you return a <span style="color:#0000ff;">Size </span>that has either <span style="color:#0000ff;">Double.PositiveInfinity</span> as width or height, an exception will be raised.  In <span style="color:#0000ff;">ArrangeOverride</span>, I have a <span style="color:#0000ff;">ArrangeChild </span>method, which I call to arrange the child element's position and perform animation. Let's look at that code now.


private void ArrangeChild(FrameworkElement element, Point finalPosition, Size availableSize)
{
    TranslateTransform trans = element.RenderTransform as TranslateTransform;
    // depending on CrazyMode, we set 0,0 as origin or a random X,Y position on the screen
    double randomX = CrazyMode ? random.Next(0, (int)availableSize.Width) : 0;
    double randomY = CrazyMode ? random.Next(0, (int)availableSize.Height) : 0;
    // create new Translate Transform
    if (trans == null)
    {
        trans = new TranslateTransform();
        element.RenderTransform = trans;
    }
    // tell the child element to arrange itself
    element.Arrange(new Rect(new Point(randomX, randomY), new Size(ItemSize, ItemSize)));

    // keep translation point and store it in it's Tag property for rendering later.
    Point translatePosition = new Point(finalPosition.X - randomX, finalPosition.Y - randomY);

    Dispatcher.BeginInvoke(new Action<frameworkelement , Point>(BeginAnimation),
            DispatcherPriority.Render, element, translatePosition);
}

private void BeginAnimation(FrameworkElement element, Point translatePosition)
{
    // hook up X and Y translate transformations
    element.RenderTransform.BeginAnimation(TranslateTransform.XProperty, new DoubleAnimation(translatePosition.X, duration), HandoffBehavior.Compose);
    element.RenderTransform.BeginAnimation(TranslateTransform.YProperty, new DoubleAnimation(translatePosition.Y, duration), HandoffBehavior.Compose);
}

I hooked up a TranslateTranform into each element here, in the ArrangeChild method. Depending on the CrazyMode dependency property, I will set the transformation origin as either (0, 0) or a random (X, Y) value. Notice thatI used a Dispatcher.BeginInvoke to call the BeginAnimation method asynchronously and placed in the queue (with Render priority), so that processing of the ArrangeOverride will not held up by the current UI thread. Lastly, duration is hardcoded to 400 milliseconds. Now let’s look at the Window Xaml. I won’t be showing the entire Xaml because it’s pretty long. Most of it is layout code, which I’m sure you can figure out by yourself.

<ItemsControl Name="itemsControl" Grid.Row="2" Focusable="false">
 <ItemsControl.ItemsPanel>
   <ItemsPanelTemplate>
     <ap:ArrangePanel x:Name="arrangePanel" 
         ItemSize="{Binding ElementName=sizeSlider, Path=Value}"
         ItemMargin="{Binding ElementName=marginSlider, Path=Value}"
         CrazyMode="{Binding ElementName=crazyModeChkBox, Path=IsChecked}" />
   </ItemsPanelTemplate>
 </ItemsControl.ItemsPanel>
</ItemsControl>

I used an ItemsControl to “store” my item source, which happens to be an ObservableCollection of Rectangle. ItemsControl is a type of container control that can contain collection of items, and click this link to see this control family. ObservableCollection class is an awesome addition to the .NET framework because it has encapsulated the plumbing for two-way databinding, where we had to do traditionally using INotifyPropertyChanged. I have specified the ArrangePanel as it’s panel template, so our custom panel will contain the item source of it’s parent. As you can see, I have databinded the ArrangePanel dependency properties to the Sliders and CheckBox on the UI. Lastly, we have to look at the setting the item source for the Window in the codebehind.

private ObservableCollection<system .Windows.Shapes.Rectangle> rectangles =
    new ObservableCollection<rectangle>();
private Random random = new Random();

public Window1()
{
    InitializeComponent();
    LoadRectangles();
    itemsControl.ItemsSource = rectangles;

}

That’s all there is to this. The difficult or rather tricky bits are in MeasureOverride and ArrangeOverride logic, and the translate tranformation. One thing I’m not really happy about this sample is that the UI tends to become a little sluggish when you add a lot of items. If you can identify the problem after seeing my code, or come up with a more efficient implementation, do leave me a note. Happy Coding! 🙂

Download the source code here.

Share this post :

Advertisements
Posted in WPF. Tags: . 5 Comments »

5 Responses to “WPF: Auto Arrange Animated Panel”

  1. Pedro Pombeiro Says:

    Good stuff, thanks for this demo! It finally set me in the right direction to implementing animation in my app. I then improved the code a bit for a general StackPanel, WrapPanel or UniformGrid scenario.

  2. Animated Panels | Software and UX Says:

    […] some code on the web that I could use to build the basis of the animation, on Ed Foh’s blog at https://codeblitz.wordpress.com/2009/03/20/wpf-auto-arrange-animated-panel/. Ed in turn was inspired by Kevin Moore’s WPF Bag of Tricks, which also includes an Animating […]

  3. Michael Bond Says:

    Hello.
    I found bug in this code: if you add marging for the Elements –
    they all will be “cutted”. But your code halp me another way 🙂
    Thanks!

  4. Prabu Says:

    Nice stuff..


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: