Monday, 2 January 2012

Variables In Animation

I'm not sure if you have had this, but one of the most annoying things with animation design in WPF is the fact that you can't use bindings. Being able to animate something to move across the whole width of an element is an obvious desire and yet is not easily supported when done just within XAML.

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
As the user checks and unchecks the component, we're going to animate a render transform on the grid holding the top three components and move it from left to right. This will be moving a distance of width - toggle width. You can now see why the arithmetic converter comes in handy.

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}"
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>
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.

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">
<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>
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!

Now all we need is the animation, which should be nice and simple
<ControlTemplate.Triggers>
<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>
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.

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).

2 comments:

  1. This looks really good. Can you tell me what is your arithmetic converter doing? Can you post the converter file too?

    ReplyDelete
  2. Great Stuff Iain. Thanks!!

    I was trying to customize your template with rounded borders to get the exact look and feel of the Iphone toggle button but the problem is when I set the CornerRadius of the toggle it gives the look of the ellipse but since it is not contained inside the Left rectangle the left rectangle looks like a seperate control while toggle looks like a different control. Its getting difficult to achieve that. Have you tried customizing it?

    ReplyDelete