Click here to Skip to main content
Click here to Skip to main content
Go to top

WPF Panel Switch Animation

, 14 Jan 2014
Rate this:
Please Sign up or sign in to vote.
A smooth transform from one layout to another

Introduction

Panel in WPF is actually a logic preset that determines the way children of the panel will be arranged visually.
So we, as developers, knowing this logic, can choose the appropriate panel that will display its content in the way we expect.

Panels can also relay their display logic upon additional parameters, such as the order of the elements, or, special Attached-Properties they provide.

Switching between Panels

On some occasions, UI is 'called' to change its display (view) from one to another, while displayed elements should remain the same.

This procedure should re-arrange the UI-Elements in a different way while making the user convinced that he is seeing the same elements, only arranged in a different way.

The Power of Animation

In the early days of WPF, it was common to see many animated controls that were 'jumping'/'spinning'/changing colors and so on.
In my opinion, this is a mis-use of Animation as a concept. I consider animation as a 'UI to User' information channel, on which UI passes additional, important information to the user!, rather than just making the UI visually impressive or fun.

If we'll take the issue at hand - switching same set of elements from one layout to another, animation becomes critical for the user's understanding of the UI.
If we'll make this switch instant - the old layout will disappear and the new one will show, the user might misunderstand the action for a new set of controls being presented to him, while if we'll animate the transition, it would be perfectly clear for the user that he is seeing the same elements, only placed in different locations.

Another benefit of 'Animated-Transition' is to do with 'Human-Natural-Conception': we as humans live in the real world (most of us, at least) on which transitions occur mostly in a gradual way. For instance - when I pick my pen and move it to a different location, I can see its movement to its new location, the pen doesn't disappear from its current place and pop-up in a new place. This would make us a bit uncomfortable, and it will take us a while to figure out what just happened.

While in the 'UI world', this 'popping-up' scenario is very common. So making these transitions behave more close to most of the transitions in the physical world will keep our user more comfortable and in sync with what is actually happening in the UI.

Background

What we want #1

As a concept - Take a set of elements and display them in different layout logics

WPF implementation - ItemsControl Items can be arrange in different layouts based on the ItemsControl's ItemsPanel property

What we want #2

As a concept - Transition between layout should be animated

WPF implementation - In order to animate elements transition from one location (prev' layout) to another (new layout), we should intercept the change in the ItemsPanel property, take a 'snap-shot' of each item at its prev' state, apply the new layout*, take another 'snap-shot' of the element, then - animate 'snapshot to snapshot' in an efficient way**.

* We will apply the new layout in an invisible way, so we will be able to take the 'after' snapshot, apply the 'before-to after' animation, and only then! Display the new layout.

** Efficient animation will be achieved by animating a specially-created, lightweight ItemsControl's Adorner for each of the ItemsControl's elements, This Adorner will have an image of the 'Before-State' and a Background of the 'After-State', then, we'll Animate the Adorner's RenderTransform properties.

Using the Code

As described above, we will do the following:

Instead of directly changing the ItemsPanel property of the ItemsControl, we'll add a new AttachedProperty named 'ChangeMonitoredItemsPanelTemplate' - this will allow us to intercept and respond to the layout change.

<ItemsControl app:ItemsControlAttached.ChangeMonitoredItemsPanelTemplate=
    "{Binding ElementName=cmbxItemsPanel,Path=SelectedValue}" Grid.Row="1" >
...

In the AttachedProperty change callback will do the following:

  • Move over all Items of the ItemsControl: Register for its Loaded event (that will be fired when new Panel will be applied and will give us the 'After' snapshot), create a matching Adorner, hold the Adorner (AnimAdorner) in the Item's Tag property.
  • Apply the new Panel to the ItemsControl's ItemsPanel property.
        private static void ChangeMonitoredItemsPanelTemplateChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            ItemsControl ic = sender as ItemsControl;
            if (e.OldValue != null)
            {
                int itemscount = ic.Items.Count;
                for (int i = 0; i < itemscount; i++)
                {
                    var fe = ic.ItemContainerGenerator.ContainerFromIndex(i) as FrameworkElement;
                    fe.Loaded += fe_Loaded;

                    var animador = new AnimAdorner(ic, fe, (Duration)ic.GetValue
                    (ItemsControlAttached.ChangePanelAnimationDurationProperty), 
                    (EasingFunctionBase)ic.GetValue(ItemsControlAttached.ChangePanelAnimationEasingFunctionProperty));
                    AdornerLayer.GetAdornerLayer(ic).Add(animador);
                    fe.Tag = animador;
                }
            }
            (sender as ItemsControl).ItemsPanel = (ItemsPanelTemplate)e.NewValue;
        }

