Diederik Krols

The XAML Brewer

An animated ScrollToSection for the Universal Hub Control

n this article I show a way to animate the scrolling to a particular HubSection of a Hub in a Universal XAML Windows app. The native Hub.ScrollToSection method comes without such an animation: instead of smoothly scrolling, it jumps directly to the target section. That’s quite scary. A lot of developers seem to encounter this problem, but I didn’t find a working solution anywhere. So here are my 2 cents.

The sample app represents the type of Windows Phone application that I created this animation for. The app’s main structure is a Hub. The first HubSection in this hub acts a menu: it contains a list of hyperlinks to the other sections in the hub. The rest of the navigation buttons are just there to verify the algorithm: it should be possible to scroll from whatever section to whatever other section, backward as well as forward:

wp_ss_20150223_0001 wp_ss_20150223_0008 wp_ss_20150223_0005 wp_ss_20150223_0006

 

The Hub itself does not have a HorizontalOffset or so. The property we need to animate here is found in its internal structure: the HorizontalOffset of the embedded ScrollViewer control. Unfortunately this is a read-only property, we can not change it through a DoubleAnimation in a StoryBoard. The scroll position can only be changed by calling one of the ChangeView method overloads. Oddly enough, if you want animation, you have to call the ChangeView that allows you to disable … animation. Here’s the official description of the disableAnimation parameter: “True to disable zoom/pan animations while changing the view; otherwise, false. The default is false”. Well, that default value ruins scrolling on the phone. The default animation is visually empty, but it probably takes some time and during that time it absorbs your own animations. So you have to explicitly disable the system animation by setting the parameter to True.

The confusion doesn’t stop here. This is the official description of the horizontalOffset parameter, the first in the ChangeView call: “A value between 0 and ScrollableWidth that specifies the distance the content should be scrolled horizontally.” To me that  sounds like the method expects a delta; e.g. +10 if you want to scroll 10 pixels to the right. It turns out that this horizontalOffset is actually the target position: with +10 you navigate to the 10th pixel in the Hub.

So here’s a working implementation of smoothly scrolling to a specific section in a hub. We first calculate the absolute current position and the relative position of the target section. Then we start to scroll towards the target in a number of steps, with an increasing wait time between the steps to simulate an easing function. That sounds cheap, but it does the trick:

public async static Task ScrollToSectionAnimated(this Hub hub, HubSection section)
{
    // Find the internal scrollviewer and its current horizontal offset.
    var viewer = hub.GetFirstDescendantOfType<ScrollViewer>();
    var current = viewer.HorizontalOffset;

    // Find the distance to scroll.
    var visual = section.TransformToVisual(hub);
    var point = visual.TransformPoint(new Point(0, 0));
    var offset = point.X;

    // Scroll in a more or less animated way.
    var increment = offset / 24;
    for (int i = 1; i < 25; i++)
    {
        viewer.ChangeView((i * increment) + current, null, null, true);
        await Task.Delay(TimeSpan.FromMilliseconds(i * 20));
    }
}

The ScrollToSectionAnimated method is implemented as an extension method. I added some extra methods in the surrounding class, to fetch the current section of the hub, and the index of this section. These make particularly sense in a Windows Phone app, where there is only one section visible. That’s the first one in Hub.SectionsInView:

public static HubSection CurrentSection(this Hub hub)
{
    if (hub.Sections.Count > 0)
    {
        return hub.SectionsInView[0];
    }
    else
    {
        return null;
    }
}

public static int CurrentIndex(this Hub hub)
{
    if (hub.Sections.Count > 0)
    {
        return hub.Sections.IndexOf(hub.CurrentSection());
    }
    else
    {
        return -1;
    }
}

So here’s the code to scroll to the next and the last section, starting from the current position in the hub:

private async void ScrollToNext_Clicked(object sender, RoutedEventArgs e)
{
    // Find out where we are.
    var index = this.MainHub.CurrentIndex();

    await this.MainHub.ScrollToSectionAnimated(this.MainHub.Sections[index + 1]);
}

private async void ScrollToLast_Clicked(object sender, RoutedEventArgs e)
{
    await this.MainHub.ScrollToSectionAnimated(this.MainHub.Sections.Last());
}

Here's how the scrolling looks like in action: