Search code examples
c#wpfblendwpf-animation

Animate button from one point to another


I have an expander containing some elements along with a button. When the expander is collapsed, I would like to display the button in the expander's header for easy access. When the expander is expanded, I want the button to be part of the expander's content.

I can get the button in the header to fade in and out when the expander is expanded and collapsed but I would like to take it a step further. I would like the button from the expander's content to move up to the location in the header when collapsed and move down to the content's location when expanded.

I can get somewhat close by using blend and drawing a line between the two buttons and converting it into a motion path. However, using this method it is very difficult to get the positioning exact. Part of me feels like drawing out the path is a bit over-complicating things. All I want is for Button A to animate to become Button B.

Currently I'm only concerned about the button's location. The two buttons are different sizes but animating the width and height of the button is more straight forward :)

This is an example program I quickly made to demonstrate what I'm working with. It's mainly just to provide a visual layout. The fading animation and path animations are not included in this example.


<Window x:Class="test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:test"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <Window.Resources>
        <Style TargetType="{x:Type Button}">
            <Setter Property="Margin" Value="5" />
            <Setter Property="Background" Value="Red" />
            <Setter Property="Foreground" Value="White" />
        </Style>
    </Window.Resources>

    <Grid>
        <Expander Margin="5" BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Stretch" IsExpanded="True">
            <Expander.Header>
                <DockPanel Margin="5">
                    <TextBlock Margin="5" VerticalAlignment="Center">Hello World</TextBlock>
                    <Button Padding="5">Button</Button>
                </DockPanel>

            </Expander.Header>
            <Expander.Content>
                <StackPanel Orientation="Horizontal">
                    <Canvas Margin="5" Width="300" Background="Black" />

                    <StackPanel>
                        <StackPanel.Resources>
                            <Style TargetType="{x:Type RadioButton}">
                                <Setter Property="Margin" Value="5" />
                            </Style>
                        </StackPanel.Resources>
                        <RadioButton IsChecked="True">Option 1</RadioButton>
                        <RadioButton>Option 2</RadioButton>
                        <RadioButton>Option 3</RadioButton>
                        <RadioButton>Option 4</RadioButton>

                        <Button Width="100" Height="50">Button</Button>
                    </StackPanel>
                </StackPanel>
            </Expander.Content>
        </Expander>
    </Grid>
</Window>

Any help is greatly appreciated. I've tried to find some resources to help but all I've been able to find is moving an element along a path, not to a specific point unless I'm missing something obvious.

Thanks


