Home | About | Search | RikReader
Transitions in WPF

Lately I've been thinking about how to simplify the implementation of Transitions in WPF. Recently I decided to turn those thoughts into code. Many people are curious how to create a "Slide In" effect like the New York Times Reader - so I have implemented my own version in addition to a simple "Fade In" and the "Wipe" effect that Joseph wrote about on LearnWPFDownload the sample project.

The goal was to create transitions that can be applied as a DataTemplate. They should work for any input and any templated realisation of the input. Like this:

<ContentControl Content="{Binding}" ContentTemplate="{StaticResource SomeTransition}" />

I started by creating a utility Element, Transition.

public class Transition : FrameworkElement

This element wraps a state change: it acceps a single value and ouputs the new value, the previous value, and a state to indicate which is which. Thus Transition has 4 dependency properties: Source, the incoming value that we will transition to; DisplayA, either the old or new value; DisplayB, the other value; and State, an enumeration value of A or B indicating which display member is the new value. The only interesting code in Transition is that which is invoked when Source changes:

private void Swap()
{
    if (this.State == TransitionState.A)
    {
        this.DisplayB = this.Source;
        this.State = TransitionState.B;
    }
    else
    {
        this.DisplayA = this.Source;
        this.State = TransitionState.A;
    }
}

The utility element is finished! So lets write our first transtition to learn about it's behavior. Compile the sample project and select "Learn About" to see what happens.

<DataTemplate x:Key="LearnAboutTransition">
    <StackPanel>
        <local:Transition Name="Transition" Source="{Binding}" />
        <TextBlock Text="{Binding ElementName=Transition, Path=State}" />
        <ContentControl Content="{Binding ElementName=Transition, Path=DisplayA}" />
        <ContentControl Content="{Binding ElementName=Transition, Path=DisplayB}" />
    </StackPanel>
</DataTemplate>

Interesting, but not terribly exciting, since it doesnt actually Transit. Lets spice things up a little. Select "Simple" to see what happens.

<DataTemplate x:Key="SimpleTransition">
    <Grid>
        <local:Transition Name="Transition" Source="{Binding}" />
        <ContentControl Name="a" Visibility="Hidden" 
                        Content="{Binding ElementName=Transition, Path=DisplayA}" />
        <ContentControl Name="b" Visibility="Hidden" 
                        Content="{Binding ElementName=Transition, Path=DisplayB}" />
    </Grid>
    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding ElementName=Transition, Path=State}" Value="A">
            <Setter TargetName="a" Property="Visibility" Value="Visible" />
        </DataTrigger>
        <DataTrigger Binding="{Binding ElementName=Transition, Path=State}" Value="B">
            <Setter TargetName="b" Property="Visibility" Value="Visible" />
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

I've replaced the StackPanel with a Grid, given a Name to the two ContentControls and set their Visibility to Hidden. I've added some DataTriggers that fire when the Transition State changes. Their purpose is to show the new value by setting the Visibility to Visible for the corresponding ContentControl. Now we're back where we started, we have an instantaneous transition. All that's left is to add some animation.

Let's step through my implementation of the New York Times Reader transition, be warned, things might get start to get a little rough here.

First, the content.

<Grid ClipToBounds="True">
    <local:Transition x:Name="transition" Source="{Binding}" />
    <Grid Name="container">
        <Grid.RenderTransform>
            <TranslateTransform X="{Binding ElementName=container, Path=ActualWidth, Converter={x:Static local:Negative.Instance}}" />
        </Grid.RenderTransform>
        <ContentControl Name="a" Visibility="Hidden" Content="{Binding ElementName=transition, Path=DisplayA}" />
        <ContentControl Name="b" Visibility="Hidden" Content="{Binding ElementName=transition, Path=DisplayB}" />
    </Grid>
</Grid>

