Riley Varga
Riley Varga

Reputation: 720

How do I properly animate my Border to move to the right

So I have this style that targets my ToggleButton, and I am trying to change the HorizontalAlignment of the border named ThumbCircle from Left to Right, I did read that changing that property is not as easy as just changing the value, I'm going to have to do some sort of LayoutTransform, however that didn't seem to work, when I click my button nothing happens, it doesnt move.

So my question is, how do I get the ThumbCircle to move to the right side of the Border that it's currently placed a.

<Style x:Key="MyToggleButton"
       TargetType="{x:Type ToggleButton}">

        <Style.Resources>
            <Color x:Key="Color.MyBtn.PrimaryColor">#2ecc71</Color>
            <Color x:Key="Color.MyBtn.SecondaryColor">#27ae60</Color>
        </Style.Resources>


        <Setter Property="Template">

            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ToggleButton}">
                    <Grid>
                        <Border Width="50" Height="12" Background="White" CornerRadius="6">

                        </Border>

                        <Border Width="25"
                                Background="#2ecc71"
                                CornerRadius="{Binding RelativeSource={RelativeSource Self}, Path=ActualHeight}"
                                HorizontalAlignment="Left"
                                x:Name="ThumbCircle">
                            <Border.Triggers>
                                <EventTrigger RoutedEvent="PreviewMouseDown">
                                    <BeginStoryboard>
                                        <Storyboard>
                                            <!-- Dont forget easing -->
                                            <DoubleAnimation Storyboard.TargetProperty="(LayoutTransform).(ScaleTransform.ScaleX)" Storyboard.TargetName="ThumbCircle" To="10" Duration="0:0:0.5" />
                                        </Storyboard>
                                    </BeginStoryboard>

                                </EventTrigger>
                            </Border.Triggers>
                        </Border>
                    </Grid>

                </ControlTemplate>
            </Setter.Value>

        </Setter>
    </Style>

Upvotes: 1

Views: 733

Answers (1)

Corentin Pane
Corentin Pane

Reputation: 4943

Why nothing is showing up

The reason why you don't see anything moving is because you're targeting the (LayoutTransform).(ScaleTransform.ScaleX) property of your ThumbCircle, but it doesn't have any value set to its LayoutTransform property.

If you add this to your ThumbCircle Border:

    <Border.LayoutTransform>
        <ScaleTransform/>
    </Border.LayoutTransform>

Then you will see something happening. But you'll see a scaling, not a translation! What you want is to translate from one side to the other.

The intuitive fix doesn't work...

The easiest way would have been to first replace the LayoutTransform with a RenderTransform and the ScaleTransform with a TranslateTransform like this:

    <Border.RenderTransform>
        <TranslateTransform x:Name="MyTranslate"/>
    </Border.LayoutTransform>

Then give a name to your Grid like this:

<Grid x:Name="MyGrid">
    ...
</Grid>

And then animating the X property of the TranslateTransform from 0 to your Grid.ActualWidth like this:

<!-- This won't run -->
<DoubleAnimation Storyboard.TargetProperty="X" Storyboard.TargetName="MyTranslate" To="{Binding ElementName=MyGrid, Path=ActualWidth}" Duration="0:0:0.5" />

But it is not possible to achieve this as it is not possible to set a Binding on any property of an Animation when used like this, because WPF makes some optimizations that prevent this as explained here.

A XAML-intensive way to do it

So a way to do it is to define proxy elements whose property we animate from 0 to 1 ,and we make the multiplication with MyGrid.ActualWidth at another location.

So your whole XAML style becomes:

<Style x:Key="MyToggleButton" TargetType="{x:Type ToggleButton}">

    <Style.Resources>
        <Color x:Key="Color.MyBtn.PrimaryColor">#2ecc71</Color>
        <Color x:Key="Color.MyBtn.SecondaryColor">#27ae60</Color>
        <!-- Aded some converters here -->
        <views:MultiMultiplierConverter x:Key="MultiMultiplierConverter"></views:MultiMultiplierConverter>
        <views:OppositeConverter x:Key="OppositeConverter"></views:OppositeConverter>
    </Style.Resources>

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ToggleButton}">
                <Grid x:Name="ContainerGrid">
                    <Border Width="50" Height="12" Background="Red" CornerRadius="6">

                    </Border>
                    <Border Width="25"
                        Background="#2ecc71"
                        CornerRadius="{Binding RelativeSource={RelativeSource Self}, Path=ActualHeight}"
                        HorizontalAlignment="Left"
                        x:Name="ThumbCircle">
                        <Border.Resources>
                            <!-- Proxy object whose X property gets animated from 0 to 1. -->
                            <!-- Could be any DependencyObject with a property of type double. -->
                            <TranslateTransform x:Key="unusedKey" x:Name="Proxy"></TranslateTransform>
                        </Border.Resources>
                        <Border.RenderTransform>
                            <TransformGroup>
                                <!-- Main translation to move from one side of the grid to the other -->
                                <TranslateTransform>
                                    <TranslateTransform.X>
                                        <MultiBinding Converter="{StaticResource MultiMultiplierConverter}" ConverterParameter="2">
                                            <Binding ElementName="Proxy" Path="X"></Binding>
                                            <Binding ElementName="ContainerGrid" Path="ActualWidth"></Binding>
                                        </MultiBinding>
                                    </TranslateTransform.X>
                                </TranslateTransform>

                                <!-- Secondary translation to adjust to the actual width of the object to translate -->
                                <TranslateTransform>
                                    <TranslateTransform.X>
                                        <MultiBinding Converter="{StaticResource MultiMultiplierConverter}" ConverterParameter="2">
                                            <Binding ElementName="Proxy" Path="X"></Binding>
                                            <Binding ElementName="ThumbCircle" Path="ActualWidth" Converter="{StaticResource OppositeConverter}"></Binding>
                                        </MultiBinding>
                                    </TranslateTransform.X>
                                </TranslateTransform>
                            </TransformGroup>
                        </Border.RenderTransform>
                        <Border.Triggers>
                            <EventTrigger RoutedEvent="MouseDown">
                                <BeginStoryboard>
                                    <Storyboard>
                                        <!-- Dont forget easing -->
                                        <DoubleAnimation Storyboard.TargetProperty="X" Storyboard.TargetName="Proxy" To="1" Duration="0:0:0.5" />
                                    </Storyboard>
                                </BeginStoryboard>
                            </EventTrigger>
                        </Border.Triggers>
                    </Border>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

And you would need to define two IValueConverter to perform some basic arithmetic operations on your Bindings:

One for multiplying all supplied values in a MultiBinding:

/// <summary>
/// Defines a converter which multiplies all provided values.
/// The given parameter indicates number of arguments to multiply.
/// </summary>
public class MultiMultiplierConverter : IMultiValueConverter {
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        double result = 1;
        int count = int.Parse((string)parameter);
        for (int i = 0; i < count; i++) {
            result *= (double)values[i];
        }
        return result;
    }
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) {
        throw new NotSupportedException("Cannot convert back");
    }
}

And one to multiply the input by -1:

/// <summary>
/// Defines a converter which multiplies the provided value by -1
/// </summary>
public class OppositeConverter : IValueConverter {
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        return (dynamic)value * -1;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        throw new NotSupportedException("Cannot convert back");
    }
}

This is not an elegant way but it works!

How to implement back and forth animations?

So far we managed to animate the thumb to the right, on click. But that's not the whole point, is it?

What we are templating is a ToggleButton: at every click, an animation to the opposite side should be triggered. More exactly, whenever the IsChecked property gets True, we should trigger an animation to the right, and whenever the IsChecked property gets False, we should trigger an animation to the left.

This is possible by adding some Trigger objects in the ControlTemplate.Triggers collection. The Trigger shall be hooked to the IsChecked property (which we have no control over) and listen to its changes. We can specify an EnterAction which is our animation to the right, and an ExitAction which is our animation to the left.

The full Style becomes:

<Style x:Key="MyToggleButton" TargetType="{x:Type ToggleButton}">

    <Style.Resources>
        <Color x:Key="Color.MyBtn.PrimaryColor">#2ecc71</Color>
        <Color x:Key="Color.MyBtn.SecondaryColor">#27ae60</Color>
        <!-- Aded some converters here -->
        <views:MultiMultiplierConverter x:Key="MultiMultiplierConverter"></views:MultiMultiplierConverter>
        <views:OppositeConverter x:Key="OppositeConverter"></views:OppositeConverter>
    </Style.Resources>

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ToggleButton}">
                <ControlTemplate.Triggers>
                    <Trigger Property="IsChecked" Value="True">
                        <!-- Animation to the right -->
                        <Trigger.EnterActions>
                            <BeginStoryboard>
                                <Storyboard>
                                    <!-- Dont forget easing -->
                                    <DoubleAnimation Storyboard.TargetProperty="X" Storyboard.TargetName="Proxy" To="1" Duration="0:0:0.5" />
                                </Storyboard>
                            </BeginStoryboard>
                        </Trigger.EnterActions>

                        <!-- Animation to the left -->
                        <Trigger.ExitActions>
                            <BeginStoryboard>
                                <Storyboard>
                                    <!-- Dont forget easing -->
                                    <DoubleAnimation Storyboard.TargetProperty="X" Storyboard.TargetName="Proxy" To="0" Duration="0:0:0.5" />
                                </Storyboard>
                            </BeginStoryboard>
                        </Trigger.ExitActions>
                    </Trigger>
                </ControlTemplate.Triggers>
                <Grid x:Name="ContainerGrid">
                    <Border Width="50" Height="12" Background="Red" CornerRadius="6">

                    </Border>
                    <Border Width="25"
                Background="#2ecc71"
                CornerRadius="{Binding RelativeSource={RelativeSource Self}, Path=ActualHeight}"
                HorizontalAlignment="Left"
                x:Name="ThumbCircle">
                        <Border.Resources>
                            <!-- Proxy object whose X property gets animated from 0 to 1. -->
                            <!-- Could be any DependencyObject with a property of type double. -->
                            <TranslateTransform x:Key="unusedKey" x:Name="Proxy"></TranslateTransform>
                        </Border.Resources>
                        <Border.RenderTransform>
                            <TransformGroup>
                                <!-- Main translation to move from one side of the grid to the other -->
                                <TranslateTransform>
                                    <TranslateTransform.X>
                                        <MultiBinding Converter="{StaticResource MultiMultiplierConverter}" ConverterParameter="2">
                                            <Binding ElementName="Proxy" Path="X"></Binding>
                                            <Binding ElementName="ContainerGrid" Path="ActualWidth"></Binding>
                                        </MultiBinding>
                                    </TranslateTransform.X>
                                </TranslateTransform>

                                <!-- Secondary translation to adjust to the actual width of the object to translate -->
                                <TranslateTransform>
                                    <TranslateTransform.X>
                                        <MultiBinding Converter="{StaticResource MultiMultiplierConverter}" ConverterParameter="2">
                                            <Binding ElementName="Proxy" Path="X"></Binding>
                                            <Binding ElementName="ThumbCircle" Path="ActualWidth" Converter="{StaticResource OppositeConverter}"></Binding>
                                        </MultiBinding>
                                    </TranslateTransform.X>
                                </TranslateTransform>
                            </TransformGroup>
                        </Border.RenderTransform>
                    </Border>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Note that this Trigger is only accepted inside a ControlTemplate.Triggers collection, it is not possible to put such a Trigger in the original Border.Triggers collection, you can read more about it here.

Upvotes: 1

Related Questions