Reputation: 720
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
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 Binding
s:
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