In the AnimAdorner constructor:

  • We will create an Image of the Item's 'Before' state
  • We will create necessary Animation objects for Transition the element from its 'Before' Position/Size/Look to its 'After' Position/Size/Look
        public AnimAdorner(UIElement adornedElement, UIElement ChildElement, 
    Duration MoveDuration, EasingFunctionBase Easing) :
            base(adornedElement)
        {
            
            var ic = adornedElement as FrameworkElement;
            var fe = ChildElement as FrameworkElement;

            var point = fe.TransformToVisual(ic).Transform(new Point(0, 0));
            sizeSrc = new Size(fe.ActualWidth, fe.ActualHeight);

            var bitmap = RenderToBitmap(ChildElement);

            var tg = new TransformGroup();
            var st = new ScaleTransform(1, 1);
            tg.Children.Add(st);
            var tt = new TranslateTransform(point.X, point.Y);
            tg.Children.Add(tt);
            var imgSrc = new Image();

            imgSrc.Visibility = System.Windows.Visibility.Visible;
            imgSrc.Source = bitmap;
            imgSrc.Stretch = Stretch.None;
                        
            gridChild = new Grid();
            gridChild.Visibility = System.Windows.Visibility.Visible;
            gridChild.Children.Add(imgSrc);
            gridChild.RenderTransform = tg;
            this.AddVisualChild(gridChild);

            Storyboard sb = new Storyboard();

            DoubleAnimation daX = new DoubleAnimation();
            daX.Duration = MoveDuration;
            daX.EasingFunction = Easing;
            Storyboard.SetTarget(daX, gridChild);
            Storyboard.SetTargetProperty(daX, new PropertyPath
            ("(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.X)"));
            sb.Children.Add(daX);

            DoubleAnimation daY = new DoubleAnimation();
            daY.Duration = MoveDuration;
            daY.EasingFunction = Easing;
            Storyboard.SetTarget(daY, gridChild);
            Storyboard.SetTargetProperty(daY, new PropertyPath
            ("(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.Y)"));
            sb.Children.Add(daY);

            DoubleAnimation daSX = new DoubleAnimation();
            daSX.Duration = MoveDuration;
            daSX.EasingFunction = Easing;
            Storyboard.SetTarget(daSX, gridChild);
            Storyboard.SetTargetProperty(daSX, new PropertyPath
            ("(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)"));
            sb.Children.Add(daSX);

            DoubleAnimation daSY = new DoubleAnimation();
            daSY.Duration = MoveDuration;
            daSY.EasingFunction = Easing;
            Storyboard.SetTarget(daSY, gridChild);
            Storyboard.SetTargetProperty(daSY, new PropertyPath
            ("(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)"));
            sb.Children.Add(daSY);

            DoubleAnimation daO = new DoubleAnimation();
            daO.To = 0;
            Storyboard.SetTarget(daO, imgSrc);
            Storyboard.SetTargetProperty(daO, new PropertyPath("Opacity"));
            sb.Children.Add(daO);

            sb2PointSize = sb;
        }

Back in the AttachedProperty class ItemsControl's Item Loaded Event (occurs after new Panel applied):

  • Set the new ('After') snapshot as the background of the Adorner.
  • Hide the Item.
  • Adjust the Adorner's Animations to Animate Position (TranslateTransform), Size(ScaleTransform), Look (Opacity of Overlaid Image to 0, thus revealing the Background with New Look)
  • Start the transition Animation
  • Unregister Loaded Event
    --- Upon Transition animation complete - show the actual Item and remove the matching Adorner used for the transition-animation.
        static void fe_Loaded(object sender, RoutedEventArgs e)
        {
            var fe = sender as FrameworkElement;
            var matchingadorner = fe.Tag as AnimAdorner;
            var bitmap =AnimAdorner.RenderToBitmap(fe);
            matchingadorner.SetDstImage(bitmap);

            fe.Visibility = Visibility.Hidden;
            var point = fe.TransformToVisual(fe.Parent as Visual).Transform(new Point(0, 0));
            var size = new Size(fe.ActualWidth, fe.ActualHeight);

            var sb = matchingadorner.sb2PointSize as Storyboard;

            DoubleAnimation daX = sb.Children[0] as DoubleAnimation;
            daX.To = point.X;
            DoubleAnimation daY = sb.Children[1] as DoubleAnimation;
            daY.To = point.Y;

            DoubleAnimation daSX = sb.Children[2] as DoubleAnimation;
            daSX.To = size.Width / matchingadorner.sizeSrc.Width;
            DoubleAnimation daSY = sb.Children[3] as DoubleAnimation;
            daSY.To = size.Height / matchingadorner.sizeSrc.Height;

            DoubleAnimation daOS = sb.Children[4] as DoubleAnimation;
            var SizeRatio = (matchingadorner.sizeSrc.Width * 
            matchingadorner.sizeSrc.Height) / (size.Width * size.Height);
            var DurationRatio = Math.Min(Math.Max(SizeRatio, 0.1), 0.9);
            daOS.Duration = new Duration(TimeSpan.FromSeconds
            (daSY.Duration.TimeSpan.TotalSeconds * DurationRatio));

            sb.Completed += (s1, e1) =>
            {
                fe.Visibility = Visibility.Visible;
                var ic = fe.Parent as ItemsControl;
                AdornerLayer.GetAdornerLayer(ic).Remove(matchingadorner);
            };
            sb.Begin();

            fe.Loaded -= fe_Loaded;
        }

