Facebook-like 'scroll to top' functionality in a ScrollViewer

Facebook did it some time ago! - Added this nifty button that makes you go to the top whenever you scroll upwards on Windows Phone.
A true UX gem which is almost expected by users because of its simple and intuitive nature. 

Yesterday I saw Tim Heuer tweeting how it's almost standard and something that the users expects.
My immediate thought was let's build it and bring it out there! 

 

Download the source and sample

 

 

 

Behavior and code explained

The solution is pretty straight forward. The solution is created using a custom behavior which injects a button and the necessary functionality into a scrollviewer. 
(Big shoutout to Joost van Schaik and Morten Nielsen for sharing their amazing insights on creating custom behaviors).

Using the behavior

Using the behavior is pretty straight forward. You simply set it on the scrollviewer and set the ScrollToTopButton property on the behavior with the button you want to inject and appear on top of your scrollviewer while scrolling backwards towards the top.

<ScrollViewer
    Grid.Row="1">
    <Interactivity:Interaction.Behaviors>
        <local:ScrollToTopBehavior>
            <local:ScrollToTopBehavior.ScrollToTopButton>
                <Button
                    x:Name="GoToTopButton"
                    Grid.RowSpan="2"
                    VerticalAlignment="Top"
                    HorizontalAlignment="Right"
                    Style="{StaticResource CleanButtonStyle}"
                    Margin="0,20,20,0">
                    <Image
                        x:Name="image"
                        Width="100"
                        Height="100"
                        Source="ms-appx:///Images/ToTop.png"
                        Stretch="None"
                        HorizontalAlignment="Right" />
                </Button>
            </local:ScrollToTopBehavior.ScrollToTopButton>
        </local:ScrollToTopBehavior>
    </Interactivity:Interaction.Behaviors>
</ScrollViewer>

Inside the behavior

To create a behavior we have to implement the IBehavior interface located inside the Behaviors SDK which is an extension shipped with Visual Studio and installed per default. 
The interface gives us an attach (hook into events and functionality) and detach (unhook events and functionality to allow GC) method which automatically is called when the behavior is attacted and detached to the scrollviewer.

In the attach method we hook into on following:

  • ScrollViewer.ViewChanging which is called continously while scrolling. This event is used to get offsets and see if inertia is enabled while scrolling.
  • ScrollViewer.Loaded to find the appropriate container inside the template of the scrollviewer and inject our button into.
public void Attach(DependencyObject associatedObject)
{
    offsets = new List<double>();
    isHidden = true;
    buttonAdded = false;

    if (!DesignMode.DesignModeEnabled)
    {
        _associatedObject = associatedObject;

        scrollviewer = _associatedObject as ScrollViewer;

        if (scrollviewer != null)
        {
            scrollviewer.ViewChanging += Scrollviewer_ViewChanging;
            scrollviewer.Unloaded += Scrollviewer_Unloaded;
        }

        if (ScrollToTopButton != null)
        {
            ScrollToTopButton.Tapped += ScrollToTopButton_Tapped;
            ScrollToTopButton.Visibility = Visibility.Collapsed;
            ScrollToTopButton.Name = "ScrollToTopButton";
        }
    }
}


Injecting the button

To find the right container in the scrollviewer template we create a recursive method to traverse down the visual tree until we find our grid. We create a container from our button and add it to our located grid. The container-grid is added to ensure we can animate the button. (Thanks @danielvistisen for the tip)

private void Scrollviewer_Loaded(object sender, RoutedEventArgs e)
{
    FindParts(scrollviewer);
}

private void FindParts(DependencyObject dp)
{
    if (buttonAdded == false)
    {
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dp); i++)
        {
            var control = VisualTreeHelper.GetChild(dp, i);

            if (buttonAdded == false && control is Grid)
            {
                container = new Grid();
                container.Children.Add(ScrollToTopButton);
                container.Visibility = Visibility.Collapsed;

                grid = control as Grid;
                grid.Children.Add(container);
                buttonAdded = true;
                break;
            }
            else
            {
                FindParts(control);
            }
        }
    }
    else
    {
        return;
    }
}