Solution

  • This is not a trivial thing to do.

    Basically you're trying to animate a button between two completely different parts of the visual tree, so you're going to have to template the whole thing and wrap everything up in a parent layout that contains both your expander and the button you're trying to animate.

    Templating your expander shouldn't be too difficult, just move your cursor to it's XAML, go to the properties tab, select "Template" and click the little square to the right to generate all the code for the new ControlTemplate. I've done it for you in the code below, but the theming will probably be all wrong so you'll want to do it yourself.

    Next step is to create two rectangles, one in the expanders header and one in its content area. Set the size of these to whatever you want your button to be but set the fill to Transparent; they're only placeholders to help with layout, and you'll use them just to align your button control later on.

    Now you'll need to go back to your ControlTemplate and wrap all the content (i.e. the top-level border) in a grid. You'll also want to place your actual button as a child of this grid.

    Finally, you'll need a behaviour that binds to the two rectangles you created (so it can calculate From and To points), the parent grid (so that it has a parent control it can animate your button relative to) and the expander (so it can trigger animations in response to the expander expanding and collapsing). The actual animation itself can then be a ThicknessAnimation which animates the button's Margin.

    Put all that together and you get this:

     xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
     xmlns:behaviors="clr-namespace:WpfApp1.Behaviors"
    
    
    <UserControl.Resources>
        <ControlTemplate x:Key="ExpanderControlTemplate1" TargetType="{x:Type Expander}">
            <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="3" SnapsToDevicePixels="True">
                <Grid x:Name="Grid">
                    <DockPanel>
                        <ToggleButton x:Name="HeaderSite" ContentTemplate="{TemplateBinding HeaderTemplate}" Content="{TemplateBinding Header}" DockPanel.Dock="Top" Foreground="{TemplateBinding Foreground}" FontWeight="{TemplateBinding FontWeight}" FontStyle="{TemplateBinding FontStyle}" FontStretch="{TemplateBinding FontStretch}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" Margin="1" MinWidth="0" MinHeight="0" Padding="{TemplateBinding Padding}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}">
                            <ToggleButton.FocusVisualStyle>
                                <Style>
                                    <Setter Property="Control.Template">
                                        <Setter.Value>
                                            <ControlTemplate>
                                                <Border>
                                                    <Rectangle Margin="0" SnapsToDevicePixels="True" Stroke="Black" StrokeThickness="1" StrokeDashArray="1 2"/>
                                                </Border>
                                            </ControlTemplate>
                                        </Setter.Value>
                                    </Setter>
                                </Style>
                            </ToggleButton.FocusVisualStyle>
                            <ToggleButton.Style>
                                <Style TargetType="{x:Type ToggleButton}">
                                    <Setter Property="Template">
                                        <Setter.Value>
                                            <ControlTemplate TargetType="{x:Type ToggleButton}">
                                                <Border Padding="{TemplateBinding Padding}">
                                                    <Grid Background="Transparent" SnapsToDevicePixels="False">
                                                        <Grid.ColumnDefinitions>
                                                            <ColumnDefinition Width="19"/>
                                                            <ColumnDefinition Width="*"/>
                                                        </Grid.ColumnDefinitions>
                                                        <Ellipse x:Name="circle" Fill="White" HorizontalAlignment="Center" Height="19" Stroke="#FF333333" VerticalAlignment="Center" Width="19"/>
                                                        <Path x:Name="arrow" Data="M1,1.5L4.5,5 8,1.5" HorizontalAlignment="Center" SnapsToDevicePixels="False" Stroke="#FF333333" StrokeThickness="2" VerticalAlignment="Center"/>
                                                        <ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Grid.Column="1" ContentStringFormat="{TemplateBinding ContentStringFormat}" HorizontalAlignment="Left" Margin="4,0,0,0" RecognizesAccessKey="True" SnapsToDevicePixels="True" VerticalAlignment="Center"/>
                                                    </Grid>
                                                </Border>
                                                <ControlTemplate.Triggers>
                                                    <Trigger Property="IsChecked" Value="True">
                                                        <Setter Property="Data" TargetName="arrow" Value="M1,4.5L4.5,1 8,4.5"/>
                                                    </Trigger>
                                                    <Trigger Property="IsMouseOver" Value="True">
                                                        <Setter Property="Stroke" TargetName="circle" Value="#FF5593FF"/>
                                                        <Setter Property="Fill" TargetName="circle" Value="#FFF3F9FF"/>
                                                        <Setter Property="Stroke" TargetName="arrow" Value="Black"/>
                                                    </Trigger>
                                                    <Trigger Property="IsPressed" Value="True">
                                                        <Setter Property="Stroke" TargetName="circle" Value="#FF3C77DD"/>
                                                        <Setter Property="StrokeThickness" TargetName="circle" Value="1.5"/>
                                                        <Setter Property="Fill" TargetName="circle" Value="#FFD9ECFF"/>
                                                        <Setter Property="Stroke" TargetName="arrow" Value="Black"/>
                                                    </Trigger>
                                                    <Trigger Property="IsEnabled" Value="False">
                                                        <Setter Property="Stroke" TargetName="circle" Value="#FFBCBCBC"/>
                                                        <Setter Property="Fill" TargetName="circle" Value="#FFE6E6E6"/>
                                                        <Setter Property="Stroke" TargetName="arrow" Value="#FF707070"/>
                                                    </Trigger>
                                                </ControlTemplate.Triggers>
                                            </ControlTemplate>
                                        </Setter.Value>
                                    </Setter>
                                </Style>
                            </ToggleButton.Style>
                        </ToggleButton>
                        <ContentPresenter x:Name="ExpandSite" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}" DockPanel.Dock="Bottom" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" Visibility="Collapsed" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                    </DockPanel>
                    <Button Width="100" Height="50" Content="Button" HorizontalAlignment="Left" VerticalAlignment="Top">
                        <i:Interaction.Behaviors>
                            <behaviors:AnimatedMarginBehavior ElementA="{Binding ElementName=ElementA}" ElementB="{Binding ElementName=ElementB}" Grid="{Binding ElementName=Grid}" Parent="{Binding RelativeSource={RelativeSource AncestorType=Expander}}" />
                        </i:Interaction.Behaviors>
                    </Button>
                </Grid>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsExpanded" Value="True">
                    <Setter Property="Visibility" TargetName="ExpandSite" Value="Visible"/>
                </Trigger>
                <Trigger Property="ExpandDirection" Value="Right">
                    <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Right"/>
                    <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Left"/>
                    <Setter Property="Style" TargetName="HeaderSite">
                        <Setter.Value>
                            <Style TargetType="{x:Type ToggleButton}">
                                <Setter Property="Template">
                                    <Setter.Value>
                                        <ControlTemplate TargetType="{x:Type ToggleButton}">
                                            <Border Padding="{TemplateBinding Padding}">
                                                <Grid Background="Transparent" SnapsToDevicePixels="False">
                                                    <Grid.RowDefinitions>
                                                        <RowDefinition Height="19"/>
                                                        <RowDefinition Height="*"/>
                                                    </Grid.RowDefinitions>
                                                    <Grid>
                                                        <Grid.LayoutTransform>
                                                            <TransformGroup>
                                                                <RotateTransform Angle="-90"/>
                                                            </TransformGroup>
                                                        </Grid.LayoutTransform>
                                                        <Ellipse x:Name="circle" Fill="White" HorizontalAlignment="Center" Height="19" Stroke="#FF333333" VerticalAlignment="Center" Width="19"/>
                                                        <Path x:Name="arrow" Data="M1,1.5L4.5,5 8,1.5" HorizontalAlignment="Center" SnapsToDevicePixels="False" Stroke="#FF333333" StrokeThickness="2" VerticalAlignment="Center"/>
                                                    </Grid>
                                                    <ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}" HorizontalAlignment="Center" Margin="0,4,0,0" Grid.Row="1" RecognizesAccessKey="True" SnapsToDevicePixels="True" VerticalAlignment="Top"/>
                                                </Grid>
                                            </Border>
                                            <ControlTemplate.Triggers>
                                                <Trigger Property="IsChecked" Value="True">
                                                    <Setter Property="Data" TargetName="arrow" Value="M1,4.5L4.5,1 8,4.5"/>
                                                </Trigger>
                                                <Trigger Property="IsMouseOver" Value="True">
                                                    <Setter Property="Stroke" TargetName="circle" Value="#FF5593FF"/>
                                                    <Setter Property="Fill" TargetName="circle" Value="#FFF3F9FF"/>
                                                    <Setter Property="Stroke" TargetName="arrow" Value="Black"/>
                                                </Trigger>
                                                <Trigger Property="IsPressed" Value="True">
                                                    <Setter Property="Stroke" TargetName="circle" Value="#FF3C77DD"/>
                                                    <Setter Property="StrokeThickness" TargetName="circle" Value="1.5"/>
                                                    <Setter Property="Fill" TargetName="circle" Value="#FFD9ECFF"/>
                                                    <Setter Property="Stroke" TargetName="arrow" Value="Black"/>
                                                </Trigger>
                                                <Trigger Property="IsEnabled" Value="False">
                                                    <Setter Property="Stroke" TargetName="circle" Value="#FFBCBCBC"/>
                                                    <Setter Property="Fill" TargetName="circle" Value="#FFE6E6E6"/>
                                                    <Setter Property="Stroke" TargetName="arrow" Value="#FF707070"/>
                                                </Trigger>
                                            </ControlTemplate.Triggers>
                                        </ControlTemplate>
                                    </Setter.Value>
                                </Setter>
                            </Style>
                        </Setter.Value>
                    </Setter>
                </Trigger>
                <Trigger Property="ExpandDirection" Value="Up">
                    <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Top"/>
                    <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Bottom"/>
                    <Setter Property="Style" TargetName="HeaderSite">
                        <Setter.Value>
                            <Style TargetType="{x:Type ToggleButton}">
                                <Setter Property="Template">
                                    <Setter.Value>
                                        <ControlTemplate TargetType="{x:Type ToggleButton}">
                                            <Border Padding="{TemplateBinding Padding}">
                                                <Grid Background="Transparent" SnapsToDevicePixels="False">
                                                    <Grid.ColumnDefinitions>
                                                        <ColumnDefinition Width="19"/>
                                                        <ColumnDefinition Width="*"/>
                                                    </Grid.ColumnDefinitions>
                                                    <Grid>
                                                        <Grid.LayoutTransform>
                                                            <TransformGroup>
                                                                <RotateTransform Angle="180"/>
                                                            </TransformGroup>
                                                        </Grid.LayoutTransform>
                                                        <Ellipse x:Name="circle" Fill="White" HorizontalAlignment="Center" Height="19" Stroke="#FF333333" VerticalAlignment="Center" Width="19"/>
                                                        <Path x:Name="arrow" Data="M1,1.5L4.5,5 8,1.5" HorizontalAlignment="Center" SnapsToDevicePixels="False" Stroke="#FF333333" StrokeThickness="2" VerticalAlignment="Center"/>
                                                    </Grid>
                                                    <ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Grid.Column="1" ContentStringFormat="{TemplateBinding ContentStringFormat}" HorizontalAlignment="Left" Margin="4,0,0,0" RecognizesAccessKey="True" SnapsToDevicePixels="True" VerticalAlignment="Center"/>
                                                </Grid>
                                            </Border>
                                            <ControlTemplate.Triggers>
                                                <Trigger Property="IsChecked" Value="True">
                                                    <Setter Property="Data" TargetName="arrow" Value="M1,4.5L4.5,1 8,4.5"/>
                                                </Trigger>
                                                <Trigger Property="IsMouseOver" Value="True">
                                                    <Setter Property="Stroke" TargetName="circle" Value="#FF5593FF"/>
                                                    <Setter Property="Fill" TargetName="circle" Value="#FFF3F9FF"/>
                                                    <Setter Property="Stroke" TargetName="arrow" Value="Black"/>
                                                </Trigger>
                                                <Trigger Property="IsPressed" Value="True">
                                                    <Setter Property="Stroke" TargetName="circle" Value="#FF3C77DD"/>
                                                    <Setter Property="StrokeThickness" TargetName="circle" Value="1.5"/>
                                                    <Setter Property="Fill" TargetName="circle" Value="#FFD9ECFF"/>
                                                    <Setter Property="Stroke" TargetName="arrow" Value="Black"/>
                                                </Trigger>
                                                <Trigger Property="IsEnabled" Value="False">
                                                    <Setter Property="Stroke" TargetName="circle" Value="#FFBCBCBC"/>
                                                    <Setter Property="Fill" TargetName="circle" Value="#FFE6E6E6"/>
                                                    <Setter Property="Stroke" TargetName="arrow" Value="#FF707070"/>
                                                </Trigger>
                                            </ControlTemplate.Triggers>
                                        </ControlTemplate>
                                    </Setter.Value>
                                </Setter>
                            </Style>
                        </Setter.Value>
                    </Setter>
                </Trigger>
                <Trigger Property="ExpandDirection" Value="Left">
                    <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Left"/>
                    <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Right"/>
                    <Setter Property="Style" TargetName="HeaderSite">
                        <Setter.Value>
                            <Style TargetType="{x:Type ToggleButton}">
                                <Setter Property="Template">
                                    <Setter.Value>
                                        <ControlTemplate TargetType="{x:Type ToggleButton}">
                                            <Border Padding="{TemplateBinding Padding}">
                                                <Grid Background="Transparent" SnapsToDevicePixels="False">
                                                    <Grid.RowDefinitions>
                                                        <RowDefinition Height="19"/>
                                                        <RowDefinition Height="*"/>
                                                    </Grid.RowDefinitions>
                                                    <Grid>
                                                        <Grid.LayoutTransform>
                                                            <TransformGroup>
                                                                <RotateTransform Angle="90"/>
                                                            </TransformGroup>
                                                        </Grid.LayoutTransform>
                                                        <Ellipse x:Name="circle" Fill="White" HorizontalAlignment="Center" Height="19" Stroke="#FF333333" VerticalAlignment="Center" Width="19"/>
                                                        <Path x:Name="arrow" Data="M1,1.5L4.5,5 8,1.5" HorizontalAlignment="Center" SnapsToDevicePixels="False" Stroke="#FF333333" StrokeThickness="2" VerticalAlignment="Center"/>
                                                    </Grid>
                                                    <ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}" HorizontalAlignment="Center" Margin="0,4,0,0" Grid.Row="1" RecognizesAccessKey="True" SnapsToDevicePixels="True" VerticalAlignment="Top"/>
                                                </Grid>
                                            </Border>
                                            <ControlTemplate.Triggers>
                                                <Trigger Property="IsChecked" Value="True">
                                                    <Setter Property="Data" TargetName="arrow" Value="M1,4.5L4.5,1 8,4.5"/>
                                                </Trigger>
                                                <Trigger Property="IsMouseOver" Value="True">
                                                    <Setter Property="Stroke" TargetName="circle" Value="#FF5593FF"/>
                                                    <Setter Property="Fill" TargetName="circle" Value="#FFF3F9FF"/>
                                                    <Setter Property="Stroke" TargetName="arrow" Value="Black"/>
                                                </Trigger>
                                                <Trigger Property="IsPressed" Value="True">
                                                    <Setter Property="Stroke" TargetName="circle" Value="#FF3C77DD"/>
                                                    <Setter Property="StrokeThickness" TargetName="circle" Value="1.5"/>
                                                    <Setter Property="Fill" TargetName="circle" Value="#FFD9ECFF"/>
                                                    <Setter Property="Stroke" TargetName="arrow" Value="Black"/>
                                                </Trigger>
                                                <Trigger Property="IsEnabled" Value="False">
                                                    <Setter Property="Stroke" TargetName="circle" Value="#FFBCBCBC"/>
                                                    <Setter Property="Fill" TargetName="circle" Value="#FFE6E6E6"/>
                                                    <Setter Property="Stroke" TargetName="arrow" Value="#FF707070"/>
                                                </Trigger>
                                            </ControlTemplate.Triggers>
                                        </ControlTemplate>
                                    </Setter.Value>
                                </Setter>
                            </Style>
                        </Setter.Value>
                    </Setter>
                </Trigger>
                <Trigger Property="IsEnabled" Value="False">
                    <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </UserControl.Resources>
    
    <Grid>
        <Expander Margin="5" BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Stretch" IsExpanded="True" Template="{DynamicResource ExpanderControlTemplate1}">
            <Expander.Header>
                <DockPanel Margin="5">
                    <TextBlock Margin="5" VerticalAlignment="Center">Hello World</TextBlock>
                    <Rectangle x:Name="ElementA" Width="100" Height="50" Fill="Transparent" />
                </DockPanel>
            </Expander.Header>
            <Expander.Content>
                <StackPanel Orientation="Horizontal">
                    <Canvas Margin="5" Width="300" Background="Black" />
    
                    <StackPanel>
                        <StackPanel.Resources>
                            <Style TargetType="{x:Type RadioButton}">
                                <Setter Property="Margin" Value="5" />
                            </Style>
                        </StackPanel.Resources>
                        <RadioButton IsChecked="True">Option 1</RadioButton>
                        <RadioButton>Option 2</RadioButton>
                        <RadioButton>Option 3</RadioButton>
                        <RadioButton>Option 4</RadioButton>
                        <Rectangle x:Name="ElementB" Width="100" Height="50" Fill="Transparent" />
                    </StackPanel>
                </StackPanel>
            </Expander.Content>
        </Expander>
    </Grid>
    

    And the behavior:

    public class AnimatedMarginBehavior : Behavior<FrameworkElement>
    {
        private ThicknessAnimation Animation = new ThicknessAnimation();
    
        public FrameworkElement ElementA
        {
            get { return GetValue(ElementAProperty) as FrameworkElement; }
            set { SetValue(ElementAProperty, value); }
        }
    
        public static readonly DependencyProperty ElementAProperty =
            DependencyProperty.Register("ElementA", typeof(FrameworkElement), typeof(AnimatedMarginBehavior),
                new PropertyMetadata(default(FrameworkElement), (d, e) => (d as AnimatedMarginBehavior).OnBindingChanged(e)));
    
        public FrameworkElement ElementB
        {
            get { return GetValue(ElementBProperty) as FrameworkElement; }
            set { SetValue(ElementBProperty, value); }
        }
    
        public static readonly DependencyProperty ElementBProperty =
            DependencyProperty.Register("ElementB", typeof(FrameworkElement), typeof(AnimatedMarginBehavior),
                new PropertyMetadata(default(FrameworkElement), (d, e) => (d as AnimatedMarginBehavior).OnBindingChanged(e)));
    
        public Expander Parent
        {
            get { return GetValue(ParentProperty) as Expander; }
            set { SetValue(ParentProperty, value); }
        }
    
        public static readonly DependencyProperty ParentProperty =
            DependencyProperty.Register("Parent", typeof(Expander), typeof(AnimatedMarginBehavior),
                new PropertyMetadata(default(Expander), (d, e) => (d as AnimatedMarginBehavior).OnBindingChanged(e)));
    
        public Grid Grid
        {
            get { return GetValue(GridProperty) as Grid; }
            set { SetValue(GridProperty, value); }
        }
    
        public static readonly DependencyProperty GridProperty =
            DependencyProperty.Register("Grid", typeof(Grid), typeof(AnimatedMarginBehavior),
                new PropertyMetadata(default(Grid), (d, e) => (d as AnimatedMarginBehavior).OnBindingChanged(e)));
    
        private void OnBindingChanged(DependencyPropertyChangedEventArgs e)
        {
            if (this.ElementA == null)
                return;
            if (this.ElementB == null)
                return;
            if (this.Parent == null)
                return;
            if (this.Grid == null)
                return;
    
            // set initial position based on whether or not the control is expanded
            var currentElement = this.Parent.IsExpanded ? this.ElementB : this.ElementA;
            UIElement container = VisualTreeHelper.GetParent(this.Grid) as UIElement;
            var pos = currentElement.TranslatePoint(new Point(0, 0), container);
            this.AssociatedObject.SetValue(FrameworkElement.MarginProperty, new Thickness(pos.X-1, pos.Y-1, 0, 0));
    
            // get notification when the expander changes state
            this.Parent.Collapsed += (_s1, _e1) =>
            {
                container = VisualTreeHelper.GetParent(this.Grid) as UIElement;
                var from = this.ElementB.TranslatePoint(new Point(0, 0), container);
                var to = this.ElementA.TranslatePoint(new Point(0, 0), container);
                this.Animation.From = new Thickness(from.X, from.Y, 0, 0);
                this.Animation.To = new Thickness(to.X, to.Y, 0, 0);
                this.Animation.Duration = TimeSpan.FromMilliseconds(500);
                this.Animation.EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseInOut };
                this.AssociatedObject.BeginAnimation(FrameworkElement.MarginProperty, this.Animation);
            };
    
            this.Parent.Expanded += (_s2, _e2) =>
            {
                container = VisualTreeHelper.GetParent(this.Grid) as UIElement;
                var from = this.ElementA.TranslatePoint(new Point(0, 0), container);
                var to = this.ElementB.TranslatePoint(new Point(0, 0), container);
                this.Animation.From = new Thickness(from.X, from.Y, 0, 0);
                this.Animation.To = new Thickness(to.X, to.Y, 0, 0);
                this.Animation.Duration = TimeSpan.FromMilliseconds(500);
                this.Animation.EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseInOut };
                this.AssociatedObject.BeginAnimation(FrameworkElement.MarginProperty, this.Animation);
            };
        }
    }
    

    That's a very rough-and-ready example that gives a general idea of how to do this, you'll definitely want to go over it in detail and clean it up a bit.

    Result:

    enter image description here