Custom Page Transitions in WP7 (part 2)

In the last post I discussed how to create a custom page transition with the Silverlight for Windows Phone Toolkit that used your own local storyboard. That’s all well and good but there some nice changes we might like to see. Lets allow the storyboard to be loaded from a file so it can be reused across multiple pages.

Reusing the storyboards

In the current solution (attached to the first post) the transition uses a storyboard which is referenced as a static resource on the page. Ultimately we would like to have this referenced in a single place so that any updates to our transition storyboard only need to take place in one place. This makes sure updates happen everywhere and reduces the chances of errors. So what we would like to do is go from this

 <toolkit:TransitionService.NavigationInTransition>
    <toolkit:NavigationInTransition>
        <toolkit:NavigationInTransition.Backward>
            <local:StoryboardTransition Storyboard="{StaticResource BatmanTransitionIn}"/>
        </toolkit:NavigationInTransition.Backward>
        <toolkit:NavigationInTransition.Forward>
            <local:StoryboardTransition Storyboard="{StaticResource BatmanTransitionIn}"/>
        </toolkit:NavigationInTransition.Forward>
    </toolkit:NavigationInTransition>
</toolkit:TransitionService.NavigationInTransition>

to this

 <toolkit:TransitionService.NavigationInTransition>
    <toolkit:NavigationInTransition>
        <toolkit:NavigationInTransition.Backward>
            <local:StoryboardTransition StoryboardFile="ClassicBatmanIn.xaml"/>
        </toolkit:NavigationInTransition.Backward>
        <toolkit:NavigationInTransition.Forward>
            <local:StoryboardTransition StoryboardFile="ClassicBatmanIn.xaml"/>
        </toolkit:NavigationInTransition.Forward>
    </toolkit:NavigationInTransition>
</toolkit:TransitionService.NavigationInTransition>

A few small changes should suffice for this. First lets add a new string property to the StoryboardTransition class that represents the path to the storyboard file in the XAP package.

 public const string StoryboardFilePropertyName = "StoryboardFile";

public string StoryboardFile
{
    get { return (string)GetValue(StoryboardFileProperty); }
    set { SetValue(StoryboardFileProperty, value); }
}

public static readonly DependencyProperty StoryboardFileProperty =
    DependencyProperty.Register(StoryboardFilePropertyName, typeof(string), typeof(StoryboardTransition), new PropertyMetadata(null));

update the StoryboardTransition class’s GetTransition method to use the file path or fall back to the storyboard property

 

 public override ITransition GetTransition(UIElement element)
{
    if (Storyboard == null)
    {
        Storyboard = GetStoryboard(StoryboardFile);
    }

    Storyboard.SetTarget(Storyboard, element);
    return new Transition(element, this.Storyboard);
}

private static Dictionary<string, string> _storyboardXamlCache;

private static Storyboard GetStoryboard(string fileName)
{
    if (_storyboardXamlCache == null)
    {
        _storyboardXamlCache = new Dictionary<string, string>();
    }

    string xaml = null;

    if (_storyboardXamlCache.ContainsKey(fileName))
    {
        xaml = _storyboardXamlCache[fileName];
    }
    else
    {
        Uri uri = new Uri(fileName, UriKind.Relative);
        StreamResourceInfo streamResourceInfo = Application.GetResourceStream(uri);
        using (StreamReader streamReader = new StreamReader(streamResourceInfo.Stream))
        {
            xaml = streamReader.ReadToEnd();
            _storyboardXamlCache[fileName] = xaml;
        }
    }

    return XamlReader.Load(xaml) as Storyboard;
}

This section of code also contains a helper function that retrieves the XAML storyboards from the XAP file and caches the XAML string so that subsequent page transitions requesting the same storyboard don’t have to go all the way out to the disk.

Another important change in GetTransition is the call to Storyboard.SetTarget(). Since we are going to be loading a generic storyboard from a file we need to set the target of the animation. The element that is passed in to the GetTransition class will be the page frame, so we will target that.

Lastly we need to take our storyboard and put it in a file. Create a new XAML file in the project called BatmanOut.xaml. Set its Build Action to Content so that it will be copied into the XAP file for deployment.

xapfileadded

Copy the contents of the BatmanTransitionOut storyboard into the XAML file. Remove the attributes for Storyboard.Target=”LayoutRoot” and the x:Key property. It should now look like the following

 

 <Storyboard xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
    <DoubleAnimation Duration="0:0:0.5" To="5000" From="0" Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationZ)">
        <DoubleAnimation.EasingFunction>
            <ExponentialEase EasingMode="EaseIn" Exponent="5"/>
        </DoubleAnimation.EasingFunction>
    </DoubleAnimation>
    <DoubleAnimation Duration="0:0:0.5" To="0.1" From="1.0"  Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleX)"/>
    <DoubleAnimation Duration="0:0:0.5" To="0.1" From="1.0"  Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleY)"/>
</Storyboard>

Do the same procedure for a new file called BatmanIn.xaml using the BatmanTransitionIn storyboard.

Because the animations depend on certain transforms being in place we need to add them to the page. Recall that previously we were animating the Grid called layout root. Now it will be the entire Page. So add the transforms to the page. You will also need to add them to Page2 because the updated GetTransition will apply the page as the storyboard target as well.

 <phone:PhoneApplicationPage.RenderTransformOrigin>
        0.5,0.5
</phone:PhoneApplicationPage.RenderTransformOrigin>
<phone:PhoneApplicationPage.RenderTransform>
    <CompositeTransform/>
</phone:PhoneApplicationPage.RenderTransform>
<phone:PhoneApplicationPage.Projection>
    <PlaneProjection/>
</phone:PhoneApplicationPage.Projection>

It would be best if we didn’t need this last step and just added the transforms in the GetTransition call programmatically, but given this is a generic method we won’t know which transforms to add. So the best case right now is to add the needed ones page by page based on what page transitions we require.

Lastly we just need to update the MainPage.xaml file to load the storyboards from these files. Update the TransitionService section to reference the files through the new StoryboardFile property.

 <toolkit:TransitionService.NavigationInTransition>
    <toolkit:NavigationInTransition>
        <toolkit:NavigationInTransition.Backward>
            <local:StoryboardTransition StoryboardFile="BatmanIn.xaml"/>
        </toolkit:NavigationInTransition.Backward>
        <toolkit:NavigationInTransition.Forward>
            <local:StoryboardTransition StoryboardFile="BatmanIn.xaml"/>
        </toolkit:NavigationInTransition.Forward>
    </toolkit:NavigationInTransition>
</toolkit:TransitionService.NavigationInTransition>
<toolkit:TransitionService.NavigationOutTransition>
    <toolkit:NavigationOutTransition>
        <toolkit:NavigationOutTransition.Backward>
            <local:StoryboardTransition StoryboardFile="BatmanOut.xaml"/>
        </toolkit:NavigationOutTransition.Backward>
        <toolkit:NavigationOutTransition.Forward>
            <local:StoryboardTransition StoryboardFile="BatmanOut.xaml"/>
        </toolkit:NavigationOutTransition.Forward>
    </toolkit:NavigationOutTransition>
</toolkit:TransitionService.NavigationOutTransition>

Now run the app. You should see the exact same behavior as before, except that the MainPage is loading its storyboard from the file while Page2 is still running its storyboard locally. If you make a change to the storyboard files and rerun the app you will see the effect just on MainPage.

Using these two mechanisms together gives us a powerful way to quickly mock up and modify new transitions in Expression Blend (using the Storyboard property) and then move that out to a file (using the StoryboardFile property) for better reuse and maintenance of our animations before we deploy the application.

The final solution is attached to this post in case you don’t feel like typing today.

StoryboardTransitions.zip