XAML Playground
about XAML and other Amenities

A bit of Physics for an InertialScrollViewer

2009-05-10T01:22:27+01:00 by Andrea Boschin

Silverlight 2.0 and 3.0 come with an useful control called ScrollViewer, that helps when long list of elements needs to be scrolled with a traditional scrollbar. New mobile devices like the iPhone introduced a new kind of ScrollViewer where the user only needs to use his thumb to move the lists of elements on the screen. This beautiful kind of ScrollViewer often give an impressive feedback with a fluid scrolling that ends in a real soft deceleration giving the impression that the bend of elements is subject to the physics rules. This is not only scenic, but give the user a natural feedback that take usability to the maximum levels. So, in this article I would want to illustrate how to apply some simple physics formula to create a reusable InertialScrollViewer to take this feedback into Silverlight applications.

Please note that I'm not a physicist. I have only searched my school notions and tried to get something to work. And it works fine as you will see... :)

How the control has to work

Before entering the coding phase of the control, we need to understand how the control has to work. From the developer perspective the InertialScrollViewer must work similar to the original ScrollViewer. It have to scroll the content generated inside of it and let the user scroll Vertically or Horizontally. My implementation of this control let the developer to choice if scroll Vertically or Horizontally but not in both the directions at the same time.

From the user perspective we have two different phases: when the user click on the band he can move it using the mouse (e.g from left to the right). While the user is moving, the band is simply locked to the mouse and if the user stop scrolling the bend also stops. Only when the user leave the mouse button while he is scrolling, it has to maintain the current speed and decrease it to zero in a variable distance. The calculation of the speed is the most difficult thing, infact we have to take note of the change of direction and not only of the difference from the starting point to the end of the scrolling. If the user start scrolling from left to right and at a given point stops he turn scrolling from the right to left, the speed needs to be recalculated from the direction change and not from the start of the scrolling action. To avoid this kind of problems I decided to sample the speed at every MouseMove event. Every time this event will be raised I take note of "ticks" and "distance" from the previous event and calculate the speed with the simple formula

speed = distance / time

For a better precision I record the last two samples and when the user leave the mouse button I calculate the medium of the two available samples.

The other phase of the user interaction enter when the user stop dragging and the band start decrease its speed. All calculation is done when this event occur. First we have to calculate the distance that the band will race with the starting speed and then we have to calculate how much time it take to decrease speed to zero. The first part use this formula:

distance = speed ^ 2 / 20 * f

This formula, I found in my physics schoolbook, use f as friction factor to determine the type of surface where the bend is moving. A number less then 0.5 determine a slippery surface so the bend will run a long distance. A number from .5 to 1 determine a rough surface. This value is exposed by my control so the developer can change it and give different feedback to the user.

To calculate the time needed for the bend going from the start speed to zero I found another formula that get a deceleration value. Here is the formula

time = -speed / -a 

Where "-a" is the negative acceleration. This formula result from a more complicated formula that I've simplified due to some 0 occurrence. The simplification process is out of the scope of this article.

Now that we have all calculation done, it is time to start implementing the control.

The InertialScrollViewer ControlTemplate

The first thing to do is implementing a templated control. Like every control in Silverlight the InertialScrollViewer will take advantage of a template that will be saved in Themes/generic.xaml. Here we cannot inherit from the ScrollViewer control because it is sealed so our custom scroller will be a ContentControl and it allow to scroll content put into itself.

