Tuesday, 2 April 2013

Smooth scrolling content on Windows Phone

     Gotta say that I am not 100% satisfied about the title of this blog post, but since I haven't found a better one I will use this one (don't get it wrong windows phone has smooth scrolling implemented). This post is focusing on how to smooth scroll the content of a ScrollViewer. If you are using the ScrollViewer you know that it already has the method ScrollToVerticalOffset that can be used to manually scroll the content but this method doesn't have any animation implemented so you will see only a fast redraw of the new content without getting the "feeling" that the content scrolled to the new position. The easiest way to achieve this task would be using a StoryBoard with an Animation on the property VerticalOffset of the ScrollViewer, but VerticalOffset is a read-only property so it cannot be set and therefore cannot be used for animation.
     The solution I found was to build a custom control and add a custom property that can be used as a target for a DoubleAnimation. The attached sample (see the link at the end of the post) is using a ListBox as the content of my Custom Control but you can use anything else inside the control (a better approach for the ListBox would be to create a custom control derived directly from ListBox and use the built-in ScrollViewer). 
      Let's start from the XAML part of the User Control:

  <ScrollViewer x:Name="scroller">  
<ContentPresenter Content="{Binding ContentArea, ElementName=userControl}"/>
</ScrollViewer>

    So we have a ScrollViewer which inside has a ContentPreseter that has its content binded to the control's property ContentArea. Now let us have a look at the .cs code.

 public static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.Register("VerticalOffset",  
typeof(double), typeof(SmoothScroller), new PropertyMetadata(VerticalOffsetPropertyChanged));
public double VerticalOffset
{
get { return (double)this.GetValue(VerticalOffsetProperty); }
set { this.SetValue(VerticalOffsetProperty, value); }
}
private static void VerticalOffsetPropertyChanged(object sender, DependencyPropertyChangedEventArgs args)
{
SmoothScroller cThis = sender as SmoothScroller;
cThis.scroller.ScrollToVerticalOffset((double)args.NewValue);
}
public static readonly DependencyProperty ContentAreaProperty = DependencyProperty.Register("ContentArea", typeof(object), typeof(SmoothScroller), null);
public object ContentArea
{
get { return (object)GetValue(ContentAreaProperty); }
set { SetValue(ContentAreaProperty, value); }
}

     The property that we will use for animation is the VerticalOffset that uses the method VerticalOffsetPropertyChanged to scroll to a vertical offset inside the control's scroller. The property VerticalOffset can be animated and we will use it to smooth scroll the ListBox content.
     Here is the content of the MainPage.xaml

  <uc:SmoothScroller x:Name="smoothScroller">  
<uc:SmoothScroller.ContentArea>
<ListBox x:Name="lstItems" ItemsSource="{Binding Items}" ScrollViewer.VerticalScrollBarVisibility="Disabled" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ItemTemplate="{StaticResource DataTemplate}" >
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</uc:SmoothScroller.ContentArea>
</uc:SmoothScroller>

     So we have the SmoothScroller control which inside its ContentArea has a ListBox (the ListBox has it's scrollviewer disabled). What the sample does is it is changing the selected item every second (incrementing the selectedindex) and scrolls the content so that the newly SelectedItem is centered in the ScrollViewer. Here is the code used for scrolling:

  void dt_Tick(object sender, EventArgs e)  
{
((ListBox)smoothScroller.ContentArea).SelectedIndex = idx;
var container = ((ListBox)smoothScroller.ContentArea).ItemContainerGenerator.ContainerFromIndex(idx) as FrameworkElement;
var transform = container.TransformToVisual(smoothScroller.scroller);
var elementLocation = transform.Transform(new Point(0, 0));
double newVerticalOffset = elementLocation.Y + smoothScroller.scroller.VerticalOffset - smoothScroller.scroller.ActualHeight / 2 ;
//Animate transition
DoubleAnimation verticalAnimation = new DoubleAnimation();
verticalAnimation.From = smoothScroller.scroller.VerticalOffset;
verticalAnimation.To = newVerticalOffset;
verticalAnimation.Duration = new Duration(TimeSpan.FromMilliseconds(500));
var sb = new Storyboard();
sb.Children.Add(verticalAnimation);
Storyboard.SetTarget(verticalAnimation, smoothScroller);
Storyboard.SetTargetProperty(verticalAnimation, new PropertyPath(SmoothScroller.VerticalOffsetProperty));
sb.Begin();
idx++;
if (idx >= 56)
dt.Stop();
}

     Pretty basic: select the new index in the ListBox, calculate the new VerticalOffset so that the newly selected item is centered in our scrollviewer and create and start a 500ms animation for the SmoothScroller.VerticalOffsetProperty. Anyway I am sure that you will better understand how it works when you will try the sample code.
     The sample is far from perfect (before starting a new animation I should check if the old one finished, should not automatically scroll when the user is manipulating the list,...).
     Here is a screenshot from the sample:

As usual let me know if you need help. The code works on both Windows Phone 7.1 and Windows Phone 8 SDK.

SOURCE CODE


0 comments:

Post a Comment