Determine scrolldirection and showing the button

By hooking into the ViewChanging event on the scrollviewer we'll continuously get the new offset as long as the user scrolls vertically and the there's not inertia included.

Inertia will be false while the user scrolls real slow eg. in reading scenarios. We only want to show it if the user actually shows towards the top. no

Each offset is added to a list which is used to determine our direction and our proximity towards the top. 

If the right conditions is meet we'll either show or hide the Button which allows us to scroll to the top.

private void Scrollviewer_ViewChanging(object sender, ScrollViewerViewChangingEventArgs e)
{
    var offset = scrollviewer.VerticalOffset;

    if (e.IsInertial)
    {
        Debug.WriteLine(offset);
        offsets.Add(offset);
        count = count + 1;

        DetermineVisualstateChange();
    }
}

private void DetermineVisualstateChange()
{
    if (count > 4)
    {
        if (offsets[count - 1] > 250.0 && offsets[count - 2] < offsets[count - 3]
            && offsets[count - 1] < offsets[count - 2])
        {
            ShowGoToTopButton();
        }
        else if ((offsets[count - 1] > 250.0 && offsets[count - 3] < offsets[count - 2]
            && offsets[count - 2] < offsets[count - 1]) || offsets[count - 1] < 250.0)
        {
            HideGoTopTopButton();
        }
    }
}

Showing and hiding the button

Hiding and showing the button is done by a combination of using the built in FadeInThemeAnimation and setting visibility on the button.
The themeanimation animates the border inside the button template and when that's done we collapse the button visibility.

Important: If you modify the button template you need to have a control with the Name set to "border" otherwise the behavior wont to work!

private void HideGoTopTopButton()
{
    if (!isHidden)
    {
        isHidden = true;

        var storyboard = new Storyboard();
        var animation = new FadeOutThemeAnimation();
        animation.SetValue(Storyboard.TargetNameProperty, "ScrollToTopButton");
        Storyboard.SetTarget(animation, ScrollToTopButton);
        storyboard.Children.Add(animation);
        storyboard.Completed += (e,a) => { container.Visibility = Visibility.Collapsed; };
        storyboard.Begin();
    }
}

private void ShowGoToTopButton()
{
    if (isHidden)
    {
        container.Visibility = Visibility.Visible;
        isHidden = false;
        var storyboard = new Storyboard();
        var animation = new FadeInThemeAnimation();
        animation.SetValue(Storyboard.TargetNameProperty, "ScrollToTopButton");
        Storyboard.SetTarget(animation, ScrollToTopButton);
        storyboard.Children.Add(animation);
        storyboard.Begin();
    }
}

Scrolling to the top

When the button is visibile and the user invokes the Tapped event we call the ChangeView event which will set the scrollviewers vertical offset to 0 - which is at the top.
Calling the ChangeView method will trigger a offset change but that's not detected by detected by the ViewChanging event.  We call the HideGoToTopButton method to hide the button afterwards.

private void ScrollToTopButton_Tapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e)
{
    scrollviewer.ChangeView(null, 0, null);
    HideGoTopTopButton();
}

 

Final words

This was infact my first custom behavior - ever. I created it after Morten Nielsen pointed out that my inspirational textbox styles implementation could have been done using behaviors.
After a few code iterations based on input from @skendrot, @dotmorten and @scottisafool I think the result is rather great. 

The final finish could be added by using @xyzzer's WinRT toolkit and have the scrolling animate all the way to the top using his scrollviewer extension but I omitted that part to avoid 3rd party dependencies. 

I hope you joined the post and find it useful. Feel free to share your thoughts, comments and maybe even improvements. I'd love to hear from you.

Happy coding!

P.S Make sure you follow me on twitter @deanihansen for more news, design tips, articles and how-to's.