The template or the InertialScrollViewer is made of a Border containing a Canvas and then a ContentControl. I put some defaults to give the InertialScrollViewer an appearance similato to the original ScrollViewer. Here is the template:

   1: <Style TargetType="local:InertialScrollViewer">
   2:         <Setter Property="Padding" Value="0"/>
   3:         <Setter Property="Background" Value="#FFFFFFFF"/>
   4:         <Setter Property="BorderThickness" Value="1"/>
   5:         <Setter Property="VerticalContentAlignment" Value="Stretch"/>
   6:         <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
   7:         <Setter Property="BorderBrush">
   8:             <Setter.Value>
   9:                 <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
  10:                     <GradientStop Color="#FFA3AEB9" Offset="0"/>
  11:                     <GradientStop Color="#FF8399A9" Offset="0.375"/>
  12:                     <GradientStop Color="#FF718597" Offset="0.375"/>
  13:                     <GradientStop Color="#FF617584" Offset="1"/>
  14:                 </LinearGradientBrush>
  15:             </Setter.Value>
  16:         </Setter>
  17:         <Setter Property="Template">
  18:             <Setter.Value>
  19:                 <ControlTemplate>
  20:                     <Border x:Name="RootElement" 
  21:                             CornerRadius="2"                            
  22:                             Padding="{TemplateBinding Padding}"
  23:                             Background="{TemplateBinding Background}"
  24:                             BorderBrush="{TemplateBinding BorderBrush}" 
  25:                             BorderThickness="{TemplateBinding BorderThickness}">
  26:                         <Border.Resources>
  27:                             <Storyboard x:Name="EasingStoryboard">
  28:                                 <DoubleAnimation />
  29:                             </Storyboard>
  30:                         </Border.Resources>
  31:                         <Canvas x:Name="CanvasElement">
  32:                             <ContentControl x:Name="ContentElement"
  33:                                             Content="{TemplateBinding Content}"
  34:                                             VerticalAlignment="Stretch"
  35:                                             HorizontalAlignment="Stretch"
  36:                                             VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
  37:                                             HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" />
  38:                         </Canvas>
  39:                     </Border>
  40:                 </ControlTemplate>
  41:             </Setter.Value>
  42:         </Setter>
  43:     </Style>

The <Border /> "RootElement" inside the template will be the outer border of the control. It receive the default values and have a shaded BorderBrush. It contains a StoryBoard into its resources. I will return on it later in the article.

The <Canvas /> "CanvasElement" is required to be able to scroll the ContentControl I put inside of it. After doing some tests I found that the Canvas is the only control that let me apply some Transform to the ContentControl and continue to show the content without any clipping. People using the control will have no knowledge about the Canvas.

Finally the ContentControl has been configured to let the content stretch to fill all the available space. Its VerticalContentAlignment and HorizontalContentAlignment property has been binded to the corresponding template properties so the user can configure the control behavior.

Let's do some code...

Now it is time to start coding. The control is about 300 line long so it is impossible to explain all the code. I will explain the key point following the control lifetime. After doing some inizialization in the control constructor the first place where it is required we write some code is the OnApplyTemplate method. Inside this method we have to hook every element of our template. The elements will be assigned to some member properties using the GetTemplateChild method. To let the control customizable I have also specified the TemplatePart attributes on top of the class. Here is the parts:

   1: [TemplatePart(Name = InertialScrollViewer.RootElementName, Type = typeof(Border))]
   2: [TemplatePart(Name = InertialScrollViewer.CanvasElementName, Type = typeof(Canvas))]
   3: [TemplatePart(Name = InertialScrollViewer.ContentElementName, Type = typeof(ContentControl))]
   4: [TemplatePart(Name = InertialScrollViewer.EasingStoryboardName, Type = typeof(Storyboard))]

Every element has its own wrapping constant to centralize the names in a single point. During the OnApplyTemplate this names will be used to pick up the elements and assign to the corresponding properties. I also create a TranslateTransform into this method and assign it to the RenderTransform property of the ContentControl. The reason to create the TranslateTransform - used to scroll the content - is that if the user customize it can easily break the control functionality.

   1: public override void OnApplyTemplate()
   2: {
   3:     // Border
   4:     this.RootElement = GetTemplateChild(InertialScrollViewer.RootElementName) as Border;
   6:     // Canvas
   7:     this.CanvasElement = GetTemplateChild(InertialScrollViewer.CanvasElementName) as Canvas;
   8:     this.CanvasElement.SizeChanged += new SizeChangedEventHandler(CanvasElement_SizeChanged);
  10:     // clip the content to the Width and Height
  11:     this.CanvasElement.Clip =
  12:         this.CanvasClipping = new RectangleGeometry
  13:         {
  14:             Rect = new Rect(0, 0, this.CanvasElement.ActualWidth, this.CanvasElement.ActualHeight)
  15:         };
  17:     // ContentControl
  18:     this.ContentElement = GetTemplateChild(InertialScrollViewer.ContentElementName) as ContentControl;
  19:     this.ContentElement.RenderTransform =
  20:         this.Translate = new TranslateTransform { X = 0, Y = 0 };
  22:     this.ContentElement.MouseLeftButtonDown += (a, b) => this.HandleMouseDown(this.GetMousePosition(b, this.Orientation));
  23:     this.ContentElement.MouseLeftButtonUp += (a, b) => this.HandleMouseUp(this.GetMousePosition(b, this.Orientation));
  24:     this.ContentElement.MouseMove += (a, b) => this.HandleMouseMove(this.GetMousePosition(b, this.Orientation));
  26:     // StoryBoard
  27:     this.EasingStoryboard = this.RootElement.Resources[InertialScrollViewer.EasingStoryboardName] as Storyboard;
  28:     this.EasingAnimation = this.EasingStoryboard.Children.OfType<DoubleAnimation>().FirstOrDefault();
  30:     // let do some inizialization
  31:     this.OnOrientationChanged(this.Orientation);
  32:     this.OnEasingFunctionChanged(this.EasingFunction);
  34:     // and call the base class
  35:     base.OnApplyTemplate();
  36: }