In The MainWindows:

  • Items are children of the ItemsControl, each item is displayed on each Layout according to: applied panel internal-logic, order in the Items list & per-Panel Attached Properties
        <ItemsControl app:ItemsControlAttached.ChangeMonitoredItemsPanelTemplate=
    "{Binding ElementName=cmbxItemsPanel,Path=SelectedValue}" Grid.Row="1" >
...
            <ItemsControl.Items>
                <Button Content="btn1" Width="30.1" Grid.Row="0" 
                Grid.Column="0" DockPanel.Dock="Bottom" 
                Canvas.Left="80" Canvas.Top="90"/>
                <TextBlock x:Name="txt" Text="txt" 
                Width="40" Background="Red" Grid.Row="1" 
                Grid.Column="1" DockPanel.Dock="Right"/>
                <Button Content="btn2" Width="30" 
                Grid.Row="2" Grid.Column="2" DockPanel.Dock="Left"/>
                <Border  Width="30.5" BorderThickness="4" 
                BorderBrush="Blue" Grid.Row="2" Grid.Column="2" 
                Height="31.4" DockPanel.Dock="Right" 
                Canvas.Left="100" Canvas.Top="40"/>
                <TextBlock Text="TEST" FontSize="24" 
                FontWeight="Bold" Grid.Row="1" Grid.Column="2" 
                DockPanel.Dock="Bottom" HorizontalAlignment="Left" 
                VerticalAlignment="Top" Background="Transparent" 
                Canvas.Left="300" Canvas.Top="120"/>

                <TextBox Width="100" Height="70" 
                Canvas.Left="400" Canvas.Top="150" DockPanel.Dock="Bottom"/>

                <Grid MinWidth="50" Height="50" 
                Background="Pink" Canvas.Left="100" Canvas.Top="50">
                    <Button Width="20" Height="10" 
                    HorizontalAlignment="Left" VerticalAlignment="Top" />
                </Grid>

                <Ellipse Width="80" Height="90" 
                Stroke="Red" Fill="Green" Opacity="0.5" 
                Grid.Row="0" Grid.Column="2" DockPanel.Dock="Left"/>

                <StackPanel Grid.Row="0" Grid.Column="0" 
                DockPanel.Dock="Right" HorizontalAlignment="Left" 
                VerticalAlignment="Top">
                    <RadioButton Content="jhzgsdfjh"/>
                    <RadioButton Content="jhzgsdfjh"/>
                    <RadioButton Content="jhzgsdfjh"/>
                    <RadioButton Content="jhzgsdfjh"/>
                </StackPanel>
            </ItemsControl.Items>
        </ItemsControl>

Additional Features

  1. I've added two AttachedProperties: ChangePanelAnimationDuration & ChangePanelAnimationEasingFunction, for better control of the transition-animation behavior.
  2. For this sample sake: Panels (templates) are ComboBox's Items, and Names are extracted using reflection in a special ValueConverter.

Points of Interest

Limitation & Compromises

In some cases (Elements), this Transition-Mechanism produces undesired 'interpretation' for the transition of the element from one Layout to another.

This can be solved, in some cases, by explicitly setting Width/Height or Alignment properties.
It must be acknowledged that on Layout-to Layout transition, not only the ItemsControl's Items change their Position/Size/Look, but also the content of each of the items Re-Arrange itself!
I've used a, what is for me, considered to be a reasonable compromise, and fade-in new look over the old one.

In theory, the same mechanism applied on top level items can be applied on each item's internal content (that might consist of items as well) recursively, yet, with performance penalty!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

ntg123
Software Developer (Senior) self employed
Israel Israel
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberfredatcodeproject15-Jan-14 5:28 
GeneralRe: My vote of 5 Pinmemberntg12315-Jan-14 9:05 
QuestionQuite cool, but don't know where I would use it PinmvpSacha Barber15-Jan-14 2:43 
AnswerRe: Quite cool, but don't know where I would use it Pinmemberntg12315-Jan-14 9:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140916.1 | Last Updated 15 Jan 2014
Article Copyright 2014 by ntg123
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid