transitions

WP7Contrib Transitions Part 3 – LongListSelector

Make sure to read parts 1 and 2 first!

By now you’re probably loving the transitions on offer from WP7Contrib, but have found a slight problem in that the Turnstile Feather animation doesn’t work with the LongListSelector as provided by the WP7 Silverlight Toolkit. Fear not! For the opensource nature of WP7Contrib means we can extend the transitions functionality to support the LongListSelector as well as the standard ListBox. It should be noted that these changes aren’t perfect, and are largely a hack to add the functionality in as short a timespan as possible. You can download the demo app here to see the changes in action.

Let’s Have At It

First you need to download the WP7Contrib sourcecode, the code in this example is associated with revision 66430. You can try applying the changes to the latest revision, however note that things have most definitely moved on since 66430.

Open up the solution you’ve downloaded, then expand the ‘WP7Contrib.View.Transitions’ project. Expand ‘Animation’ and open up ‘TurnstileFeatherAnimator.cs’ and add the following properties to the class:

/// <summary>
/// Is BackwardIn or BackwardOut
/// </summary>
protected bool IsBackward { get; set; }

/// <summary>
/// Run the completion action first rather than on completion
/// </summary>
protected bool RunCompletionAnimationFirst { get; set; }

private Action _completionAnimationAction;
private Action _actualBeginAction;
private Action _completionAction;

Change the datatype of the ‘ListBox’ property to ‘Control’, this is so we can use either a ListBox or a LongListSelector without modifying client code.

The IsBackward property tells us if this is a Backward animation, this is required to fix some bugs in the way the LongListSelector handles itself when the page is restored when the user navigates backward.

The RunCompletionAnimationFirst property tells the animator to run the completion animation first or not. This is necessary because I perform a Turnstile animation on the RootElement to complete the feather effect. This turnstile animation needs to run first if the elements are animating in, and last if the elements are animating out.

The three Action properties are necessary to order animations in the correct order, and then perform the actual completion action. These properties will make more sense when you see the implementation details.

Now replace all of the methods in the ‘Public Methods’ region with the following:

/// <summary>
/// The begin.
/// </summary>
/// <param name="completionAction">
/// The completion action.
/// </param>
public override void Begin(Action completionAction)
{
    _completionAction = completionAction;
    completionAction = new Action(AnimationsCompleted);

    if (RunCompletionAnimationFirst)
    {
        _completionAnimationAction = ActualBegin;
        _actualBeginAction = completionAction;

        HideListItems();
        BeginCompletionAnimation();
    }
    else
    {
        _completionAnimationAction = completionAction;
        _actualBeginAction = BeginCompletionAnimation;

        ActualBegin();
    }
}

private void BeginCompletionAnimation()
{
    CompletionAnimation.RootElement = this.RootElement;
    CompletionAnimation.Begin(_completionAnimationAction);
}

private void HideListItems()
{
    if (ListBox is LongListSelector)
    {
        foreach (ContentPresenter li in ((LongListSelector)ListBox).GetItemsWithContainers(false, true).OfType<ContentPresenter>())
            li.Opacity = 0;
    }
    else if (ListBox is ListBox)
    {
        foreach (ContentControl li in ((ListBox)ListBox).GetVisualDescendants().OfType<ListBoxItem>().Where(lbi => IsOnCurrentPage(lbi) && lbi.IsEnabled))
            li.Opacity = 0;
    }
}

private void AnimationsCompleted()
{
    if (!this.IsBackward && ListBox is LongListSelector)
    {
        LongListSelector listBox = (LongListSelector)ListBox;

        listBox.UpdateLayout();
        listBox.SelectedItem = null;
    }

    _completionAction();

    _completionAnimationAction = null;
    _actualBeginAction = null;
    _completionAction = null;
}

private void ActualBegin()
{
    if (ListBox is ListBox)
        ActualBeginListBox();
    else if (ListBox is LongListSelector)
        ActualBeginLongListSelector();
    else
        throw new InvalidOperationException("ListBox must be of type System.Windows.Controls.ListBox or Microsoft.Phone.Controls.LongListSelector");

    base.Begin(_actualBeginAction);
}

private void ActualBeginListBox()
{
    Storyboard = new Storyboard();
    ListBox listBox = (ListBox)ListBox;

    double liCounter = 0;
    var listBoxItems = listBox.GetVisualDescendants().OfType<ListBoxItem>().Where(lbi => IsOnCurrentPage(lbi) && lbi.IsEnabled).ToList();

    if (HoldSelectedItem && Direction == Directions.Out && listBox.SelectedItem != null)
    {
        //move selected container to end
        var selectedContainer = listBox.ItemContainerGenerator.ContainerFromItem(listBox.SelectedItem);
        listBoxItems.Remove(selectedContainer as ListBoxItem);
        listBoxItems.Add(selectedContainer as ListBoxItem);
    }

    foreach (ContentControl li in listBoxItems)
    {
        GeneralTransform gt = li.TransformToVisual(RootElement);
        Point globalCoords = gt.Transform(new Point(0, 0));
        double heightAdjustment = li.Content is FrameworkElement ? ((li.Content as FrameworkElement).ActualHeight / 2) : (li.ActualHeight / 2);
        //double yCoord = globalCoords.Y + ((((System.Windows.FrameworkElement)(((System.Windows.Controls.ContentControl)(li)).Content)).ActualHeight) / 2);
        double yCoord = globalCoords.Y + heightAdjustment;

        double offsetAmount = (RootElement.ActualHeight / 2) - yCoord;

        PlaneProjection pp = new PlaneProjection();
        pp.GlobalOffsetY = offsetAmount * -1;
        pp.CenterOfRotationX = 0;
        li.Projection = pp;

        CompositeTransform ct = new CompositeTransform();
        ct.TranslateY = offsetAmount;
        li.RenderTransform = ct;

        var beginTime = TimeSpan.FromMilliseconds((FeatherDelay * liCounter) + InitialDelay);

        if (Direction == Directions.In)
        {
            li.Opacity = 0;

            DoubleAnimationUsingKeyFrames daukf = new DoubleAnimationUsingKeyFrames();

            EasingDoubleKeyFrame edkf1 = new EasingDoubleKeyFrame();
            edkf1.KeyTime = beginTime;
            edkf1.Value = Angle;
            daukf.KeyFrames.Add(edkf1);

            EasingDoubleKeyFrame edkf2 = new EasingDoubleKeyFrame();
            edkf2.KeyTime = TimeSpan.FromMilliseconds(Duration).Add(beginTime);
            edkf2.Value = 0;

            ExponentialEase ee = new ExponentialEase();
            ee.EasingMode = EasingMode.EaseOut;
            ee.Exponent = 6;

            edkf2.EasingFunction = ee;
            daukf.KeyFrames.Add(edkf2);

            Storyboard.SetTarget(daukf, li);
            Storyboard.SetTargetProperty(daukf, new PropertyPath("(UIElement.Projection).(PlaneProjection.RotationY)"));
            Storyboard.Children.Add(daukf);

            DoubleAnimation da = new DoubleAnimation();
            da.Duration = TimeSpan.FromMilliseconds(0);
            da.BeginTime = beginTime;
            da.To = 1;

            Storyboard.SetTarget(da, li);
            Storyboard.SetTargetProperty(da, new PropertyPath("(UIElement.Opacity)"));
            Storyboard.Children.Add(da);
        }
        else
        {
            li.Opacity = 1;

            DoubleAnimation da = new DoubleAnimation();
            da.BeginTime = beginTime;
            da.Duration = TimeSpan.FromMilliseconds(Duration);
            da.To = Angle;

            ExponentialEase ee = new ExponentialEase();
            ee.EasingMode = EasingMode.EaseIn;
            ee.Exponent = 6;

            da.EasingFunction = ee;

            Storyboard.SetTarget(da, li);
            Storyboard.SetTargetProperty(da, new PropertyPath("(UIElement.Projection).(PlaneProjection.RotationY)"));
            Storyboard.Children.Add(da);

            da = new DoubleAnimation();
            da.Duration = TimeSpan.FromMilliseconds(10);
            da.To = 0;
            da.BeginTime = TimeSpan.FromMilliseconds(Duration).Add(beginTime);

            Storyboard.SetTarget(da, li);
            Storyboard.SetTargetProperty(da, new PropertyPath("(UIElement.Opacity)"));
            Storyboard.Children.Add(da);
        }

        liCounter++;
    }
}

