Layout to layout animations in WCP: Part 3

Ok, it took me a lot longer to get to this, but I finally have a post about a better way to do layout to layout animations. In part 1, I introduced a simple tile panel and then in part 2, I made modifications so that it animates the children as they are rearranged. There were a couple of things I don’t like about my solution in part 2:

1. It requires modifying the panel and having enough information to compute the previous layout position

2. We were changing the render transforms of the children during the layout pass. This is a bit fragile because Avalon is also applying and using transforms at this stage

Before jumping into my new implementation, I want to mention a couple of other techniques out there.

Kevin Moore implemented some seriously crazy animations using CompositionTarget.Rendering to get a callback at each frame of rendering. This works great if you want to use a physics engine for your animations.

Pete Blois used a clever technique of wrapping each child with a ContentControl that sees when its layout is changed and applies the render transform to itself. This means you can turn on animations for any child in any panel by wrapping it in this ContentControl.

The implementation I want to present in this post allows you to add layout animations to any panel without modifying the panel or adding requirements to the children. How is that possible? Well, the basic principle is the same as I presented in part 2. After children are arranged, we’ll apply a render transform to put the child back at its previous location, and animate that transform so it decays to 0 over time. We’ll take advantage of the LayoutUpdated event that UIElements fire when their layout changes, and the ability to use attached properties to store previous layout information. The basic LayoutUpdated handler algorithm goes something like this:

· Compute the position where the child was arranged by looking at the current position and undoing any render transform

· Read the position it was previously arranged from the attached property

· If the positions are different, apply a render transform from the newly arranged position back to the current position (not the arranged position, but where it actually is including current transforms), and animate the transform away. If the arrange positions were the same, then it’s already in the right place, or an animation is taking care of it

 

Update: Pete Blois noted that LayoutUpdated is a global event and fires when any layout updated. So, expect lots of calls to it!

I’ve wrapped it up into a class below that you construct with a panel. You can modify the sample we used in part 1 by just adding:

            new PanelLayoutAnimator(_tilePanel);

to the Window’s constructor.

An alternate form factor would be to create a ContentControl that applied a PanelLayoutAnimator to a child. That would let you just wrap the panel with this control in the Xaml to get animations. If there is interest, I can provide the exact details on how to do that.

 

Update: Pete Blois had a good idea on another form facotr, and Ben Constable noted something similar. One trick you can use is to register an attached property along with a global property changed handler on it. When the property changed, you can see what it's attached to and create a PanelLayoutAnimator pointing to it. Then, you can do something like <WrapPanel PanelLayoutAnimator.Active="true" />. If anyone wants details on this, I can provide them.

Here’s the code:

public class PanelLayoutAnimator

{

    public PanelLayoutAnimator(Panel panelToAnimate)

    {

        _panel = panelToAnimate;

        _panel.LayoutUpdated += new EventHandler(PanelLayoutUpdated);

    }

    void PanelLayoutUpdated(object sender, EventArgs e)

    {

        // At this point, the panel has moved the children to the new locations, but hasn't

        // been rendered

        foreach (UIElement child in _panel.Children)

        {

            // Figure out where child actually is right now. This is a combination of where the

            // panel put it and any render transform currently applied

            Point currentPosition = child.TransformToAncestor(_panel).Transform(new Point());

            // See what transform is being applied

            Transform currentTransform = child.RenderTransform;

            // Compute where the panel actually arranged it to

            Point arrangePosition = currentPosition;

            if (currentTransform != null)

            {

                // Undo any transform we applied

                arrangePosition = currentTransform.Inverse.Transform(arrangePosition);

            }

            // If we had previously stored an arrange position, see if it has moved

            if (child.ReadLocalValue(SavedArrangePositionProperty) != DependencyProperty.UnsetValue)

            {

                Point savedArrangePosition = (Point)child.GetValue(SavedArrangePositionProperty);

                // If the arrange position hasn't changed, then we've already set up animations, etc

                // and don't need to do anything

                if (!AreReallyClose(savedArrangePosition, arrangePosition))

                {

                    // If we apply the current transform to the saved arrange position, we'll see where

                    // it was last rendered

              Point lastRenderPosition = currentTransform.Transform(savedArrangePosition);

                    // Transform the child from the new location back to the old position

                    TranslateTransform newTransform = new TranslateTransform();

                    child.RenderTransform = newTransform;

                    // Decay the transformation with an animation

                    newTransform.BeginAnimation(TranslateTransform.XProperty, MakeAnimation(lastRenderPosition.X - arrangePosition.X));

                    newTransform.BeginAnimation(TranslateTransform.YProperty, MakeAnimation(lastRenderPosition.Y - arrangePosition.Y));

                }

            }

            // Save off the previous arrange position

            child.SetValue(SavedArrangePositionProperty, arrangePosition);

        }

    }

    // Check if two points are really close. If you don't do epsilon comparisons, you can get lost in the

    // noise of floating point operations

    private bool AreReallyClose(Point p1, Point p2)

    {

        return (Math.Abs(p1.X - p2.X) < .001 && Math.Abs(p1.Y - p2.Y) < .001);

    }

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

        return animation;

    }

    // dependency property we attach to children to save their last arrange position

    private static readonly DependencyProperty SavedArrangePositionProperty

       = DependencyProperty.RegisterAttached("SavedArrangePosition", typeof(Point), typeof(PanelLayoutAnimator));

    private Panel _panel;

}