So here I'm going to show you the standard technique to work around this which can be reused on any style/control you're designing. For this we'll be using the arithmetic converter codebase from the previous post as well (which I'll include in the posted solution).
The key to this whole problem is to use a coordinate transformation (see here for some classics) so that we can factor out our binding value. This means we animate a variable in a consistant manner, such as changing it from 0 to 1, then the property we actually want to animate s bound to this variable.
These variables will just be attached properties, which can be reused on any element we need them for. The code below shows two (X and Y) but you can define as many as you require.
public class Variables
{
public static readonly DependencyProperty XProperty = DependencyProperty.RegisterAttached(
"X", typeof(double), typeof(Variables), new FrameworkPropertyMetadata(null));
public static void SetX(UIElement element, double value)
{
element.SetValue(XProperty, value);
}
public static double GetX(UIElement element)
{
return (Double)element.GetValue(XProperty);
}
public static readonly DependencyProperty YProperty = DependencyProperty.RegisterAttached(
"Y", typeof(double), typeof(Variables), new FrameworkPropertyMetadata(null));
public static void SetY(UIElement element, double value)
{
element.SetValue(YProperty, value);
}
public static double GetY(UIElement element)
{
return (Double)element.GetValue(YProperty);
}
}
There is really nothing special about these. They just provide us a way to start ancillary data for our animations (or really any requirement you have).
So let's try this out. I'm going to implement the iPhone-styled checkbox which acts like a sliding toggle button. To aid this, I'm attaching a style package which allows the configurability of extra properties such as state text, state backgrounds, toggle width and toggle background. I'll not show the code here as it really is just some more appropriately named attached properties.
There are going to be four components
- the left rectangle holding the checked information
- the central toggle
- the right rectangle holding the unchecked information
- A framing border around the component
So lets take a look at each of these components individually, firstly the left rectangle:
<Border Background="{TemplateBinding Styles:ToggleCheckBox.CheckedBackground}"
HorizontalAlignment="Left" Height="{Binding Height, RelativeSource={RelativeSource TemplatedParent}}">
<Border.Width>
<MultiBinding Converter="{ArithmeticConverter:ArithmeticConverter}" ConverterParameter="x-y">
<Binding Path="Width" RelativeSource="{RelativeSource TemplatedParent}"/>
<Binding Path="(Styles:ToggleCheckBox.ToggleWidth)"
RelativeSource="{RelativeSource TemplatedParent}"/>
</MultiBinding>
</Border.Width>
<TextBlock FontFamily="{TemplateBinding FontFamily}"
Foreground="{TemplateBinding Styles:ToggleCheckBox.CheckedForeground}"
HorizontalAlignment="Center" VerticalAlignment="Center" FontWeight="{TemplateBinding FontWeight}"
Text="{TemplateBinding Styles:ToggleCheckBox.CheckedText}" FontSize="{TemplateBinding FontSize}"/>
</Border>
Here you can see how we bind to the style properties that we've defined (each which can then be configured on the checkbox itself). There isn't really much to this except to note the usage of the arithmetic converter to set the width to width - toggle width. This is just to make sure the text centres on the rectangle.
Next, the toggle
<Border Background="{TemplateBinding Styles:ToggleCheckBox.ToggleBackground}"Again, nothing really exciting. The main thing to notice here is that we're using a render transform to move the position of the toggle relative to the grid it's contained in. The reason for this is to make sure that it gets rendered (even if hidden). Given that we're using render transforms to change visibility of the components then if you don't do this, some of them will never actually be rendered.
HorizontalAlignment="Left" x:Name="Toggle" Width="{TemplateBinding Styles:ToggleCheckBox.ToggleWidth}"
Height="{Binding Height, RelativeSource={RelativeSource TemplatedParent}}"
BorderBrush="#FF939393" BorderThickness="1" CornerRadius="1">
<Border.RenderTransform>
<TranslateTransform>
<TranslateTransform.X>
<MultiBinding Converter="{ArithmeticConverter:ArithmeticConverter}" ConverterParameter="x-y">
<Binding Path="Width" RelativeSource="{RelativeSource TemplatedParent}"/>
<Binding Path="(Styles:ToggleCheckBox.ToggleWidth)"
RelativeSource="{RelativeSource TemplatedParent}"/>
</MultiBinding>
</TranslateTransform.X>
</TranslateTransform>
</Border.RenderTransform>
</Border>
I'm leaving the right rectangle and framing border out as they pretty much follow the same details. Now the key to this whole enterprise, the grid that will actually be getting animated (containing the left, toggle and right components).
<Grid x:Name="Container">Here is where we actually see the binding to our variable. Since we're moving this left when unchecked then we'll be setting it between 0 and -1 (you could of course do 0 to 1 and just rejig the maths). So we want our relative position of the grid to be X * (Width - Toggle Width) so that it will move the components far enough left or right to hide one of the sides. The converter above does just that. Yay!
<Grid.RenderTransform>
<TranslateTransform>
<TranslateTransform.X>
<MultiBinding Converter="{ArithmeticConverter:ArithmeticConverter}" ConverterParameter="x*(y-z)">
<Binding Path="(Packages:Variables.X)" RelativeSource="{RelativeSource TemplatedParent}"/>
<Binding Path="Width" RelativeSource="{RelativeSource TemplatedParent}"/>
<Binding Path="(Styles:ToggleCheckBox.ToggleWidth)"
RelativeSource="{RelativeSource TemplatedParent}"/>
</MultiBinding>
</TranslateTransform.X>
</TranslateTransform>
</Grid.RenderTransform>
Now all we need is the animation, which should be nice and simple
<ControlTemplate.Triggers>All we need to do is animate our attached variable X between 0 and -1 when the checkbox state changes and we'll slide it the correct distance left and right, as though it was bound to the width itself.
<Trigger Property="IsChecked" Value="False">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Packages:Variables.X)"
From="0" To="-1" FillBehavior="HoldEnd" Duration="0:0:0.25" DecelerationRatio="0.5"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Packages:Variables.X)"
From="-1" To="0" FillBehavior="HoldEnd" Duration="0:0:0.25" DecelerationRatio="0.5"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
Here are some screenshots of it in action:
And the XAML:
<Window x:Class="ItsAllAboutTheXaml.DynamicAnimation.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Styles="clr-namespace:ItsAllAboutTheXaml.DynamicAnimation.Controls.Styles" Title="Window1" Height="300" Width="300">
<StackPanel HorizontalAlignment="Left">
<CheckBox Styles:ToggleCheckBox.CheckedText="ACT" Styles:ToggleCheckBox.UncheckedText="OOM"
Style="{StaticResource ToggleCheckBoxStyle}" Margin="4"/>
<CheckBox FontWeight="Normal" Styles:ToggleCheckBox.CheckedText="ON"
Styles:ToggleCheckBox.UncheckedText="OFF" Style="{StaticResource ToggleCheckBoxStyle}"
Width="70" Height="25" Margin="4"/>
<CheckBox Styles:ToggleCheckBox.CheckedForeground="LightGray" FontSize="10"
Styles:ToggleCheckBox.ToggleWidth="50" Styles:ToggleCheckBox.CheckedText="IN"
Styles:ToggleCheckBox.UncheckedText="OUT" Style="{StaticResource ToggleCheckBoxStyle}"
Width="200" Margin="4"/>
<CheckBox Styles:ToggleCheckBox.CheckedBackground="Red" Style="{StaticResource ToggleCheckBoxStyle}"
Width="75" Margin="4"/>
</StackPanel>
</Window>
It's as simple as that and with multiple variables and the arithmetic converter you can really create some incredibly customised animations to fit all your requirements.
You can get the code here (this should include the dll for the ArithmeticConverter).