private void ActualBeginLongListSelector()
{
    Storyboard = new Storyboard();
    LongListSelector listBox = (LongListSelector)ListBox;

    double liCounter = 0;
    var listBoxItems = listBox.GetItemsWithContainers(false, true).OfType<ContentPresenter>().ToList();

    if (HoldSelectedItem && Direction == Directions.Out && listBox.SelectedItem != null)
    {
        //move selected container to end
        var listBoxItemsInView = listBox.GetItemsWithContainers(true, true).OfType<ContentPresenter>();
        var selectedContainer = listBoxItemsInView.Where(i => i.Content == listBox.SelectedItem).FirstOrDefault();

        if (selectedContainer != null)
        {
            listBoxItems.Remove(selectedContainer);
            listBoxItems.Add(selectedContainer);
        }
    }

    foreach (ContentPresenter li in listBoxItems)
    {
        GeneralTransform gt = li.TransformToVisual(RootElement);
        Point globalCoords = gt.Transform(new Point(0, 0));
        //double heightAdjustment = li.Content is FrameworkElement ? ((li.Content as FrameworkElement).ActualHeight / 2) : (li.ActualHeight / 2);
        double heightAdjustment = li.ActualHeight / 2;
        //double yCoord = globalCoords.Y + ((((System.Windows.FrameworkElement)(((System.Windows.Controls.ContentControl)(li)).Content)).ActualHeight) / 2);
        double yCoord = globalCoords.Y + heightAdjustment;

        double offsetAmount = (RootElement.ActualHeight / 2) - yCoord;

        PlaneProjection pp = new PlaneProjection();
        pp.GlobalOffsetY = offsetAmount * -1;
        pp.CenterOfRotationX = 0;
        li.Projection = pp;

        CompositeTransform ct = new CompositeTransform();
        ct.TranslateY = offsetAmount;
        li.RenderTransform = ct;

        var beginTime = TimeSpan.FromMilliseconds((FeatherDelay * liCounter) + InitialDelay);

        if (Direction == Directions.In)
        {
            li.Opacity = 0;

            DoubleAnimationUsingKeyFrames daukf = new DoubleAnimationUsingKeyFrames();

            EasingDoubleKeyFrame edkf1 = new EasingDoubleKeyFrame();
            edkf1.KeyTime = beginTime;
            edkf1.Value = Angle;
            daukf.KeyFrames.Add(edkf1);

            EasingDoubleKeyFrame edkf2 = new EasingDoubleKeyFrame();
            edkf2.KeyTime = TimeSpan.FromMilliseconds(Duration).Add(beginTime);
            edkf2.Value = 0;

            ExponentialEase ee = new ExponentialEase();
            ee.EasingMode = EasingMode.EaseOut;
            ee.Exponent = 6;

            edkf2.EasingFunction = ee;
            daukf.KeyFrames.Add(edkf2);

            Storyboard.SetTarget(daukf, li);
            Storyboard.SetTargetProperty(daukf, new PropertyPath("(UIElement.Projection).(PlaneProjection.RotationY)"));
            Storyboard.Children.Add(daukf);

            DoubleAnimation da = new DoubleAnimation();
            da.Duration = TimeSpan.FromMilliseconds(0);
            da.BeginTime = beginTime;
            da.To = 1;

            Storyboard.SetTarget(da, li);
            Storyboard.SetTargetProperty(da, new PropertyPath("(UIElement.Opacity)"));
            Storyboard.Children.Add(da);
        }
        else
        {
            li.Opacity = 1;

            DoubleAnimation da = new DoubleAnimation();
            da.BeginTime = beginTime;
            da.Duration = TimeSpan.FromMilliseconds(Duration);
            da.To = Angle;

            ExponentialEase ee = new ExponentialEase();
            ee.EasingMode = EasingMode.EaseIn;
            ee.Exponent = 6;

            da.EasingFunction = ee;

            Storyboard.SetTarget(da, li);
            Storyboard.SetTargetProperty(da, new PropertyPath("(UIElement.Projection).(PlaneProjection.RotationY)"));
            Storyboard.Children.Add(da);

            da = new DoubleAnimation();
            da.Duration = TimeSpan.FromMilliseconds(10);
            da.To = 0;
            da.BeginTime = TimeSpan.FromMilliseconds(Duration).Add(beginTime);

            Storyboard.SetTarget(da, li);
            Storyboard.SetTargetProperty(da, new PropertyPath("(UIElement.Opacity)"));
            Storyboard.Children.Add(da);
        }

        liCounter++;
    }
}

This is a lot of code! Fortunately this is the only major change to add the functionality we need. In the Begin method we set up our actions mentioned in the properties earlier. The completionAction is the action performed when all animations are completed, just like the original implementation. _actualBeginAction is the action the ActualBegin() method will execute upon completion. This will either be the completionAction, or an action to trigger the completion animation.

The completion animation is badly named, since it can run either at the start or the end of the feather animation.

After the actions are set up, we call the first animation. The first animation will then call the second animation, which will call the completionAction. So as an overview the method calls will be:

