Corrado's Blog 2.0

Online thoughts of a technology funatic

Scroll with snapping in Windows Phone 8

I’m working on a Windows Phone 8 app that has an “unconventional” UI as it does not follow Windows Phone UX guidelines (it’s a company private app) and so it has home page made up of several screens horizontally aligned that user can pan.

Untitled

I started wrapping the screens inside a horizontally oriented WrapPanel and content, as expected, scrolled.
Mission accomplished? well not really: Content was scrolling but final experience was poor because of missing snapping feature, it was extremely easy lo leave home page with more than one screen visible.

image

To overcome this issue I decides to create a Blend Behavior that attached to a ScrollViewer would take care of smoothly bring into view the correct screen depending on scroll direction.
Solution wasn’t as immediate as previous one since I decided to use an ItemsControl as ScrollViewer’s content since I have the same requirements in other views where content is databound and I had to “dig into” ScrollViewer and attach to both horizontal and vertical scrollbars (yes, I need to support both scrolling direction) but at the end it worked.

Yeah! but, there’s another UX issue: ScrollViewer has the ‘compression’ feature, the one often used to create the famous “Pull to refresh” functionality in lists. Since final experience was not acceptable and since I haven’t found a reliable way of disabling it I abandoned this 2nd solution and decided to go handling all scrolling details.

Enter the SnappingScrollViewer

I won’t bother you with a long code listing (you can get the source code here) but what I did basically was:

  1. -Create a class that inherits from Canvas
  2. -Register Canvas Manipulation events
  3. -Use ManipulationMode.Delta event to set Canvas left/top offset to scroll content
  4. -Use ManipulationMode.Completed event to detect current scrolling position and scroll the screen to proper position using an animation.

Since animation target requires a dependency property and my class is based on Canvas SetLeft/SetTop method I used an old XAML trick adding a custom ScrollOffset dependency property and invoking proper method inside property’s changed event:

protected virtual void OnScrollOffsetChanged(double oldValue, double newValue)
        {
            if (this.IsVertical)
            {
                SetTop(this.content, newValue);
            }
            else
            {
                SetLeft(this.content, newValue);
            }
        }

content is ScrollViewer’s ItemsControl that I get into Loaded event.

private void OnLoaded(object sender, RoutedEventArgs e)
        {
            this.content = (ItemsControl)this.Children.First();
            this.totalPages = (int)(this.IsVertical ? (this.content.ActualHeight / this.ItemSize) : (this.content.ActualWidth / this.ItemSize));
            this.snapAmount = this.ItemSize * this.Inertia;
            this.itemIndex = this.InitialIndex;
            if (this.IsVertical)
            {
                SetTop(this.content, -this.InitialIndex * this.ItemSize);
            }
            else
            {
                SetLeft(this.content, -this.InitialIndex * this.ItemSize);
            }
        }

Use is very simple:

<smoothScroller:SnappingScrollViewer x:Name="LayoutRoot"
                                   ItemSize="480"
                                   InitialIndex="2"
                                   IsVertical="False"
                                   Background="White">
        <ItemsControl Background="#FFE01818">
            <ListBoxItem>
                <Grid Background="Aqua"
                      Width="480"
                      Height="800" />
            </ListBoxItem>
            <ListBoxItem>
                <Grid Background="YellowGreen"
                      Width="480"
                      Height="800" />
            </ListBoxItem>

            <ListBoxItem>
                <Grid Background="Orange"
                      Width="480"
                      Height="800" />
            </ListBoxItem>
            <ListBoxItem>
                <Grid Background="Red"
                      Width="480"
                      Height="800" />
            </ListBoxItem>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel Orientation="Horizontal" />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
    </smoothScroller:SnappingScrollViewer>

Limitations in sample code: All content must have the same dimension and no support of dynamically added items (did that in production code but wanted to keep demo simple Smile)

Go grab the code and enhance it depending on your needs.

Note for Windows Phone 8.1: ScrollViewer in Windows Phone 8.1 has a HorizontalSnapPointType property that can be used to “snap” content, so this XAML will work as expected

<ScrollViewer HorizontalScrollMode="Enabled"
                      HorizontalScrollBarVisibility="Visible"
                      HorizontalSnapPointsType="Mandatory">
            <StackPanel Orientation="Horizontal">
                <Grid Height="800"
                      Background="#FF1EAA4B"
                      Width="480" />
                <Grid Height="800"
                      Background="#FFDAB611"
                      Width="480" />
                <Grid Height="800"
                      Background="#FF9C0EC5"
                      Width="480" />
            </StackPanel>
        </ScrollViewer>

Unfortunately Compression effect is still present Sad smile if anyone knows how to disable it just let me know, I’m more than interested.

Enjoy!