This time we have the same Transition and ContentControl elements as the "Simple" transtion. There is an outer grid which clips to bounds (any content which sits outside the bounds of the grid will not be visible), and a "container" grid with a TranslateTransform. Through the binding on the transform, the "container" grid has been moved to the left by its ActualWidth (it's entire width as displayed). Why? As we will find out very soon, the location of the "container" grid is to be animated, unfortunately due to cross-threading issues some animation properties can't be dynamic. By setting what is actually the final value here we can work around the animation limitation.

Next, lets look at the Triggers.

<DataTemplate.Triggers>
    <DataTrigger Binding="{Binding ElementName=transition, Path=State}" Value="A">
        <Setter TargetName="a" Property="Visibility" Value="Visible" />
        <Setter TargetName="a" Property="RenderTransform">
            <Setter.Value>
                <TranslateTransform X="{Binding ElementName=container, Path=ActualWidth}" />
            </Setter.Value>
        </Setter>
        <DataTrigger.EnterActions>
            <BeginStoryboard Storyboard="{StaticResource SlideStoryboard}" />
        </DataTrigger.EnterActions>
    </DataTrigger>
    <DataTrigger Binding="{Binding ElementName=transition, Path=State}" Value="B">
        <Setter TargetName="b" Property="Visibility" Value="Visible" />
        <Setter TargetName="b" Property="RenderTransform">
            <Setter.Value>
                <TranslateTransform X="{Binding ElementName=container, Path=ActualWidth}" />
            </Setter.Value>
        </Setter>
        <DataTrigger.EnterActions>
            <BeginStoryboard Storyboard="{StaticResource SlideStoryboard}" />
        </DataTrigger.EnterActions>
    </DataTrigger>
</DataTemplate.Triggers>

The two triggers are basically the same, they just affect the particular ContentControl in question. As per the "Simple" transition they also set the Visibility for the new element to Visible. In addition, they set a TranslateTransform on the new value. This postions the display of the new value to sit at the right edge of the "container" grid. Imagine the highlighted section of the following table is the visible area of the outer Grid, this is what we want to happen.

Start of animation:   Old New
End of animation: Old New  

The last thing each DataTrigger does is to begin a Storyboard. The Storyboard is contained in the resources for the DataTemplate, so lets see them:

<DataTemplate.Resources>
    <Visibility x:Key="Visible">Visible</Visibility>
    <Storyboard x:Key="SlideStoryboard">
        <DoubleAnimation 
            Storyboard.TargetName="container" 
            Storyboard.TargetProperty="RenderTransform.(TranslateTransform.X)"
            From="0" FillBehavior="Stop"
            Duration="0:0:0.2"
            DecelerationRatio="0.5"/>
        <ObjectAnimationUsingKeyFrames 
            Storyboard.TargetName="a" 
            Storyboard.TargetProperty="Visibility" 
            Duration="0:0:0.2" FillBehavior="Stop">
            <DiscreteObjectKeyFrame KeyTime="0:0:0" Value="{StaticResource Visible}" />
        </ObjectAnimationUsingKeyFrames>
        <ObjectAnimationUsingKeyFrames 
            Storyboard.TargetName="b"
            Storyboard.TargetProperty="Visibility" 
            Duration="0:0:0.2" FillBehavior="Stop">
            <DiscreteObjectKeyFrame KeyTime="0:0:0" Value="{StaticResource Visible}" />
        </ObjectAnimationUsingKeyFrames>
    </Storyboard>
</DataTemplate.Resources>

The main thing we are interested in is the DoubleAnimation. It animates the X property of the TranslateTransform applied to the "container" Grid. It has a From value of 0, but nothing assigned to To. Remember how I explained earlier about the value bound to the "container" Grid's TranslateTransform? - this is where it comes into play. We end up sliding the Grid left, by its entire width. The two ObjectAnimationUsingKeyFrames are to ensure that the Old value is visible for the duration of the animation.

And that's it! A New York Times Reader Transition in almost pure XAML!

So why are these transitions implemented as a DataTemplate? Just personal preference. I think these could just as easily be rewritten as custom elements. For example, as subclasses of Transition which could in turn subclass ContentControl. But I'll leave that as an exercise for the reader.

My approach is actually very different to that taken by the New York Times Reader. After a bit of Snooping it turns out that the NY Times Reader uses RenderTargetBitmap to obtain snapshots of the old and new values, then animates a pair of Images.

[Download the sample project]

  • Windows Presentation Foundataion