Completion animation first: Begin() > BeginCompletionAnimation() > HideListItems() > ActualBegin() > AnimationsCompleted() > CompletionAction()
Completion animation last: Begin() > ActualBegin() > BeginCompletionAnimation() > AnimationsCompleted() > CompletionAction()

We have an additional call to HideListItems() if the completion animation is run first. This is because the screen pivots in and then animates the list. The problem is if the list is visible from the outset you would see the list turnstile-in, then suddenly disappear and animate.

ActualBegin() is where we perform the actual Feather animation. We check the type of control passed in, and trigger the correct animation code. The code is largely the same between the two except for a little setup where we use the methods provided by the LongListSelector to get the containers for the visible elements. This means implementation on the surface is actually easier than with the ListBox where we have to work out which elements are visible.

AnimationsCompleted()

So why do we have an additional method that we run at the end of the animation? This is neccesary to fix a bug in the LongListSelector where the control won’t respond to input from the user after the animation is completed. We call UpdateLayout() to trigger the LongListSelector to sort itself out. Finally we set the SelectedItem to null. This is an implementation detail I’ve found useful for myself – however, you may well want to remove this line. In essence by leaving the item selected, it is the last item to animate off of the list. However, I never want the item to remain selected, so I clear the selection automatically. This isn’t ideal behaviour for a professional control, so don’t go duplicating this kind of hideous hack in your own controls!

Finishing Up

Now we need to tweak the animators themselves to support the new functionality. Update the animators as follows:

public TurnstileFeatherBackwardInAnimator()
{
    this.IsBackward = true;
    this.Duration = 350;
    this.Angle = 50;
    this.FeatherDelay = 50;
    this.Direction = Directions.In;
    this.CompletionAnimation = new TurnstileBackwardInAnimator();
    this.RunCompletionAnimationFirst = true;
}
public TurnstileFeatherBackwardOutAnimator()
{
    this.IsBackward = true;
    this.Duration = 250;
    this.Angle = -80;
    this.FeatherDelay = 50;
    this.Direction = Directions.Out;
    this.CompletionAnimation = new TurnstileBackwardOutAnimator();
}
public TurnstileFeatherForwardInAnimator()
{
    this.Duration = 350;
    this.Angle = -80;
    this.FeatherDelay = 50;
    this.Direction = Directions.In;
    this.CompletionAnimation = new TurnstileForwardInAnimator();
    this.RunCompletionAnimationFirst = true;
}
public TurnstileFeatherForwardOutAnimator()
{
    this.Duration = 250;
    this.Angle = 50;
    this.FeatherDelay = 50;
    this.Direction = Directions.Out;
    this.HoldSelectedItem = true;
    this.CompletionAnimation = new TurnstileForwardOutAnimator();
}

And you’re done!

LongListSelector Scrolling

OK, I may have lied a little about you being done. The LongListSelector has one final issue when we animate, it doesn’t retain its scroll position. The easiest way to solve this is to sub-class AnimatedBasePage and perform the necessary ‘tweaks’ to the selector in the AnimationsComplete() method. But before I show you the code, you’ll need ‘Linq to Visual Tree’ by Colin Eberhardt. The code file is already included in the demo app. Now you’ve got the necessary code, you can add the AnimationsComplete() method to your newly created page class:

public class TransitionPage : AnimatedBasePage
{
    protected override void AnimationsComplete(AnimationType animationType)
    {
        if (animationType == AnimationType.NavigateBackwardIn || animationType == AnimationType.NavigateBackwardOut)
        {
            foreach (LongListSelector listBox in this.Descendants<LongListSelector>())
            {
                listBox.UpdateLayout();

                if (listBox.SelectedItem != null)
                    listBox.ScrollTo(listBox.SelectedItem);
                else
                {
                    object firstObj = listBox.GetItemsInView().Where(i => i != null).FirstOrDefault();

                    if (firstObj != null)
                        listBox.ScrollTo(firstObj);
                }
            }
        }

        base.AnimationsComplete(animationType);
    }
}

This code picks up if the animation is a backward one, and will automatically loop through all of the LongListSelectors (if you have multiple selectors across PivotItems or PanoramaItems they’ll all lose their position) and corrects their position.

Am I actually done now?

Yes, yes you are! It’s been a long road just to add a Feather Turnstile animation to a LongListSelector, but I believe it’s well worth it. The effect looks great and adds a great deal of professionalism to your app.

This concludes the ‘Transitions’ series of blog posts. As always, you can download the sample app here.

Issues

As with all things, there are a few gotchas. The first is the dreaded ‘AG_E_PARSER_BAD_TYPE’ error. This error can occur when you subclass AnimatedBasePage. To this date I still haven’t resolved the issue – it’s very inconsistent and affects some projects but not others. The easiest workaround is to use extension methods and call those from each individual page. It’s far from ideal and a simply hideous hack – but it does work.

The other issue you may get is a page simply refusing to navigate to another. Chances are you’re requesting a navigation too quickly – the page hasn’t finished initialising. The easiest solution in this case is to use the ‘OneShotDispatcherTimer’ from the Kawagoe Toolkit. You can use the timer like so:

OneShotDispatcherTimer timer = new OneShotDispatcherTimer()
{
    Duration = new TimeSpan(0, 0, 1)
};

timer.Fired += (object sender, EventArgs e) => NavigationService.Navigate(new Uri("/NextPage.xaml", UriKind.Relative));
timer.Start();
Posted by Dan in C#, Tutorials, Windows Phone, 0 comments

WP7Contrib Transitions Part 2 – More Advanced Transitions

Make sure to read part 1 first!

WP7Contrib offers numerous transitions, these include:

Continuum
The selected item in a list will animate out, and the next page will have the heading animate in. The reverse happens on returning to the original page. This is a great effect if the user is selecting a specific item to view more detail.

Rotate
The page rotates in/out. A bit ostentatious for my tastes.

Slide
There are various slide animations offered, these mimic the kinds of transitions you get with iOS. SlideUp and SlideDown are ideal for popups.

Turnstile
The default transition we all know and love

Turnstile Feather
My personal favourite, and one we’ll see a lot more of in Part 3. This is the same as Turnstile, except it will animate out each element in a list from top to bottom. This is the same effect you see on the phone home screen when you select an app to launch.

If you want to see all of the transitions on offer, download the WP7Contrib source code and open the ‘PageTransitions’ solution within the ‘Spikes’ folder. This provides a demo of all of the transitions on offer within WP7Contrib.

Let’s Play

Let’s try the Turnstile Feather and Continuum transitions out. You’ll need a project with a screen that’s a ListBox, if you don’t have one, download the demo app from the first part of this series. If you want to see the demo right now, click here to download the demo app for part 2.

Go into the code-behind for the page with the ListBox and add the following override:

protected override AnimatorHelperBase GetAnimation(AnimationType animationType, Uri toOrFrom)
{
    return base.GetAnimation(animationType, toOrFrom);
}

This is an extremely powerful override that allows you to fully customise the kind of animation used when the page is navigated to or from. The first parameter is the type of animation you need to return. The second parameter is the uri for the page that the user is either navigating to, or navigating from. If ever you’re not sure, use the base implementation which will almost always provide a Turnstile animation. The Turnstile animation is a good fall-back animation, so it’s often wise to handle specific cases yourself, and let the base implementation handle the rest.

Now you could run the app right now and it’ll work, but we want to trigger a Continuum transition on the way out, and a Turnstile Feather animation on the way back in. To do this, we need to tell which animations we want WP7Contrib to use:

protected override AnimatorHelperBase GetAnimation(AnimationType animationType, Uri toOrFrom)
{
    switch (animationType)
    {
        case AnimationType.NavigateForwardOut:
        case AnimationType.NavigateBackwardOut:
            return this.GetContinuumAnimation(TheListBox, animationType);

        case AnimationType.NavigateBackwardIn:
            return new TurnstileFeatherBackwardInAnimator()
            {
                ListBox = TheListBox,
                RootElement = LayoutRoot
            };

        case AnimationType.NavigateForwardIn:
            return new TurnstileFeatherForwardInAnimator()
            {
                ListBox = TheListBox,
                RootElement = LayoutRoot
            };
    }

    return base.GetAnimation(animationType, toOrFrom);
}

Notice we let WP7Contrib handle any animations we don’t explicitly handle ourselves. This ensures if the app is extended in the future the user experience isn’t unduly affected.

The Continuum animation has three properties, a StoryBoard, RootElement and LayoutRoot. This allows you to customise the animation applied to the selected element, however most of the time the default animation is the correct one. For this reason we get WP7Contrib to create the animation for us by calling GetContinuumAnimation.

The Turnstile Feather animation requires the ListBox you wish to animate and the RootElement. The RootElement in this case is the same element you set as the AnimationContext. You could set a different RootElement if required, but it’s unlikely you’ll ever need to do this.

Now run your app and admire your handy work! You can download a fully functional demo project here.

Want yet more? Click here for part 3.

Posted by Dan in C#, Tutorials, Windows Phone, 6 comments