The last thing to take note is the Clipping. The Canvas is a control that do not have a frame. The content can be put at negative coordinates but it is not clipped if there is some space available. So I have created a RectangleGeometry and I use it to clip the Canvas at his ActualWidth and ActualHeight. This is done in the CanvasElement_SizeChanged event handler every time the size of the element will be changed.

During the OnApplyTemplate method I attach some event handlers to the ContentControl to handle the Mouse events MouseMove, MouseLeftButtonDown and MouseLeftButtonUp. This events let me know when the user try to scroll the content.

In the MouseLeftButtonDown I do some inizialization. I Capture the mouse, inizializa the Translation and then start the sampling using an instance of the SpeedSample class. In the MouseMove I do the sampling using the same specialized class SpeedSampler. The SpeedSampler class is responsible to count the incoming events from MouseMove and record the last two samples. In my opinon the class is pretty simple and do not need any explanation. In the same event I also apply the value to the TranslateTransform to move the content to the required position.

When the user leave the mouse button the flow goes to the MouseLeftButtonUp event handler and inside of it I do alle the required calculation. First of all I take note of the current speed and then I calculate the distance and the duration of the deceleration.

   1: /// <summary>
   2: /// Handles the mouse up.
   3: /// </summary>
   4: /// <param name="mousePosition">The mouse position.</param>
   5: private void HandleMouseUp(double mousePosition)
   6: {
   7:     if (this.GetBoundary(this.Orientation) >= 0) return;
   9:     this.IsCaptured = false;
  10:     this.ContentElement.ReleaseMouseCapture();
  11:     double speed = this.Sampler.GetSpeed();
  13:     if (this.EasingStoryboard != null && this.EasingAnimation != null)
  14:     {
  15:         this.EasingAnimation.To = this.ComputeNextPosition(Math.Pow(speed, 2) / (20 * this.FrictionFactor) / 1000 * this.Sampler.Direction);
  16:         this.EasingAnimation.Duration = TimeSpan.FromSeconds(-(speed / 1000) / -this.Deceleration);
  17:         this.EasingStoryboard.Begin();
  18:     }
  19: }

Once I have calculated this parameters the only thing I have to do is to assign them to the Storyboard and start the animation. The EasingStoryboard has been picked up from the template and will run the scroller to its calculated end position. If the user will click on the content during the easing animation I simply stop the animation and forget all the calculation.

The Storyboard has a default QuadraticEasing function applied that to me is the better choice to give a natural behavior. The control expose an EasingFunction property to let the user cutomize the animation easing. Putting and ElasticEase give the control an astounding elastic behavior.

The control expose some other properties to let the user customize it. Orientation let you decide if scrolling is vertical or horizontal. FrictionFactor and Deceleration let customize the physics parameters. I suggest you to try by yourself changing the properties and the easing to give the control the preferred behavior.

Some final words.

Attached to this article you will find the complete source of the control. I would like to have your feedback about how it works. I made many tests and to me it is working fine but I'm sure that it is not perfect. If you find some strange behavior please post a comment and I will try to get rid of it.

Download: (~1.1 MB)

Video: (~2.6 MB)